mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-10-27 11:18:14 +01:00
This change extends OIDC authentication by enabling explicit redirection to the OAuth provider when navigating to the login page with the manual override parameter (/login?autoLaunch=1). Use case: directly launch audiobookshelf from within e.g. Nextcloud using the external sites app (use something like https://abs.example.org/login?autoLaunch=1 as URL) while keeping the possibility to launch audiobookshelf using its built-in authentication mechanism. Assuming the username or mail address used in Nextcloud and audiobookshelf are identical the user will be logged in to his or her account no matter which method is used.
326 lines
12 KiB
Vue
326 lines
12 KiB
Vue
<template>
|
|
<div id="page-wrapper" class="w-full h-screen overflow-y-auto">
|
|
<div class="absolute z-0 top-0 left-0 px-6 py-3">
|
|
<div class="flex items-center">
|
|
<img src="~static/icon.svg" alt="Audiobookshelf Logo" class="w-10 min-w-10 h-10" />
|
|
<h1 class="text-xl ml-4 hidden lg:block hover:underline">audiobookshelf</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative z-10 w-full flex h-full items-center justify-center">
|
|
<div v-if="criticalError" class="w-full max-w-md rounded-sm border border-error/25 bg-error/10 p-4">
|
|
<p class="text-center text-lg font-semibold">{{ $strings.MessageServerCouldNotBeReached }}</p>
|
|
</div>
|
|
<div v-else-if="showInitScreen" class="w-full max-w-lg px-4 md:px-8 pb-8 pt-4">
|
|
<p class="text-3xl text-white text-center mb-4">Initial Server Setup</p>
|
|
<div class="w-full h-px bg-white/10 my-4" />
|
|
|
|
<form @submit.prevent="submitServerSetup">
|
|
<p class="text-lg font-semibold mb-2 pl-1 text-center">Create Root User</p>
|
|
<ui-text-input-with-label v-model.trim="newRoot.username" label="Username" :disabled="processing" class="w-full mb-3 text-sm" />
|
|
<ui-text-input-with-label v-model="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
|
<ui-text-input-with-label v-model="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
|
|
|
<p class="text-lg font-semibold mt-6 mb-2 pl-1 text-center">Directory Paths</p>
|
|
<ui-text-input-with-label v-model="ConfigPath" label="Config Path" disabled class="w-full mb-3 text-sm" />
|
|
<ui-text-input-with-label v-model="MetadataPath" label="Metadata Path" disabled class="w-full mb-3 text-sm" />
|
|
|
|
<div class="w-full flex justify-end py-3">
|
|
<ui-btn type="submit" :disabled="processing" color="bg-primary" class="leading-none">{{ processing ? 'Initializing...' : $strings.ButtonSubmit }}</ui-btn>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 lg:-mt-40">
|
|
<div class="bg-bg rounded-md shadow-lg border border-white/5 p-4">
|
|
<p class="text-2xl font-semibold text-center text-white mb-4">{{ $strings.HeaderLogin }}</p>
|
|
|
|
<div class="w-full h-px bg-white/10 my-4" />
|
|
|
|
<p v-if="loginCustomMessage" class="py-2 default-style mb-2" v-html="loginCustomMessage"></p>
|
|
|
|
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
|
|
|
|
<div v-if="showNewAuthSystemMessage" class="mb-4">
|
|
<widgets-alert type="warning">
|
|
<div>
|
|
<p>{{ $strings.MessageAuthenticationSecurityMessage }}</p>
|
|
<a v-if="showNewAuthSystemAdminMessage" href="https://github.com/advplyr/audiobookshelf/discussions/4460" target="_blank" class="underline">{{ $strings.LabelMoreInfo }}</a>
|
|
</div>
|
|
</widgets-alert>
|
|
</div>
|
|
|
|
<form v-show="login_local" @submit.prevent="submitForm">
|
|
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
|
|
<ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" />
|
|
|
|
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label>
|
|
<ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" inputName="password" />
|
|
<div class="w-full flex justify-end py-3">
|
|
<ui-btn type="submit" :disabled="processing" color="bg-primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
|
|
</div>
|
|
</form>
|
|
|
|
<div v-if="login_local && login_openid" class="w-full h-px bg-white/10 my-4" />
|
|
|
|
<div class="w-full flex py-3">
|
|
<a v-if="login_openid" :href="openidAuthUri" class="w-full abs-btn outline-hidden rounded-md shadow-md relative border border-gray-600 text-center bg-primary text-white px-8 py-2 leading-none">
|
|
{{ openIDButtonText }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
layout: 'blank',
|
|
data() {
|
|
return {
|
|
error: null,
|
|
criticalError: null,
|
|
processing: false,
|
|
username: '',
|
|
password: null,
|
|
showInitScreen: false,
|
|
isInit: false,
|
|
newRoot: {
|
|
username: 'root',
|
|
password: ''
|
|
},
|
|
confirmPassword: '',
|
|
ConfigPath: '',
|
|
MetadataPath: '',
|
|
login_local: true,
|
|
login_openid: false,
|
|
authFormData: null,
|
|
// New JWT auth system re-login flags
|
|
showNewAuthSystemMessage: false,
|
|
showNewAuthSystemAdminMessage: false
|
|
}
|
|
},
|
|
watch: {
|
|
user(newVal) {
|
|
if (newVal) {
|
|
if (!this.$store.state.libraries.currentLibraryId) {
|
|
// No libraries available to this user
|
|
if (this.$store.getters['user/getIsRoot']) {
|
|
// If root user go to config/libraries
|
|
this.$router.replace('/config/libraries')
|
|
} else {
|
|
this.$router.replace('/oops?message=No libraries available')
|
|
}
|
|
} else {
|
|
if (this.$route.query.redirect) {
|
|
const isAdminUser = this.$store.getters['user/getIsAdminOrUp']
|
|
const redirect = this.$route.query.redirect
|
|
// If not admin user then do not redirect to config pages other than your stats
|
|
if (isAdminUser || !redirect.startsWith('/config/') || redirect === '/config/stats') {
|
|
this.$router.replace(redirect)
|
|
return
|
|
}
|
|
}
|
|
|
|
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
user() {
|
|
return this.$store.state.user.user
|
|
},
|
|
openidAuthUri() {
|
|
return `${process.env.serverUrl}/auth/openid?callback=${location.href.split('?').shift()}`
|
|
},
|
|
openIDButtonText() {
|
|
return this.authFormData?.authOpenIDButtonText || 'Login with OpenId'
|
|
},
|
|
loginCustomMessage() {
|
|
return this.authFormData?.authLoginCustomMessage || null
|
|
}
|
|
},
|
|
methods: {
|
|
async submitServerSetup() {
|
|
if (!this.newRoot.username || !this.newRoot.username.trim()) {
|
|
this.$toast.error(this.$strings.ToastUserRootRequireName)
|
|
return
|
|
}
|
|
if (this.newRoot.password !== this.confirmPassword) {
|
|
this.$toast.error(this.$strings.ToastUserPasswordMismatch)
|
|
return
|
|
}
|
|
if (!this.newRoot.password) {
|
|
if (!confirm('Are you sure you want to create the root user with no password?')) {
|
|
return
|
|
}
|
|
}
|
|
this.processing = true
|
|
|
|
const payload = {
|
|
newRoot: { ...this.newRoot }
|
|
}
|
|
const success = await this.$axios
|
|
.$post('/init', payload)
|
|
.then(() => true)
|
|
.catch((error) => {
|
|
console.error('Failed', error.response)
|
|
const errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
|
|
this.$toast.error(errorMsg)
|
|
return false
|
|
})
|
|
|
|
if (!success) {
|
|
this.processing = false
|
|
return
|
|
}
|
|
|
|
location.reload()
|
|
},
|
|
setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices }) {
|
|
this.$store.commit('setServerSettings', serverSettings)
|
|
this.$store.commit('setSource', Source)
|
|
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
|
|
this.$setServerLanguageCode(serverSettings.language)
|
|
|
|
if (serverSettings.chromecastEnabled) {
|
|
console.log('Chromecast enabled import script')
|
|
require('@/plugins/chromecast.js').default(this)
|
|
}
|
|
|
|
this.$store.commit('libraries/setLastLoad', 0) // Ensure libraries get loaded again when switching users
|
|
this.$store.commit('libraries/setCurrentLibrary', { id: userDefaultLibraryId })
|
|
this.$store.commit('user/setUser', user)
|
|
// Access token only returned from login, not authorize
|
|
if (user.accessToken) {
|
|
this.$store.commit('user/setAccessToken', user.accessToken)
|
|
}
|
|
|
|
this.$store.dispatch('user/loadUserSettings')
|
|
},
|
|
async submitForm() {
|
|
this.error = null
|
|
this.showNewAuthSystemMessage = false
|
|
this.showNewAuthSystemAdminMessage = false
|
|
this.processing = true
|
|
|
|
const payload = {
|
|
username: this.username,
|
|
password: this.password || ''
|
|
}
|
|
const authRes = await this.$axios.$post('/login', payload).catch((error) => {
|
|
console.error('Failed', error.response)
|
|
if (error.response) this.error = error.response.data
|
|
else this.error = 'Unknown Error'
|
|
return false
|
|
})
|
|
|
|
if (authRes?.error) {
|
|
this.error = authRes.error
|
|
} else if (authRes) {
|
|
this.setUser(authRes)
|
|
}
|
|
this.processing = false
|
|
},
|
|
checkAuth() {
|
|
const token = localStorage.getItem('token')
|
|
if (!token) return false
|
|
|
|
this.processing = true
|
|
|
|
this.$store.commit('user/setAccessToken', token)
|
|
|
|
return this.$axios
|
|
.$post('/api/authorize', null, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
})
|
|
.then((res) => {
|
|
// Force re-login if user is using an old token with no expiration
|
|
if (res.user.isOldToken) {
|
|
this.username = res.user.username
|
|
this.showNewAuthSystemMessage = true
|
|
// Admin user sees link to github discussion
|
|
this.showNewAuthSystemAdminMessage = res.user.type === 'admin' || res.user.type === 'root'
|
|
return false
|
|
}
|
|
|
|
this.setUser(res)
|
|
return true
|
|
})
|
|
.catch((error) => {
|
|
console.error('Authorize error', error)
|
|
return false
|
|
})
|
|
.finally(() => {
|
|
this.processing = false
|
|
})
|
|
},
|
|
checkStatus() {
|
|
this.processing = true
|
|
this.$axios
|
|
.$get('/status')
|
|
.then((data) => {
|
|
this.isInit = data.isInit
|
|
this.showInitScreen = !data.isInit
|
|
this.$setServerLanguageCode(data.language)
|
|
if (this.showInitScreen) {
|
|
this.ConfigPath = data.ConfigPath || ''
|
|
this.MetadataPath = data.MetadataPath || ''
|
|
} else {
|
|
this.authFormData = data.authFormData
|
|
this.updateLoginVisibility(data.authMethods || [])
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error('Status check failed', error)
|
|
this.criticalError = 'Status check failed'
|
|
})
|
|
.finally(() => {
|
|
this.processing = false
|
|
})
|
|
},
|
|
updateLoginVisibility(authMethods) {
|
|
if (this.$route.query?.error) {
|
|
this.error = this.$route.query.error
|
|
|
|
// Remove error query string
|
|
const newurl = new URL(location.href)
|
|
newurl.searchParams.delete('error')
|
|
window.history.replaceState({ path: newurl.href }, '', newurl.href)
|
|
}
|
|
|
|
if (authMethods.includes('local') || !authMethods.length) {
|
|
this.login_local = true
|
|
} else {
|
|
this.login_local = false
|
|
}
|
|
|
|
if (authMethods.includes('openid')) {
|
|
// Auto redirect unless query string ?autoLaunch=0 OR when explicity requested through ?autoLaunch=1
|
|
if ((this.authFormData?.authOpenIDAutoLaunch && this.$route.query?.autoLaunch !== '0') || this.$route.query?.autoLaunch == '1') {
|
|
window.location.href = this.openidAuthUri
|
|
}
|
|
|
|
this.login_openid = true
|
|
} else {
|
|
this.login_openid = false
|
|
}
|
|
}
|
|
},
|
|
async mounted() {
|
|
// Token passed as query parameter after successful oidc login
|
|
if (this.$route.query?.accessToken) {
|
|
localStorage.setItem('token', this.$route.query.accessToken)
|
|
}
|
|
if (localStorage.getItem('token')) {
|
|
if (await this.checkAuth()) return // if valid user no need to check status
|
|
}
|
|
|
|
this.checkStatus()
|
|
}
|
|
}
|
|
</script>
|