mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Editing accounts, change root account username, removed token expiration
This commit is contained in:
		
							parent
							
								
									e534d015be
								
							
						
					
					
						commit
						1f2afe4d92
					
				| @ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <modals-modal v-model="show" :width="800" :height="500" :processing="processing"> | ||||
|   <modals-modal v-model="show" :width="800" :height="'unset'" :processing="processing"> | ||||
|     <template #outer> | ||||
|       <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden"> | ||||
|         <p class="font-book text-3xl text-white truncate">{{ title }}</p> | ||||
| @ -8,18 +8,22 @@ | ||||
|     <form @submit.prevent="submitForm"> | ||||
|       <div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300"> | ||||
|         <div class="w-full p-8"> | ||||
|           <div class="flex py-2"> | ||||
|           <div class="flex py-2 -mx-2"> | ||||
|             <div class="w-1/2 px-2"> | ||||
|               <ui-text-input-with-label v-model="newUser.username" label="Username" class="mx-2" /> | ||||
|             <ui-text-input-with-label v-model="newUser.password" label="Password" type="password" class="mx-2" /> | ||||
|             </div> | ||||
|             <div class="w-1/2 px-2"> | ||||
|               <ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? 'Password' : 'Change Password'" type="password" class="mx-2" /> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="flex py-2"> | ||||
|             <div class="px-2"> | ||||
|               <ui-input-dropdown v-model="newUser.type" label="Account Type" :editable="false" :items="accountTypes" /> | ||||
|               <ui-input-dropdown v-model="newUser.type" label="Account Type" :disabled="isEditingRoot" :editable="false" :items="accountTypes" /> | ||||
|             </div> | ||||
|             <div class="flex-grow" /> | ||||
|             <div class="flex items-center pt-4 px-2"> | ||||
|               <p class="px-3 font-semibold">Is Active</p> | ||||
|               <ui-toggle-switch v-model="newUser.isActive" /> | ||||
|             <div v-show="!isEditingRoot" class="flex items-center pt-4 px-2"> | ||||
|               <p class="px-3 font-semibold" :class="isEditingRoot ? 'text-gray-300' : ''">Is Active</p> | ||||
|               <ui-toggle-switch v-model="newUser.isActive" :disabled="isEditingRoot" /> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="flex pt-4"> | ||||
| @ -68,7 +72,10 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     title() { | ||||
|       return this.isNew ? 'Add New Account' : `Update "${(this.account || {}).username}" Account` | ||||
|       return this.isNew ? 'Add New Account' : `Update Account: ${(this.account || {}).username}` | ||||
|     }, | ||||
|     isEditingRoot() { | ||||
|       return this.account && this.account.type === 'root' | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
| @ -77,6 +84,39 @@ export default { | ||||
|         this.$toast.error('Enter a username') | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       if (this.isNew) { | ||||
|         this.submitCreateAccount() | ||||
|       } else { | ||||
|         this.submitUpdateAccount() | ||||
|       } | ||||
|     }, | ||||
|     submitUpdateAccount() { | ||||
|       var account = { ...this.newUser } | ||||
|       if (!account.password || account.type === 'root') { | ||||
|         delete account.password | ||||
|       } | ||||
|       if (account.type === 'root' && !account.isActive) return | ||||
| 
 | ||||
|       this.processing = true | ||||
|       this.$axios | ||||
|         .$patch(`/api/user/${this.account.id}`, account) | ||||
|         .then((data) => { | ||||
|           this.processing = false | ||||
|           if (data.error) { | ||||
|             this.$toast.error(`Failed to update account: ${data.error}`) | ||||
|           } else { | ||||
|             this.$toast.success('Account updated') | ||||
|             this.show = false | ||||
|           } | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to update account', error) | ||||
|           this.processing = false | ||||
|           this.$toast.error('Failed to update account') | ||||
|         }) | ||||
|     }, | ||||
|     submitCreateAccount() { | ||||
|       if (!this.newUser.password) { | ||||
|         this.$toast.error('Must have a password, only root user can have an empty password') | ||||
|         return | ||||
| @ -84,7 +124,6 @@ export default { | ||||
| 
 | ||||
|       var account = { ...this.newUser } | ||||
|       this.processing = true | ||||
|       if (this.isNew) { | ||||
|       this.$axios | ||||
|         .$post('/api/user', account) | ||||
|         .then((data) => { | ||||
| @ -92,7 +131,6 @@ export default { | ||||
|           if (data.error) { | ||||
|             this.$toast.error(`Failed to create account: ${data.error}`) | ||||
|           } else { | ||||
|               console.log('New Account:', data.user) | ||||
|             this.$toast.success('New account created') | ||||
|             this.show = false | ||||
|           } | ||||
| @ -100,9 +138,8 @@ export default { | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to create account', error) | ||||
|           this.processing = false | ||||
|             this.$toast.success('New account created') | ||||
|           this.$toast.error('Failed to create account') | ||||
|         }) | ||||
|       } | ||||
|     }, | ||||
|     toggleActive() { | ||||
|       this.newUser.isActive = !this.newUser.isActive | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| <template> | ||||
|   <div class="w-full"> | ||||
|     <p class="px-1 text-sm font-semibold">{{ label }}</p> | ||||
|   <div class="w-full" :class="disabled ? 'cursor-not-allowed' : ''"> | ||||
|     <p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p> | ||||
|     <div ref="wrapper" class="relative"> | ||||
|       <form @submit.prevent="submitForm"> | ||||
|         <div ref="inputWrapper" class="flex-wrap relative w-full shadow-sm flex items-center bg-primary border border-gray-600 rounded px-2 py-2"> | ||||
|           <input ref="input" v-model="textInput" :readonly="!editable" class="h-full w-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" /> | ||||
|         <div ref="inputWrapper" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'bg-bg pointer-events-none text-gray-400' : 'bg-primary'"> | ||||
|           <input ref="input" v-model="textInput" :disabled="disabled" :readonly="!editable" class="h-full w-full bg-transparent focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" /> | ||||
|         </div> | ||||
|       </form> | ||||
| 
 | ||||
| @ -33,6 +33,7 @@ | ||||
| export default { | ||||
|   props: { | ||||
|     value: [String, Number], | ||||
|     disabled: Boolean, | ||||
|     label: String, | ||||
|     items: { | ||||
|       type: Array, | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <div class="border rounded-full border-black-100 flex items-center cursor-pointer w-12 justify-end" :class="toggleColor" @click="clickToggle"> | ||||
|       <span class="rounded-full border w-6 h-6 border-black-50 bg-white shadow transform transition-transform duration-100" :class="!toggleValue ? '-translate-x-6' : ''"> </span> | ||||
|     <div class="border rounded-full border-black-100 flex items-center cursor-pointer w-12 justify-start" :class="className" @click="clickToggle"> | ||||
|       <span class="rounded-full border w-6 h-6 border-black-50 shadow transform transition-transform duration-100" :class="switchClassName"></span> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @ -17,7 +17,8 @@ export default { | ||||
|     offColor: { | ||||
|       type: String, | ||||
|       default: 'primary' | ||||
|     } | ||||
|     }, | ||||
|     disabled: Boolean | ||||
|   }, | ||||
|   computed: { | ||||
|     toggleValue: { | ||||
| @ -28,12 +29,18 @@ export default { | ||||
|         this.$emit('input', val) | ||||
|       } | ||||
|     }, | ||||
|     toggleColor() { | ||||
|     className() { | ||||
|       if (this.disabled) return 'bg-bg cursor-not-allowed' | ||||
|       return this.toggleValue ? `bg-${this.onColor}` : `bg-${this.offColor}` | ||||
|     }, | ||||
|     switchClassName() { | ||||
|       var bgColor = this.disabled ? 'bg-gray-300' : 'bg-white' | ||||
|       return this.toggleValue ? 'translate-x-6 ' + bgColor : bgColor | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     clickToggle() { | ||||
|       if (this.disabled) return | ||||
|       this.toggleValue = !this.toggleValue | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "1.0.4", | ||||
|   "version": "1.0.5", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -27,6 +27,7 @@ | ||||
|             </td> | ||||
|             <td> | ||||
|               <div class="w-full flex justify-center"> | ||||
|                 <span class="material-icons hover:text-gray-400 cursor-pointer text-base pr-2" @click="editUser(user)">edit</span> | ||||
|                 <span v-show="user.type !== 'root'" class="material-icons text-base hover:text-error cursor-pointer" @click="deleteUserClick(user)">delete</span> | ||||
|               </div> | ||||
|             </td> | ||||
| @ -76,7 +77,7 @@ | ||||
|     </div> | ||||
|     <div class="fixed bottom-0 left-0 w-10 h-10" @dblclick="setDeveloperMode"></div> | ||||
| 
 | ||||
|     <modals-account-modal v-model="showAccountModal" /> | ||||
|     <modals-account-modal v-model="showAccountModal" :account="selectedAccount" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -91,6 +92,7 @@ export default { | ||||
|     return { | ||||
|       isResettingAudiobooks: false, | ||||
|       users: [], | ||||
|       selectedAccount: null, | ||||
|       showAccountModal: false, | ||||
|       isDeletingUser: false, | ||||
|       newServerSettings: {} | ||||
| @ -145,10 +147,6 @@ export default { | ||||
|     scanCovers() { | ||||
|       this.$root.socket.emit('scan_covers') | ||||
|     }, | ||||
|     clickAddUser() { | ||||
|       this.showAccountModal = true | ||||
|       // this.$toast.info('Under Construction: User management coming soon.') | ||||
|     }, | ||||
|     loadUsers() { | ||||
|       this.$axios | ||||
|         .$get('/api/users') | ||||
| @ -175,6 +173,14 @@ export default { | ||||
|           }) | ||||
|       } | ||||
|     }, | ||||
|     clickAddUser() { | ||||
|       this.selectedAccount = null | ||||
|       this.showAccountModal = true | ||||
|     }, | ||||
|     editUser(user) { | ||||
|       this.selectedAccount = user | ||||
|       this.showAccountModal = true | ||||
|     }, | ||||
|     deleteUserClick(user) { | ||||
|       if (this.isDeletingUser) return | ||||
|       if (confirm(`Are you sure you want to permanently delete user "${user.username}"?`)) { | ||||
| @ -198,7 +204,7 @@ export default { | ||||
|     }, | ||||
|     addUpdateUser(user) { | ||||
|       if (!this.users) return | ||||
|       var index = this.users.find((u) => u.id === user.id) | ||||
|       var index = this.users.findIndex((u) => u.id === user.id) | ||||
|       if (index >= 0) { | ||||
|         this.users.splice(index, 1, user) | ||||
|       } else { | ||||
|  | ||||
| @ -27,7 +27,7 @@ export default { | ||||
|     return { | ||||
|       error: null, | ||||
|       processing: false, | ||||
|       username: 'root', | ||||
|       username: '', | ||||
|       password: null | ||||
|     } | ||||
|   }, | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| export default function ({ $axios, store }) { | ||||
|   $axios.onRequest(config => { | ||||
|     // console.log('Making request to ' + config.url)
 | ||||
|     if (config.url.startsWith('http:') || config.url.startsWith('https:')) { | ||||
|       return | ||||
|     } | ||||
| @ -11,6 +10,7 @@ export default function ({ $axios, store }) { | ||||
| 
 | ||||
|     if (process.env.NODE_ENV === 'development') { | ||||
|       config.url = `/dev${config.url}` | ||||
|       console.log('Making request to ' + config.url) | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "1.0.4", | ||||
|   "version": "1.0.5", | ||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks.", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -4,7 +4,7 @@ const User = require('./objects/User') | ||||
| const { isObject } = require('./utils/index') | ||||
| 
 | ||||
| class ApiController { | ||||
|   constructor(db, scanner, auth, streamManager, rssFeeds, downloadManager, emitter) { | ||||
|   constructor(db, scanner, auth, streamManager, rssFeeds, downloadManager, emitter, clientEmitter) { | ||||
|     this.db = db | ||||
|     this.scanner = scanner | ||||
|     this.auth = auth | ||||
| @ -12,6 +12,7 @@ class ApiController { | ||||
|     this.rssFeeds = rssFeeds | ||||
|     this.downloadManager = downloadManager | ||||
|     this.emitter = emitter | ||||
|     this.clientEmitter = clientEmitter | ||||
| 
 | ||||
|     this.router = express() | ||||
|     this.init() | ||||
| @ -34,12 +35,13 @@ class ApiController { | ||||
|     this.router.get('/metadata/:id/:trackIndex', this.getMetadata.bind(this)) | ||||
|     this.router.patch('/match/:id', this.match.bind(this)) | ||||
| 
 | ||||
|     this.router.get('/users', this.getUsers.bind(this)) | ||||
|     this.router.post('/user', this.createUser.bind(this)) | ||||
|     this.router.delete('/user/:id', this.deleteUser.bind(this)) | ||||
|     this.router.delete('/user/audiobook/:id', this.resetUserAudiobookProgress.bind(this)) | ||||
|     this.router.patch('/user/password', this.userChangePassword.bind(this)) | ||||
|     this.router.patch('/user/settings', this.userUpdateSettings.bind(this)) | ||||
|     this.router.get('/users', this.getUsers.bind(this)) | ||||
|     this.router.post('/user', this.createUser.bind(this)) | ||||
|     this.router.patch('/user/:id', this.updateUser.bind(this)) | ||||
|     this.router.delete('/user/:id', this.deleteUser.bind(this)) | ||||
| 
 | ||||
|     this.router.patch('/serverSettings', this.updateServerSettings.bind(this)) | ||||
| 
 | ||||
| @ -273,7 +275,7 @@ class ApiController { | ||||
|     var newUser = new User(account) | ||||
|     var success = await this.db.insertUser(newUser) | ||||
|     if (success) { | ||||
|       this.emitter('user_added', newUser) | ||||
|       this.clientEmitter(req.user.id, 'user_added', newUser) | ||||
|       res.json({ | ||||
|         user: newUser.toJSONForBrowser() | ||||
|       }) | ||||
| @ -284,6 +286,36 @@ class ApiController { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async updateUser(req, res) { | ||||
|     if (req.user.type !== 'root') { | ||||
|       Logger.error('User other than root attempting to update user', req.user) | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     var user = this.db.users.find(u => u.id === req.params.id) | ||||
|     if (!user) { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| 
 | ||||
|     var account = req.body | ||||
|     // Updating password
 | ||||
|     if (account.password) { | ||||
|       account.pash = await this.auth.hashPass(account.password) | ||||
|       delete account.password | ||||
|     } | ||||
| 
 | ||||
|     var hasUpdated = user.update(account) | ||||
|     if (hasUpdated) { | ||||
|       await this.db.updateEntity('user', user) | ||||
|     } | ||||
| 
 | ||||
|     this.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser()) | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       user: user.toJSONForBrowser() | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async deleteUser(req, res) { | ||||
|     if (req.params.id === 'root') { | ||||
|       return res.sendStatus(500) | ||||
| @ -304,7 +336,7 @@ class ApiController { | ||||
| 
 | ||||
|     var userJson = user.toJSONForBrowser() | ||||
|     await this.db.removeEntity('user', user.id) | ||||
|     this.emitter('user_removed', userJson) | ||||
|     this.clientEmitter(req.user.id, 'user_removed', userJson) | ||||
|     res.json({ | ||||
|       success: true | ||||
|     }) | ||||
|  | ||||
| @ -68,7 +68,7 @@ class Auth { | ||||
|   } | ||||
| 
 | ||||
|   generateAccessToken(payload) { | ||||
|     return jwt.sign(payload, process.env.TOKEN_SECRET, { expiresIn: '1800s' }); | ||||
|     return jwt.sign(payload, process.env.TOKEN_SECRET); | ||||
|   } | ||||
| 
 | ||||
|   verifyToken(token) { | ||||
|  | ||||
| @ -94,7 +94,7 @@ class Db { | ||||
|   } | ||||
| 
 | ||||
|   insertSettings(settings) { | ||||
|     return this.settingsDb.insert(settings).then((results) => { | ||||
|     return this.settingsDb.insert([settings]).then((results) => { | ||||
|       Logger.debug(`[DB] Inserted ${results.inserted} settings`) | ||||
|       this.settings = this.settings.concat(settings) | ||||
|     }).catch((error) => { | ||||
|  | ||||
| @ -34,7 +34,7 @@ class Server { | ||||
|     this.streamManager = new StreamManager(this.db, this.MetadataPath) | ||||
|     this.rssFeeds = new RssFeeds(this.Port, this.db) | ||||
|     this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.emitter.bind(this)) | ||||
|     this.apiController = new ApiController(this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.emitter.bind(this)) | ||||
|     this.apiController = new ApiController(this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.emitter.bind(this), this.clientEmitter.bind(this)) | ||||
|     this.hlsController = new HlsController(this.db, this.scanner, this.auth, this.streamManager, this.emitter.bind(this), this.MetadataPath) | ||||
| 
 | ||||
|     this.server = null | ||||
| @ -54,11 +54,27 @@ class Server { | ||||
|     return this.db.serverSettings | ||||
|   } | ||||
| 
 | ||||
|   getClientsForUser(userId) { | ||||
|     return Object.values(this.clients).filter(c => c.user && c.user.id === userId) | ||||
|   } | ||||
| 
 | ||||
|   emitter(ev, data) { | ||||
|     // Logger.debug('EMITTER', ev)
 | ||||
|     this.io.emit(ev, data) | ||||
|   } | ||||
| 
 | ||||
|   clientEmitter(userId, ev, data) { | ||||
|     var clients = this.getClientsForUser(userId) | ||||
|     if (!clients.length) { | ||||
|       return Logger.error(`[Server] clientEmitter - no clients found for user ${userId}`) | ||||
|     } | ||||
|     clients.forEach((client) => { | ||||
|       if (client.socket) { | ||||
|         client.socket.emit(ev, data) | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async fileAddedUpdated({ path, fullPath }) { } | ||||
|   async fileRemoved({ path, fullPath }) { } | ||||
| 
 | ||||
|  | ||||
| @ -68,6 +68,22 @@ class User { | ||||
|     this.settings = user.settings || this.getDefaultUserSettings() | ||||
|   } | ||||
| 
 | ||||
|   update(payload) { | ||||
|     var hasUpdates = false | ||||
|     const keysToCheck = ['pash', 'type', 'username', 'isActive'] | ||||
|     keysToCheck.forEach((key) => { | ||||
|       if (payload[key] !== undefined) { | ||||
|         if (key === 'isActive' || payload[key]) { // pash, type, username must evaluate to true (cannot be null or empty)
 | ||||
|           if (payload[key] !== this[key]) { | ||||
|             hasUpdates = true | ||||
|             this[key] = payload[key] | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|     return hasUpdates | ||||
|   } | ||||
| 
 | ||||
|   updateAudiobookProgress(stream) { | ||||
|     if (!this.audiobooks) this.audiobooks = {} | ||||
|     if (!this.audiobooks[stream.audiobookId]) { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user