added display name to users

This commit is contained in:
zipben 2025-06-04 16:04:28 +00:00
parent cbb8950b46
commit c0294f4c39
16 changed files with 112 additions and 14 deletions

1
.gitignore vendored
View file

@ -8,6 +8,7 @@
/media/ /media/
/metadata/ /metadata/
/plugins/ /plugins/
*.epub
/client/.nuxt/ /client/.nuxt/
/client/dist/ /client/dist/
/dist/ /dist/

View file

@ -116,7 +116,7 @@ export default {
return this.$store.getters['user/getIsAdminOrUp'] return this.$store.getters['user/getIsAdminOrUp']
}, },
username() { username() {
return this.user ? this.user.username : 'err' return this.user ? this.user.displayName || this.user.username : 'err'
}, },
numMediaItemsSelected() { numMediaItemsSelected() {
return this.selectedMediaItems.length return this.selectedMediaItems.length

View file

@ -18,9 +18,14 @@
</div> </div>
</div> </div>
<div v-show="!isEditingRoot" class="flex py-2"> <div v-show="!isEditingRoot" class="flex py-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model.trim="newUser.displayName" :label="'Display Name'" />
</div>
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-model.trim="newUser.email" :label="$strings.LabelEmail" /> <ui-text-input-with-label v-model.trim="newUser.email" :label="$strings.LabelEmail" />
</div> </div>
</div>
<div v-show="!isEditingRoot" class="flex py-2">
<div class="px-2 w-52"> <div class="px-2 w-52">
<ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" small @input="userTypeUpdated" /> <ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" small @input="userTypeUpdated" />
</div> </div>
@ -387,6 +392,7 @@ export default {
username: null, username: null,
email: null, email: null,
password: null, password: null,
displayName: null,
type: 'user', type: 'user',
isActive: true, isActive: true,
permissions: { permissions: {

View file

@ -4,6 +4,7 @@
<table id="accounts"> <table id="accounts">
<tr> <tr>
<th>{{ $strings.LabelUsername }}</th> <th>{{ $strings.LabelUsername }}</th>
<th>{{ $strings.LabelDisplayName }}</th>
<th class="w-20">{{ $strings.LabelAccountType }}</th> <th class="w-20">{{ $strings.LabelAccountType }}</th>
<th class="hidden lg:table-cell">{{ $strings.LabelActivity }}</th> <th class="hidden lg:table-cell">{{ $strings.LabelActivity }}</th>
<th class="w-32 hidden sm:table-cell">{{ $strings.LabelLastSeen }}</th> <th class="w-32 hidden sm:table-cell">{{ $strings.LabelLastSeen }}</th>
@ -17,6 +18,9 @@
<p class="pl-2 truncate">{{ user.username }}</p> <p class="pl-2 truncate">{{ user.username }}</p>
</div> </div>
</td> </td>
<td>
<p class="truncate">{{ user.displayName || '-' }}</p>
</td>
<td class="text-sm">{{ user.type }}</td> <td class="text-sm">{{ user.type }}</td>
<td class="hidden lg:table-cell"> <td class="hidden lg:table-cell">
<div v-if="usersOnline[user.id]?.session?.displayTitle"> <div v-if="usersOnline[user.id]?.session?.displayTitle">

View file

@ -12,6 +12,16 @@
<ui-text-input-with-label disabled :value="usertype" :label="$strings.LabelAccountType" /> <ui-text-input-with-label disabled :value="usertype" :label="$strings.LabelAccountType" />
</div> </div>
</div> </div>
<form @submit.prevent="submitDisplayName">
<div class="flex -mx-2 mt-4">
<div class="w-2/3 px-2">
<ui-text-input-with-label v-model="displayNameInput" :label="'Display Name'" />
</div>
<div class="px-2 flex items-end">
<ui-btn type="submit" color="bg-success">Update</ui-btn>
</div>
</div>
</form>
<div class="py-4"> <div class="py-4">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelLanguage }}</p> <p class="px-1 text-sm font-semibold">{{ $strings.LabelLanguage }}</p>
<ui-dropdown v-model="selectedLanguage" :items="$languageCodeOptions" small class="max-w-48" @input="updateLocalLanguage" /> <ui-dropdown v-model="selectedLanguage" :items="$languageCodeOptions" small class="max-w-48" @input="updateLocalLanguage" />
@ -88,6 +98,7 @@ export default {
confirmPassword: null, confirmPassword: null,
changingPassword: false, changingPassword: false,
selectedLanguage: '', selectedLanguage: '',
displayNameInput: '',
newEReaderDevice: { newEReaderDevice: {
name: '', name: '',
email: '' email: ''
@ -103,7 +114,9 @@ export default {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
}, },
user() { user() {
return this.$store.state.user.user || null const user = this.$store.state.user.user || null
console.log('[Account Page] Current user object:', user)
return user
}, },
username() { username() {
return this.user.username return this.user.username
@ -111,6 +124,9 @@ export default {
usertype() { usertype() {
return this.user.type return this.user.type
}, },
displayName() {
return this.user.displayName || ''
},
isRoot() { isRoot() {
return this.usertype === 'root' return this.usertype === 'root'
}, },
@ -237,11 +253,26 @@ export default {
}, },
ereaderDevicesUpdated(ereaderDevices) { ereaderDevicesUpdated(ereaderDevices) {
this.ereaderDevices = ereaderDevices this.ereaderDevices = ereaderDevices
},
async submitDisplayName() {
try {
await this.$axios.$patch(`/api/users/${this.user.id}`, {
displayName: this.displayNameInput
})
this.$toast.success('Display name updated successfully')
// Update the user in the store
const user = { ...this.user, displayName: this.displayNameInput }
this.$store.commit('user/setUser', user)
} catch (error) {
console.error('Failed to update display name:', error)
this.$toast.error('Failed to update display name')
}
} }
}, },
mounted() { mounted() {
this.selectedLanguage = this.$languageCodes.current this.selectedLanguage = this.$languageCodes.current
this.ereaderDevices = this.$store.state.libraries.ereaderDevices || [] this.ereaderDevices = this.$store.state.libraries.ereaderDevices || []
this.displayNameInput = this.displayName
} }
} }
</script> </script>

View file

@ -107,7 +107,7 @@ export default {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
}, },
username() { username() {
return this.user.username return this.user.displayName || this.user.username
}, },
userOnline() { userOnline() {
return this.$store.getters['users/getIsUserOnline'](this.user.id) return this.$store.getters['users/getIsUserOnline'](this.user.id)

View file

@ -92,7 +92,7 @@ export default {
}, },
computed: { computed: {
username() { username() {
return this.user.username return this.user.displayName || this.user.username
}, },
userOnline() { userOnline() {
return this.$store.getters['users/getIsUserOnline'](this.user.id) return this.$store.getters['users/getIsUserOnline'](this.user.id)

View file

@ -189,7 +189,7 @@
<div v-for="comment in sortedComments" :key="comment.id" class="bg-bg border rounded-lg p-4" :class="comment.userId === currentUser.id ? 'border-white border-4' : 'border-gray-700'"> <div v-for="comment in sortedComments" :key="comment.id" class="bg-bg border rounded-lg p-4" :class="comment.userId === currentUser.id ? 'border-white border-4' : 'border-gray-700'">
<div class="flex justify-between items-start mb-2"> <div class="flex justify-between items-start mb-2">
<div> <div>
<nuxt-link :to="'/user/' + comment.userId" class="font-semibold hover:text-white hover:underline">{{ comment.user.username }}</nuxt-link> <nuxt-link :to="'/user/' + comment.userId" class="font-semibold hover:text-white hover:underline">{{ comment.user.displayName || comment.user.username }}</nuxt-link>
<span class="text-sm text-gray-400 ml-2"> <span class="text-sm text-gray-400 ml-2">
{{ formatDate(comment.createdAt) }} {{ formatDate(comment.createdAt) }}
</span> </span>
@ -1012,6 +1012,7 @@ export default {
this.$root.socket.on('episode_download_started', this.episodeDownloadStarted) this.$root.socket.on('episode_download_started', this.episodeDownloadStarted)
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished) this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
this.$root.socket.on('episode_download_queue_cleared', this.episodeDownloadQueueCleared) this.$root.socket.on('episode_download_queue_cleared', this.episodeDownloadQueueCleared)
this.$root.socket.on('user_updated', this.loadComments)
}, },
beforeDestroy() { beforeDestroy() {
this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated) this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
@ -1024,6 +1025,7 @@ export default {
this.$root.socket.off('episode_download_started', this.episodeDownloadStarted) this.$root.socket.off('episode_download_started', this.episodeDownloadStarted)
this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished) this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished)
this.$root.socket.off('episode_download_queue_cleared', this.episodeDownloadQueueCleared) this.$root.socket.off('episode_download_queue_cleared', this.episodeDownloadQueueCleared)
this.$root.socket.off('user_updated', this.loadComments)
} }
} }
</script> </script>

View file

@ -2,7 +2,7 @@
<div class="bg-bg min-h-screen"> <div class="bg-bg min-h-screen">
<div class="max-w-6xl mx-auto px-4 py-8"> <div class="max-w-6xl mx-auto px-4 py-8">
<div v-if="user" class="mb-8"> <div v-if="user" class="mb-8">
<h1 class="text-3xl font-semibold mb-2">{{ user.username }}'s Profile</h1> <h1 class="text-3xl font-semibold mb-2">{{ user.displayName || user.username }}'s Profile</h1>
<p class="text-gray-400">Member since {{ formatDate(user.createdAt) }}</p> <p class="text-gray-400">Member since {{ formatDate(user.createdAt) }}</p>
</div> </div>

View file

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.24.0", "version": "2.24.1",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast server", "description": "Self-hosted audiobook and podcast server",
"main": "index.js", "main": "index.js",

View file

@ -82,7 +82,7 @@ class LibraryItemController {
include: [{ include: [{
model: Database.userModel, model: Database.userModel,
as: 'user', as: 'user',
attributes: ['id', 'username'] attributes: ['id', 'username', 'displayName']
}], }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}) })
@ -1224,7 +1224,7 @@ class LibraryItemController {
include: [{ include: [{
model: Database.userModel, model: Database.userModel,
as: 'user', as: 'user',
attributes: ['id', 'username'] attributes: ['id', 'username', 'displayName']
}], }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}) })
@ -1267,7 +1267,7 @@ class LibraryItemController {
include: [{ include: [{
model: Database.userModel, model: Database.userModel,
as: 'user', as: 'user',
attributes: ['id', 'username'] attributes: ['id', 'username', 'displayName']
}] }]
}) })
@ -1291,7 +1291,7 @@ class LibraryItemController {
include: [{ include: [{
model: Database.userModel, model: Database.userModel,
as: 'user', as: 'user',
attributes: ['id', 'username'] attributes: ['id', 'username', 'displayName']
}] }]
}) })

