mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Reset password and users table on settings page
This commit is contained in:
		
							parent
							
								
									9331b5870f
								
							
						
					
					
						commit
						f4cb5d101e
					
				| @ -62,3 +62,7 @@ | ||||
|   border-right: 6px solid transparent; | ||||
|   border-top: 6px solid white; | ||||
| } | ||||
| 
 | ||||
| .icon-text { | ||||
|   font-size: 1.1rem; | ||||
| } | ||||
| @ -26,10 +26,11 @@ export default { | ||||
|   data() { | ||||
|     return { | ||||
|       menuItems: [ | ||||
|         // { | ||||
|         //   value: 'settings', | ||||
|         //   text: 'Settings' | ||||
|         // }, | ||||
|         { | ||||
|           value: 'account', | ||||
|           text: 'Account', | ||||
|           to: '/account' | ||||
|         }, | ||||
|         { | ||||
|           value: 'logout', | ||||
|           text: 'Logout' | ||||
| @ -72,8 +73,6 @@ export default { | ||||
|     menuAction(action) { | ||||
|       if (action === 'logout') { | ||||
|         this.logout() | ||||
|       } else if (action === 'settings') { | ||||
|         // Show settings modal | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| @ -83,7 +82,6 @@ export default { | ||||
| 
 | ||||
| <style> | ||||
| #appbar { | ||||
|   /* box-shadow: 0px 8px 8px #111111aa; */ | ||||
|   box-shadow: 0px 5px 5px #11111155; | ||||
| } | ||||
| </style> | ||||
| @ -12,11 +12,18 @@ | ||||
|     <transition name="menu"> | ||||
|       <ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-activedescendant="listbox-option-3"> | ||||
|         <template v-for="item in items"> | ||||
|           <nuxt-link :key="item.value" v-if="item.to" :to="item.to"> | ||||
|             <li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)"> | ||||
|               <div class="flex items-center"> | ||||
|                 <span class="font-normal ml-3 block truncate font-sans">{{ item.text }}</span> | ||||
|               </div> | ||||
|             </li> | ||||
|           </nuxt-link> | ||||
|           <li v-else :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)"> | ||||
|             <div class="flex items-center"> | ||||
|               <span class="font-normal ml-3 block truncate font-sans">{{ item.text }}</span> | ||||
|             </div> | ||||
|           </li> | ||||
|         </template> | ||||
|       </ul> | ||||
|     </transition> | ||||
|  | ||||
							
								
								
									
										92
									
								
								client/pages/account.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								client/pages/account.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,92 @@ | ||||
| <template> | ||||
|   <div class="w-full h-full p-8"> | ||||
|     <div class="w-full max-w-2xl mx-auto"> | ||||
|       <h1 class="text-2xl">Account</h1> | ||||
| 
 | ||||
|       <div class="my-4"> | ||||
|         <div class="flex -mx-2"> | ||||
|           <div class="w-2/3 px-2"> | ||||
|             <ui-text-input-with-label disabled :value="username" label="Username" /> | ||||
|           </div> | ||||
|           <div class="w-1/3 px-2"> | ||||
|             <ui-text-input-with-label disabled :value="usertype" label="Account Type" /> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="w-full h-px bg-primary my-4" /> | ||||
| 
 | ||||
|         <p class="mb-4 text-lg">Change Password</p> | ||||
|         <form @submit.prevent="submitChangePassword"> | ||||
|           <ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" label="Password" class="my-2" /> | ||||
|           <ui-text-input-with-label v-model="newPassword" :disabled="changingPassword" type="password" label="New Password" class="my-2" /> | ||||
|           <ui-text-input-with-label v-model="confirmPassword" :disabled="changingPassword" type="password" label="Confirm Password" class="my-2" /> | ||||
|           <div class="flex items-center py-2"> | ||||
|             <p v-if="isRoot" class="text-error py-2 text-xs">* Root user is the only user that can have an empty password</p> | ||||
|             <div class="flex-grow" /> | ||||
|             <ui-btn type="submit" :loading="changingPassword" color="success">Submit</ui-btn> | ||||
|           </div> | ||||
|         </form> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   data() { | ||||
|     return { | ||||
|       password: null, | ||||
|       newPassword: null, | ||||
|       confirmPassword: null, | ||||
|       changingPassword: false | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     user() { | ||||
|       return this.$store.state.user || null | ||||
|     }, | ||||
|     username() { | ||||
|       return this.user.username | ||||
|     }, | ||||
|     usertype() { | ||||
|       return this.user.type | ||||
|     }, | ||||
|     isRoot() { | ||||
|       return this.usertype === 'root' | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     resetForm() { | ||||
|       this.password = null | ||||
|       this.newPassword = null | ||||
|       this.confirmPassword = null | ||||
|     }, | ||||
|     submitChangePassword() { | ||||
|       if (this.newPassword !== this.confirmPassword) { | ||||
|         return this.$toast.error('New password and confirm password do not match') | ||||
|       } | ||||
|       this.changingPassword = true | ||||
|       this.$axios | ||||
|         .$patch('/api/user/password', { | ||||
|           password: this.password, | ||||
|           newPassword: this.newPassword | ||||
|         }) | ||||
|         .then((res) => { | ||||
|           if (res.success) { | ||||
|             this.$toast.success('Password Changed Successfully') | ||||
|             this.resetForm() | ||||
|           } else { | ||||
|             this.$toast.error(res.error || 'Unknown Error') | ||||
|           } | ||||
|           this.changingPassword = false | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error(error) | ||||
|           this.$toast.error('Api call failed') | ||||
|           this.changingPassword = false | ||||
|         }) | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
| } | ||||
| </script> | ||||
| @ -1,10 +1,29 @@ | ||||
| <template> | ||||
|   <div class="page p-6" :class="streamAudiobook ? 'streaming' : ''"> | ||||
|     <div class="w-full max-w-4xl mx-auto"> | ||||
|       <h1 class="text-2xl mb-2">Config</h1> | ||||
|       <div class="flex items-center mb-2"> | ||||
|         <h1 class="text-2xl">Users</h1> | ||||
|         <div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddUser"> | ||||
|           <span class="material-icons" style="font-size: 1.4rem">add</span> | ||||
|         </div> | ||||
|         <!-- <ui-btn small :padding-x="4" class="h-8">Create User</ui-btn> --> | ||||
|       </div> | ||||
|       <div class="h-0.5 bg-primary bg-opacity-50 w-full" /> | ||||
|       <div class="p-4 text-center h-20"> | ||||
|         <p>Nothing much here yet...</p> | ||||
|       <div class="p-4 text-center"> | ||||
|         <table id="accounts" class="mb-8"> | ||||
|           <tr> | ||||
|             <th>Username</th> | ||||
|             <th>Account Type</th> | ||||
|             <th style="width: 200px">Created At</th> | ||||
|           </tr> | ||||
|           <tr v-for="user in users" :key="user.id"> | ||||
|             <td>{{ user.username }}</td> | ||||
|             <td>{{ user.type }}</td> | ||||
|             <td class="text-sm font-mono"> | ||||
|               {{ new Date(user.createdAt).toISOString() }} | ||||
|             </td> | ||||
|           </tr> | ||||
|         </table> | ||||
|       </div> | ||||
|       <div class="h-0.5 bg-primary bg-opacity-50 w-full" /> | ||||
|       <div class="flex items-center py-4 mb-8"> | ||||
| @ -16,7 +35,7 @@ | ||||
|       <div class="h-0.5 bg-primary bg-opacity-50 w-full" /> | ||||
| 
 | ||||
|       <div class="flex items-center py-4"> | ||||
|         <ui-btn color="error" small :padding-x="4" :loading="isResettingAudiobooks" @click="resetAudiobooks">Reset All Audiobooks</ui-btn> | ||||
|         <ui-btn color="bg" small :padding-x="4" :loading="isResettingAudiobooks" @click="resetAudiobooks">Reset All Audiobooks</ui-btn> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="h-0.5 bg-primary bg-opacity-50 w-full" /> | ||||
| @ -41,7 +60,8 @@ | ||||
| export default { | ||||
|   data() { | ||||
|     return { | ||||
|       isResettingAudiobooks: false | ||||
|       isResettingAudiobooks: false, | ||||
|       users: null | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @ -53,6 +73,19 @@ export default { | ||||
|     scan() { | ||||
|       this.$root.socket.emit('scan') | ||||
|     }, | ||||
|     clickAddUser() { | ||||
|       this.$toast.info('Under Construction: User management coming soon.') | ||||
|     }, | ||||
|     loadUsers() { | ||||
|       this.$axios | ||||
|         .$get('/api/users') | ||||
|         .then((users) => { | ||||
|           this.users = users | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed', error) | ||||
|         }) | ||||
|     }, | ||||
|     resetAudiobooks() { | ||||
|       if (confirm('WARNING! This action will remove all audiobooks from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) { | ||||
|         this.isResettingAudiobooks = true | ||||
| @ -70,6 +103,39 @@ export default { | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
|   mounted() { | ||||
|     this.loadUsers() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style> | ||||
| #accounts { | ||||
|   table-layout: fixed; | ||||
|   border-collapse: collapse; | ||||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| #accounts td, | ||||
| #accounts th { | ||||
|   border: 1px solid #2e2e2e; | ||||
|   padding: 8px 8px; | ||||
|   text-align: left; | ||||
| } | ||||
| 
 | ||||
| #accounts tr:nth-child(even) { | ||||
|   background-color: #3a3a3a; | ||||
| } | ||||
| 
 | ||||
| #accounts tr:hover { | ||||
|   background-color: #444; | ||||
| } | ||||
| 
 | ||||
| #accounts th { | ||||
|   font-size: 0.8rem; | ||||
|   font-weight: 600; | ||||
|   padding-top: 5px; | ||||
|   padding-bottom: 5px; | ||||
|   background-color: #333; | ||||
| } | ||||
| </style> | ||||
| @ -28,7 +28,9 @@ 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.delete('/user/audiobook/:id', this.resetUserAudiobookProgress.bind(this)) | ||||
|     this.router.patch('/user/password', this.userChangePassword.bind(this)) | ||||
| 
 | ||||
|     this.router.post('/authorize', this.authorize.bind(this)) | ||||
| 
 | ||||
| @ -156,6 +158,11 @@ class ApiController { | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   getUsers(req, res) { | ||||
|     if (req.user.type !== 'root') return res.sendStatus(403) | ||||
|     return res.json(this.db.users.map(u => u.toJSONForBrowser())) | ||||
|   } | ||||
| 
 | ||||
|   async resetUserAudiobookProgress(req, res) { | ||||
|     req.user.resetAudiobookProgress(req.params.id) | ||||
|     await this.db.updateEntity('user', req.user) | ||||
| @ -163,6 +170,10 @@ class ApiController { | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   userChangePassword(req, res) { | ||||
|     this.auth.userChangePassword(req, res) | ||||
|   } | ||||
| 
 | ||||
|   getGenres(req, res) { | ||||
|     res.json({ | ||||
|       genres: this.db.getGenres() | ||||
|  | ||||
| @ -114,65 +114,50 @@ class Auth { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async checkAuth(req, res) { | ||||
|     var username = req.body.username | ||||
|     Logger.debug('Check Auth', username, !!req.body.password) | ||||
|   comparePassword(password, user) { | ||||
|     if (user.type === 'root' && !password && !user.pash) return true | ||||
|     if (!password || !user.pash) return false | ||||
|     return bcrypt.compare(password, user.pash) | ||||
|   } | ||||
| 
 | ||||
|     var matchingUser = this.users.find(u => u.username === username) | ||||
|     if (!matchingUser) { | ||||
|   async userChangePassword(req, res) { | ||||
|     var { password, newPassword } = req.body | ||||
|     newPassword = newPassword || '' | ||||
|     var matchingUser = this.users.find(u => u.id === req.user.id) | ||||
| 
 | ||||
|     // Only root can have an empty password
 | ||||
|     if (matchingUser.type !== 'root' && !newPassword) { | ||||
|       return res.json({ | ||||
|         error: 'User not found' | ||||
|         error: 'Invalid new password - Only root can have an empty password' | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     var cleanedUser = { ...matchingUser } | ||||
|     delete cleanedUser.pash | ||||
| 
 | ||||
|     // check for empty password (default)
 | ||||
|     if (!req.body.password) { | ||||
|       if (!matchingUser.pash) { | ||||
|         res.cookie('user', username, { signed: true }) | ||||
|     var compare = await this.comparePassword(password, matchingUser) | ||||
|     if (!compare) { | ||||
|       return res.json({ | ||||
|           user: cleanedUser | ||||
|         }) | ||||
|       } else { | ||||
|         return res.json({ | ||||
|           error: 'Invalid Password' | ||||
|         error: 'Invalid password' | ||||
|       }) | ||||
|     } | ||||
|     } | ||||
| 
 | ||||
|     // Set root password first time
 | ||||
|     if (matchingUser.type === 'root' && !matchingUser.pash && req.body.password && req.body.password.length > 1) { | ||||
|       console.log('Set root pash') | ||||
|       var pw = await this.hashPass(req.body.password) | ||||
|     var pw = '' | ||||
|     if (newPassword) { | ||||
|       pw = await this.hashPass(newPassword) | ||||
|       if (!pw) { | ||||
|         return res.json({ | ||||
|           error: 'Hash failed' | ||||
|         }) | ||||
|       } | ||||
|       this.users = this.users.map(u => { | ||||
|         if (u.username === matchingUser.username) { | ||||
|           u.pash = pw | ||||
|         } | ||||
|         return u | ||||
|       }) | ||||
|       await this.saveAuthDb() | ||||
|       return res.json({ | ||||
|         setroot: true, | ||||
|         user: cleanedUser | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     var compare = await bcrypt.compare(req.body.password, matchingUser.pash) | ||||
|     if (compare) { | ||||
|       res.cookie('user', username, { signed: true }) | ||||
|     matchingUser.pash = pw | ||||
|     var success = await this.db.updateEntity('user', matchingUser) | ||||
|     if (success) { | ||||
|       res.json({ | ||||
|         user: cleanedUser | ||||
|         success: true | ||||
|       }) | ||||
|     } else { | ||||
|       res.json({ | ||||
|         error: 'Invalid Password' | ||||
|         error: 'Unknown error' | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -143,8 +143,10 @@ class Db { | ||||
|       this[arrayKey] = this[arrayKey].map(e => { | ||||
|         return e.id === entity.id ? entity : e | ||||
|       }) | ||||
|       return true | ||||
|     }).catch((error) => { | ||||
|       Logger.error(`[DB] Update entity ${entityName} Failed: ${error}`) | ||||
|       return false | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -3,7 +3,6 @@ const express = require('express') | ||||
| const http = require('http') | ||||
| const SocketIO = require('socket.io') | ||||
| const fs = require('fs-extra') | ||||
| const cookieparser = require('cookie-parser') | ||||
| 
 | ||||
| const Auth = require('./Auth') | ||||
| const Watcher = require('./Watcher') | ||||
| @ -101,7 +100,6 @@ class Server { | ||||
| 
 | ||||
|     this.server = http.createServer(app) | ||||
| 
 | ||||
|     app.use(cookieparser('secret_family_recipe')) | ||||
|     app.use(this.auth.cors) | ||||
| 
 | ||||
|     // Static path to generated nuxt
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user