次の処理をsignin apiから読み取るように

This commit is contained in:
kakkokari-gtyih 2024-10-03 20:34:05 +09:00
parent 65aac75a33
commit adabff708d
10 changed files with 255 additions and 184 deletions

View File

@ -545,11 +545,6 @@ export class UserEntityService implements OnModuleInit {
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964 publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
followersVisibility: profile!.followersVisibility, followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility, followingVisibility: profile!.followingVisibility,
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
: false,
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id, id: role.id,
name: role.name, name: role.name,
@ -564,6 +559,14 @@ export class UserEntityService implements OnModuleInit {
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined, moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}), } : {}),
...(isDetailed && (isMe || iAmModerator) ? {
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
: false,
} : {}),
...(isDetailed && isMe ? { ...(isDetailed && isMe ? {
avatarId: user.avatarId, avatarId: user.avatarId,
bannerId: user.bannerId, bannerId: user.bannerId,

View File

@ -346,21 +346,6 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false, nullable: false, optional: false,
enum: ['public', 'followers', 'private'], enum: ['public', 'followers', 'private'],
}, },
twoFactorEnabled: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
usePasswordLessLogin: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
securityKeys: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
roles: { roles: {
type: 'array', type: 'array',
nullable: false, optional: false, nullable: false, optional: false,
@ -382,6 +367,18 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'string', type: 'string',
nullable: false, optional: true, nullable: false, optional: true,
}, },
twoFactorEnabled: {
type: 'boolean',
nullable: false, optional: true,
},
usePasswordLessLogin: {
type: 'boolean',
nullable: false, optional: true,
},
securityKeys: {
type: 'boolean',
nullable: false, optional: true,
},
//#region relations //#region relations
isFollowing: { isFollowing: {
type: 'boolean', type: 'boolean',

View File

@ -12,6 +12,7 @@ import type {
MiMeta, MiMeta,
SigninsRepository, SigninsRepository,
UserProfilesRepository, UserProfilesRepository,
UserSecurityKeysRepository,
UsersRepository, UsersRepository,
} from '@/models/_.js'; } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -25,9 +26,18 @@ import { CaptchaService } from '@/core/CaptchaService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { RateLimiterService } from './RateLimiterService.js'; import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js'; import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify'; import type { FastifyReply, FastifyRequest } from 'fastify';
type SigninErrorResponse = {
id: string;
next?: 'captcha' | 'password' | 'totp';
} | {
id: string;
next: 'passkey';
authRequest: PublicKeyCredentialRequestOptionsJSON;
};
@Injectable() @Injectable()
export class SigninApiService { export class SigninApiService {
constructor( constructor(
@ -43,6 +53,9 @@ export class SigninApiService {
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
@Inject(DI.userSecurityKeysRepository)
private userSecurityKeysRepository: UserSecurityKeysRepository,
@Inject(DI.signinsRepository) @Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository, private signinsRepository: SigninsRepository,
@ -60,7 +73,7 @@ export class SigninApiService {
request: FastifyRequest<{ request: FastifyRequest<{
Body: { Body: {
username: string; username: string;
password: string; password?: string;
token?: string; token?: string;
credential?: AuthenticationResponseJSON; credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string; 'hcaptcha-response'?: string;
@ -79,7 +92,7 @@ export class SigninApiService {
const password = body['password']; const password = body['password'];
const token = body['token']; const token = body['token'];
function error(status: number, error: { id: string }) { function error(status: number, error: SigninErrorResponse) {
reply.code(status); reply.code(status);
return { error }; return { error };
} }
@ -103,11 +116,6 @@ export class SigninApiService {
return; return;
} }
if (typeof password !== 'string') {
reply.code(400);
return;
}
if (token != null && typeof token !== 'string') { if (token != null && typeof token !== 'string') {
reply.code(400); reply.code(400);
return; return;
@ -132,11 +140,36 @@ export class SigninApiService {
} }
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
if (password == null) {
reply.code(403);
if (profile.twoFactorEnabled) {
return {
error: {
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
next: 'password',
},
} satisfies { error: SigninErrorResponse };
} else {
return {
error: {
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
next: 'captcha',
},
} satisfies { error: SigninErrorResponse };
}
}
if (typeof password !== 'string') {
reply.code(400);
return;
}
// Compare password // Compare password
const same = await bcrypt.compare(password, profile.password!); const same = await bcrypt.compare(password, profile.password!);
const fail = async (status?: number, failure?: { id: string }) => { const fail = async (status?: number, failure?: SigninErrorResponse) => {
// Append signin history // Append signin history
await this.signinsRepository.insert({ await this.signinsRepository.insert({
id: this.idService.gen(), id: this.idService.gen(),
@ -217,7 +250,7 @@ export class SigninApiService {
id: '93b86c4b-72f9-40eb-9815-798928603d1e', id: '93b86c4b-72f9-40eb-9815-798928603d1e',
}); });
} }
} else { } else if (securityKeysAvailable) {
if (!same && !profile.usePasswordLessLogin) { if (!same && !profile.usePasswordLessLogin) {
return await fail(403, { return await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
@ -226,8 +259,28 @@ export class SigninApiService {
const authRequest = await this.webAuthnService.initiateAuthentication(user.id); const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
reply.code(200); reply.code(403);
return authRequest; return {
error: {
id: '06e661b9-8146-4ae3-bde5-47138c0ae0c4',
next: 'passkey',
authRequest,
},
} satisfies { error: SigninErrorResponse };
} else {
if (!same || !profile.twoFactorEnabled) {
return await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
});
} else {
reply.code(403);
return {
error: {
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
next: 'totp',
},
} satisfies { error: SigninErrorResponse };
}
} }
// never get here // never get here
} }

View File

@ -22,7 +22,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill'; import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -32,7 +31,6 @@ import MkButton from '@/components/MkButton.vue';
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill'; import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
const props = defineProps<{ const props = defineProps<{
user: Misskey.entities.UserDetailed;
credentialRequest: CredentialRequestOptions; credentialRequest: CredentialRequestOptions;
isPerformingPasswordlessLogin?: boolean; isPerformingPasswordlessLogin?: boolean;
}>(); }>();

View File

@ -23,14 +23,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput> </MkInput>
<div v-if="!user.twoFactorEnabled"> <div v-if="needCaptcha">
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/> <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/> <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
</div> </div>
<MkButton type="submit" :disabled="!user.twoFactorEnabled && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> <MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</form> </form>
</div> </div>
</div> </div>
@ -62,6 +62,7 @@ import MkCaptcha from '@/components/MkCaptcha.vue';
const props = defineProps<{ const props = defineProps<{
user: Misskey.entities.UserDetailed; user: Misskey.entities.UserDetailed;
needCaptcha: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -82,10 +83,11 @@ const turnstileResponse = ref<string | null>(null);
const captchaFailed = computed((): boolean => { const captchaFailed = computed((): boolean => {
return ( return (
instance.enableHcaptcha && !hCaptchaResponse.value || (instance.enableHcaptcha && !hCaptchaResponse.value) ||
instance.enableMcaptcha && !mCaptchaResponse.value || (instance.enableMcaptcha && !mCaptchaResponse.value) ||
instance.enableRecaptcha && !reCaptchaResponse.value || (instance.enableRecaptcha && !reCaptchaResponse.value) ||
instance.enableTurnstile && !turnstileResponse.value); (instance.enableTurnstile && !turnstileResponse.value)
);
}); });
function resetPassword(): void { function resetPassword(): void {

View File

@ -32,6 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="passwordPageEl" ref="passwordPageEl"
:user="userInfo!" :user="userInfo!"
:needCaptcha="needCaptcha"
@passwordSubmitted="onPasswordSubmitted" @passwordSubmitted="onPasswordSubmitted"
/> />
@ -49,7 +50,6 @@ SPDX-License-Identifier: AGPL-3.0-only
v-else-if="page === 'passkey'" v-else-if="page === 'passkey'"
key="passkey" key="passkey"
:user="userInfo!"
:credentialRequest="credentialRequest!" :credentialRequest="credentialRequest!"
:isPerformingPasswordlessLogin="doingPasskeyFromInputPage" :isPerformingPasswordlessLogin="doingPasskeyFromInputPage"
@ -64,14 +64,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, shallowRef, useTemplateRef } from 'vue'; import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import { login } from '@/account.js'; import { login } from '@/account.js';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
@ -98,9 +97,11 @@ const props = withDefaults(defineProps<{
}); });
const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input'); const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input');
const passwordPageEl = useTemplateRef('passwordPageEl');
const waiting = ref(false); const waiting = ref(false);
const passwordPageEl = useTemplateRef('passwordPageEl');
const needCaptcha = ref(false);
const userInfo = ref<null | Misskey.entities.UserDetailed>(null); const userInfo = ref<null | Misskey.entities.UserDetailed>(null);
const password = ref(''); const password = ref('');
@ -142,14 +143,11 @@ function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
emit('login', res.signinResponse); emit('login', res.signinResponse);
}).catch(onLoginFailed); }).catch(onLoginFailed);
} else if (userInfo.value != null) { } else if (userInfo.value != null) {
misskeyApi('signin', { tryLogin({
username: userInfo.value.username, username: userInfo.value.username,
password: password.value, password: password.value,
credential: credential.toJSON(), credential: credential.toJSON(),
}).then(async (res) => { });
emit('login', res);
await onLoginSucceeded(res);
}).catch(onLoginFailed);
} }
} }
@ -171,17 +169,18 @@ async function onUsernameSubmitted(username: string) {
title: i18n.ts.noSuchUser, title: i18n.ts.noSuchUser,
text: i18n.ts.signinFailed, text: i18n.ts.signinFailed,
}); });
} else if (userInfo.value.usePasswordLessLogin) { waiting.value = false;
page.value = 'passkey'; return;
} else {
page.value = 'password';
} }
waiting.value = false; await tryLogin({
username,
});
} }
async function onPasswordSubmitted(pw: PwResponse) { async function onPasswordSubmitted(pw: PwResponse) {
waiting.value = true; waiting.value = true;
password.value = pw.password;
if (userInfo.value == null) { if (userInfo.value == null) {
await os.alert({ await os.alert({
@ -189,48 +188,17 @@ async function onPasswordSubmitted(pw: PwResponse) {
title: i18n.ts.noSuchUser, title: i18n.ts.noSuchUser,
text: i18n.ts.signinFailed, text: i18n.ts.signinFailed,
}); });
waiting.value = false;
return; return;
} else { } else {
if (!userInfo.value.twoFactorEnabled) { await tryLogin({
if ( username: userInfo.value.username,
(instance.enableHcaptcha || instance.enableMcaptcha || instance.enableRecaptcha || instance.enableTurnstile) && password: pw.password,
(pw.captcha.hCaptchaResponse == null && pw.captcha.mCaptchaResponse == null && pw.captcha.reCaptchaResponse == null && pw.captcha.turnstileResponse == null) 'hcaptcha-response': pw.captcha.hCaptchaResponse,
) { 'm-captcha-response': pw.captcha.mCaptchaResponse,
// 2FACAPTCHACAPTCHA 'g-recaptcha-response': pw.captcha.reCaptchaResponse,
onLoginFailed(); 'turnstile-response': pw.captcha.turnstileResponse,
waiting.value = false; });
return;
} else {
await misskeyApi('signin', {
username: userInfo.value.username,
password: pw.password,
'h-captcha-response': pw.captcha.hCaptchaResponse,
'm-captcha-response': pw.captcha.mCaptchaResponse,
'g-recaptcha-response': pw.captcha.reCaptchaResponse,
'turnstile-response': pw.captcha.turnstileResponse,
}).then(async (res) => {
emit('login', res);
await onLoginSucceeded(res);
}).catch(onLoginFailed);
}
} else if (userInfo.value.securityKeys) {
password.value = pw.password;
await misskeyApi('signin', {
username: userInfo.value.username,
password: pw.password,
}).then((res) => {
credentialRequest.value = parseRequestOptionsFromJSON({
publicKey: res,
});
page.value = 'passkey';
waiting.value = false;
}).catch(onLoginFailed);
} else {
password.value = pw.password;
page.value = 'totp';
waiting.value = false;
}
} }
} }
@ -243,19 +211,37 @@ async function onTotpSubmitted(token: string) {
title: i18n.ts.noSuchUser, title: i18n.ts.noSuchUser,
text: i18n.ts.signinFailed, text: i18n.ts.signinFailed,
}); });
waiting.value = false;
return; return;
} else { } else {
await misskeyApi('signin', { await tryLogin({
username: userInfo.value.username, username: userInfo.value.username,
password: password.value, password: password.value,
token, token,
}).then(async (res) => { });
emit('login', res);
await onLoginSucceeded(res);
}).catch(onLoginFailed);
} }
} }
async function tryLogin(req: Partial<Misskey.entities.SigninRequest>): Promise<Misskey.entities.SigninResponse> {
if (userInfo.value == null) {
throw new Error('No such user');
}
const _req = {
username: userInfo.value.username,
...req,
};
return await misskeyApi('signin', _req).then(async (res) => {
emit('login', res);
await onLoginSucceeded(res);
return res;
}).catch((err) => {
onLoginFailed(err);
return Promise.reject(err);
});
}
async function onLoginSucceeded(res: Misskey.entities.SigninResponse) { async function onLoginSucceeded(res: Misskey.entities.SigninResponse) {
if (props.autoSet) { if (props.autoSet) {
await login(res.i); await login(res.i);
@ -265,92 +251,128 @@ async function onLoginSucceeded(res: Misskey.entities.SigninResponse) {
function onLoginFailed(err?: any): void { function onLoginFailed(err?: any): void {
const id = err?.id ?? null; const id = err?.id ?? null;
switch (id) { if (typeof err === 'object' && 'next' in err) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': { switch (err.next) {
os.alert({ case 'captcha': {
type: 'error', page.value = 'password';
title: i18n.ts.loginFailed, break;
text: i18n.ts.noSuchUser, }
}); case 'password': {
break; page.value = 'password';
break;
}
case 'totp': {
page.value = 'totp';
break;
}
case 'passkey': {
if (webAuthnSupported() && 'authRequest' in err) {
credentialRequest.value = parseRequestOptionsFromJSON({
publicKey: err.authRequest,
});
page.value = 'passkey';
} else {
page.value = 'totp';
}
break;
}
} }
case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': { } else {
os.alert({ switch (id) {
type: 'error', case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
title: i18n.ts.loginFailed, os.alert({
text: i18n.ts.incorrectPassword, type: 'error',
}); title: i18n.ts.loginFailed,
break; text: i18n.ts.noSuchUser,
} });
case 'e03a5f46-d309-4865-9b69-56282d94e1eb': { break;
showSuspendedDialog(); }
break; case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
} os.alert({
case '22d05606-fbcf-421a-a2db-b32610dcfd1b': { type: 'error',
os.alert({ title: i18n.ts.loginFailed,
type: 'error', text: i18n.ts.incorrectPassword,
title: i18n.ts.loginFailed, });
text: i18n.ts.rateLimitExceeded, break;
}); }
break; case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
} showSuspendedDialog();
case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': { break;
os.alert({ }
type: 'error', case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
title: i18n.ts.loginFailed, os.alert({
text: i18n.ts.incorrectTotp, type: 'error',
}); title: i18n.ts.loginFailed,
break; text: i18n.ts.rateLimitExceeded,
} });
case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': { break;
os.alert({ }
type: 'error', case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': {
title: i18n.ts.loginFailed, os.alert({
text: i18n.ts.unknownWebAuthnKey, type: 'error',
}); title: i18n.ts.loginFailed,
break; text: i18n.ts.incorrectTotp,
} });
case '93b86c4b-72f9-40eb-9815-798928603d1e': { break;
os.alert({ }
type: 'error', case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
title: i18n.ts.loginFailed, os.alert({
text: i18n.ts.passkeyVerificationFailed, type: 'error',
}); title: i18n.ts.loginFailed,
break; text: i18n.ts.unknownWebAuthnKey,
} });
case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': { break;
os.alert({ }
type: 'error', case '93b86c4b-72f9-40eb-9815-798928603d1e': {
title: i18n.ts.loginFailed, os.alert({
text: i18n.ts.passkeyVerificationFailed, type: 'error',
}); title: i18n.ts.loginFailed,
break; text: i18n.ts.passkeyVerificationFailed,
} });
case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': { break;
os.alert({ }
type: 'error', case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
title: i18n.ts.loginFailed, os.alert({
text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled, type: 'error',
}); title: i18n.ts.loginFailed,
break; text: i18n.ts.passkeyVerificationFailed,
} });
default: { break;
console.error(err); }
os.alert({ case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': {
type: 'error', os.alert({
title: i18n.ts.loginFailed, type: 'error',
text: JSON.stringify(err), title: i18n.ts.loginFailed,
}); text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled,
});
break;
}
default: {
console.error(err);
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: JSON.stringify(err),
});
}
} }
} }
if (doingPasskeyFromInputPage.value === true) { if (doingPasskeyFromInputPage.value === true) {
doingPasskeyFromInputPage.value = false; doingPasskeyFromInputPage.value = false;
page.value = 'input'; page.value = 'input';
password.value = '';
} }
passwordPageEl.value?.resetCaptcha(); passwordPageEl.value?.resetCaptcha();
waiting.value = false; nextTick(() => {
waiting.value = false;
});
} }
onBeforeUnmount(() => {
password.value = '';
userInfo.value = null;
});
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@ -68,7 +68,6 @@ function onLogin(res) {
height: 100%; height: 100%;
max-height: 450px; max-height: 450px;
box-sizing: border-box; box-sizing: border-box;
text-align: center;
background: var(--panel); background: var(--panel);
border-radius: var(--radius); border-radius: var(--radius);
} }

