mirror of
https://github.com/gotson/komga.git
synced 2026-05-09 05:10:19 +02:00
feat(webui): oauth2 login
This commit is contained in:
parent
7438bf4c95
commit
73d8dab60c
9 changed files with 174 additions and 78 deletions
|
|
@ -66,6 +66,7 @@ export default Vue.extend({
|
||||||
{text: this.$t('authentication_activity.ip').toString(), value: 'ip'},
|
{text: this.$t('authentication_activity.ip').toString(), value: 'ip'},
|
||||||
{text: this.$t('authentication_activity.user_agent').toString(), value: 'userAgent'},
|
{text: this.$t('authentication_activity.user_agent').toString(), value: 'userAgent'},
|
||||||
{text: this.$t('authentication_activity.success').toString(), value: 'success'},
|
{text: this.$t('authentication_activity.success').toString(), value: 'success'},
|
||||||
|
{text: this.$t('authentication_activity.source').toString(), value: 'source'},
|
||||||
{text: this.$t('authentication_activity.error').toString(), value: 'error'},
|
{text: this.$t('authentication_activity.error').toString(), value: 'error'},
|
||||||
{text: this.$t('authentication_activity.datetime').toString(), value: 'dateTime', groupable: false},
|
{text: this.$t('authentication_activity.datetime').toString(), value: 'dateTime', groupable: false},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@
|
||||||
"datetime": "Date Time",
|
"datetime": "Date Time",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"ip": "Ip",
|
"ip": "IP",
|
||||||
|
"source": "Source",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"user_agent": "User Agent"
|
"user_agent": "User Agent"
|
||||||
},
|
},
|
||||||
|
|
@ -541,7 +542,10 @@
|
||||||
"ERR_1020": "Book to upgrade does not belong to provided series",
|
"ERR_1020": "Book to upgrade does not belong to provided series",
|
||||||
"ERR_1021": "Destination file already exists",
|
"ERR_1021": "Destination file already exists",
|
||||||
"ERR_1022": "Newly imported book could not be scanned",
|
"ERR_1022": "Newly imported book could not be scanned",
|
||||||
"ERR_1023": "Book already present in ReadingList"
|
"ERR_1023": "Book already present in ReadingList",
|
||||||
|
"ERR_1024": "OAuth2 login error: no email attribute",
|
||||||
|
"ERR_1025": "OAuth2 login error: no local user exist with that email",
|
||||||
|
"ERR_1026": "OpenID Connect login error: email not verified"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"age_rating": "age rating",
|
"age_rating": "age rating",
|
||||||
|
|
@ -573,7 +577,7 @@
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"create_user_account": "Create user account",
|
"create_user_account": "Create user account",
|
||||||
"login": "Login",
|
"login": "Sign in",
|
||||||
"unclaimed_html": "This Komga server is not yet active, you need to create a user account to be able to access it.<br/><br/>Choose an <strong>email</strong> and <strong>password</strong> and click on <strong>Create user account</strong>."
|
"unclaimed_html": "This Komga server is not yet active, you need to create a user account to be able to access it.<br/><br/>Choose an <strong>email</strong> and <strong>password</strong> and click on <strong>Create user account</strong>."
|
||||||
},
|
},
|
||||||
"media_analysis": {
|
"media_analysis": {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import komgaUsers from './plugins/komga-users.plugin'
|
||||||
import komgaTransientBooks from './plugins/komga-transientbooks.plugin'
|
import komgaTransientBooks from './plugins/komga-transientbooks.plugin'
|
||||||
import komgaSse from './plugins/komga-sse.plugin'
|
import komgaSse from './plugins/komga-sse.plugin'
|
||||||
import komgaTasks from './plugins/komga-tasks.plugin'
|
import komgaTasks from './plugins/komga-tasks.plugin'
|
||||||
|
import komgaOauth2 from './plugins/komga-oauth2.plugin'
|
||||||
import vuetify from './plugins/vuetify'
|
import vuetify from './plugins/vuetify'
|
||||||
import logger from './plugins/logger.plugin'
|
import logger from './plugins/logger.plugin'
|
||||||
import './public-path'
|
import './public-path'
|
||||||
|
|
@ -47,6 +48,7 @@ Vue.use(komgaLibraries, {store: store, http: Vue.prototype.$http})
|
||||||
Vue.use(komgaSse, {eventHub: Vue.prototype.$eventHub, store: store})
|
Vue.use(komgaSse, {eventHub: Vue.prototype.$eventHub, store: store})
|
||||||
Vue.use(actuator, {http: Vue.prototype.$http})
|
Vue.use(actuator, {http: Vue.prototype.$http})
|
||||||
Vue.use(komgaTasks, {http: Vue.prototype.$http})
|
Vue.use(komgaTasks, {http: Vue.prototype.$http})
|
||||||
|
Vue.use(komgaOauth2, {http: Vue.prototype.$http})
|
||||||
|
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
|
||||||
17
komga-webui/src/plugins/komga-oauth2.plugin.ts
Normal file
17
komga-webui/src/plugins/komga-oauth2.plugin.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import {AxiosInstance} from 'axios'
|
||||||
|
import _Vue from 'vue'
|
||||||
|
import KomgaOauht2Service from '@/services/komga-oauth2.service'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(
|
||||||
|
Vue: typeof _Vue,
|
||||||
|
{http}: { http: AxiosInstance }) {
|
||||||
|
Vue.prototype.$komgaOauth2 = new KomgaOauht2Service(http)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'vue/types/vue' {
|
||||||
|
interface Vue {
|
||||||
|
$komgaOauth2: KomgaOauht2Service;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
komga-webui/src/services/komga-oauth2.service.ts
Normal file
24
komga-webui/src/services/komga-oauth2.service.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import {AxiosInstance} from 'axios'
|
||||||
|
import {OAuth2ClientDto} from '@/types/komga-oauth2'
|
||||||
|
|
||||||
|
const API_OAUTH2 = '/api/v1/oauth2'
|
||||||
|
|
||||||
|
export default class KomgaOauht2Service {
|
||||||
|
private http: AxiosInstance
|
||||||
|
|
||||||
|
constructor(http: AxiosInstance) {
|
||||||
|
this.http = http
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProviders(): Promise<OAuth2ClientDto[]> {
|
||||||
|
try {
|
||||||
|
return (await this.http.get(`${API_OAUTH2}/providers`)).data
|
||||||
|
} catch (e) {
|
||||||
|
let msg = 'An error occurred while trying to retrieve oauth2 providers'
|
||||||
|
if (e.response.data.message) {
|
||||||
|
msg += `: ${e.response.data.message}`
|
||||||
|
}
|
||||||
|
throw new Error(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
komga-webui/src/types/komga-oauth2.ts
Normal file
4
komga-webui/src/types/komga-oauth2.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface OAuth2ClientDto {
|
||||||
|
name: string,
|
||||||
|
registrationId: string,
|
||||||
|
}
|
||||||
|
|
@ -42,4 +42,5 @@ interface AuthenticationActivityDto {
|
||||||
success: Boolean,
|
success: Boolean,
|
||||||
error?: string,
|
error?: string,
|
||||||
dateTime: string,
|
dateTime: string,
|
||||||
|
source?: string,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
komga-webui/src/types/social.ts
Normal file
10
komga-webui/src/types/social.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export const socialButtons = {
|
||||||
|
google: {
|
||||||
|
color: '#4285F4',
|
||||||
|
text: 'white',
|
||||||
|
},
|
||||||
|
facebook: {
|
||||||
|
color: '#4267B2',
|
||||||
|
text: 'white',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -1,88 +1,111 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ma-3">
|
<div class="ma-3">
|
||||||
<v-row align="center" justify="center">
|
<v-container style="max-width: 550px">
|
||||||
<v-img src="../assets/logo.svg"
|
<v-row align="center" justify="center">
|
||||||
:max-width="logoWidth"
|
<v-img src="../assets/logo.svg"
|
||||||
/>
|
:max-width="logoWidth"
|
||||||
</v-row>
|
/>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<form novalidate @submit.prevent="performLogin">
|
<form novalidate @submit.prevent="performLogin">
|
||||||
<v-row justify="center" v-if="unclaimed">
|
<v-row justify="center" v-if="unclaimed">
|
||||||
<v-col
|
<v-col
|
||||||
cols="12" sm="8" md="6" lg="4" xl="2"
|
class="text-body-1 mt-2"
|
||||||
class="text-body-1 mt-2"
|
|
||||||
>
|
|
||||||
<v-alert type="info"
|
|
||||||
icon="mdi-account-plus"
|
|
||||||
prominent
|
|
||||||
text
|
|
||||||
v-html="$t('login.unclaimed_html')"
|
|
||||||
>
|
>
|
||||||
</v-alert>
|
<v-alert type="info"
|
||||||
</v-col>
|
icon="mdi-account-plus"
|
||||||
</v-row>
|
prominent
|
||||||
|
text
|
||||||
|
v-html="$t('login.unclaimed_html')"
|
||||||
|
>
|
||||||
|
</v-alert>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<v-row justify="center">
|
<v-row>
|
||||||
<v-col cols="12" sm="8" md="6" lg="4" xl="2">
|
<v-col>
|
||||||
<v-text-field v-model="form.login"
|
<v-text-field v-model="form.login"
|
||||||
:label="$t('common.email')"
|
:label="$t('common.email')"
|
||||||
:error-messages="getErrors('login')"
|
:error-messages="getErrors('login')"
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
autofocus
|
autofocus
|
||||||
@blur="$v.form.login.$touch()"
|
@blur="$v.form.login.$touch()"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-row justify="center">
|
<v-row>
|
||||||
<v-col cols="12" sm="8" md="6" lg="4" xl="2">
|
<v-col>
|
||||||
<v-text-field v-model="form.password"
|
<v-text-field v-model="form.password"
|
||||||
:label="$t('common.password')"
|
:label="$t('common.password')"
|
||||||
:error-messages="getErrors('password')"
|
:error-messages="getErrors('password')"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
@input="$v.form.password.$touch()"
|
@input="$v.form.password.$touch()"
|
||||||
@blur="$v.form.password.$touch()"
|
@blur="$v.form.password.$touch()"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-row justify="center">
|
<v-row>
|
||||||
<v-col cols="12" sm="8" md="6" lg="4" xl="2">
|
<v-col>
|
||||||
<v-btn color="primary"
|
<v-btn color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="unclaimed"
|
:disabled="unclaimed"
|
||||||
>{{ $t('login.login') }}
|
>{{ $t('login.login') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn v-if="unclaimed"
|
<v-btn v-if="unclaimed"
|
||||||
class="mx-4"
|
class="mx-4"
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="claim"
|
@click="claim"
|
||||||
>{{ $t('login.create_user_account') }}
|
>{{ $t('login.create_user_account') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-row justify="center">
|
<v-divider class="my-4"/>
|
||||||
<v-col cols="6" sm="4" md="3" lg="2" xl="1">
|
|
||||||
<v-select v-model="locale"
|
<v-row>
|
||||||
:items="locales"
|
<v-col
|
||||||
:label="$t('home.translation')"
|
v-for="provider in oauth2Providers"
|
||||||
prepend-icon="mdi-translate"
|
:key="provider.registrationId"
|
||||||
|
cols="auto"
|
||||||
|
class="py-1"
|
||||||
>
|
>
|
||||||
</v-select>
|
<v-btn
|
||||||
</v-col>
|
:disabled="unclaimed"
|
||||||
|
:href="`${urls.originNoSlash}/oauth2/authorization/${provider.registrationId}`"
|
||||||
|
min-width="250"
|
||||||
|
:class="$_.get(socialButtons[provider.registrationId.toLowerCase()], 'text') ? `${socialButtons[provider.registrationId.toLowerCase()].text}--text` : undefined"
|
||||||
|
:color="$_.get(socialButtons[provider.registrationId.toLowerCase()], 'color')"
|
||||||
|
>
|
||||||
|
<v-icon left>mdi-{{ provider.registrationId }}</v-icon>
|
||||||
|
Sign in with {{ provider.name }}
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<v-col cols="6" sm="4" md="3" lg="2" xl="1">
|
<v-row justify="center">
|
||||||
<v-select v-model="theme"
|
<v-col cols="6">
|
||||||
:items="themes"
|
<v-select v-model="locale"
|
||||||
:label="$t('home.theme')"
|
:items="locales"
|
||||||
:prepend-icon="themeIcon"
|
:label="$t('home.translation')"
|
||||||
>
|
prepend-icon="mdi-translate"
|
||||||
</v-select>
|
>
|
||||||
</v-col>
|
</v-select>
|
||||||
</v-row>
|
</v-col>
|
||||||
</form>
|
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-select v-model="theme"
|
||||||
|
:items="themes"
|
||||||
|
:label="$t('home.theme')"
|
||||||
|
:prepend-icon="themeIcon"
|
||||||
|
>
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</form>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
<v-snackbar
|
<v-snackbar
|
||||||
v-model="snackbar"
|
v-model="snackbar"
|
||||||
|
|
@ -103,11 +126,17 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import {email, required} from 'vuelidate/lib/validators'
|
import {email, required} from 'vuelidate/lib/validators'
|
||||||
import {Theme} from '@/types/themes'
|
import {Theme} from '@/types/themes'
|
||||||
|
import {OAuth2ClientDto} from '@/types/komga-oauth2'
|
||||||
|
import urls from '@/functions/urls'
|
||||||
|
import {socialButtons} from '@/types/social'
|
||||||
|
import {convertErrorCodes} from '@/functions/error-codes'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
urls,
|
||||||
|
socialButtons,
|
||||||
form: {
|
form: {
|
||||||
login: '',
|
login: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
|
@ -115,6 +144,7 @@ export default Vue.extend({
|
||||||
snackbar: false,
|
snackbar: false,
|
||||||
snackText: '',
|
snackText: '',
|
||||||
unclaimed: false,
|
unclaimed: false,
|
||||||
|
oauth2Providers: [] as OAuth2ClientDto[],
|
||||||
locales: this.$i18n.availableLocales.map((x: any) => ({text: this.$i18n.t('common.locale_name', x), value: x})),
|
locales: this.$i18n.availableLocales.map((x: any) => ({text: this.$i18n.t('common.locale_name', x), value: x})),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -184,6 +214,9 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getClaimStatus()
|
this.getClaimStatus()
|
||||||
|
this.$komgaOauth2.getProviders()
|
||||||
|
.then((providers) => this.oauth2Providers = providers)
|
||||||
|
if(this.$route.query.error) this.showSnack(convertErrorCodes(this.$route.query.error.toString()))
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getErrors(fieldName: string): string[] {
|
getErrors(fieldName: string): string[] {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue