mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Fix:User permissions for collection API routes and UI #951
This commit is contained in:
		
							parent
							
								
									e362456895
								
							
						
					
					
						commit
						8ec4bd4279
					
				| @ -386,14 +386,14 @@ export default { | ||||
|           { | ||||
|             func: 'toggleFinished', | ||||
|             text: `Mark as ${this.itemIsFinished ? 'Not Finished' : 'Finished'}` | ||||
|           }, | ||||
|           { | ||||
|             func: 'openCollections', | ||||
|             text: 'Add to Collection' | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|       if (this.userCanUpdate) { | ||||
|         items.push({ | ||||
|           func: 'openCollections', | ||||
|           text: 'Add to Collection' | ||||
|         }) | ||||
|         items.push({ | ||||
|           func: 'showEditModalFiles', | ||||
|           text: 'Files' | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
|     <div class="w-full h-full bg-primary relative rounded overflow-hidden"> | ||||
|       <covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" /> | ||||
|     </div> | ||||
|     <div v-show="isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none"> | ||||
|     <div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none"> | ||||
|       <div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit"> | ||||
|         <span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span> | ||||
|       </div> | ||||
| @ -69,6 +69,9 @@ export default { | ||||
|     isAlternativeBookshelfView() { | ||||
|       const constants = this.$constants || this.$nuxt.$constants | ||||
|       return this.bookshelfView == constants.BookshelfView.TITLES | ||||
|     }, | ||||
|     userCanUpdate() { | ||||
|       return this.store.getters['user/getUserCanUpdate'] | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -20,7 +20,7 @@ | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="absolute bottom-0 left-0 right-0 w-full py-2 px-4 flex"> | ||||
|             <ui-btn small color="error" type="button" @click.stop="removeClick">Remove</ui-btn> | ||||
|             <ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">Remove</ui-btn> | ||||
|             <div class="flex-grow" /> | ||||
|             <ui-btn color="success" type="submit">Save</ui-btn> | ||||
|           </div> | ||||
| @ -85,6 +85,9 @@ export default { | ||||
|     }, | ||||
|     books() { | ||||
|       return this.collection.books || [] | ||||
|     }, | ||||
|     userCanDelete() { | ||||
|       return this.$store.getters['user/getUserCanDelete'] | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -27,15 +27,15 @@ | ||||
|         <span class="material-icons text-lg text-white text-opacity-70 hover:text-opacity-100 cursor-pointer">radio_button_unchecked</span> | ||||
|       </div> --> | ||||
|     </div> | ||||
|     <div class="w-40 absolute top-0 -right-24 h-full transform transition-transform" :class="!isHovering ? 'translate-x-0' : '-translate-x-24'"> | ||||
|     <div class="w-40 absolute top-0 -right-24 h-full transform transition-transform" :class="!isHovering ? 'translate-x-0' : translateDistance"> | ||||
|       <div class="flex h-full items-center"> | ||||
|         <ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top"> | ||||
|           <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" /> | ||||
|         </ui-tooltip> | ||||
|         <div class="mx-1" :class="isHovering ? '' : 'ml-6'"> | ||||
|         <div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'"> | ||||
|           <ui-icon-btn icon="edit" borderless @click="clickEdit" /> | ||||
|         </div> | ||||
|         <div class="mx-1"> | ||||
|         <div v-if="userCanDelete" class="mx-1"> | ||||
|           <ui-icon-btn icon="close" borderless @click="removeClick" /> | ||||
|         </div> | ||||
|       </div> | ||||
| @ -71,6 +71,11 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     translateDistance() { | ||||
|       if (!this.userCanUpdate && !this.userCanDelete) return 'translate-x-0' | ||||
|       else if (!this.userCanUpdate || !this.userCanDelete) return '-translate-x-12' | ||||
|       return '-translate-x-24' | ||||
|     }, | ||||
|     media() { | ||||
|       return this.book.media || {} | ||||
|     }, | ||||
| @ -113,6 +118,12 @@ export default { | ||||
|     coverWidth() { | ||||
|       if (this.bookCoverAspectRatio === 1) return this.coverSize * 1.6 | ||||
|       return this.coverSize | ||||
|     }, | ||||
|     userCanUpdate() { | ||||
|       return this.$store.getters['user/getUserCanUpdate'] | ||||
|     }, | ||||
|     userCanDelete() { | ||||
|       return this.$store.getters['user/getUserCanDelete'] | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -19,9 +19,9 @@ | ||||
|               {{ streaming ? 'Streaming' : 'Play' }} | ||||
|             </ui-btn> | ||||
| 
 | ||||
|             <ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" /> | ||||
|             <ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" /> | ||||
| 
 | ||||
|             <ui-icon-btn icon="delete" class="mx-0.5" @click="removeClick" /> | ||||
|             <ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="my-8 max-w-2xl"> | ||||
| @ -92,6 +92,12 @@ export default { | ||||
|     }, | ||||
|     showPlayButton() { | ||||
|       return this.playableBooks.length | ||||
|     }, | ||||
|     userCanUpdate() { | ||||
|       return this.$store.getters['user/getUserCanUpdate'] | ||||
|     }, | ||||
|     userCanDelete() { | ||||
|       return this.$store.getters['user/getUserCanDelete'] | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -150,7 +150,7 @@ | ||||
|               <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" /> | ||||
|             </ui-tooltip> | ||||
| 
 | ||||
|             <ui-tooltip v-if="!isPodcast" text="Collections" direction="top"> | ||||
|             <ui-tooltip v-if="!isPodcast && userCanUpdate" text="Collections" direction="top"> | ||||
|               <ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" /> | ||||
|             </ui-tooltip> | ||||
| 
 | ||||
|  | ||||
| @ -24,18 +24,11 @@ class CollectionController { | ||||
|   } | ||||
| 
 | ||||
|   findOne(req, res) { | ||||
|     var collection = this.db.collections.find(c => c.id === req.params.id) | ||||
|     if (!collection) { | ||||
|       return res.status(404).send('Collection not found') | ||||
|     } | ||||
|     res.json(collection.toJSONExpanded(this.db.libraryItems)) | ||||
|     res.json(req.collection.toJSONExpanded(this.db.libraryItems)) | ||||
|   } | ||||
| 
 | ||||
|   async update(req, res) { | ||||
|     var collection = this.db.collections.find(c => c.id === req.params.id) | ||||
|     if (!collection) { | ||||
|       return res.status(404).send('Collection not found') | ||||
|     } | ||||
|     const collection = req.collection | ||||
|     var wasUpdated = collection.update(req.body) | ||||
|     var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
|     if (wasUpdated) { | ||||
| @ -46,10 +39,7 @@ class CollectionController { | ||||
|   } | ||||
| 
 | ||||
|   async delete(req, res) { | ||||
|     var collection = this.db.collections.find(c => c.id === req.params.id) | ||||
|     if (!collection) { | ||||
|       return res.status(404).send('Collection not found') | ||||
|     } | ||||
|     const collection = req.collection | ||||
|     var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
|     await this.db.removeEntity('collection', collection.id) | ||||
|     this.emitter('collection_removed', jsonExpanded) | ||||
| @ -57,10 +47,7 @@ class CollectionController { | ||||
|   } | ||||
| 
 | ||||
|   async addBook(req, res) { | ||||
|     var collection = this.db.collections.find(c => c.id === req.params.id) | ||||
|     if (!collection) { | ||||
|       return res.status(404).send('Collection not found') | ||||
|     } | ||||
|     const collection = req.collection | ||||
|     var libraryItem = this.db.libraryItems.find(li => li.id === req.body.id) | ||||
|     if (!libraryItem) { | ||||
|       return res.status(500).send('Book not found') | ||||
| @ -80,11 +67,7 @@ class CollectionController { | ||||
| 
 | ||||
|   // DELETE: api/collections/:id/book/:bookId
 | ||||
|   async removeBook(req, res) { | ||||
|     var collection = this.db.collections.find(c => c.id === req.params.id) | ||||
|     if (!collection) { | ||||
|       return res.status(404).send('Collection not found') | ||||
|     } | ||||
| 
 | ||||
|     const collection = req.collection | ||||
|     if (collection.books.includes(req.params.bookId)) { | ||||
|       collection.removeBook(req.params.bookId) | ||||
|       var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
| @ -96,10 +79,7 @@ class CollectionController { | ||||
| 
 | ||||
|   // POST: api/collections/:id/batch/add
 | ||||
|   async addBatch(req, res) { | ||||
|     var collection = this.db.collections.find(c => c.id === req.params.id) | ||||
|     if (!collection) { | ||||
|       return res.status(404).send('Collection not found') | ||||
|     } | ||||
|     const collection = req.collection | ||||
|     if (!req.body.books || !req.body.books.length) { | ||||
|       return res.status(500).send('Invalid request body') | ||||
|     } | ||||
| @ -120,10 +100,7 @@ class CollectionController { | ||||
| 
 | ||||
|   // POST: api/collections/:id/batch/remove
 | ||||
|   async removeBatch(req, res) { | ||||
|     var collection = this.db.collections.find(c => c.id === req.params.id) | ||||
|     if (!collection) { | ||||
|       return res.status(404).send('Collection not found') | ||||
|     } | ||||
|     const collection = req.collection | ||||
|     if (!req.body.books || !req.body.books.length) { | ||||
|       return res.status(500).send('Invalid request body') | ||||
|     } | ||||
| @ -141,5 +118,25 @@ class CollectionController { | ||||
|     } | ||||
|     res.json(collection.toJSONExpanded(this.db.libraryItems)) | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     if (req.params.id) { | ||||
|       var collection = this.db.collections.find(c => c.id === req.params.id) | ||||
|       if (!collection) { | ||||
|         return res.status(404).send('Collection not found') | ||||
|       } | ||||
|       req.collection = collection | ||||
|     } | ||||
| 
 | ||||
|     if (req.method == 'DELETE' && !req.user.canDelete) { | ||||
|       Logger.warn(`[CollectionController] User attempted to delete without permission`, req.user.username) | ||||
|       return res.sendStatus(403) | ||||
|     } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { | ||||
|       Logger.warn('[CollectionController] User attempted to update without permission', req.user.username) | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     next() | ||||
|   } | ||||
| } | ||||
| module.exports = new CollectionController() | ||||
| @ -116,16 +116,16 @@ class ApiRouter { | ||||
|     //
 | ||||
|     // Collection Routes
 | ||||
|     //
 | ||||
|     this.router.post('/collections', CollectionController.create.bind(this)) | ||||
|     this.router.post('/collections', CollectionController.middleware.bind(this), CollectionController.create.bind(this)) | ||||
|     this.router.get('/collections', CollectionController.findAll.bind(this)) | ||||
|     this.router.get('/collections/:id', CollectionController.findOne.bind(this)) | ||||
|     this.router.patch('/collections/:id', CollectionController.update.bind(this)) | ||||
|     this.router.delete('/collections/:id', CollectionController.delete.bind(this)) | ||||
|     this.router.get('/collections/:id', CollectionController.middleware.bind(this), CollectionController.findOne.bind(this)) | ||||
|     this.router.patch('/collections/:id', CollectionController.middleware.bind(this), CollectionController.update.bind(this)) | ||||
|     this.router.delete('/collections/:id', CollectionController.middleware.bind(this), CollectionController.delete.bind(this)) | ||||
| 
 | ||||
|     this.router.post('/collections/:id/book', CollectionController.addBook.bind(this)) | ||||
|     this.router.delete('/collections/:id/book/:bookId', CollectionController.removeBook.bind(this)) | ||||
|     this.router.post('/collections/:id/batch/add', CollectionController.addBatch.bind(this)) | ||||
|     this.router.post('/collections/:id/batch/remove', CollectionController.removeBatch.bind(this)) | ||||
|     this.router.post('/collections/:id/book', CollectionController.middleware.bind(this), CollectionController.addBook.bind(this)) | ||||
|     this.router.delete('/collections/:id/book/:bookId', CollectionController.middleware.bind(this), CollectionController.removeBook.bind(this)) | ||||
|     this.router.post('/collections/:id/batch/add', CollectionController.middleware.bind(this), CollectionController.addBatch.bind(this)) | ||||
|     this.router.post('/collections/:id/batch/remove', CollectionController.middleware.bind(this), CollectionController.removeBatch.bind(this)) | ||||
| 
 | ||||
|     //
 | ||||
|     // Current User Routes (Me)
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user