mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-11-07 08:34:10 +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">
|
||||
<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 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" />
|
||||
</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,25 +124,22 @@ export default {
|
||||
|
||||
var account = { ...this.newUser }
|
||||
this.processing = true
|
||||
if (this.isNew) {
|
||||
this.$axios
|
||||
.$post('/api/user', account)
|
||||
.then((data) => {
|
||||
this.processing = false
|
||||
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
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to create account', error)
|
||||
this.processing = false
|
||||
this.$axios
|
||||
.$post('/api/user', account)
|
||||
.then((data) => {
|
||||
this.processing = false
|
||||
if (data.error) {
|
||||
this.$toast.error(`Failed to create account: ${data.error}`)
|
||||
} else {
|
||||
this.$toast.success('New account created')
|
||||
})
|
||||
}
|
||||
this.show = false
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to create account', error)
|
||||
this.processing = false
|
||||
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,
|
||||
@ -121,4 +122,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
@ -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