View file

@ -23,7 +23,11 @@ class MeController {
* @param {Response} res * @param {Response} res
*/ */
getCurrentUser(req, res) { getCurrentUser(req, res) {
res.json(req.user.toOldJSONForBrowser()) const userJson = req.user.toOldJSONForBrowser()
Logger.info('[MeController] getCurrentUser response:', {
user: userJson
})
res.json(userJson)
} }
/** /**

View file

@ -172,6 +172,7 @@ class UserController {
type: userType, type: userType,
username: req.body.username, username: req.body.username,
email: typeof req.body.email === 'string' ? req.body.email : null, email: typeof req.body.email === 'string' ? req.body.email : null,
displayName: typeof req.body.displayName === 'string' ? req.body.displayName : null,
pash, pash,
token, token,
isActive: !!req.body.isActive, isActive: !!req.body.isActive,
@ -203,6 +204,13 @@ class UserController {
* @param {Response} res * @param {Response} res
*/ */
async update(req, res) { async update(req, res) {
Logger.info('[UserController] Update request:', {
params: req.params,
body: req.body,
user: req.user?.username,
reqUser: req.reqUser?.username
})
const user = req.reqUser const user = req.reqUser
if (user.isRoot && !req.user.isRoot) { if (user.isRoot && !req.user.isRoot) {
@ -228,6 +236,9 @@ class UserController {
if (updatePayload.username && typeof updatePayload.username !== 'string') { if (updatePayload.username && typeof updatePayload.username !== 'string') {
return res.status(400).send('Invalid username') return res.status(400).send('Invalid username')
} }
if (updatePayload.displayName && typeof updatePayload.displayName !== 'string') {
return res.status(400).send('Invalid display name')
}
if (updatePayload.type && !Database.userModel.accountTypes.includes(updatePayload.type)) { if (updatePayload.type && !Database.userModel.accountTypes.includes(updatePayload.type)) {
return res.status(400).send('Invalid account type') return res.status(400).send('Invalid account type')
} }
@ -322,6 +333,10 @@ class UserController {
user.lastSeen = updatePayload.lastSeen user.lastSeen = updatePayload.lastSeen
hasUpdates = true hasUpdates = true
} }
if (updatePayload.displayName !== undefined) {
user.displayName = updatePayload.displayName
hasUpdates = true
}
if (hasUpdates) { if (hasUpdates) {
if (shouldUpdateToken) { if (shouldUpdateToken) {
@ -474,6 +489,14 @@ class UserController {
* @param {NextFunction} next * @param {NextFunction} next
*/ */
async middleware(req, res, next) { async middleware(req, res, next) {
Logger.info('[UserController] Middleware request:', {
method: req.method,
path: req.path,
params: req.params,
userId: req.user?.id,
username: req.user?.username
})
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) { if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
return res.sendStatus(403) return res.sendStatus(403)
} else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) { } else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) {
@ -505,7 +528,7 @@ class UserController {
include: [{ include: [{
model: Database.userModel, model: Database.userModel,
as: 'user', as: 'user',
attributes: ['id', 'username'] attributes: ['id', 'username', 'displayName']
}], }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}) })

View file

@ -0,0 +1,22 @@
const { DataTypes } = require('sequelize')
async function up({ context: { queryInterface, logger } }) {
logger.info('[Migration] Adding displayName column to users table')
await queryInterface.addColumn('users', 'displayName', {
type: DataTypes.STRING,
allowNull: true
})
logger.info('[Migration] Successfully added displayName column to users table')
}
async function down({ context: { queryInterface, logger } }) {
logger.info('[Migration] Removing displayName column from users table')
await queryInterface.removeColumn('users', 'displayName')
logger.info('[Migration] Successfully removed displayName column from users table')
}
module.exports = { up, down }

View file

@ -83,7 +83,8 @@ class Comment extends Model {
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
user: this.user ? { user: this.user ? {
id: this.user.id, id: this.user.id,
username: this.user.username username: this.user.username,
displayName: this.user.displayName
} : null } : null
} }
} }

View file

@ -87,6 +87,8 @@ class User extends Model {
/** @type {string} */ /** @type {string} */
this.username this.username
/** @type {string} */ /** @type {string} */
this.displayName
/** @type {string} */
this.email this.email
/** @type {string} */ /** @type {string} */
this.pash this.pash
@ -425,6 +427,7 @@ class User extends Model {
primaryKey: true primaryKey: true
}, },
username: DataTypes.STRING, username: DataTypes.STRING,
displayName: DataTypes.STRING,
email: DataTypes.STRING, email: DataTypes.STRING,
pash: DataTypes.STRING, pash: DataTypes.STRING,
type: DataTypes.STRING, type: DataTypes.STRING,
@ -518,6 +521,7 @@ class User extends Model {
const json = { const json = {
id: this.id, id: this.id,
username: this.username, username: this.username,
displayName: this.displayName,
email: this.email, email: this.email,
type: this.type, type: this.type,
token: this.type === 'root' && hideRootToken ? '' : this.token, token: this.type === 'root' && hideRootToken ? '' : this.token,