Editing accounts, change root account username, removed token expiration

This commit is contained in:
Mark Cooper 2021-09-05 18:20:29 -05:00
parent e534d015be
commit 1f2afe4d92
13 changed files with 170 additions and 55 deletions

View File

@ -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

View File

@ -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>

View File

@ -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
}
}

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.0.4",
"version": "1.0.5",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {

View File

@ -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 {

View File

@ -27,7 +27,7 @@ export default {
return {
error: null,
processing: false,
username: 'root',
username: '',
password: null
}
},

View File

@ -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)
}
})

View File

@ -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": {

View File

@ -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
})

View File

@ -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) {

View File

@ -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) => {

View File

@ -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 }) { }

View File

@ -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]) {