View File

@ -3040,7 +3040,7 @@ type Signin = components['schemas']['Signin'];
// @public (undocumented) // @public (undocumented)
type SigninRequest = { type SigninRequest = {
username: string; username: string;
password: string; password?: string;
token?: string; token?: string;
credential?: AuthenticationResponseJSON; credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string | null; 'hcaptcha-response'?: string | null;

View File

@ -3782,16 +3782,13 @@ export type components = {
followingVisibility: 'public' | 'followers' | 'private'; followingVisibility: 'public' | 'followers' | 'private';
/** @enum {string} */ /** @enum {string} */
followersVisibility: 'public' | 'followers' | 'private'; followersVisibility: 'public' | 'followers' | 'private';
/** @default false */
twoFactorEnabled: boolean;
/** @default false */
usePasswordLessLogin: boolean;
/** @default false */
securityKeys: boolean;
roles: components['schemas']['RoleLite'][]; roles: components['schemas']['RoleLite'][];
followedMessage?: string | null; followedMessage?: string | null;
memo: string | null; memo: string | null;
moderationNote?: string; moderationNote?: string;
twoFactorEnabled?: boolean;
usePasswordLessLogin?: boolean;
securityKeys?: boolean;
isFollowing?: boolean; isFollowing?: boolean;
isFollowed?: boolean; isFollowed?: boolean;
hasPendingFollowRequestFromYou?: boolean; hasPendingFollowRequestFromYou?: boolean;

View File

@ -269,7 +269,7 @@ export type SignupPendingResponse = {
export type SigninRequest = { export type SigninRequest = {
username: string; username: string;
password: string; password?: string;
token?: string; token?: string;
credential?: AuthenticationResponseJSON; credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string | null; 'hcaptcha-response'?: string | null;