feat(sign-in): メールアドレスログインを実装 (MisskeyIO#836)

Co-authored-by: まっちゃてぃー。 <56515516+mattyatea@users.noreply.github.com>
This commit is contained in:
あわわわとーにゅ 2024-12-22 00:09:33 +09:00 committed by GitHub
parent 3ecc340168
commit 58513c1b81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 236 additions and 22 deletions

View file

@ -5,6 +5,7 @@
import { defineAsyncComponent, reactive, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { set as gtagSet, time as gtagTime } from 'vue-gtag';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
@ -14,7 +15,6 @@ import { apiUrl } from '@/config.js';
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
import { generateClientTransactionId, misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
import { set as gtagSet, time as gtagTime } from 'vue-gtag';
import { instance } from '@/instance.js';
// TODO: 他のタブと永続化されたstateを同期

View file

@ -6,24 +6,32 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<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>
<div v-show="withAvatar && !loginWithEmailAddress" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
<MkInfo v-if="message">
{{ message }}
</MkInfo>
<div v-if="!totpLogin" class="normal-signin _gaps_m">
<MkInput v-model="username" :debounce="true" :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">
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
<MkInput v-model="username" :debounce="true" :placeholder="loginWithEmailAddress ? i18n.ts.emailAddress : i18n.ts.username" type="text" :pattern="loginWithEmailAddress ? '^[a-zA-Z0-9_@.]+$' : '^[a-zA-Z0-9_]+$'" :spellcheck="false" :autocomplete="loginWithEmailAddress ? 'email webauthn' : 'username webauthn'" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
<template #prefix>
<i v-if="loginWithEmailAddress" class="ti ti-mail"></i>
<span v-else>@</span>
</template>
<template v-if="!loginWithEmailAddress" #suffix>@{{ host }}</template>
<template #caption>
<button class="_textButton" type="button" tabindex="-1" @click="loginWithEmailAddress = !loginWithEmailAddress">{{ loginWithEmailAddress ? i18n.ts.usernameLogin : i18n.ts.emailAddressLogin }}</button>
</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>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
<template #caption>
<button class="_textButton" type="button" tabindex="-1" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button>
</template>
</MkInput>
<MkCaptcha v-if="!user?.twoFactorEnabled && instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
<MkCaptcha v-if="!user?.twoFactorEnabled && instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
<MkCaptcha v-if="!user?.twoFactorEnabled && instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkCaptcha v-if="!user?.twoFactorEnabled && instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
<MkButton type="submit" large primary rounded :disabled="!user || captchaFailed || signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
<MkButton type="submit" large primary rounded :disabled="(!loginWithEmailAddress && !user) || captchaFailed || 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">
@ -70,6 +78,7 @@ import { instance } from '@/instance.js';
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
const signing = ref(false);
const loginWithEmailAddress = ref(false);
const userAbortController = ref<AbortController>();
const user = ref<Misskey.entities.UserDetailed | null>(null);
const username = ref('');
@ -119,7 +128,9 @@ const props = defineProps({
},
});
function onUsernameChange(): void {
async function onUsernameChange(): Promise<void> {
if (loginWithEmailAddress.value) return;
if (userAbortController.value) {
userAbortController.value.abort();
}
@ -168,8 +179,15 @@ async function queryKey(): Promise<void> {
});
}
function onSubmit(): void {
async function onSubmit(): Promise<void> {
signing.value = true;
if (loginWithEmailAddress.value) {
user.value = await misskeyApi('users/get-security-info', {
email: username.value,
password: password.value,
});
}
if (!totpLogin.value && user.value?.twoFactorEnabled) {
if (webAuthnSupported() && user.value.securityKeys) {
misskeyApi('signin', {

View file

@ -27,7 +27,7 @@ import { i18n } from '@/i18n.js';
withDefaults(defineProps<{
autoSet?: boolean;
message?: string,
message?: string;
}>(), {
autoSet: false,
message: '',