mirror of
https://github.com/gotson/komga.git
synced 2025-12-20 07:23:34 +01: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.user_agent').toString(), value: 'userAgent'},
|
||||
{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.datetime').toString(), value: 'dateTime', groupable: false},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@
|
|||
"datetime": "Date Time",
|
||||
"email": "Email",
|
||||
"error": "Error",
|
||||
"ip": "Ip",
|
||||
"ip": "IP",
|
||||
"source": "Source",
|
||||
"success": "Success",
|
||||
"user_agent": "User Agent"
|
||||
},
|
||||
|
|
@ -541,7 +542,10 @@
|
|||
"ERR_1020": "Book to upgrade does not belong to provided series",
|
||||
"ERR_1021": "Destination file already exists",
|
||||
"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": {
|
||||
"age_rating": "age rating",
|
||||
|
|
@ -573,7 +577,7 @@
|
|||
},
|
||||
"login": {
|
||||
"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>."
|
||||
},
|
||||
"media_analysis": {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import komgaUsers from './plugins/komga-users.plugin'
|
|||
import komgaTransientBooks from './plugins/komga-transientbooks.plugin'
|
||||
import komgaSse from './plugins/komga-sse.plugin'
|
||||
import komgaTasks from './plugins/komga-tasks.plugin'
|
||||
import komgaOauth2 from './plugins/komga-oauth2.plugin'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import logger from './plugins/logger.plugin'
|
||||
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(actuator, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaTasks, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaOauth2, {http: Vue.prototype.$http})
|
||||
|
||||
|
||||
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,
|
||||
error?: 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>
|
||||
<div class="ma-3">
|
||||
<v-row align="center" justify="center">
|
||||
<v-img src="../assets/logo.svg"
|
||||
:max-width="logoWidth"
|
||||
/>
|
||||
</v-row>
|
||||
<v-container style="max-width: 550px">
|
||||
<v-row align="center" justify="center">
|
||||
<v-img src="../assets/logo.svg"
|
||||
:max-width="logoWidth"
|
||||
/>
|
||||
</v-row>
|
||||
|
||||
<form novalidate @submit.prevent="performLogin">
|
||||
<v-row justify="center" v-if="unclaimed">
|
||||
<v-col
|
||||
cols="12" sm="8" md="6" lg="4" xl="2"
|
||||
class="text-body-1 mt-2"
|
||||
>
|
||||
<v-alert type="info"
|
||||
icon="mdi-account-plus"
|
||||
prominent
|
||||
text
|
||||
v-html="$t('login.unclaimed_html')"
|
||||
<form novalidate @submit.prevent="performLogin">
|
||||
<v-row justify="center" v-if="unclaimed">
|
||||
<v-col
|
||||
class="text-body-1 mt-2"
|
||||
>
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-alert type="info"
|
||||
icon="mdi-account-plus"
|
||||
prominent
|
||||
text
|
||||
v-html="$t('login.unclaimed_html')"
|
||||
>
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" sm="8" md="6" lg="4" xl="2">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" sm="8" md="6" lg="4" xl="2">
|
||||
<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="current-password"
|
||||
@input="$v.form.password.$touch()"
|
||||
@blur="$v.form.password.$touch()"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" sm="8" md="6" lg="4" xl="2">
|
||||
<v-btn color="primary"
|
||||
type="submit"
|
||||
:disabled="unclaimed"
|
||||
>{{ $t('login.login') }}
|
||||
</v-btn>
|
||||
<v-btn v-if="unclaimed"
|
||||
class="mx-4"
|
||||
color="primary"
|
||||
@click="claim"
|
||||
>{{ $t('login.create_user_account') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn color="primary"
|
||||
type="submit"
|
||||
:disabled="unclaimed"
|
||||
>{{ $t('login.login') }}
|
||||
</v-btn>
|
||||
<v-btn v-if="unclaimed"
|
||||
class="mx-4"
|
||||
color="primary"
|
||||
@click="claim"
|
||||
>{{ $t('login.create_user_account') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row justify="center">
|
||||
<v-col cols="6" sm="4" md="3" lg="2" xl="1">
|
||||
<v-select v-model="locale"
|
||||
:items="locales"
|
||||
:label="$t('home.translation')"
|
||||
prepend-icon="mdi-translate"
|
||||
<v-divider class="my-4"/>
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="provider in oauth2Providers"
|
||||
:key="provider.registrationId"
|
||||
cols="auto"
|
||||
class="py-1"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-btn
|
||||
: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-select v-model="theme"
|
||||
:items="themes"
|
||||
:label="$t('home.theme')"
|
||||
:prepend-icon="themeIcon"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</form>
|
||||
<v-row justify="center">
|
||||
<v-col cols="6">
|
||||
<v-select v-model="locale"
|
||||
:items="locales"
|
||||
:label="$t('home.translation')"
|
||||
prepend-icon="mdi-translate"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<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-model="snackbar"
|
||||
|
|
@ -103,11 +126,17 @@
|
|||
import Vue from 'vue'
|
||||
import {email, required} from 'vuelidate/lib/validators'
|
||||
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({
|
||||
name: 'Login',
|
||||
data: function () {
|
||||
return {
|
||||
urls,
|
||||
socialButtons,
|
||||
form: {
|
||||
login: '',
|
||||
password: '',
|
||||
|
|
@ -115,6 +144,7 @@ export default Vue.extend({
|
|||
snackbar: false,
|
||||
snackText: '',
|
||||
unclaimed: false,
|
||||
oauth2Providers: [] as OAuth2ClientDto[],
|
||||
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() {
|
||||
this.getClaimStatus()
|
||||
this.$komgaOauth2.getProviders()
|
||||
.then((providers) => this.oauth2Providers = providers)
|
||||
if(this.$route.query.error) this.showSnack(convertErrorCodes(this.$route.query.error.toString()))
|
||||
},
|
||||
methods: {
|
||||
getErrors(fieldName: string): string[] {
|
||||
|
|
|
|||
Loading…
Reference in a new issue