diff --git a/locales/index.d.ts b/locales/index.d.ts index 440f24ac84..c4bab73d8a 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -7152,6 +7152,36 @@ export interface Locale extends ILocale { * 入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。 */ "emailSent": ParameterizedString<"email">; + "_errors": { + /** + * メールアドレスが入力されていないか、不正な値です。 + */ + "emailInvalid": string; + /** + * このメールアドレスを使用して登録することはできません。 + */ + "emailNotAllowed": string; + /** + * 招待コードが入力されていないか、不正な値です。 + */ + "invitationCodeInvalid": string; + /** + * 招待コードが見つからなかったか、既に使用されています。 + */ + "invitationCodeNotFoundOrUsed": string; + /** + * 招待コードの有効期限が切れています。 + */ + "invitationCodeExpired": string; + /** + * このユーザー名は既に使用されています。 + */ + "usernameAlreadyUsed": string; + /** + * このユーザー名で登録することはできません。 + */ + "usernameNotAllowed": string; + }; }; "_accountDelete": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5d8e1a5e72..066505bdf6 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1853,6 +1853,14 @@ _signup: almostThere: "ほとんど完了です" emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。" emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。" + _errors: + emailInvalid: "メールアドレスが入力されていないか、不正な値です。" + emailNotAllowed: "このメールアドレスを使用して登録することはできません。" + invitationCodeInvalid: "招待コードが入力されていないか、不正な値です。" + invitationCodeNotFoundOrUsed: "招待コードが見つからなかったか、既に使用されています。" + invitationCodeExpired: "招待コードの有効期限が切れています。" + usernameAlreadyUsed: "このユーザー名は既に使用されています。" + usernameNotAllowed: "このユーザー名で登録することはできません。" _accountDelete: accountDelete: "アカウントの削除" diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 3865392b7f..87fce599b4 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -18,6 +18,7 @@ import generateUserToken from '@/misc/generate-native-user-token.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import { bindThis } from '@/decorators.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import UsersChart from '@/core/chart/charts/users.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserService } from '@/core/UserService.js'; @@ -59,13 +60,13 @@ export class SignupService { // Validate 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) { // Validate 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 @@ -78,12 +79,12 @@ export class SignupService { // Check username duplication 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 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(); @@ -91,7 +92,7 @@ export class SignupService { if (!opts.ignorePreservedUsernames && !isTheFirstUser) { const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); 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(), }); - 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({ id: this.idService.gen(), diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 3ec5e5d3e6..ab8cfd1e90 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -16,6 +16,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { EmailService } from '@/core/EmailService.js'; import { MiLocalUser } from '@/models/User.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { bindThis } from '@/decorators.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { SigninService } from './SigninService.js'; @@ -74,6 +75,11 @@ export class SignupApiService { ) { const body = request.body; + function error(status: number, error: { id: string, [x: string]: string }) { + reply.code(status); + return { error }; + } + // Verify *Captcha // ただしテスト時はこの機構は障害となるため無効にする if (process.env.NODE_ENV !== 'test') { @@ -115,24 +121,30 @@ export class SignupApiService { const emailAddress = body['emailAddress']; if (this.meta.emailRequiredForSignup) { - if (emailAddress == null || typeof emailAddress !== 'string') { - reply.code(400); - return; + if (emailAddress == null || typeof emailAddress !== 'string' || emailAddress === '') { + return error(400, { + id: '33b104c9-2f22-4640-b27a-40979bde4a77', + message: 'Email address is not present or is invalid.', + }); } const res = await this.emailService.validateEmailForAccount(emailAddress); if (!res.available) { - reply.code(400); - return; + return error(400, { + id: '75ece55a-7869-49b1-b796-c0634224fcae', + message: 'You cannot use this email address.', + }); } } let ticket: MiRegistrationTicket | null = null; if (this.meta.disableRegistration) { - if (invitationCode == null || typeof invitationCode !== 'string') { - reply.code(400); - return; + if (invitationCode == null || typeof invitationCode !== 'string' || invitationCode === '') { + return error(400, { + id: 'c8324ccf-7153-47a0-90a3-682eb06ba10d', + message: 'Invitation code is not present or is invalid.', + }); } ticket = await this.registrationTicketsRepository.findOneBy({ @@ -140,47 +152,69 @@ export class SignupApiService { }); if (ticket == null || ticket.usedById != null) { - reply.code(400); - return; + return error(400, { + id: 'f08118af-8358-441c-b992-b5b0bbd337d2', + message: 'Invitation code not found or already used.', + }); } if (ticket.expiresAt && ticket.expiresAt < new Date()) { - reply.code(400); - return; + return error(400, { + id: '3277822c-29dd-4bc9-ad57-47af702f78b8', + message: 'Invitation code has expired.', + }); } // メアド認証が有効の場合 if (this.meta.emailRequiredForSignup) { // メアド認証済みならエラー if (ticket.usedBy) { - reply.code(400); - return; + return error(400, { + id: 'f08118af-8358-441c-b992-b5b0bbd337d2', + message: 'Invitation code not found or already used.', + }); } // 認証しておらず、メール送信から30分以内ならエラー if (ticket.usedAt && ticket.usedAt.getTime() + (1000 * 60 * 30) > Date.now()) { - reply.code(400); - return; + return error(400, { + id: 'f08118af-8358-441c-b992-b5b0bbd337d2', + message: 'Invitation code not found or already used.', + }); } } else if (ticket.usedAt) { - reply.code(400); - return; + return error(400, { + id: 'f08118af-8358-441c-b992-b5b0bbd337d2', + message: 'Invitation code not found or already used.', + }); } } if (this.meta.emailRequiredForSignup) { 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 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()); 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 }); @@ -236,6 +270,41 @@ export class SignupApiService { token: secret, }; } 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()); } } @@ -247,11 +316,20 @@ export class SignupApiService { const code = body['code']; + function error(status: number, error: { id: string, [x: string]: string }) { + reply.code(status); + return { error }; + } + try { const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code }); 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({ diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 3d1c44fc90..176ed024e3 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -268,6 +268,7 @@ async function onSubmit(): Promise { const res = await fetch(`${config.apiUrl}/signup`, { method: 'POST', + redirect: 'error', headers: { 'Content-Type': 'application/json', }, @@ -278,14 +279,14 @@ async function onSubmit(): Promise { }); if (res) { - if (res.status === 204 || instance.emailRequiredForSignup) { + if (res.status === 204 && instance.emailRequiredForSignup) { os.alert({ type: 'success', title: i18n.ts._signup.almostThere, text: i18n.tsx._signup.emailSent({ email: email.value }), }); emit('signupEmailPending'); - } else { + } else if (res.status === 200) { const resJson = (await res.json()) as Misskey.entities.SignupResponse; if (_DEV_) console.log(resJson); @@ -294,23 +295,67 @@ async function onSubmit(): Promise { if (props.autoSet) { 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; } -function onSignupApiError() { +function onSignupApiError(message?: { title?: string; text: string }): void { submitting.value = false; hcaptcha.value?.reset?.(); mcaptcha.value?.reset?.(); recaptcha.value?.reset?.(); turnstile.value?.reset?.(); testcaptcha.value?.reset?.(); - os.alert({ type: 'error', - text: i18n.ts.somethingHappened, + ...(message ?? { text: i18n.ts.somethingHappened }), }); }