summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkSignin.vue
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-12-27 14:36:33 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2022-12-27 14:36:33 +0900
commit9384f5399da39e53855beb8e7f8ded1aa56bf72e (patch)
treece5959571a981b9c4047da3c7b3fd080aa44222c /packages/frontend/src/components/MkSignin.vue
parentwip: retention for dashboard (diff)
downloadsharkey-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.vue259
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>