次の処理をsignin apiから読み取るように
This commit is contained in:
		
							parent
							
								
									65aac75a33
								
							
						
					
					
						commit
						adabff708d
					
				|  | @ -545,11 +545,6 @@ export class UserEntityService implements OnModuleInit { | |||
| 				publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
 | ||||
| 				followersVisibility: profile!.followersVisibility, | ||||
| 				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 => ({ | ||||
| 					id: role.id, | ||||
| 					name: role.name, | ||||
|  | @ -564,6 +559,14 @@ export class UserEntityService implements OnModuleInit { | |||
| 				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 ? { | ||||
| 				avatarId: user.avatarId, | ||||
| 				bannerId: user.bannerId, | ||||
|  |  | |||
|  | @ -346,21 +346,6 @@ export const packedUserDetailedNotMeOnlySchema = { | |||
| 			nullable: false, optional: false, | ||||
| 			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: { | ||||
| 			type: 'array', | ||||
| 			nullable: false, optional: false, | ||||
|  | @ -382,6 +367,18 @@ export const packedUserDetailedNotMeOnlySchema = { | |||
| 			type: 'string', | ||||
| 			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
 | ||||
| 		isFollowing: { | ||||
| 			type: 'boolean', | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ import type { | |||
| 	MiMeta, | ||||
| 	SigninsRepository, | ||||
| 	UserProfilesRepository, | ||||
| 	UserSecurityKeysRepository, | ||||
| 	UsersRepository, | ||||
| } from '@/models/_.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 { RateLimiterService } from './RateLimiterService.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'; | ||||
| 
 | ||||
| type SigninErrorResponse = { | ||||
| 	id: string; | ||||
| 	next?: 'captcha' | 'password' | 'totp'; | ||||
| } | { | ||||
| 	id: string; | ||||
| 	next: 'passkey'; | ||||
| 	authRequest: PublicKeyCredentialRequestOptionsJSON; | ||||
| }; | ||||
| 
 | ||||
| @Injectable() | ||||
| export class SigninApiService { | ||||
| 	constructor( | ||||
|  | @ -43,6 +53,9 @@ export class SigninApiService { | |||
| 		@Inject(DI.userProfilesRepository) | ||||
| 		private userProfilesRepository: UserProfilesRepository, | ||||
| 
 | ||||
| 		@Inject(DI.userSecurityKeysRepository) | ||||
| 		private userSecurityKeysRepository: UserSecurityKeysRepository, | ||||
| 
 | ||||
| 		@Inject(DI.signinsRepository) | ||||
| 		private signinsRepository: SigninsRepository, | ||||
| 
 | ||||
|  | @ -60,7 +73,7 @@ export class SigninApiService { | |||
| 		request: FastifyRequest<{ | ||||
| 			Body: { | ||||
| 				username: string; | ||||
| 				password: string; | ||||
| 				password?: string; | ||||
| 				token?: string; | ||||
| 				credential?: AuthenticationResponseJSON; | ||||
| 				'hcaptcha-response'?: string; | ||||
|  | @ -79,7 +92,7 @@ export class SigninApiService { | |||
| 		const password = body['password']; | ||||
| 		const token = body['token']; | ||||
| 
 | ||||
| 		function error(status: number, error: { id: string }) { | ||||
| 		function error(status: number, error: SigninErrorResponse) { | ||||
| 			reply.code(status); | ||||
| 			return { error }; | ||||
| 		} | ||||
|  | @ -103,11 +116,6 @@ export class SigninApiService { | |||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		if (typeof password !== 'string') { | ||||
| 			reply.code(400); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		if (token != null && typeof token !== 'string') { | ||||
| 			reply.code(400); | ||||
| 			return; | ||||
|  | @ -132,11 +140,36 @@ export class SigninApiService { | |||
| 		} | ||||
| 
 | ||||
| 		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
 | ||||
| 		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
 | ||||
| 			await this.signinsRepository.insert({ | ||||
| 				id: this.idService.gen(), | ||||
|  | @ -217,7 +250,7 @@ export class SigninApiService { | |||
| 					id: '93b86c4b-72f9-40eb-9815-798928603d1e', | ||||
| 				}); | ||||
| 			} | ||||
| 		} else { | ||||
| 		} else if (securityKeysAvailable) { | ||||
| 			if (!same && !profile.usePasswordLessLogin) { | ||||
| 				return await fail(403, { | ||||
| 					id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', | ||||
|  | @ -226,8 +259,28 @@ export class SigninApiService { | |||
| 
 | ||||
| 			const authRequest = await this.webAuthnService.initiateAuthentication(user.id); | ||||
| 
 | ||||
| 			reply.code(200); | ||||
| 			return authRequest; | ||||
| 			reply.code(403); | ||||
| 			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
 | ||||
| 	} | ||||
|  |  | |||
|  | @ -22,7 +22,6 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { ref, onMounted } from 'vue'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill'; | ||||
| 
 | ||||
| import { i18n } from '@/i18n.js'; | ||||
|  | @ -32,7 +31,6 @@ import MkButton from '@/components/MkButton.vue'; | |||
| import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	user: Misskey.entities.UserDetailed; | ||||
| 	credentialRequest: CredentialRequestOptions; | ||||
| 	isPerformingPasswordlessLogin?: boolean; | ||||
| }>(); | ||||
|  |  | |||
|  | @ -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> | ||||
| 			</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.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.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> | ||||
| 			</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> | ||||
| 	</div> | ||||
| </div> | ||||
|  | @ -62,6 +62,7 @@ import MkCaptcha from '@/components/MkCaptcha.vue'; | |||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	user: Misskey.entities.UserDetailed; | ||||
| 	needCaptcha: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
|  | @ -82,10 +83,11 @@ const turnstileResponse = ref<string | null>(null); | |||
| 
 | ||||
| const captchaFailed = computed((): boolean => { | ||||
| 	return ( | ||||
| 		instance.enableHcaptcha && !hCaptchaResponse.value || | ||||
| 		instance.enableMcaptcha && !mCaptchaResponse.value || | ||||
| 		instance.enableRecaptcha && !reCaptchaResponse.value || | ||||
| 		instance.enableTurnstile && !turnstileResponse.value); | ||||
| 		(instance.enableHcaptcha && !hCaptchaResponse.value) || | ||||
| 		(instance.enableMcaptcha && !mCaptchaResponse.value) || | ||||
| 		(instance.enableRecaptcha && !reCaptchaResponse.value) || | ||||
| 		(instance.enableTurnstile && !turnstileResponse.value) | ||||
| 	); | ||||
| }); | ||||
| 
 | ||||
| function resetPassword(): void { | ||||
|  |  | |||
|  | @ -32,6 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 			ref="passwordPageEl" | ||||
| 
 | ||||
| 			:user="userInfo!" | ||||
| 			:needCaptcha="needCaptcha" | ||||
| 
 | ||||
| 			@passwordSubmitted="onPasswordSubmitted" | ||||
| 		/> | ||||
|  | @ -49,7 +50,6 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 			v-else-if="page === 'passkey'" | ||||
| 			key="passkey" | ||||
| 
 | ||||
| 			:user="userInfo!" | ||||
| 			:credentialRequest="credentialRequest!" | ||||
| 			:isPerformingPasswordlessLogin="doingPasskeyFromInputPage" | ||||
| 
 | ||||
|  | @ -64,14 +64,13 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| </template> | ||||
| 
 | ||||
| <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 { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; | ||||
| 
 | ||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | ||||
| import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; | ||||
| import { login } from '@/account.js'; | ||||
| import { instance } from '@/instance.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import * as os from '@/os.js'; | ||||
| 
 | ||||
|  | @ -98,9 +97,11 @@ const props = withDefaults(defineProps<{ | |||
| }); | ||||
| 
 | ||||
| const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input'); | ||||
| const passwordPageEl = useTemplateRef('passwordPageEl'); | ||||
| const waiting = ref(false); | ||||
| 
 | ||||
| const passwordPageEl = useTemplateRef('passwordPageEl'); | ||||
| const needCaptcha = ref(false); | ||||
| 
 | ||||
| const userInfo = ref<null | Misskey.entities.UserDetailed>(null); | ||||
| const password = ref(''); | ||||
| 
 | ||||
|  | @ -142,14 +143,11 @@ function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void { | |||
| 			emit('login', res.signinResponse); | ||||
| 		}).catch(onLoginFailed); | ||||
| 	} else if (userInfo.value != null) { | ||||
| 		misskeyApi('signin', { | ||||
| 		tryLogin({ | ||||
| 			username: userInfo.value.username, | ||||
| 			password: password.value, | ||||
| 			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, | ||||
| 			text: i18n.ts.signinFailed, | ||||
| 		}); | ||||
| 	} else if (userInfo.value.usePasswordLessLogin) { | ||||
| 		page.value = 'passkey'; | ||||
| 	} else { | ||||
| 		page.value = 'password'; | ||||
| 		waiting.value = false; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	waiting.value = false; | ||||
| 	await tryLogin({ | ||||
| 		username, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function onPasswordSubmitted(pw: PwResponse) { | ||||
| 	waiting.value = true; | ||||
| 	password.value = pw.password; | ||||
| 
 | ||||
| 	if (userInfo.value == null) { | ||||
| 		await os.alert({ | ||||
|  | @ -189,48 +188,17 @@ async function onPasswordSubmitted(pw: PwResponse) { | |||
| 			title: i18n.ts.noSuchUser, | ||||
| 			text: i18n.ts.signinFailed, | ||||
| 		}); | ||||
| 		waiting.value = false; | ||||
| 		return; | ||||
| 	} else { | ||||
| 		if (!userInfo.value.twoFactorEnabled) { | ||||
| 			if ( | ||||
| 				(instance.enableHcaptcha || instance.enableMcaptcha || instance.enableRecaptcha || instance.enableTurnstile) && | ||||
| 				(pw.captcha.hCaptchaResponse == null && pw.captcha.mCaptchaResponse == null && pw.captcha.reCaptchaResponse == null && pw.captcha.turnstileResponse == null) | ||||
| 			) { | ||||
| 				// 2FAが無効で、CAPTCHAが有効で、かつCAPTCHAが未入力の場合 | ||||
| 				onLoginFailed(); | ||||
| 				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; | ||||
| 		} | ||||
| 		await tryLogin({ | ||||
| 			username: userInfo.value.username, | ||||
| 			password: pw.password, | ||||
| 			'hcaptcha-response': pw.captcha.hCaptchaResponse, | ||||
| 			'm-captcha-response': pw.captcha.mCaptchaResponse, | ||||
| 			'g-recaptcha-response': pw.captcha.reCaptchaResponse, | ||||
| 			'turnstile-response': pw.captcha.turnstileResponse, | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -243,19 +211,37 @@ async function onTotpSubmitted(token: string) { | |||
| 			title: i18n.ts.noSuchUser, | ||||
| 			text: i18n.ts.signinFailed, | ||||
| 		}); | ||||
| 		waiting.value = false; | ||||
| 		return; | ||||
| 	} else { | ||||
| 		await misskeyApi('signin', { | ||||
| 		await tryLogin({ | ||||
| 			username: userInfo.value.username, | ||||
| 			password: password.value, | ||||
| 			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) { | ||||
| 	if (props.autoSet) { | ||||
| 		await login(res.i); | ||||
|  | @ -265,92 +251,128 @@ async function onLoginSucceeded(res: Misskey.entities.SigninResponse) { | |||
| function onLoginFailed(err?: any): void { | ||||
| 	const id = err?.id ?? null; | ||||
| 
 | ||||
| 	switch (id) { | ||||
| 		case '6cc579cc-885d-43d8-95c2-b8c7fc963280': { | ||||
| 			os.alert({ | ||||
| 				type: 'error', | ||||
| 				title: i18n.ts.loginFailed, | ||||
| 				text: i18n.ts.noSuchUser, | ||||
| 			}); | ||||
| 			break; | ||||
| 	if (typeof err === 'object' && 'next' in err) { | ||||
| 		switch (err.next) { | ||||
| 			case 'captcha': { | ||||
| 				page.value = 'password'; | ||||
| 				break; | ||||
| 			} | ||||
| 			case 'password': { | ||||
| 				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': { | ||||
| 			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; | ||||
| 		} | ||||
| 		case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': { | ||||
| 			os.alert({ | ||||
| 				type: 'error', | ||||
| 				title: i18n.ts.loginFailed, | ||||
| 				text: i18n.ts.incorrectTotp, | ||||
| 			}); | ||||
| 			break; | ||||
| 		} | ||||
| 		case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': { | ||||
| 			os.alert({ | ||||
| 				type: 'error', | ||||
| 				title: i18n.ts.loginFailed, | ||||
| 				text: i18n.ts.unknownWebAuthnKey, | ||||
| 			}); | ||||
| 			break; | ||||
| 		} | ||||
| 		case '93b86c4b-72f9-40eb-9815-798928603d1e': { | ||||
| 			os.alert({ | ||||
| 				type: 'error', | ||||
| 				title: i18n.ts.loginFailed, | ||||
| 				text: i18n.ts.passkeyVerificationFailed, | ||||
| 			}); | ||||
| 			break; | ||||
| 		} | ||||
| 		case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': { | ||||
| 			os.alert({ | ||||
| 				type: 'error', | ||||
| 				title: i18n.ts.loginFailed, | ||||
| 				text: i18n.ts.passkeyVerificationFailed, | ||||
| 			}); | ||||
| 			break; | ||||
| 		} | ||||
| 		case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': { | ||||
| 			os.alert({ | ||||
| 				type: 'error', | ||||
| 				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), | ||||
| 			}); | ||||
| 	} else { | ||||
| 		switch (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; | ||||
| 			} | ||||
| 			case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					title: i18n.ts.loginFailed, | ||||
| 					text: i18n.ts.incorrectTotp, | ||||
| 				}); | ||||
| 				break; | ||||
| 			} | ||||
| 			case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					title: i18n.ts.loginFailed, | ||||
| 					text: i18n.ts.unknownWebAuthnKey, | ||||
| 				}); | ||||
| 				break; | ||||
| 			} | ||||
| 			case '93b86c4b-72f9-40eb-9815-798928603d1e': { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					title: i18n.ts.loginFailed, | ||||
| 					text: i18n.ts.passkeyVerificationFailed, | ||||
| 				}); | ||||
| 				break; | ||||
| 			} | ||||
| 			case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					title: i18n.ts.loginFailed, | ||||
| 					text: i18n.ts.passkeyVerificationFailed, | ||||
| 				}); | ||||
| 				break; | ||||
| 			} | ||||
| 			case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					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) { | ||||
| 		doingPasskeyFromInputPage.value = false; | ||||
| 		page.value = 'input'; | ||||
| 		password.value = ''; | ||||
| 	} | ||||
| 	passwordPageEl.value?.resetCaptcha(); | ||||
| 	waiting.value = false; | ||||
| 	nextTick(() => { | ||||
| 		waiting.value = false; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| onBeforeUnmount(() => { | ||||
| 	password.value = ''; | ||||
| 	userInfo.value = null; | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" module> | ||||
|  |  | |||
|  | @ -68,7 +68,6 @@ function onLogin(res) { | |||
| 	height: 100%; | ||||
| 	max-height: 450px; | ||||
| 	box-sizing: border-box; | ||||
| 	text-align: center; | ||||
| 	background: var(--panel); | ||||
| 	border-radius: var(--radius); | ||||
| } | ||||
|  |  | |||
|  | @ -3040,7 +3040,7 @@ type Signin = components['schemas']['Signin']; | |||
| // @public (undocumented) | ||||
| type SigninRequest = { | ||||
|     username: string; | ||||
|     password: string; | ||||
|     password?: string; | ||||
|     token?: string; | ||||
|     credential?: AuthenticationResponseJSON; | ||||
|     'hcaptcha-response'?: string | null; | ||||
|  |  | |||
|  | @ -3782,16 +3782,13 @@ export type components = { | |||
|       followingVisibility: 'public' | 'followers' | 'private'; | ||||
|       /** @enum {string} */ | ||||
|       followersVisibility: 'public' | 'followers' | 'private'; | ||||
|       /** @default false */ | ||||
|       twoFactorEnabled: boolean; | ||||
|       /** @default false */ | ||||
|       usePasswordLessLogin: boolean; | ||||
|       /** @default false */ | ||||
|       securityKeys: boolean; | ||||
|       roles: components['schemas']['RoleLite'][]; | ||||
|       followedMessage?: string | null; | ||||
|       memo: string | null; | ||||
|       moderationNote?: string; | ||||
|       twoFactorEnabled?: boolean; | ||||
|       usePasswordLessLogin?: boolean; | ||||
|       securityKeys?: boolean; | ||||
|       isFollowing?: boolean; | ||||
|       isFollowed?: boolean; | ||||
|       hasPendingFollowRequestFromYou?: boolean; | ||||
|  |  | |||
|  | @ -269,7 +269,7 @@ export type SignupPendingResponse = { | |||
| 
 | ||||
| export type SigninRequest = { | ||||
| 	username: string; | ||||
| 	password: string; | ||||
| 	password?: string; | ||||
| 	token?: string; | ||||
| 	credential?: AuthenticationResponseJSON; | ||||
| 	'hcaptcha-response'?: string | null; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue