hippofish/packages/frontend/src/components/MkSignin.vue

242 lines
6.9 KiB
Vue
Raw Normal View History

<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
2018-02-10 08:22:14 +01:00
<template>
2023-05-14 03:21:56 +02:00
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
<div class="_gaps_m">
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
<MkInfo v-if="message">
{{ message }}
</MkInfo>
2023-01-06 05:40:17 +01:00
<div v-if="!totpLogin" class="normal-signin _gaps_m">
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
2020-10-31 01:39:22 +01:00
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password>
2023-09-30 21:53:52 +02:00
<template #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
Migrate to Vue3 (#6587) * Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * wip * wip * wip * Update init.ts * Update drive-window.vue * wip * wip * Use PascalCase for components * Use PascalCase for components * update dep * wip * wip * wip * Update init.ts * wip * Update paging.ts * Update test.vue * watch deep * wip * lint * wip * wip * wip * wip * wiop * wip * Update webpack.config.ts * alllow null poll * wip * wip * wip * wiop * UI redesign & refactor (#6714) * wip * wip * wip * wip * wip * Update drive.vue * Update word-mute.vue * wip * wip * wip * clean up * wip * Update default.vue * wip * Update notes.vue * Update mfm.ts * Update index.home.vue * Update post-form.vue * Update post-form-attaches.vue * wip * Update post-form.vue * Update sidebar.vue * wip * wip * Update index.vue * wip * Update default.vue * Update index.vue * Update index.vue * wip * Update post-form-attaches.vue * Update note.vue * wip * clean up * Update notes.vue * wip * wip * Update ja-JP.yml * wip * wip * Update index.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update default.vue * wip * Update _dark.json5 * wip * wip * wip * clean up * wip * wip * Update index.vue * Update test.vue * wip * wip * fix * wip * wip * wip * wip * clena yop * wip * wip * Update store.ts * Update messaging-room.vue * Update default.widgets.vue * fix * wip * wip * Update modal.vue * wip * Update os.ts * Update os.ts * Update deck.vue * Update init.ts * wip * Update ja-JP.yml * v-sizeは単にwindowのresizeを監視するだけで良いかもしれない * Update modal.vue * wip * Update tooltip.ts * wip * wip * wip * wip * wip * Update image-viewer.vue * wip * wip * Update style.scss * Update style.scss * Update visitor.vue * wip * Update init.ts * Update init.ts * wip * wip * Update visitor.vue * Update visitor.vue * Update visitor.vue * Update visitor.vue * wip * wip * Update modal.vue * Update header.vue * Update menu.vue * Update about.vue * Update about-misskey.vue * wip * wip * Update visitor.vue * Update tooltip.ts * wip * Update drive.vue * wip * Update style.scss * Update header.vue * wip * wip * Update users.user.vue * Update announcements.vue * wip * wip * wip * Update emojis.vue * wip * Update emojis.vue * Update style.scss * Update users.vue * wip * Update style.scss * wip * Update welcome.entrance.vue * Update radio.vue * Update size.ts * Update emoji-edit-dialog.vue * wip * Update emojis.vue * wip * Update emojis.vue * Update emojis.vue * Update emojis.vue * wip * wip * wip * wip * Update file-dialog.vue * wip * wip * Update token-generate-window.vue * Update notification-setting-window.vue * wip * wip * Update _error_.vue * Update ja-JP.yml * wip * wip * Update store.ts * Update emojis.vue * Update emojis.vue * Update emojis.vue * Update announcements.vue * Update store.ts * wip * Update page-editor.vue * wip * wip * Update modal.vue * wip * Update select-file.ts * Update timeline.vue * Update emojis.vue * Update os.ts * wip * Update user-select.vue * Update mfm.ts * Update get-file-info.ts * Update drive.vue * Update init.ts * Update mfm.ts * wip * wip * Update window.vue * Update note.vue * wip * wip * Update user-info.vue * wip * wip * wip * wip * wip * Update header.vue * Update header.vue * wip * Update explore.vue * wip * wip * wip * Update webpack.config.ts * wip * wip * wip * wip * wip * wip * Update autocomplete.ts * wip * wip * wip * Update toast.vue * wip * Update post-form-dialog.vue * wip * wip * wip * wip * wip * Update users.vue * wip * Update explore.vue * wip * wip * wip * Update package.json * wip * Update icon-dialog.vue * wip * wip * Update user-preview.ts * wip * wip * wip * wip * wip * Update instance.vue * Update user-name.vue * Update federation.vue * Update instance.vue * wip * wip * Update tag.vue * wip * wip * wip * wip * wip * Update instance.vue * wip * Update os.ts * Update os.ts * wip * wip * wip * Update router.ts * wip * Update init.ts * Update note.vue * Update messages.vue * wip * wip * wip * wip * wip * google * wip * wip * wip * wip * Update theme-editor.vue * wip * wip * Update room.vue * Update channel-editor.vue * wip * Update window.vue * Update window.vue * wip * Update window.vue * Update window.vue * wip * Update menu.vue * wip * wip * wip * wip * Update messaging-room.vue * wip * Update post-form.vue * Update default.widgets.vue * Update window.vue * wip
2020-10-17 13:12:00 +02:00
</MkInput>
<MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
2020-10-31 01:39:22 +01:00
</div>
2021-11-19 11:36:12 +01:00
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
2020-10-31 01:39:22 +01:00
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
<p>{{ i18n.ts.useSecurityKey }}</p>
2021-11-19 11:36:12 +01:00
<MkButton v-if="!queryingKey" @click="queryKey">
{{ i18n.ts.retry }}
2020-10-31 01:39:22 +01:00
</MkButton>
</div>
2021-11-19 11:36:12 +01:00
<div v-if="user && user.securityKeys" class="or-hr">
<p class="or-msg">{{ i18n.ts.or }}</p>
2020-10-31 01:39:22 +01:00
</div>
<div class="twofa-group totp-group">
<p style="margin-bottom:0;">{{ i18n.ts['2fa'] }}</p>
2023-05-19 06:58:09 +02:00
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
<template #label>{{ i18n.ts.password }}</template>
2023-09-30 21:53:52 +02:00
<template #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
2020-10-31 01:39:22 +01:00
</MkInput>
<MkInput v-model="token" type="text" pattern="^([0-9]{6}|[A-Z0-9]{32})$" autocomplete="one-time-code" :spellcheck="false" required>
<template #label>{{ i18n.ts.token }}</template>
2023-09-30 21:53:52 +02:00
<template #prefix><i class="ph-keyhole ph-bold pg-lg"></i></template>
2020-10-31 01:39:22 +01:00
</MkInput>
<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
2020-10-31 01:39:22 +01:00
</div>
</div>
</div>
2018-02-10 08:22:14 +01:00
</form>
</template>
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import { toUnicode } from 'punycode/';
2023-09-20 09:44:12 +02:00
import * as Misskey from 'misskey-js';
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
2023-09-19 09:37:43 +02:00
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import MkButton from '@/components/MkButton.vue';
2023-01-07 07:09:46 +01:00
import MkInput from '@/components/MkInput.vue';
import MkInfo from '@/components/MkInfo.vue';
2023-09-19 09:37:43 +02:00
import { host as configHost } from '@/config.js';
import * as os from '@/os.js';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
2018-02-10 08:22:14 +01:00
let signing = $ref(false);
2023-09-20 09:44:12 +02:00
let user = $ref<Misskey.entities.UserDetailed | null>(null);
let username = $ref('');
let password = $ref('');
let token = $ref('');
let host = $ref(toUnicode(configHost));
let totpLogin = $ref(false);
let queryingKey = $ref(false);
let credentialRequest = $ref<CredentialRequestOptions | null>(null);
let hCaptchaResponse = $ref(null);
let reCaptchaResponse = $ref(null);
2018-12-18 16:40:29 +01:00
const emit = defineEmits<{
(ev: 'login', v: any): void;
}>();
2018-12-18 16:40:29 +01:00
const props = defineProps({
withAvatar: {
type: Boolean,
required: false,
2022-12-20 07:15:34 +01:00
default: true,
},
autoSet: {
type: Boolean,
required: false,
default: false,
},
message: {
type: String,
required: false,
2022-12-20 07:15:34 +01:00
default: '',
},
});
function onUsernameChange(): void {
os.api('users/show', {
2022-12-20 07:15:34 +01:00
username: username,
2022-05-23 22:43:13 +02:00
}).then(userResponse => {
user = userResponse;
}, () => {
user = null;
});
}
function onLogin(res: any): Promise<void> | void {
if (props.autoSet) {
return login(res.i);
}
}
async function queryKey(): Promise<void> {
queryingKey = true;
await webAuthnRequest(credentialRequest)
.catch(() => {
queryingKey = false;
return Promise.reject(null);
}).then(credential => {
credentialRequest = null;
queryingKey = false;
signing = true;
return os.api('signin', {
username,
password,
credential: credential.toJSON(),
'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(): void {
signing = true;
if (!totpLogin && user && user.twoFactorEnabled) {
if (webAuthnSupported() && user.securityKeys) {
os.api('signin', {
username,
password,
2022-12-20 07:15:34 +01:00
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
}).then(res => {
totpLogin = true;
signing = false;
credentialRequest = parseRequestOptionsFromJSON({
publicKey: res,
});
})
.then(() => queryKey())
.catch(loginFailed);
} else {
totpLogin = true;
signing = false;
}
} else {
os.api('signin', {
username,
password,
2022-12-20 07:15:34 +01:00
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
token: user?.twoFactorEnabled ? token : undefined,
}).then(res => {
emit('login', res);
onLogin(res);
}).catch(loginFailed);
}
}
function loginFailed(err: any): void {
switch (err.id) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
2022-12-20 07:15:34 +01:00
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.error(err);
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
2022-12-20 07:15:34 +01:00
text: JSON.stringify(err),
});
2018-02-10 08:22:14 +01:00
}
}
totpLogin = false;
signing = false;
}
function resetPassword(): void {
os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
}, 'closed');
}
2018-02-10 08:22:14 +01:00
</script>
2023-05-14 03:21:56 +02:00
<style lang="scss" module>
.avatar {
margin: 0 auto 0 auto;
width: 64px;
height: 64px;
background: #ddd;
background-position: center;
background-size: cover;
border-radius: var(--radius-full);
}
2018-02-10 08:22:14 +01:00
</style>