feat(webui): oauth2 login

This commit is contained in:
Gauthier Roebroeck 2021-09-27 11:34:25 +08:00
parent 7438bf4c95
commit 73d8dab60c
9 changed files with 174 additions and 78 deletions

View file

@ -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},
)

View file

@ -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": {

View file

@ -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

View 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;
}
}

View 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)
}
}
}

View file

@ -0,0 +1,4 @@
export interface OAuth2ClientDto {
name: string,
registrationId: string,
}

View file

@ -42,4 +42,5 @@ interface AuthenticationActivityDto {
success: Boolean,
error?: string,
dateTime: string,
source?: string,
}

View file

@ -0,0 +1,10 @@
export const socialButtons = {
google: {
color: '#4285F4',
text: 'white',
},
facebook: {
color: '#4267B2',
text: 'white',
},
}

View file

@ -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[] {