fix: 登録時のエラーを詳細に表示するように

This commit is contained in:
kakkokari-gtyih 2024-11-09 17:50:46 +09:00
parent e0a83e9c9e
commit 9ee5465427
5 changed files with 197 additions and 33 deletions

30
locales/index.d.ts vendored
View File

@ -7152,6 +7152,36 @@ export interface Locale extends ILocale {
* ({email})30 * ({email})30
*/ */
"emailSent": ParameterizedString<"email">; "emailSent": ParameterizedString<"email">;
"_errors": {
/**
*
*/
"emailInvalid": string;
/**
* 使
*/
"emailNotAllowed": string;
/**
*
*/
"invitationCodeInvalid": string;
/**
* 使
*/
"invitationCodeNotFoundOrUsed": string;
/**
*
*/
"invitationCodeExpired": string;
/**
* 使
*/
"usernameAlreadyUsed": string;
/**
*
*/
"usernameNotAllowed": string;
};
}; };
"_accountDelete": { "_accountDelete": {
/** /**

View File

@ -1853,6 +1853,14 @@ _signup:
almostThere: "ほとんど完了です" almostThere: "ほとんど完了です"
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。" emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。"
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。" emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。"
_errors:
emailInvalid: "メールアドレスが入力されていないか、不正な値です。"
emailNotAllowed: "このメールアドレスを使用して登録することはできません。"
invitationCodeInvalid: "招待コードが入力されていないか、不正な値です。"
invitationCodeNotFoundOrUsed: "招待コードが見つからなかったか、既に使用されています。"
invitationCodeExpired: "招待コードの有効期限が切れています。"
usernameAlreadyUsed: "このユーザー名は既に使用されています。"
usernameNotAllowed: "このユーザー名で登録することはできません。"
_accountDelete: _accountDelete:
accountDelete: "アカウントの削除" accountDelete: "アカウントの削除"

View File

@ -18,6 +18,7 @@ import generateUserToken from '@/misc/generate-native-user-token.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import UsersChart from '@/core/chart/charts/users.js'; import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { UserService } from '@/core/UserService.js'; import { UserService } from '@/core/UserService.js';
@ -59,13 +60,13 @@ export class SignupService {
// Validate username // Validate username
if (!this.userEntityService.validateLocalUsername(username)) { if (!this.userEntityService.validateLocalUsername(username)) {
throw new Error('INVALID_USERNAME'); throw new IdentifiableError('be85f7f4-1dd3-4107-bce4-07cdb0cbb0c3', 'INVALID_USERNAME');
} }
if (password != null && passwordHash == null) { if (password != null && passwordHash == null) {
// Validate password // Validate password
if (!this.userEntityService.validatePassword(password)) { if (!this.userEntityService.validatePassword(password)) {
throw new Error('INVALID_PASSWORD'); throw new IdentifiableError('d5f4959c-a881-41e8-b755-718fbf161258', 'INVALID_PASSWORD');
} }
// Generate hash of password // Generate hash of password
@ -78,12 +79,12 @@ export class SignupService {
// Check username duplication // Check username duplication
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new Error('DUPLICATED_USERNAME'); throw new IdentifiableError('d412327a-1bd7-4b70-a982-7eec000db8fc', 'DUPLICATED_USERNAME');
} }
// Check deleted username duplication // Check deleted username duplication
if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) { if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) {
throw new Error('USED_USERNAME'); throw new IdentifiableError('dd5f52be-2c95-4c39-ba45-dc2d74b3dd81', 'USED_USERNAME');
} }
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent(); const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
@ -91,7 +92,7 @@ export class SignupService {
if (!opts.ignorePreservedUsernames && !isTheFirstUser) { if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) { if (isPreserved) {
throw new Error('USED_USERNAME'); throw new IdentifiableError('adad138b-9c63-41bf-931e-6b050fd3bb8d', 'DENIED_USERNAME');
} }
} }
@ -121,7 +122,9 @@ export class SignupService {
host: IsNull(), host: IsNull(),
}); });
if (exist) throw new Error(' the username is already used'); if (exist) {
throw new IdentifiableError('d412327a-1bd7-4b70-a982-7eec000db8fc', 'DUPLICATED_USERNAME');
}
account = await transactionalEntityManager.save(new MiUser({ account = await transactionalEntityManager.save(new MiUser({
id: this.idService.gen(), id: this.idService.gen(),

View File

@ -16,6 +16,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { EmailService } from '@/core/EmailService.js'; import { EmailService } from '@/core/EmailService.js';
import { MiLocalUser } from '@/models/User.js'; import { MiLocalUser } from '@/models/User.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { SigninService } from './SigninService.js'; import { SigninService } from './SigninService.js';
@ -74,6 +75,11 @@ export class SignupApiService {
) { ) {
const body = request.body; const body = request.body;
function error(status: number, error: { id: string, [x: string]: string }) {
reply.code(status);
return { error };
}
// Verify *Captcha // Verify *Captcha
// ただしテスト時はこの機構は障害となるため無効にする // ただしテスト時はこの機構は障害となるため無効にする
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
@ -115,24 +121,30 @@ export class SignupApiService {
const emailAddress = body['emailAddress']; const emailAddress = body['emailAddress'];
if (this.meta.emailRequiredForSignup) { if (this.meta.emailRequiredForSignup) {
if (emailAddress == null || typeof emailAddress !== 'string') { if (emailAddress == null || typeof emailAddress !== 'string' || emailAddress === '') {
reply.code(400); return error(400, {
return; id: '33b104c9-2f22-4640-b27a-40979bde4a77',
message: 'Email address is not present or is invalid.',
});
} }
const res = await this.emailService.validateEmailForAccount(emailAddress); const res = await this.emailService.validateEmailForAccount(emailAddress);
if (!res.available) { if (!res.available) {
reply.code(400); return error(400, {
return; id: '75ece55a-7869-49b1-b796-c0634224fcae',
message: 'You cannot use this email address.',
});
} }
} }
let ticket: MiRegistrationTicket | null = null; let ticket: MiRegistrationTicket | null = null;
if (this.meta.disableRegistration) { if (this.meta.disableRegistration) {
if (invitationCode == null || typeof invitationCode !== 'string') { if (invitationCode == null || typeof invitationCode !== 'string' || invitationCode === '') {
reply.code(400); return error(400, {
return; id: 'c8324ccf-7153-47a0-90a3-682eb06ba10d',
message: 'Invitation code is not present or is invalid.',
});
} }
ticket = await this.registrationTicketsRepository.findOneBy({ ticket = await this.registrationTicketsRepository.findOneBy({
@ -140,47 +152,69 @@ export class SignupApiService {
}); });
if (ticket == null || ticket.usedById != null) { if (ticket == null || ticket.usedById != null) {
reply.code(400); return error(400, {
return; id: 'f08118af-8358-441c-b992-b5b0bbd337d2',
message: 'Invitation code not found or already used.',
});
} }
if (ticket.expiresAt && ticket.expiresAt < new Date()) { if (ticket.expiresAt && ticket.expiresAt < new Date()) {
reply.code(400); return error(400, {
return; id: '3277822c-29dd-4bc9-ad57-47af702f78b8',
message: 'Invitation code has expired.',
});
} }
// メアド認証が有効の場合 // メアド認証が有効の場合
if (this.meta.emailRequiredForSignup) { if (this.meta.emailRequiredForSignup) {
// メアド認証済みならエラー // メアド認証済みならエラー
if (ticket.usedBy) { if (ticket.usedBy) {
reply.code(400); return error(400, {
return; id: 'f08118af-8358-441c-b992-b5b0bbd337d2',
message: 'Invitation code not found or already used.',
});
} }
// 認証しておらず、メール送信から30分以内ならエラー // 認証しておらず、メール送信から30分以内ならエラー
if (ticket.usedAt && ticket.usedAt.getTime() + (1000 * 60 * 30) > Date.now()) { if (ticket.usedAt && ticket.usedAt.getTime() + (1000 * 60 * 30) > Date.now()) {
reply.code(400); return error(400, {
return; id: 'f08118af-8358-441c-b992-b5b0bbd337d2',
message: 'Invitation code not found or already used.',
});
} }
} else if (ticket.usedAt) { } else if (ticket.usedAt) {
reply.code(400); return error(400, {
return; id: 'f08118af-8358-441c-b992-b5b0bbd337d2',
message: 'Invitation code not found or already used.',
});
} }
} }
if (this.meta.emailRequiredForSignup) { if (this.meta.emailRequiredForSignup) {
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); return error(400, {
id: '9c20a0c3-c9e7-418f-8058-767f4e345bd4',
code: 'DUPLICATED_USERNAME',
message: 'Username already exists.',
});
} }
// Check deleted username duplication // Check deleted username duplication
if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) { if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) {
throw new FastifyReplyError(400, 'USED_USERNAME'); return error(400, {
id: '90e84f35-599a-468c-b420-98139fe9f988',
code: 'USED_USERNAME',
message: 'Username was previously used by another user.',
});
} }
const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) { if (isPreserved) {
throw new FastifyReplyError(400, 'DENIED_USERNAME'); return error(400, {
id: 'e26cbcc3-7a0c-4cf4-988f-533f56ca72bf',
code: 'DENIED_USERNAME',
message: 'This username is not allowed.',
});
} }
const code = secureRndstr(16, { chars: L_CHARS }); const code = secureRndstr(16, { chars: L_CHARS });
@ -236,6 +270,41 @@ export class SignupApiService {
token: secret, token: secret,
}; };
} catch (err) { } catch (err) {
if (err instanceof IdentifiableError) {
switch (err.id) {
case 'be85f7f4-1dd3-4107-bce4-07cdb0cbb0c3':
return error(400, {
id: 'f6bff66c-a3f9-48b8-b56c-3c3ffc49bfdd',
code: 'INVALID_USERNAME',
message: 'Username is invalid.',
});
case 'd5f4959c-a881-41e8-b755-718fbf161258':
return error(400, {
id: '6dffa54e-9f5f-4c07-9662-e5c75ab63ee5',
code: 'INVALID_PASSWORD',
message: 'Password is invalid.',
});
case 'd412327a-1bd7-4b70-a982-7eec000db8fc':
return error(400, {
id: '9c20a0c3-c9e7-418f-8058-767f4e345bd4',
code: 'DUPLICATED_USERNAME',
message: 'Username already exists.',
});
case 'dd5f52be-2c95-4c39-ba45-dc2d74b3dd81':
return error(400, {
id: '90e84f35-599a-468c-b420-98139fe9f988',
code: 'USED_USERNAME',
message: 'Username was previously used by another user.',
});
case 'adad138b-9c63-41bf-931e-6b050fd3bb8d':
return error(400, {
id: 'e26cbcc3-7a0c-4cf4-988f-533f56ca72bf',
code: 'DENIED_USERNAME',
message: 'This username is not allowed.',
});
}
}
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString()); throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
} }
} }
@ -247,11 +316,20 @@ export class SignupApiService {
const code = body['code']; const code = body['code'];
function error(status: number, error: { id: string, [x: string]: string }) {
reply.code(status);
return { error };
}
try { try {
const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code }); const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code });
if (this.idService.parse(pendingUser.id).date.getTime() + (1000 * 60 * 30) < Date.now()) { if (this.idService.parse(pendingUser.id).date.getTime() + (1000 * 60 * 30) < Date.now()) {
throw new FastifyReplyError(400, 'EXPIRED'); return error(400, {
id: 'e8b5b1ce-c7fe-456f-b06b-467cd16c060f',
code: 'EXPIRED',
message: 'This link has expired.',
});
} }
const { account, secret } = await this.signupService.signup({ const { account, secret } = await this.signupService.signup({

View File

@ -268,6 +268,7 @@ async function onSubmit(): Promise<void> {
const res = await fetch(`${config.apiUrl}/signup`, { const res = await fetch(`${config.apiUrl}/signup`, {
method: 'POST', method: 'POST',
redirect: 'error',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -278,14 +279,14 @@ async function onSubmit(): Promise<void> {
}); });
if (res) { if (res) {
if (res.status === 204 || instance.emailRequiredForSignup) { if (res.status === 204 && instance.emailRequiredForSignup) {
os.alert({ os.alert({
type: 'success', type: 'success',
title: i18n.ts._signup.almostThere, title: i18n.ts._signup.almostThere,
text: i18n.tsx._signup.emailSent({ email: email.value }), text: i18n.tsx._signup.emailSent({ email: email.value }),
}); });
emit('signupEmailPending'); emit('signupEmailPending');
} else { } else if (res.status === 200) {
const resJson = (await res.json()) as Misskey.entities.SignupResponse; const resJson = (await res.json()) as Misskey.entities.SignupResponse;
if (_DEV_) console.log(resJson); if (_DEV_) console.log(resJson);
@ -294,23 +295,67 @@ async function onSubmit(): Promise<void> {
if (props.autoSet) { if (props.autoSet) {
await login(resJson.token); await login(resJson.token);
} }
} else {
const resJson = (await res.json()) as {
error?: {
id: string;
code?: string;
message?: string;
};
};
let message: string | null = null;
if (resJson.error != null) {
if (resJson.error.message != null) {
message = resJson.error.message;
} else if (resJson.error.id != null) {
message = i18n.ts.somethingHappened + '\n' + resJson.error.id;
}
switch (resJson.error.id) {
case '33b104c9-2f22-4640-b27a-40979bde4a77':
message = i18n.ts._signup._errors.emailInvalid;
break;
case '75ece55a-7869-49b1-b796-c0634224fcae':
message = i18n.ts._signup._errors.emailNotAllowed;
break;
case 'c8324ccf-7153-47a0-90a3-682eb06ba10d':
message = i18n.ts._signup._errors.invitationCodeInvalid;
break;
case '3277822c-29dd-4bc9-ad57-47af702f78b8':
message = i18n.ts._signup._errors.invitationCodeExpired;
break;
case 'f08118af-8358-441c-b992-b5b0bbd337d2':
message = i18n.ts._signup._errors.invitationCodeNotFoundOrUsed;
break;
case '9c20a0c3-c9e7-418f-8058-767f4e345bd4':
case '90e84f35-599a-468c-b420-98139fe9f988':
message = i18n.ts._signup._errors.usernameAlreadyUsed;
break;
case 'e26cbcc3-7a0c-4cf4-988f-533f56ca72bf':
message = i18n.ts._signup._errors.usernameNotAllowed;
break;
}
}
onSignupApiError(message ? { title: i18n.ts.somethingHappened, text: message } : undefined);
} }
} }
submitting.value = false; submitting.value = false;
} }
function onSignupApiError() { function onSignupApiError(message?: { title?: string; text: string }): void {
submitting.value = false; submitting.value = false;
hcaptcha.value?.reset?.(); hcaptcha.value?.reset?.();
mcaptcha.value?.reset?.(); mcaptcha.value?.reset?.();
recaptcha.value?.reset?.(); recaptcha.value?.reset?.();
turnstile.value?.reset?.(); turnstile.value?.reset?.();
testcaptcha.value?.reset?.(); testcaptcha.value?.reset?.();
os.alert({ os.alert({
type: 'error', type: 'error',
text: i18n.ts.somethingHappened, ...(message ?? { text: i18n.ts.somethingHappened }),
}); });
} }
</script> </script>