diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
| commit | 9384f5399da39e53855beb8e7f8ded1aa56bf72e (patch) | |
| tree | ce5959571a981b9c4047da3c7b3fd080aa44222c /packages/frontend/src/components/MkSignin.vue | |
| parent | wip: retention for dashboard (diff) | |
| download | sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.gz sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.bz2 sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.zip | |
rename: client -> frontend
Diffstat (limited to 'packages/frontend/src/components/MkSignin.vue')
| -rw-r--r-- | packages/frontend/src/components/MkSignin.vue | 259 |
1 files changed, 259 insertions, 0 deletions
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue new file mode 100644 index 0000000000..96f18f8d61 --- /dev/null +++ b/packages/frontend/src/components/MkSignin.vue @@ -0,0 +1,259 @@ +<template> +<form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> + <div class="auth _section _formRoot"> + <div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div> + <MkInfo v-if="message"> + {{ message }} + </MkInfo> + <div v-if="!totpLogin" class="normal-signin"> + <MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required data-cy-signin-username @update:model-value="onUsernameChange"> + <template #prefix>@</template> + <template #suffix>@{{ host }}</template> + </MkInput> + <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password> + <template #prefix><i class="ti ti-lock"></i></template> + <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> + </MkInput> + <MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> + </div> + <div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> + <div v-if="user && user.securityKeys" class="twofa-group tap-group"> + <p>{{ i18n.ts.tapSecurityKey }}</p> + <MkButton v-if="!queryingKey" @click="queryKey"> + {{ i18n.ts.retry }} + </MkButton> + </div> + <div v-if="user && user.securityKeys" class="or-hr"> + <p class="or-msg">{{ i18n.ts.or }}</p> + </div> + <div class="twofa-group totp-group"> + <p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p> + <MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" :with-password-toggle="true" required> + <template #label>{{ i18n.ts.password }}</template> + <template #prefix><i class="ti ti-lock"></i></template> + </MkInput> + <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" :spellcheck="false" required> + <template #label>{{ i18n.ts.token }}</template> + <template #prefix><i class="ti ti-123"></i></template> + </MkInput> + <MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> + </div> + </div> + </div> + <div class="social _section"> + <a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="ti ti-brand-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a> + <a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="ti ti-brand-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a> + <a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="ti ti-brand-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a> + </div> +</form> +</template> + +<script lang="ts" setup> +import { defineAsyncComponent } from 'vue'; +import { toUnicode } from 'punycode/'; +import { showSuspendedDialog } from '../scripts/show-suspended-dialog'; +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/form/input.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import { apiUrl, host as configHost } from '@/config'; +import { byteify, hexify } from '@/scripts/2fa'; +import * as os from '@/os'; +import { login } from '@/account'; +import { instance } from '@/instance'; +import { i18n } from '@/i18n'; + +let signing = $ref(false); +let user = $ref(null); +let username = $ref(''); +let password = $ref(''); +let token = $ref(''); +let host = $ref(toUnicode(configHost)); +let totpLogin = $ref(false); +let credential = $ref(null); +let challengeData = $ref(null); +let queryingKey = $ref(false); +let hCaptchaResponse = $ref(null); +let reCaptchaResponse = $ref(null); + +const meta = $computed(() => instance); + +const emit = defineEmits<{ + (ev: 'login', v: any): void; +}>(); + +const props = defineProps({ + withAvatar: { + type: Boolean, + required: false, + default: true, + }, + autoSet: { + type: Boolean, + required: false, + default: false, + }, + message: { + type: String, + required: false, + default: '', + }, +}); + +function onUsernameChange() { + os.api('users/show', { + username: username, + }).then(userResponse => { + user = userResponse; + }, () => { + user = null; + }); +} + +function onLogin(res) { + if (props.autoSet) { + return login(res.i); + } +} + +function queryKey() { + queryingKey = true; + return navigator.credentials.get({ + publicKey: { + challenge: byteify(challengeData.challenge, 'base64'), + allowCredentials: challengeData.securityKeys.map(key => ({ + id: byteify(key.id, 'hex'), + type: 'public-key', + transports: ['usb', 'nfc', 'ble', 'internal'], + })), + timeout: 60 * 1000, + }, + }).catch(() => { + queryingKey = false; + return Promise.reject(null); + }).then(credential => { + queryingKey = false; + signing = true; + return os.api('signin', { + username, + password, + signature: hexify(credential.response.signature), + authenticatorData: hexify(credential.response.authenticatorData), + clientDataJSON: hexify(credential.response.clientDataJSON), + credentialId: credential.id, + challengeId: challengeData.challengeId, + 'hcaptcha-response': hCaptchaResponse, + 'g-recaptcha-response': reCaptchaResponse, + }); + }).then(res => { + emit('login', res); + return onLogin(res); + }).catch(err => { + if (err === null) return; + os.alert({ + type: 'error', + text: i18n.ts.signinFailed, + }); + signing = false; + }); +} + +function onSubmit() { + signing = true; + console.log('submit'); + if (!totpLogin && user && user.twoFactorEnabled) { + if (window.PublicKeyCredential && user.securityKeys) { + os.api('signin', { + username, + password, + 'hcaptcha-response': hCaptchaResponse, + 'g-recaptcha-response': reCaptchaResponse, + }).then(res => { + totpLogin = true; + signing = false; + challengeData = res; + return queryKey(); + }).catch(loginFailed); + } else { + totpLogin = true; + signing = false; + } + } else { + os.api('signin', { + username, + password, + 'hcaptcha-response': hCaptchaResponse, + 'g-recaptcha-response': reCaptchaResponse, + token: user && user.twoFactorEnabled ? token : undefined, + }).then(res => { + emit('login', res); + onLogin(res); + }).catch(loginFailed); + } +} + +function loginFailed(err) { + switch (err.id) { + case '6cc579cc-885d-43d8-95c2-b8c7fc963280': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.noSuchUser, + }); + break; + } + case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.incorrectPassword, + }); + break; + } + case 'e03a5f46-d309-4865-9b69-56282d94e1eb': { + showSuspendedDialog(); + break; + } + case '22d05606-fbcf-421a-a2db-b32610dcfd1b': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.rateLimitExceeded, + }); + break; + } + default: { + console.log(err); + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: JSON.stringify(err), + }); + } + } + + challengeData = null; + totpLogin = false; + signing = false; +} + +function resetPassword() { + os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, { + }, 'closed'); +} +</script> + +<style lang="scss" scoped> +.eppvobhk { + > .auth { + > .avatar { + margin: 0 auto 0 auto; + width: 64px; + height: 64px; + background: #ddd; + background-position: center; + background-size: cover; + border-radius: 100%; + } + } +} +</style> |