feat(webui): add UI settings section with OAuth2 options

This commit is contained in:
Gauthier Roebroeck 2025-02-05 17:40:48 +08:00
parent 3b1504c329
commit 961832e1a1
10 changed files with 297 additions and 58 deletions

View file

@ -275,6 +275,7 @@
"settings": "Settings",
"sidecars": "Sidecars",
"tags": "Tags",
"ui": "User Interface",
"unavailable": "Unavailable",
"unlock_all": "Unlock all",
"url": "URL",
@ -1023,6 +1024,11 @@
"less": "Less titles",
"more": "More titles"
},
"ui_settings": {
"label_oauth2_auto_login": "Automatic OAuth2 login",
"label_oauth2_hide_login": "Hide login fields if OAuth2 is enabled",
"tooltip_oauth2_auto_login": "Requires a single OAuth2 provider, and 'hide login fields' enabled"
},
"updates": {
"available": "Updates are available",
"latest_installed": "The latest version of Komga is already installed"

View file

@ -80,7 +80,7 @@ Vue.use(komgaMetrics, {http: Vue.prototype.$http})
Vue.use(komgaHistory, {http: Vue.prototype.$http})
Vue.use(komgaAnnouncements, {http: Vue.prototype.$http})
Vue.use(komgaReleases, {http: Vue.prototype.$http})
Vue.use(komgaSettings, {http: Vue.prototype.$http})
Vue.use(komgaSettings, {store: store, http: Vue.prototype.$http})
Vue.use(komgaFonts, {http: Vue.prototype.$http})
Vue.config.productionTip = false

View file

@ -1,12 +1,41 @@
import {AxiosInstance} from 'axios'
import _Vue from 'vue'
import KomgaSettingsService from '@/services/komga-settings.service'
import {Module} from 'vuex'
import {LibraryDto} from '@/types/komga-libraries'
import {ClientSettingDto} from '@/types/komga-clientsettings'
let service = KomgaSettingsService
const vuexModule: Module<any, any> = {
state: {
clientSettings: [] as ClientSettingDto[],
},
getters: {
getClientSettingByKey: (state) => (key: string) => {
return state.clientSettings.find((it: ClientSettingDto) => it.key === key)
},
},
mutations: {
setClientSettings(state, settings) {
state.clientSettings = settings
},
},
actions: {
async getClientSettings({commit}) {
commit('setClientSettings', await service.getClientSettings())
},
},
}
export default {
install(
Vue: typeof _Vue,
{http}: { http: AxiosInstance }) {
{store, http}: { store: any, http: AxiosInstance }) {
service = new KomgaSettingsService(http)
Vue.prototype.$komgaSettings = new KomgaSettingsService(http)
store.registerModule('komgaSettings', vuexModule)
},
}

View file

@ -82,6 +82,12 @@ const router = new Router({
beforeEnter: adminGuard,
component: () => import(/* webpackChunkName: "settings-server" */ './views/SettingsServer.vue'),
},
{
path: '/settings/ui',
name: 'settings-ui',
beforeEnter: adminGuard,
component: () => import(/* webpackChunkName: "settings-ui" */ './views/UISettings.vue'),
},
{
path: '/settings/metrics',
name: 'metrics',

View file

@ -1,7 +1,9 @@
import {AxiosInstance} from 'axios'
import {SettingsDto, SettingsUpdateDto} from '@/types/komga-settings'
import {ClientSettingDto, ClientSettingGlobalUpdateDto, ClientSettingUserUpdateDto} from '@/types/komga-clientsettings'
const API_SETTINGS = '/api/v1/settings'
const API_CLIENT_SETTINGS = '/api/v1/client-settings'
export default class KomgaSettingsService {
private http: AxiosInstance
@ -33,4 +35,40 @@ export default class KomgaSettingsService {
throw new Error(msg)
}
}
async getClientSettings(): Promise<ClientSettingDto[]> {
try {
return (await this.http.get(`${API_CLIENT_SETTINGS}/list`)).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve client settings'
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
async updateClientSettingGlobal(setting: ClientSettingGlobalUpdateDto) {
try {
await this.http.put(`${API_CLIENT_SETTINGS}/global`, setting)
} catch (e) {
let msg = 'An error occurred while trying to update global client setting'
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
async updateClientSettingUser(setting: ClientSettingUserUpdateDto) {
try {
await this.http.put(`${API_CLIENT_SETTINGS}/user`, setting)
} catch (e) {
let msg = 'An error occurred while trying to update user client setting'
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
}

View file

@ -0,0 +1,22 @@
export interface ClientSettingDto {
key: string,
value: string,
allowUnauthorized?: boolean,
userId?: string,
}
export interface ClientSettingGlobalUpdateDto {
key: string,
value: string,
allowUnauthorized: boolean,
}
export interface ClientSettingUserUpdateDto {
key: string,
value: string,
}
export enum CLIENT_SETTING {
WEBUI_OAUTH2_HIDE_LOGIN = 'webui.oauth2.hide_login',
WEBUI_OAUTH2_AUTO_LOGIN = 'webui.oauth2.auto_login',
}

View file

@ -210,6 +210,10 @@
<v-list-item-title>{{ $t('common.settings') }}</v-list-item-title>
</v-list-item>
<v-list-item :to="{name: 'settings-ui'}">
<v-list-item-title>{{ $t('common.ui') }}</v-list-item-title>
</v-list-item>
<v-list-item :to="{name: 'metrics'}">
<v-list-item-title>{{ $t('metrics.title') }}</v-list-item-title>
</v-list-item>
@ -450,7 +454,7 @@ export default Vue.extend({
},
logout() {
this.$store.dispatch('logout')
this.$router.push({name: 'login'})
this.$router.push({name: 'login', query: {'logout': true}})
},
addLibrary() {
this.$store.dispatch('dialogAddLibrary')

View file

@ -1,7 +1,7 @@
<template>
<div class="ma-3">
<v-container style="max-width: 550px">
<v-row align="center" justify="center">
<v-row align="center" justify="center" class="ma-3">
<v-img src="../assets/logo.svg"
:max-width="logoWidth"
/>
@ -22,66 +22,68 @@
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field v-model="form.login"
:label="$t('common.email')"
:error-messages="getErrors('login')"
autocomplete="username"
autofocus
@blur="$v.form.login.$touch()"
/>
</v-col>
</v-row>
<div v-if="!hideLogin">
<v-row>
<v-col>
<v-text-field v-model="form.login"
:label="$t('common.email')"
:error-messages="getErrors('login')"
:autocomplete="hideLogin ? '' : 'username'"
autofocus
@blur="$v.form.login.$touch()"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field v-model="form.password"
:label="$t('common.password')"
:error-messages="getErrors('password')"
type="password"
autocomplete="current-password"
@input="$v.form.password.$touch()"
@blur="$v.form.password.$touch()"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field v-model="form.password"
:label="$t('common.password')"
:error-messages="getErrors('password')"
type="password"
:autocomplete="hideLogin ? '' : 'current-password'"
@input="$v.form.password.$touch()"
@blur="$v.form.password.$touch()"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-checkbox v-model="rememberMe"
:label="$t('common.remember-me')"
hide-details
class="mt-0"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-checkbox v-model="rememberMe"
:label="$t('common.remember-me')"
hide-details
class="mt-0"
/>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-btn color="primary"
type="submit"
:disabled="unclaimed"
>{{ $t('login.login') }}
</v-btn>
</v-col>
<v-col cols="auto">
<v-btn v-if="unclaimed"
color="primary"
@click="claim"
>{{ $t('login.create_user_account') }}
</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-btn color="primary"
type="submit"
:disabled="unclaimed"
>{{ $t('login.login') }}
</v-btn>
</v-col>
<v-col cols="auto">
<v-btn v-if="unclaimed"
color="primary"
@click="claim"
>{{ $t('login.create_user_account') }}
</v-btn>
</v-col>
</v-row>
<v-divider class="my-4 mt-2"/>
<v-divider class="my-4 mt-2"/>
<v-row>
</div>
<v-row justify="center">
<v-col
v-for="provider in oauth2Providers"
:key="provider.registrationId"
cols="auto"
class="py-1"
>
<v-btn
:disabled="unclaimed"
@ -141,6 +143,7 @@ import {OAuth2ClientDto} from '@/types/komga-oauth2'
import urls from '@/functions/urls'
import {socialButtons} from '@/types/social'
import {convertErrorCodes} from '@/functions/error-codes'
import {CLIENT_SETTING} from '@/types/komga-clientsettings'
export default Vue.extend({
name: 'LoginView',
@ -157,6 +160,7 @@ export default Vue.extend({
unclaimed: false,
oauth2Providers: [] as OAuth2ClientDto[],
locales: this.$i18n.availableLocales.map((x: any) => ({text: this.$i18n.t('common.locale_name', x), value: x})),
clientSettings: [] as ClientSettingDto[],
}
},
validations: {
@ -166,6 +170,18 @@ export default Vue.extend({
},
},
computed: {
hideLogin(): boolean {
return !this.unclaimed
&& this.oauth2Providers.length > 0
&& (this.clientSettings.find(x => x.key == CLIENT_SETTING.WEBUI_OAUTH2_HIDE_LOGIN)?.value === 'true')
},
autoOauth2Login(): boolean {
return !this.unclaimed
&& this.oauth2Providers.length == 1
&& (this.clientSettings.find(x => x.key == CLIENT_SETTING.WEBUI_OAUTH2_AUTO_LOGIN)?.value === 'true')
&& !this.$route.query.error
&& !this.$route.query.logout
},
logoWidth(): number {
let l = 100
switch (this.$vuetify.breakpoint.name) {
@ -232,11 +248,12 @@ export default Vue.extend({
},
},
},
mounted() {
async mounted() {
this.getClaimStatus()
this.$komgaOauth2.getProviders()
.then((providers) => this.oauth2Providers = providers)
this.clientSettings = await this.$komgaSettings.getClientSettings()
this.oauth2Providers = await this.$komgaOauth2.getProviders()
if (this.$route.query.error) this.showSnack(convertErrorCodes(this.$route.query.error.toString()))
if (this.hideLogin && this.autoOauth2Login) this.oauth2Login(this.oauth2Providers[0])
},
methods: {
oauth2Login(provider: OAuth2ClientDto) {
@ -283,6 +300,7 @@ export default Vue.extend({
})
await this.$store.dispatch('getLibraries')
await this.$store.dispatch('getClientSettings')
if (this.$route.query.redirect) {
await this.$router.push({path: this.$route.query.redirect.toString()})

View file

@ -42,6 +42,7 @@ export default Vue.extend({
await this.$store.dispatch('getMe')
await this.$store.dispatch('getLibraries')
await this.$store.dispatch('getClientSettings')
this.$router.back()
} catch (e) {
this.$router.push({name: 'login', query: {redirect: this.$route.query.redirect}})

View file

@ -0,0 +1,115 @@
<template>
<v-container fluid class="pa-6">
<v-row>
<v-col cols="auto">
<v-checkbox
v-model="form.oauth2HideLogin"
@change="$v.form.oauth2HideLogin.$touch()"
:label="$t('ui_settings.label_oauth2_hide_login')"
hide-details
/>
<v-checkbox
v-model="form.oauth2AutoLogin"
@change="$v.form.oauth2AutoLogin.$touch()"
:label="$t('ui_settings.label_oauth2_auto_login')"
hide-details
>
<template v-slot:append>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon v-on="on">
mdi-information-outline
</v-icon>
</template>
{{ $t('ui_settings.tooltip_oauth2_auto_login') }}
</v-tooltip>
</template>
</v-checkbox>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-btn @click="refreshSettings"
:disabled="discardDisabled"
>{{ $t('common.discard') }}
</v-btn>
</v-col>
<v-col cols="auto">
<v-btn color="primary"
:disabled="saveDisabled"
@click="saveSettings"
>{{ $t('common.save_changes') }}
</v-btn>
</v-col>
</v-row>
</v-container>
</template>
<script lang="ts">
import Vue from 'vue'
import {helpers} from 'vuelidate/lib/validators'
import ConfirmationDialog from '@/components/dialogs/ConfirmationDialog.vue'
import FileBrowserDialog from '@/components/dialogs/FileBrowserDialog.vue'
import {CLIENT_SETTING, ClientSettingDto} from '@/types/komga-clientsettings'
const contextPath = helpers.regex('contextPath', /^\/[-a-zA-Z0-9_\/]*[a-zA-Z0-9]$/)
export default Vue.extend({
name: 'UISettings',
components: {FileBrowserDialog, ConfirmationDialog},
data: () => ({
form: {
oauth2HideLogin: false,
oauth2AutoLogin: false,
},
existingSettings: [] as ClientSettingDto[],
}),
validations: {
form: {
oauth2HideLogin: {},
oauth2AutoLogin: {},
},
},
mounted() {
this.refreshSettings()
},
computed: {
saveDisabled(): boolean {
return this.$v.form.$invalid || !this.$v.form.$anyDirty
},
discardDisabled(): boolean {
return !this.$v.form.$anyDirty
},
},
methods: {
async refreshSettings() {
await this.$store.dispatch('getClientSettings')
this.form.oauth2HideLogin = this.$store.getters.getClientSettingByKey(CLIENT_SETTING.WEBUI_OAUTH2_HIDE_LOGIN)?.value === 'true'
this.form.oauth2AutoLogin = this.$store.getters.getClientSettingByKey(CLIENT_SETTING.WEBUI_OAUTH2_AUTO_LOGIN)?.value === 'true'
this.$v.form.$reset()
},
async saveSettings() {
if (this.$v.form?.oauth2HideLogin?.$dirty)
await this.$komgaSettings.updateClientSettingGlobal({
key: CLIENT_SETTING.WEBUI_OAUTH2_HIDE_LOGIN,
value: this.form.oauth2HideLogin ? 'true' : 'false',
allowUnauthorized: true,
})
if (this.$v.form?.oauth2AutoLogin?.$dirty)
await this.$komgaSettings.updateClientSettingGlobal({
key: CLIENT_SETTING.WEBUI_OAUTH2_AUTO_LOGIN,
value: this.form.oauth2AutoLogin ? 'true' : 'false',
allowUnauthorized: true,
})
await this.refreshSettings()
},
},
})
</script>
<style scoped>
</style>