mirror of
https://github.com/gotson/komga.git
synced 2026-04-22 15:00:59 +02:00
feat(webui): add UI settings section with OAuth2 options
This commit is contained in:
parent
3b1504c329
commit
961832e1a1
10 changed files with 297 additions and 58 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
komga-webui/src/types/komga-clientsettings.ts
Normal file
22
komga-webui/src/types/komga-clientsettings.ts
Normal 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',
|
||||
}
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()})
|
||||
|
|
|
|||
|
|
@ -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}})
|
||||
|
|
|
|||
115
komga-webui/src/views/UISettings.vue
Normal file
115
komga-webui/src/views/UISettings.vue
Normal 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>
|
||||
Loading…
Reference in a new issue