diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue
index c4625253..4bb130ee 100644
--- a/client/components/app/Appbar.vue
+++ b/client/components/app/Appbar.vue
@@ -30,8 +30,10 @@
{{ isAllSelected ? 'Select None' : 'Select All' }}
- edit
- delete
+
+ edit
+
+ delete
close
@@ -69,6 +71,12 @@ export default {
},
audiobooksShowing() {
return this.$store.getters['audiobooks/getFiltered']()
+ },
+ userCanUpdate() {
+ return this.$store.getters['user/getUserCanUpdate']
+ },
+ userCanDelete() {
+ return this.$store.getters['user/getUserCanDelete']
}
},
methods: {
diff --git a/client/components/cards/BookCard.vue b/client/components/cards/BookCard.vue
index 5446c9d8..cec42de6 100644
--- a/client/components/cards/BookCard.vue
+++ b/client/components/cards/BookCard.vue
@@ -19,13 +19,16 @@
play_circle_filled
-
+
+
edit
-
+
+
{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}
+
@@ -156,6 +159,12 @@ export default {
classes.push('border-2 border-yellow-400')
}
return classes
+ },
+ userCanUpdate() {
+ return this.$store.getters['user/getUserCanUpdate']
+ },
+ userCanDelete() {
+ return this.$store.getters['user/getUserCanDelete']
}
},
methods: {
diff --git a/client/components/controls/OrderSelect.vue b/client/components/controls/OrderSelect.vue
index 43d8af27..9620b476 100644
--- a/client/components/controls/OrderSelect.vue
+++ b/client/components/controls/OrderSelect.vue
@@ -48,6 +48,10 @@ export default {
text: 'Added At',
value: 'addedAt'
},
+ {
+ text: 'Volume #',
+ value: 'book.volumeNumber'
+ },
{
text: 'Duration',
value: 'duration'
diff --git a/client/components/modals/AccountModal.vue b/client/components/modals/AccountModal.vue
index ab27e99d..fa12610a 100644
--- a/client/components/modals/AccountModal.vue
+++ b/client/components/modals/AccountModal.vue
@@ -18,7 +18,7 @@
+
+
+
Permissions
+
+
+
+
+
+
+
Submit
@@ -144,6 +175,13 @@ export default {
toggleActive() {
this.newUser.isActive = !this.newUser.isActive
},
+ userTypeUpdated(type) {
+ this.newUser.permissions = {
+ download: type !== 'guest',
+ update: type === 'admin',
+ delete: type === 'admin'
+ }
+ },
init() {
this.isNew = !this.account
if (this.account) {
@@ -151,14 +189,20 @@ export default {
username: this.account.username,
password: this.account.password,
type: this.account.type,
- isActive: this.account.isActive
+ isActive: this.account.isActive,
+ permissions: { ...this.account.permissions }
}
} else {
this.newUser = {
username: null,
password: null,
type: 'user',
- isActive: true
+ isActive: true,
+ permissions: {
+ download: true,
+ update: false,
+ delete: false
+ }
}
}
}
diff --git a/client/components/modals/EditModal.vue b/client/components/modals/EditModal.vue
index 469dd41b..163d03ac 100644
--- a/client/components/modals/EditModal.vue
+++ b/client/components/modals/EditModal.vue
@@ -6,7 +6,7 @@
@@ -58,6 +58,15 @@ export default {
show: {
handler(newVal) {
if (newVal) {
+ var availableTabIds = this.availableTabs.map((tab) => tab.id)
+ if (!availableTabIds.length) {
+ this.show = false
+ return
+ }
+ if (!availableTabIds.includes(this.selectedTab)) {
+ this.selectedTab = availableTabIds[0]
+ }
+
if (this.audiobook && this.audiobook.id === this.selectedAudiobookId) {
if (this.fetchOnShow) this.fetchFull()
return
@@ -86,6 +95,20 @@ export default {
this.$store.commit('setEditModalTab', val)
}
},
+ userCanUpdate() {
+ return this.$store.getters['user/getUserCanUpdate']
+ },
+ userCanDownload() {
+ return this.$store.getters['user/getUserCanDownload']
+ },
+ availableTabs() {
+ if (!this.userCanUpdate && !this.userCanDownload) return []
+ return this.tabs.filter((tab) => {
+ if ((tab.id === 'download' || tab.id === 'tracks') && this.userCanDownload) return true
+ if (tab.id !== 'download' && tab.id !== 'tracks' && this.userCanUpdate) return true
+ return false
+ })
+ },
height() {
var maxHeightAllowed = window.innerHeight - 150
return Math.min(maxHeightAllowed, 650)
diff --git a/client/components/modals/edit-tabs/Details.vue b/client/components/modals/edit-tabs/Details.vue
index a1797cb1..6542dd62 100644
--- a/client/components/modals/edit-tabs/Details.vue
+++ b/client/components/modals/edit-tabs/Details.vue
@@ -54,7 +54,7 @@
@@ -113,12 +113,9 @@ export default {
book() {
return this.audiobook ? this.audiobook.book || {} : {}
},
- // userAudiobook() {
- // return this.$store.getters['user/getUserAudiobook'](this.audiobookId)
- // },
- // userProgress() {
- // return this.userAudiobook ? this.userAudiobook.progress : 0
- // },
+ userCanDelete() {
+ return this.$store.getters['user/getUserCanDelete']
+ },
genres() {
return this.$store.state.audiobooks.genres
},
diff --git a/client/components/modals/edit-tabs/Tracks.vue b/client/components/modals/edit-tabs/Tracks.vue
index 1a7eaeb1..91066a1a 100644
--- a/client/components/modals/edit-tabs/Tracks.vue
+++ b/client/components/modals/edit-tabs/Tracks.vue
@@ -1,7 +1,7 @@
-
+
Edit Track Order
@@ -11,7 +11,7 @@
Filename |
Size |
Duration |
-
Download |
+
Download |
@@ -27,7 +27,7 @@
{{ $secondsToTimestamp(track.duration) }}
|
-
+ |
download
|
@@ -58,12 +58,18 @@ export default {
}
}
},
- computed: {},
+ computed: {
+ userCanUpdate() {
+ return this.$store.getters['user/getUserCanUpdate']
+ },
+ userCanDownload() {
+ return this.$store.getters['user/getUserCanDownload']
+ }
+ },
methods: {
init() {
this.audioFiles = this.audiobook.audioFiles
this.tracks = this.audiobook.tracks
- console.log('INIT', this.audiobook)
}
}
}
diff --git a/client/components/tables/AudioFilesTable.vue b/client/components/tables/AudioFilesTable.vue
index 8472957d..b1201bd1 100644
--- a/client/components/tables/AudioFilesTable.vue
+++ b/client/components/tables/AudioFilesTable.vue
@@ -4,7 +4,7 @@
Other Audio Files
{{ files.length }}
-
+
Manage Tracks
@@ -56,7 +56,11 @@ export default {
showTracks: false
}
},
- computed: {},
+ computed: {
+ userCanUpdate() {
+ return this.$store.getters['user/getUserCanUpdate']
+ }
+ },
methods: {
clickBar() {
this.showTracks = !this.showTracks
diff --git a/client/components/tables/TracksTable.vue b/client/components/tables/TracksTable.vue
index d63e895c..72bf1d43 100644
--- a/client/components/tables/TracksTable.vue
+++ b/client/components/tables/TracksTable.vue
@@ -4,7 +4,7 @@
Audio Tracks
{{ tracks.length }}
-
+
Manage Tracks
@@ -19,7 +19,7 @@
Filename |
Size |
Duration |
-
Download |
+
Download |
@@ -35,7 +35,7 @@
{{ $secondsToTimestamp(track.duration) }}
|
-
+ |
download
|
@@ -60,7 +60,14 @@ export default {
showTracks: false
}
},
- computed: {},
+ computed: {
+ userCanDownload() {
+ return this.$store.getters['user/getUserCanDownload']
+ },
+ userCanUpdate() {
+ return this.$store.getters['user/getUserCanUpdate']
+ }
+ },
methods: {
clickBar() {
this.showTracks = !this.showTracks
diff --git a/client/package.json b/client/package.json
index 13819c8b..73f21f9d 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
- "version": "1.0.7",
+ "version": "1.0.8",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {
diff --git a/client/pages/audiobook/_id/edit.vue b/client/pages/audiobook/_id/edit.vue
index 58cb7385..a0fa6da7 100644
--- a/client/pages/audiobook/_id/edit.vue
+++ b/client/pages/audiobook/_id/edit.vue
@@ -71,6 +71,9 @@ export default {
if (!store.state.user.user) {
return redirect(`/login?redirect=${route.path}`)
}
+ if (!store.getters['user/getUserCanUpdate']) {
+ return redirect('/?error=unauthorized')
+ }
var audiobook = await app.$axios.$get(`/api/audiobook/${params.id}`).catch((error) => {
console.error('Failed', error)
return false
@@ -82,7 +85,6 @@ export default {
return {
audiobook,
files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : []
- // files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, index: ++index })) : []
}
},
data() {
diff --git a/client/pages/audiobook/_id/index.vue b/client/pages/audiobook/_id/index.vue
index 4f5509ce..bcf2187f 100644
--- a/client/pages/audiobook/_id/index.vue
+++ b/client/pages/audiobook/_id/index.vue
@@ -36,11 +36,11 @@
{{ streaming ? 'Streaming' : 'Play' }}
-
+
-
+
@@ -240,6 +240,15 @@ export default {
},
streaming() {
return this.streamAudiobook && this.streamAudiobook.id === this.audiobookId
+ },
+ userCanUpdate() {
+ return this.$store.getters['user/getUserCanUpdate']
+ },
+ userCanDelete() {
+ return this.$store.getters['user/getUserCanDelete']
+ },
+ userCanDownload() {
+ return this.$store.getters['user/getUserCanDownload']
}
},
methods: {
diff --git a/client/plugins/toast.js b/client/plugins/toast.js
index 8e56f149..24192c9d 100644
--- a/client/plugins/toast.js
+++ b/client/plugins/toast.js
@@ -1,10 +1,10 @@
-import Vue from "vue";
-import Toast from "vue-toastification";
-import "vue-toastification/dist/index.css";
+import Vue from "vue"
+import Toast from "vue-toastification"
+import "vue-toastification/dist/index.css"
const options = {
- hideProgressBar: true
-};
+ hideProgressBar: true,
+ draggable: false
+}
-
-Vue.use(Toast, options);
\ No newline at end of file
+Vue.use(Toast, options)
\ No newline at end of file
diff --git a/client/store/user.js b/client/store/user.js
index 4ed251b0..1500bee4 100644
--- a/client/store/user.js
+++ b/client/store/user.js
@@ -24,6 +24,15 @@ export const getters = {
},
getFilterOrderKey: (state) => {
return Object.values(state.settings).join('-')
+ },
+ getUserCanUpdate: (state) => {
+ return state.user && state.user.permissions ? !!state.user.permissions.update : false
+ },
+ getUserCanDelete: (state) => {
+ return state.user && state.user.permissions ? !!state.user.permissions.delete : false
+ },
+ getUserCanDownload: (state) => {
+ return state.user && state.user.permissions ? !!state.user.permissions.download : false
}
}
diff --git a/package.json b/package.json
index fef9eae2..6b1c0176 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
- "version": "1.0.7",
+ "version": "1.0.8",
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
"main": "index.js",
"scripts": {
diff --git a/server/ApiController.js b/server/ApiController.js
index d29e9c41..e23d2b73 100644
--- a/server/ApiController.js
+++ b/server/ApiController.js
@@ -89,6 +89,10 @@ class ApiController {
}
async deleteAllAudiobooks(req, res) {
+ if (!req.user.isRoot) {
+ Logger.warn('User other than root attempted to delete all audiobooks', req.user)
+ return res.sendStatus(403)
+ }
Logger.info('Removing all Audiobooks')
var success = await this.db.recreateAudiobookDb()
if (success) res.sendStatus(200)
@@ -130,6 +134,10 @@ class ApiController {
}
async deleteAudiobook(req, res) {
+ if (!req.user.canDelete) {
+ Logger.warn('User attempted to delete without permission', req.user)
+ return res.sendStatus(403)
+ }
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
@@ -138,6 +146,10 @@ class ApiController {
}
async batchDeleteAudiobooks(req, res) {
+ if (!req.user.canDelete) {
+ Logger.warn('User attempted to delete without permission', req.user)
+ return res.sendStatus(403)
+ }
var { audiobookIds } = req.body
if (!audiobookIds || !audiobookIds.length) {
return res.sendStatus(500)
@@ -155,6 +167,10 @@ class ApiController {
}
async batchUpdateAudiobooks(req, res) {
+ if (!req.user.canUpdate) {
+ Logger.warn('User attempted to batch update without permission', req.user)
+ return res.sendStatus(403)
+ }
var audiobooks = req.body
if (!audiobooks || !audiobooks.length) {
return res.sendStatus(500)
@@ -185,6 +201,10 @@ class ApiController {
}
async updateAudiobookTracks(req, res) {
+ if (!req.user.canUpdate) {
+ Logger.warn('User attempted to update audiotracks without permission', req.user)
+ return res.sendStatus(403)
+ }
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
var orderedFileData = req.body.orderedFileData
@@ -196,6 +216,10 @@ class ApiController {
}
async updateAudiobook(req, res) {
+ if (!req.user.canUpdate) {
+ Logger.warn('User attempted to update without permission', req.user)
+ return res.sendStatus(403)
+ }
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
var hasUpdates = audiobook.update(req.body)
@@ -276,6 +300,10 @@ class ApiController {
}
async createUser(req, res) {
+ if (!req.user.isRoot) {
+ Logger.warn('Non-root user attempted to create user', req.user)
+ return res.sendStatus(403)
+ }
var account = req.body
account.id = (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
account.pash = await this.auth.hashPass(account.password)
@@ -297,7 +325,7 @@ class ApiController {
}
async updateUser(req, res) {
- if (req.user.type !== 'root') {
+ if (!req.user.isRoot) {
Logger.error('User other than root attempting to update user', req.user)
return res.sendStatus(403)
}
@@ -327,6 +355,10 @@ class ApiController {
}
async deleteUser(req, res) {
+ if (!req.user.isRoot) {
+ Logger.error('User other than root attempting to delete user', req.user)
+ return res.sendStatus(403)
+ }
if (req.params.id === 'root') {
return res.sendStatus(500)
}
@@ -353,6 +385,10 @@ class ApiController {
}
async updateServerSettings(req, res) {
+ if (!req.user.isRoot) {
+ Logger.error('User other than root attempting to update server settings', req.user)
+ return res.sendStatus(403)
+ }
var settingsUpdate = req.body
if (!settingsUpdate || !isObject(settingsUpdate)) {
return res.sendStatus(500)
@@ -368,6 +404,10 @@ class ApiController {
}
async download(req, res) {
+ if (!req.user.canDownload) {
+ Logger.error('User attempting to download without permission', req.user)
+ return res.sendStatus(403)
+ }
var downloadId = req.params.id
Logger.info('Download Request', downloadId)
var download = this.downloadManager.getDownload(downloadId)
diff --git a/server/Db.js b/server/Db.js
index 65d7b5f8..8b877e76 100644
--- a/server/Db.js
+++ b/server/Db.js
@@ -75,11 +75,11 @@ class Db {
async load() {
var p1 = this.audiobooksDb.select(() => true).then((results) => {
this.audiobooks = results.data.map(a => new Audiobook(a))
- Logger.info(`Audiobooks Loaded ${this.audiobooks.length}`)
+ Logger.info(`[DB] Audiobooks Loaded ${this.audiobooks.length}`)
})
var p2 = this.usersDb.select(() => true).then((results) => {
this.users = results.data.map(u => new User(u))
- Logger.info(`Users Loaded ${this.users.length}`)
+ Logger.info(`[DB] Users Loaded ${this.users.length}`)
})
var p3 = this.settingsDb.select(() => true).then((results) => {
if (results.data && results.data.length) {
diff --git a/server/objects/User.js b/server/objects/User.js
index 8e2c6253..790102e4 100644
--- a/server/objects/User.js
+++ b/server/objects/User.js
@@ -11,13 +11,28 @@ class User {
this.isActive = true
this.createdAt = null
this.audiobooks = null
+
this.settings = {}
+ this.permissions = {}
if (user) {
this.construct(user)
}
}
+ get isRoot() {
+ return this.type === 'root'
+ }
+ get canDelete() {
+ return !!this.permissions.delete
+ }
+ get canUpdate() {
+ return !!this.permissions.update
+ }
+ get canDownload() {
+ return !!this.permissions.download
+ }
+
getDefaultUserSettings() {
return {
orderBy: 'book.title',
@@ -28,6 +43,14 @@ class User {
}
}
+ getDefaultUserPermissions() {
+ return {
+ download: true,
+ update: true,
+ delete: this.id === 'root'
+ }
+ }
+
audiobooksToJSON() {
if (!this.audiobooks) return null
var _map = {}
@@ -50,7 +73,8 @@ class User {
audiobooks: this.audiobooksToJSON(),
isActive: this.isActive,
createdAt: this.createdAt,
- settings: this.settings
+ settings: this.settings,
+ permissions: this.permissions
}
}
@@ -64,7 +88,8 @@ class User {
audiobooks: this.audiobooksToJSON(),
isActive: this.isActive,
createdAt: this.createdAt,
- settings: this.settings
+ settings: this.settings,
+ permissions: this.permissions
}
}
@@ -86,10 +111,12 @@ class User {
this.isActive = (user.isActive === undefined || user.id === 'root') ? true : !!user.isActive
this.createdAt = user.createdAt || Date.now()
this.settings = user.settings || this.getDefaultUserSettings()
+ this.permissions = user.permissions || this.getDefaultUserPermissions()
}
update(payload) {
var hasUpdates = false
+ // Update the following keys:
const keysToCheck = ['pash', 'type', 'username', 'isActive']
keysToCheck.forEach((key) => {
if (payload[key] !== undefined) {
@@ -101,6 +128,15 @@ class User {
}
}
})
+ // And update permissions
+ if (payload.permissions) {
+ for (const key in payload.permissions) {
+ if (payload.permissions[key] !== this.permissions[key]) {
+ hasUpdates = true
+ this.permissions[key] = payload.permissions[key]
+ }
+ }
+ }
return hasUpdates
}