From d5ca5d8293e2ee112b2bfe58a966ac5f43721cb8 Mon Sep 17 00:00:00 2001 From: Hampus Date: Wed, 14 Oct 2026 16:41:33 +0200 Subject: [PATCH] fix(auth): fix approval signups and auto-join invites (#3467) --- fluxer_admin/openapi-admin.json | 10 +- .../InstanceConfigAdminController.ts | 14 +- ...eConfigPendingRegistrationApproval.test.ts | 207 +++++++++++++++++- fluxer_api/src/api/auth/AuthRegistration.ts | 14 +- .../src/api/auth/services/SsoService.ts | 13 +- .../api/auth/tests/RegistrationPolicy.test.ts | 63 ++++++ .../tests/RegistrationSignupRaces.test.ts | 7 +- fluxer_api/src/api/auth/tests/SsoFlow.test.ts | 78 ++++++- .../src/api/auth/tests/UsernameSignIn.test.ts | 23 ++ .../instance/InstanceConfigRepository.test.ts | 1 + .../api/instance/InstanceConfigRepository.ts | 1 + .../src/api/middleware/ServiceMiddleware.ts | 1 + fluxer_api/src/api/openapi/openapi.json | 2 + .../app/components/setup/SetupWizardSteps.tsx | 9 +- .../AccountSwitcherRegisterStep.tsx | 9 +- .../auth/components/pages/RegisterPage.tsx | 9 +- .../auth/flow/AuthRegisterFormCore.tsx | 20 +- .../auth/flow/RegisterFormFields.test.ts | 61 ++++++ .../features/auth/flow/RegisterFormFields.ts | 38 ++++ .../auth/flow/ResourceAuthPages.test.tsx | 25 ++- .../features/auth/flow/ResourceAuthPages.tsx | 12 +- .../components/pages/InviteRegisterPage.tsx | 12 +- .../src/content/docs/admin-api/instance.mdx | 5 +- .../content/docs/http-api/authentication.mdx | 6 +- .../src/content/docs/http-api/errors.md | 4 + packages/constants/src/ApiErrorCodes.ts | 1 + .../RegistrationCredentialsRequiredError.ts | 11 + packages/errors/src/i18n/ErrorCodeMappings.ts | 1 + packages/errors/src/i18n/ErrorI18nMessages.ts | 2 + .../errors/src/i18n/weblate/locales/ar.json | 1 + .../errors/src/i18n/weblate/locales/bg.json | 1 + .../errors/src/i18n/weblate/locales/cs.json | 1 + .../errors/src/i18n/weblate/locales/da.json | 1 + .../errors/src/i18n/weblate/locales/de.json | 1 + .../errors/src/i18n/weblate/locales/el.json | 1 + .../src/i18n/weblate/locales/en-GB.json | 1 + .../src/i18n/weblate/locales/es-419.json | 1 + .../src/i18n/weblate/locales/es-ES.json | 1 + .../errors/src/i18n/weblate/locales/fi.json | 1 + .../errors/src/i18n/weblate/locales/fr.json | 1 + .../errors/src/i18n/weblate/locales/he.json | 1 + .../errors/src/i18n/weblate/locales/hi.json | 1 + .../errors/src/i18n/weblate/locales/hr.json | 1 + .../errors/src/i18n/weblate/locales/hu.json | 1 + .../errors/src/i18n/weblate/locales/id.json | 1 + .../errors/src/i18n/weblate/locales/it.json | 1 + .../errors/src/i18n/weblate/locales/ja.json | 1 + .../errors/src/i18n/weblate/locales/ko.json | 1 + .../errors/src/i18n/weblate/locales/lt.json | 1 + .../errors/src/i18n/weblate/locales/nl.json | 1 + .../errors/src/i18n/weblate/locales/no.json | 1 + .../errors/src/i18n/weblate/locales/pl.json | 1 + .../src/i18n/weblate/locales/pt-BR.json | 1 + .../errors/src/i18n/weblate/locales/ro.json | 1 + .../errors/src/i18n/weblate/locales/ru.json | 1 + .../src/i18n/weblate/locales/sv-SE.json | 1 + .../errors/src/i18n/weblate/locales/th.json | 1 + .../errors/src/i18n/weblate/locales/tr.json | 1 + .../errors/src/i18n/weblate/locales/uk.json | 1 + .../errors/src/i18n/weblate/locales/vi.json | 1 + .../src/i18n/weblate/locales/zh-CN.json | 1 + .../src/i18n/weblate/locales/zh-TW.json | 1 + .../errors/src/i18n/weblate/messages.json | 1 + .../schema/src/domains/admin/AdminSchemas.ts | 1 + 64 files changed, 622 insertions(+), 72 deletions(-) create mode 100644 fluxer_app/src/features/auth/flow/RegisterFormFields.test.ts create mode 100644 fluxer_app/src/features/auth/flow/RegisterFormFields.ts create mode 100644 packages/errors/src/domains/auth/RegistrationCredentialsRequiredError.ts diff --git a/fluxer_admin/openapi-admin.json b/fluxer_admin/openapi-admin.json index 5be9ac0e8..ae9d42f0e 100644 --- a/fluxer_admin/openapi-admin.json +++ b/fluxer_admin/openapi-admin.json @@ -4258,7 +4258,7 @@ "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} } }, - "description": "Decides a registration waiting for manual review and returns the registrations still waiting. Approving removes its pending registration trait, rejecting also prevents the account from logging in. Requires REGISTRATION_REVIEW permission.", + "description": "Decides a registration waiting for manual review and returns the registrations still waiting. Approving removes its pending registration trait and joins the account to the community it was invited to and the single community, rejecting also prevents the account from logging in. Requires REGISTRATION_REVIEW permission.", "security": [{"adminApiKey": []}], "parameters": [ { @@ -16526,7 +16526,8 @@ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" }, "registration_url_id": {"nullable": true, "type": "string", "minLength": 1, "maxLength": 128}, - "client_ip": {"nullable": true, "type": "string"} + "client_ip": {"nullable": true, "type": "string"}, + "invite_code": {"nullable": true, "type": "string", "maxLength": 256} }, "required": [ "user_id", @@ -16536,7 +16537,8 @@ "email", "requested_at", "registration_url_id", - "client_ip" + "client_ip", + "invite_code" ], "additionalProperties": false }, @@ -17409,6 +17411,7 @@ "REACTION_EMAIL_VERIFICATION_REQUIRED", "REPORT_EMAIL_VERIFICATION_REQUIRED", "REGISTRATION_CLOSED", + "REGISTRATION_CREDENTIALS_REQUIRED", "REGISTRATION_PENDING_APPROVAL", "REGISTRATION_REJECTED", "REGISTRATION_URL_INVALID", @@ -17704,6 +17707,7 @@ "Email verification is required for this action.", "Email verification is required for this action.", "Registration is closed on this instance.", + "This instance reviews new accounts. Sign up from the sign-up page with a password so you can sign in once approved.", "This registration is waiting for admin approval.", "This registration request was rejected.", "This registration link is invalid or has expired.", diff --git a/fluxer_api/src/api/admin/controllers/InstanceConfigAdminController.ts b/fluxer_api/src/api/admin/controllers/InstanceConfigAdminController.ts index efb48fc9a..f9fb5c23f 100644 --- a/fluxer_api/src/api/admin/controllers/InstanceConfigAdminController.ts +++ b/fluxer_api/src/api/admin/controllers/InstanceConfigAdminController.ts @@ -3,6 +3,7 @@ import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions'; import {recordAdminRead, recordAdminWrite} from '@app/api/admin/AdminAuditRecorder'; import {startAttachmentDecayReconcile} from '@app/api/attachment/AttachmentDecayReconcileRun'; +import {maybeAutoJoinInvite} from '@app/api/auth/AuthRegistration'; import {createUserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import { @@ -768,7 +769,7 @@ export function InstanceConfigAdminController(app: HonoApp) { operationId: 'update_admin_pending_registration', summary: 'Approve or reject a pending registration', description: - 'Decides a registration waiting for manual review and returns the registrations still waiting. Approving removes its pending registration trait, rejecting also prevents the account from logging in. Requires REGISTRATION_REVIEW permission.', + 'Decides a registration waiting for manual review and returns the registrations still waiting. Approving removes its pending registration trait and joins the account to the community it was invited to and the single community, rejecting also prevents the account from logging in. Requires REGISTRATION_REVIEW permission.', responseSchema: PendingRegistrationListResponse, statusCode: 200, security: 'adminApiKey', @@ -777,7 +778,10 @@ export function InstanceConfigAdminController(app: HonoApp) { async (ctx) => { const userId = ctx.req.valid('param').user_id.toString(); const decision = ctx.req.valid('json').status === 'approved' ? 'approve' : 'reject'; - await updatePendingRegistrationUser(ctx, userId, decision); + const pendingRegistration = (await instanceConfigRepository.getPendingRegistrations()).find( + (entry) => entry.user_id === userId, + ); + await updatePendingRegistrationUser(ctx, userId, decision, pendingRegistration?.invite_code ?? null); await instanceConfigRepository.removePendingRegistration(userId); return ctx.json({pending_registrations: await instanceConfigRepository.getPendingRegistrations()}); }, @@ -875,6 +879,7 @@ async function updatePendingRegistrationUser( ctx: Context, userId: string, decision: 'approve' | 'reject', + inviteCode: string | null, ): Promise { const userRepository = ctx.get('userRepository'); const user = await userRepository.findUnique(createUserID(BigInt(userId))); @@ -896,6 +901,11 @@ async function updatePendingRegistrationUser( } await userRepository.patchUpsert(user.id, {traits: traits.size > 0 ? traits : null}, user.toRow()); if (decision === 'approve' && wasPendingApproval) { + await maybeAutoJoinInvite(ctx.get('inviteService'), { + userId: user.id, + inviteCode, + requestCache: ctx.get('requestCache'), + }); await ctx.get('singleCommunityService').joinStockCommunity(user.id, ctx.get('requestCache')); } await recordAdminWrite(ctx, { diff --git a/fluxer_api/src/api/admin/tests/InstanceConfigPendingRegistrationApproval.test.ts b/fluxer_api/src/api/admin/tests/InstanceConfigPendingRegistrationApproval.test.ts index 4350275bc..b4434e950 100644 --- a/fluxer_api/src/api/admin/tests/InstanceConfigPendingRegistrationApproval.test.ts +++ b/fluxer_api/src/api/admin/tests/InstanceConfigPendingRegistrationApproval.test.ts @@ -6,7 +6,8 @@ import { createUniqueUsername, setUserACLs, } from '@app/api/auth/tests/AuthTestUtils'; -import {setupTestGuildWithMembers} from '@app/api/guild/tests/GuildTestUtils'; +import {Config} from '@app/api/Config'; +import {createChannelInvite, createGuild, setupTestGuildWithMembers} from '@app/api/guild/tests/GuildTestUtils'; import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons'; import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; import {createApiTestHarness} from '@app/api/test/ApiTestHarness'; @@ -21,6 +22,18 @@ interface PendingRegistrationResponse { user_id: string; } +function invitedRegistrationBody(prefix: string, inviteCode?: string): Record { + return { + email: createUniqueEmail(prefix), + username: createUniqueUsername(prefix), + global_name: 'Invited person', + password: 'invited-since-1999', + date_of_birth: '2000-01-01', + consent: true, + invite_code: inviteCode, + }; +} + describe('pending registration approval and the stock community', () => { let harness: ApiTestHarness; @@ -102,6 +115,198 @@ describe('pending registration approval and the stock community', () => { .execute(); }); + it('joins the invited community on approval', async () => { + const admin = await setUserACLs(harness, await createTestAccount(harness), [ + AdminACLs.AUTHENTICATE, + AdminACLs.REGISTRATION_REVIEW, + ]); + const {owner, guild} = await setupTestGuildWithMembers(harness, 0); + const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!); + await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); + + const pending = await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body(invitedRegistrationBody('invitedapproval', invite.code)) + .execute(); + + await createBuilder(harness, owner.token) + .get(`/guilds/${guild.id}/members/${pending.user_id}`) + .expect(HTTP_STATUS.NOT_FOUND) + .execute(); + + await createBuilder(harness, admin.token) + .patch(`/admin/instance/pending-registrations/${pending.user_id}`) + .body({status: 'approved'}) + .expect(HTTP_STATUS.OK) + .execute(); + + await createBuilder(harness, owner.token) + .get(`/guilds/${guild.id}/members/${pending.user_id}`) + .expect(HTTP_STATUS.OK) + .execute(); + }); + + it('lists the invite code on a pending registration', async () => { + const admin = await setUserACLs(harness, await createTestAccount(harness), [ + AdminACLs.AUTHENTICATE, + AdminACLs.REGISTRATION_REVIEW, + ]); + const {owner, guild} = await setupTestGuildWithMembers(harness, 0); + const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!); + await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); + + const pending = await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body(invitedRegistrationBody('invitedlisted', invite.code)) + .execute(); + + const listed = await createBuilder(harness, admin.token) + .get('/admin/instance/pending-registrations') + .expect(HTTP_STATUS.OK) + .execute(); + expect(listed.pending_registrations).toEqual([ + expect.objectContaining({user_id: pending.user_id, invite_code: invite.code}), + ]); + }); + + it('does not join the invited community on rejection', async () => { + const admin = await setUserACLs(harness, await createTestAccount(harness), [ + AdminACLs.AUTHENTICATE, + AdminACLs.REGISTRATION_REVIEW, + ]); + const {owner, guild} = await setupTestGuildWithMembers(harness, 0); + const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!); + await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); + + const pending = await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body(invitedRegistrationBody('invitedrejected', invite.code)) + .execute(); + + await createBuilder(harness, admin.token) + .patch(`/admin/instance/pending-registrations/${pending.user_id}`) + .body({status: 'rejected'}) + .expect(HTTP_STATUS.OK) + .execute(); + + await createBuilder(harness, owner.token) + .get(`/guilds/${guild.id}/members/${pending.user_id}`) + .expect(HTTP_STATUS.NOT_FOUND) + .execute(); + }); + + it('joins the auto-join community on approval of a registration without an invite', async () => { + const admin = await setUserACLs(harness, await createTestAccount(harness), [ + AdminACLs.AUTHENTICATE, + AdminACLs.REGISTRATION_REVIEW, + ]); + const {owner, guild} = await setupTestGuildWithMembers(harness, 0); + const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!); + await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); + + const originalAutoJoinInviteCode = Config.instance.autoJoinInviteCode; + Config.instance.autoJoinInviteCode = invite.code; + try { + const pending = await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body(invitedRegistrationBody('autojoinapproval')) + .execute(); + + const listed = await createBuilder(harness, admin.token) + .get('/admin/instance/pending-registrations') + .expect(HTTP_STATUS.OK) + .execute(); + expect(listed.pending_registrations).toEqual([ + expect.objectContaining({user_id: pending.user_id, invite_code: invite.code}), + ]); + + await createBuilder(harness, admin.token) + .patch(`/admin/instance/pending-registrations/${pending.user_id}`) + .body({status: 'approved'}) + .expect(HTTP_STATUS.OK) + .execute(); + + await createBuilder(harness, owner.token) + .get(`/guilds/${guild.id}/members/${pending.user_id}`) + .expect(HTTP_STATUS.OK) + .execute(); + } finally { + Config.instance.autoJoinInviteCode = originalAutoJoinInviteCode; + } + }); + + it('does not join the auto-join community when the pending entry has no invite', async () => { + const admin = await setUserACLs(harness, await createTestAccount(harness), [ + AdminACLs.AUTHENTICATE, + AdminACLs.REGISTRATION_REVIEW, + ]); + const {owner, guild} = await setupTestGuildWithMembers(harness, 0); + const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!); + await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); + + const pending = await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body(invitedRegistrationBody('noinviteapproval')) + .execute(); + + const originalAutoJoinInviteCode = Config.instance.autoJoinInviteCode; + Config.instance.autoJoinInviteCode = invite.code; + try { + await createBuilder(harness, admin.token) + .patch(`/admin/instance/pending-registrations/${pending.user_id}`) + .body({status: 'approved'}) + .expect(HTTP_STATUS.OK) + .execute(); + } finally { + Config.instance.autoJoinInviteCode = originalAutoJoinInviteCode; + } + + await createBuilder(harness, owner.token) + .get(`/guilds/${guild.id}/members/${pending.user_id}`) + .expect(HTTP_STATUS.NOT_FOUND) + .execute(); + }); + + it('approves and joins the single community when the invite is gone', async () => { + const admin = await setUserACLs(harness, await createTestAccount(harness), [ + AdminACLs.AUTHENTICATE, + AdminACLs.REGISTRATION_REVIEW, + ]); + const {owner, guild: stockGuild} = await setupTestGuildWithMembers(harness, 0); + const invitedGuild = await createGuild(harness, owner.token, 'Invited Guild'); + const invite = await createChannelInvite(harness, owner.token, invitedGuild.system_channel_id!); + await getInstanceConfigRepository().setInstancePolicyConfig({ + single_community_enabled: true, + single_community_guild_id: stockGuild.id, + }); + await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); + + const pending = await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body(invitedRegistrationBody('invitegone', invite.code)) + .execute(); + + await createBuilder(harness, owner.token) + .delete(`/invites/${invite.code}`) + .expect(HTTP_STATUS.NO_CONTENT) + .execute(); + + await createBuilder(harness, admin.token) + .patch(`/admin/instance/pending-registrations/${pending.user_id}`) + .body({status: 'approved'}) + .expect(HTTP_STATUS.OK) + .execute(); + + await createBuilder(harness, owner.token) + .get(`/guilds/${stockGuild.id}/members/${pending.user_id}`) + .expect(HTTP_STATUS.OK) + .execute(); + await createBuilder(harness, owner.token) + .get(`/guilds/${invitedGuild.id}/members/${pending.user_id}`) + .expect(HTTP_STATUS.NOT_FOUND) + .execute(); + }); + it('lets the review ACL alone list and decide registrations', async () => { const reviewer = await setUserACLs(harness, await createTestAccount(harness), [ AdminACLs.AUTHENTICATE, diff --git a/fluxer_api/src/api/auth/AuthRegistration.ts b/fluxer_api/src/api/auth/AuthRegistration.ts index 5250cd375..402a5461f 100644 --- a/fluxer_api/src/api/auth/AuthRegistration.ts +++ b/fluxer_api/src/api/auth/AuthRegistration.ts @@ -55,6 +55,7 @@ import {getRegionalMinimumAge} from '@fluxer/constants/src/RegionalMinimumAge'; import {ProfileFieldPrivacyFlags, UserFlags} from '@fluxer/constants/src/UserConstants'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {RegistrationClosedError} from '@fluxer/errors/src/domains/auth/RegistrationClosedError'; +import {RegistrationCredentialsRequiredError} from '@fluxer/errors/src/domains/auth/RegistrationCredentialsRequiredError'; import {RegistrationUrlInvalidError} from '@fluxer/errors/src/domains/auth/RegistrationUrlInvalidError'; import {ContentBlockedError} from '@fluxer/errors/src/domains/content/ContentBlockedError'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; @@ -163,6 +164,9 @@ async function registerAccount( const registrationAccess = setupTokenVerified ? {pendingApproval: false, registrationUrl: null} : await resolveRegistrationAccess(instanceConfigRepository, data.registration_url_code); + if (registrationAccess.pendingApproval && !hasSignInCredentials(data, usernameMode)) { + throw new RegistrationCredentialsRequiredError(); + } const geoipResult = await lookupGeoip(clientIp); const countryCode = geoipResult.countryCode; const collectDateOfBirth = appPublicConfig.registration.collect_date_of_birth; @@ -303,6 +307,7 @@ async function registerAccount( requested_at: now.toISOString(), registration_url_id: registrationAccess.registrationUrl?.id ?? null, client_ip: clientIp, + invite_code: data.invite_code?.trim() || config.instance.autoJoinInviteCode || null, }); } grantBootstrapAdmin = bootstrapAdminEligible && (await instanceConfigRepository.claimAdminBootstrap(userId)); @@ -405,6 +410,11 @@ function assertUsernameModeRegistration(data: RegisterRequest): void { } } +function hasSignInCredentials(data: RegisterRequest, usernameMode: boolean): boolean { + if (!data.password) return false; + return usernameMode || Boolean(data.email); +} + function requestedUsernameFromOlderApp(config: APIConfig, email: string | null | undefined): string | undefined { if (email == null) return undefined; const localPart = getLocalPartAtInstance(config, email.trim()); @@ -495,7 +505,7 @@ async function resolveRegistrationAccess( }; } -async function maybeAutoJoinInvite( +export async function maybeAutoJoinInvite( inviteService: InviteService, params: { userId: UserID; @@ -513,7 +523,7 @@ async function maybeAutoJoinInvite( requestCache, }); } catch (error) { - Logger.warn({inviteCode: normalizedInviteCode, error}, 'Failed to auto-join invite on registration'); + Logger.warn({inviteCode: normalizedInviteCode, error}, 'Failed to auto-join invite'); } } diff --git a/fluxer_api/src/api/auth/services/SsoService.ts b/fluxer_api/src/api/auth/services/SsoService.ts index d898ac11f..44367f2c7 100644 --- a/fluxer_api/src/api/auth/services/SsoService.ts +++ b/fluxer_api/src/api/auth/services/SsoService.ts @@ -2,6 +2,7 @@ import {createHash, randomBytes} from 'node:crypto'; import type {ApiContext} from '@app/api/ApiContext'; +import {maybeAutoJoinInvite} from '@app/api/auth/AuthRegistration'; import * as AuthSession from '@app/api/auth/AuthSession'; import {assertEmailNotBlocklisted} from '@app/api/auth/EmailBlocklist'; import {SsoIdentityRepository} from '@app/api/auth/services/SsoIdentityRepository'; @@ -28,6 +29,7 @@ import { isTestSsoProvider, validateSsoPublicOutboundUrl, } from '@app/api/instance/SsoConfigValidation'; +import type {InviteService} from '@app/api/invite/InviteService'; import {Logger} from '@app/api/Logger'; import {profileSubstringBlocklistCache} from '@app/api/middleware/ProfileSubstringBlocklistCache'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; @@ -265,6 +267,7 @@ export class SsoService { private readonly discriminatorService: DiscriminatorService, private readonly kvActivityTracker: KVActivityTracker, private readonly singleCommunityService: SingleCommunityService, + private readonly inviteService: InviteService, ) {} async getPublicStatus(): Promise { @@ -406,10 +409,16 @@ export class SsoService { } await assertEmailNotBlocklisted(emailLower, 'email'); const pendingApproval = registrationConfig.mode === 'approval'; - const user = await this.provisionUserFromClaims(claims, config, {pendingApproval}); + const autoJoinInviteCode = this.apiContext.services.config.instance.autoJoinInviteCode?.trim() || null; + const user = await this.provisionUserFromClaims(claims, config, {pendingApproval, autoJoinInviteCode}); if (pendingApproval) { throw new RegistrationPendingApprovalError(); } + await maybeAutoJoinInvite(this.inviteService, { + userId: user.id, + inviteCode: autoJoinInviteCode, + requestCache, + }); await this.singleCommunityService.joinStockCommunity(user.id, requestCache); return user; } @@ -468,6 +477,7 @@ export class SsoService { config: ResolvedSsoConfig, options?: { pendingApproval?: boolean; + autoJoinInviteCode?: string | null; }, ): Promise { const {users, snowflake, cache} = this.apiContext.services; @@ -535,6 +545,7 @@ export class SsoService { requested_at: now.toISOString(), registration_url_id: null, client_ip: null, + invite_code: options.autoJoinInviteCode ?? null, }); } createAttempted = true; diff --git a/fluxer_api/src/api/auth/tests/RegistrationPolicy.test.ts b/fluxer_api/src/api/auth/tests/RegistrationPolicy.test.ts index 66bf07b2f..1478f3ad4 100644 --- a/fluxer_api/src/api/auth/tests/RegistrationPolicy.test.ts +++ b/fluxer_api/src/api/auth/tests/RegistrationPolicy.test.ts @@ -91,4 +91,67 @@ describe('Auth registration policy', () => { .expect(403, APIErrorCodes.REGISTRATION_PENDING_APPROVAL) .execute(); }); + + it('refuses an approval registration without sign-in credentials', async () => { + await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); + await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body({global_name: 'Nobody Signs In', date_of_birth: '2000-01-01', consent: true}) + .expect(400, APIErrorCodes.REGISTRATION_CREDENTIALS_REQUIRED) + .execute(); + expect(await getInstanceConfigRepository().getPendingRegistrations()).toEqual([]); + }); + + it('refuses an approval registration with an email but no password', async () => { + await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); + const {password: _password, ...body} = registrationBody('nopassword'); + await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body(body) + .expect(400, APIErrorCodes.REGISTRATION_CREDENTIALS_REQUIRED) + .execute(); + expect(await getInstanceConfigRepository().getPendingRegistrations()).toEqual([]); + }); + + it('refuses an approval registration with a password but no email', async () => { + await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); + const {email: _email, ...body} = registrationBody('noemail'); + await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body(body) + .expect(400, APIErrorCodes.REGISTRATION_CREDENTIALS_REQUIRED) + .execute(); + expect(await getInstanceConfigRepository().getPendingRegistrations()).toEqual([]); + }); + + it('keeps the registration URL seat when an approval registration is refused', async () => { + const repository = getInstanceConfigRepository(); + await repository.setRegistrationConfig({mode: 'open', admin_registration_urls_enabled: true}); + const {code} = await repository.createRegistrationUrl({ + label: 'Reviewed link', + createdByUserId: '1', + expiresAt: null, + maxUses: 1, + approvalRequired: true, + }); + await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body({global_name: 'Seat Keeper', date_of_birth: '2000-01-01', consent: true, registration_url_code: code}) + .expect(400, APIErrorCodes.REGISTRATION_CREDENTIALS_REQUIRED) + .execute(); + const registration = await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body({...registrationBody('seatkeeper'), registration_url_code: code}) + .execute(); + expect(registration.registration_pending_approval).toBe(true); + }); + + it('still accepts an unclaimed registration in open mode', async () => { + await getInstanceConfigRepository().setRegistrationConfig({mode: 'open'}); + const registration = await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body({global_name: 'Open Unclaimed', date_of_birth: '2000-01-01', consent: true}) + .execute(); + expect(registration.token.length).toBeGreaterThan(0); + }); }); diff --git a/fluxer_api/src/api/auth/tests/RegistrationSignupRaces.test.ts b/fluxer_api/src/api/auth/tests/RegistrationSignupRaces.test.ts index b185e9876..5b9c872cb 100644 --- a/fluxer_api/src/api/auth/tests/RegistrationSignupRaces.test.ts +++ b/fluxer_api/src/api/auth/tests/RegistrationSignupRaces.test.ts @@ -428,8 +428,11 @@ describe('signups racing on registration URLs and pending approvals', () => { await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); const fresh = await register('pendingafter'); - const listed = (await readPendingRegistrations()).map((entry) => entry.user_id); - expect(listed.toSorted()).toEqual([account.userId, fresh.json.user_id].toSorted()); + const pendingRegistrations = await readPendingRegistrations(); + expect(pendingRegistrations.map((entry) => entry.user_id).toSorted()).toEqual( + [account.userId, fresh.json.user_id].toSorted(), + ); + expect(pendingRegistrations.find((entry) => entry.user_id === account.userId)?.invite_code).toBeNull(); const decided = await createBuilder(harness, admin.token) .patch(`/admin/instance/pending-registrations/${account.userId}`) diff --git a/fluxer_api/src/api/auth/tests/SsoFlow.test.ts b/fluxer_api/src/api/auth/tests/SsoFlow.test.ts index c0a00b189..bb73722c6 100644 --- a/fluxer_api/src/api/auth/tests/SsoFlow.test.ts +++ b/fluxer_api/src/api/auth/tests/SsoFlow.test.ts @@ -9,10 +9,13 @@ import { setUserACLs, type TestAccount, } from '@app/api/auth/tests/AuthTestUtils'; -import {setupTestGuildWithMembers} from '@app/api/guild/tests/GuildTestUtils'; +import {Config} from '@app/api/Config'; +import {createChannelInvite, setupTestGuildWithMembers} from '@app/api/guild/tests/GuildTestUtils'; import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons'; import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder'; +import {AdminACLs} from '@fluxer/constants/src/AdminACLs'; +import type {PendingRegistrationListResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas'; import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it} from 'vitest'; interface SsoStartResponse { @@ -503,6 +506,79 @@ describe('Auth SSO flow', () => { .expect(200) .execute(); }); + it('joins a provisioned user to the auto-join invite community', async () => { + const {owner, guild} = await setupTestGuildWithMembers(harness, 0); + const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!); + await enableSso(harness, admin.token); + const originalAutoJoinInviteCode = Config.instance.autoJoinInviteCode; + Config.instance.autoJoinInviteCode = invite.code; + try { + const startData = await createBuilderWithoutAuth(harness) + .post('/auth/sso/start') + .body({}) + .execute(); + const completeData = await createBuilderWithoutAuth(harness) + .post('/auth/sso/complete') + .body({ + code: `sso-auto-join-${Date.now()}@example.com`, + state: startData.state, + }) + .execute(); + await createBuilder(harness, owner.token) + .get(`/guilds/${guild.id}/members/${completeData.user_id}`) + .expect(200) + .execute(); + } finally { + Config.instance.autoJoinInviteCode = originalAutoJoinInviteCode; + } + }); + it('replays the auto-join invite when an SSO registration is approved', async () => { + const reviewer = await setUserACLs(harness, await createTestAccount(harness), [ + AdminACLs.AUTHENTICATE, + AdminACLs.REGISTRATION_REVIEW, + ]); + const {owner, guild} = await setupTestGuildWithMembers(harness, 0); + const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!); + await enableSso(harness, admin.token); + await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); + const originalAutoJoinInviteCode = Config.instance.autoJoinInviteCode; + Config.instance.autoJoinInviteCode = invite.code; + try { + const startData = await createBuilderWithoutAuth(harness) + .post('/auth/sso/start') + .body({}) + .execute(); + await createBuilderWithoutAuth(harness) + .post('/auth/sso/complete') + .body({ + code: `sso-auto-join-approval-${Date.now()}@example.com`, + state: startData.state, + }) + .expect(403, 'REGISTRATION_PENDING_APPROVAL') + .execute(); + const listed = await createBuilder(harness, reviewer.token) + .get('/admin/instance/pending-registrations') + .expect(200) + .execute(); + expect(listed.pending_registrations).toEqual([expect.objectContaining({invite_code: invite.code})]); + const pendingUserId = listed.pending_registrations[0]!.user_id; + await createBuilder(harness, owner.token) + .get(`/guilds/${guild.id}/members/${pendingUserId}`) + .expect(404) + .execute(); + await createBuilder(harness, reviewer.token) + .patch(`/admin/instance/pending-registrations/${pendingUserId}`) + .body({status: 'approved'}) + .expect(200) + .execute(); + await createBuilder(harness, owner.token) + .get(`/guilds/${guild.id}/members/${pendingUserId}`) + .expect(200) + .execute(); + } finally { + Config.instance.autoJoinInviteCode = originalAutoJoinInviteCode; + } + }); }); describe('existing user login', () => { let admin: TestAccount; diff --git a/fluxer_api/src/api/auth/tests/UsernameSignIn.test.ts b/fluxer_api/src/api/auth/tests/UsernameSignIn.test.ts index 10f25b674..8501a94ee 100644 --- a/fluxer_api/src/api/auth/tests/UsernameSignIn.test.ts +++ b/fluxer_api/src/api/auth/tests/UsernameSignIn.test.ts @@ -404,6 +404,29 @@ describe('Username sign-in', () => { expect(user.username.toLowerCase().startsWith('sharedname')).toBe(true); expect(user.isUnclaimedAccount()).toBe(true); }); + + it('refuses a credential-less approval registration on a username instance', async () => { + await useUsernameMode(); + await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); + await createBuilderWithoutAuth(harness) + .post('/auth/register') + .body({global_name: 'No Credentials', date_of_birth: '2000-01-01'}) + .expect(400, APIErrorCodes.REGISTRATION_CREDENTIALS_REQUIRED) + .execute(); + expect(await getInstanceConfigRepository().getPendingRegistrations()).toEqual([]); + }); + + it('accepts an approval registration with a username and password', async () => { + await useUsernameMode(); + await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'}); + const registration = await createBuilderWithoutAuth<{registration_pending_approval: true; user_id: string}>( + harness, + ) + .post('/auth/register') + .body({username: 'reviewedperson', password: PASSWORD, date_of_birth: '2000-01-01'}) + .execute(); + expect(registration).toEqual({registration_pending_approval: true, user_id: expect.any(String)}); + }); }); describe('login', () => { diff --git a/fluxer_api/src/api/instance/InstanceConfigRepository.test.ts b/fluxer_api/src/api/instance/InstanceConfigRepository.test.ts index 981c9d230..aaca23faa 100644 --- a/fluxer_api/src/api/instance/InstanceConfigRepository.test.ts +++ b/fluxer_api/src/api/instance/InstanceConfigRepository.test.ts @@ -936,6 +936,7 @@ function pendingRegistration(userId: string) { requested_at: `2026-09-01T00:00:${userId.slice(-2)}.000Z`, registration_url_id: null, client_ip: '127.0.0.1', + invite_code: null, }; } diff --git a/fluxer_api/src/api/instance/InstanceConfigRepository.ts b/fluxer_api/src/api/instance/InstanceConfigRepository.ts index 48f230ab5..d2774b993 100644 --- a/fluxer_api/src/api/instance/InstanceConfigRepository.ts +++ b/fluxer_api/src/api/instance/InstanceConfigRepository.ts @@ -1093,6 +1093,7 @@ const StoredPendingRegistrationSchema = PendingRegistrationResponse.extend({ requested_at: StoredRegistrationTimestampSchema, registration_url_id: StoredNullableStringSchema.pipe(StoredRegistrationUrlIdSchema.nullable()), client_ip: StoredNullableStringSchema, + invite_code: StoredNullableStringSchema, }) satisfies z.ZodType; function isRegistrationUrlUsable(registrationUrl: InstanceRegistrationUrl, now: Date): boolean { diff --git a/fluxer_api/src/api/middleware/ServiceMiddleware.ts b/fluxer_api/src/api/middleware/ServiceMiddleware.ts index 495374bba..3f3657511 100644 --- a/fluxer_api/src/api/middleware/ServiceMiddleware.ts +++ b/fluxer_api/src/api/middleware/ServiceMiddleware.ts @@ -521,6 +521,7 @@ class RequestServices implements RequestScopedServices { getDiscriminatorService(), getKVActivityTracker(), this.singleCommunityService, + this.inviteService, ); return this.cachedSsoService; } diff --git a/fluxer_api/src/api/openapi/openapi.json b/fluxer_api/src/api/openapi/openapi.json index 8a069c4d9..848db787d 100644 --- a/fluxer_api/src/api/openapi/openapi.json +++ b/fluxer_api/src/api/openapi/openapi.json @@ -39859,6 +39859,7 @@ "REACTION_EMAIL_VERIFICATION_REQUIRED", "REPORT_EMAIL_VERIFICATION_REQUIRED", "REGISTRATION_CLOSED", + "REGISTRATION_CREDENTIALS_REQUIRED", "REGISTRATION_PENDING_APPROVAL", "REGISTRATION_REJECTED", "REGISTRATION_URL_INVALID", @@ -40154,6 +40155,7 @@ "Email verification is required for this action.", "Email verification is required for this action.", "Registration is closed on this instance.", + "This instance reviews new accounts. Sign up from the sign-up page with a password so you can sign in once approved.", "This registration is waiting for admin approval.", "This registration request was rejected.", "This registration link is invalid or has expired.", diff --git a/fluxer_app/src/features/app/components/setup/SetupWizardSteps.tsx b/fluxer_app/src/features/app/components/setup/SetupWizardSteps.tsx index 64093ba48..ee1f29cb0 100644 --- a/fluxer_app/src/features/app/components/setup/SetupWizardSteps.tsx +++ b/fluxer_app/src/features/app/components/setup/SetupWizardSteps.tsx @@ -5,6 +5,7 @@ import {SetupWelcomeGate} from '@app/features/app/components/setup/SetupWelcomeG import type {SetupBrandingAssetKind} from '@app/features/app/components/setup/SetupWizardClient'; import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; import {AuthRegisterFormCore} from '@app/features/auth/flow/AuthRegisterFormCore'; +import {credentialRegisterFields} from '@app/features/auth/flow/RegisterFormFields'; import {Button} from '@app/features/ui/button/Button'; import {ColorPickerField} from '@app/features/ui/components/form/ColorPickerField'; import {Input} from '@app/features/ui/components/form/FormInput'; @@ -751,13 +752,7 @@ export const AdminAccountStep = observer(
Create account} redirectPath="" offerRecoveryKit={usernameSignIn} diff --git a/fluxer_app/src/features/auth/components/pages/RegisterPage.tsx b/fluxer_app/src/features/auth/components/pages/RegisterPage.tsx index 0fd3a5673..c5b2488f3 100644 --- a/fluxer_app/src/features/auth/components/pages/RegisterPage.tsx +++ b/fluxer_app/src/features/auth/components/pages/RegisterPage.tsx @@ -11,6 +11,7 @@ import {AUTH_LOGIN_METHOD_QUERY_PARAM} from '@app/features/auth/flow/auth_login_ import {AuthClientPermissionStep} from '@app/features/auth/flow/client_intro/AuthClientPermissionStep'; import {AuthClientPreferencesStep} from '@app/features/auth/flow/client_intro/AuthClientPreferencesStep'; import {useDesktopClientIntroFlow} from '@app/features/auth/flow/client_intro/useDesktopClientIntroFlow'; +import {credentialRegisterFields} from '@app/features/auth/flow/RegisterFormFields'; import {useAuthPresentation} from '@app/features/auth/flow/useAuthPresentation'; import {AuthCardVariant} from '@app/features/auth/state/AuthLayoutContext'; import {useAuthRuntimeTarget} from '@app/features/auth/state/AuthRuntimeTarget'; @@ -116,13 +117,7 @@ const RegisterPageContent = observer(function RegisterPageContent({runtimeSnapsh
Create account} redirectPath={redirectTo} offerRecoveryKit={usernameSignIn} diff --git a/fluxer_app/src/features/auth/flow/AuthRegisterFormCore.tsx b/fluxer_app/src/features/auth/flow/AuthRegisterFormCore.tsx index 0ab2b2ca8..b2369f41e 100644 --- a/fluxer_app/src/features/auth/flow/AuthRegisterFormCore.tsx +++ b/fluxer_app/src/features/auth/flow/AuthRegisterFormCore.tsx @@ -7,6 +7,7 @@ import * as AuthenticationCommands from '@app/features/auth/commands/Authenticat import FormField from '@app/features/auth/flow/AuthFormField'; import styles from '@app/features/auth/flow/AuthPageStyles.module.css'; import {DateOfBirthField} from '@app/features/auth/flow/DateOfBirthField'; +import {type RegisterFormFields, readRegistrationUrlCode} from '@app/features/auth/flow/RegisterFormFields'; import { getRegistrationLegalConsentConfig, RegistrationLegalConsent, @@ -83,17 +84,8 @@ const USERNAME_AVAILABLE_DESCRIPTOR = msg({ const MAX_USERNAME_LENGTH = 32; const USERNAME_PATTERN = /^[a-zA-Z0-9_]+$/; -interface FieldConfig { - showEmail?: boolean; - showPassword?: boolean; - showPasswordConfirmation?: boolean; - showUsername?: boolean; - showUsernameValidation?: boolean; - requireUsername?: boolean; -} - interface AuthRegisterFormCoreProps { - fields?: FieldConfig; + fields?: RegisterFormFields; submitLabel: React.ReactNode; redirectPath: string; runtimeSnapshot: RuntimeConfigSnapshot; @@ -136,10 +128,10 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ const firstFieldName = showEmail ? 'email' : requireUsername ? 'username' : 'global_name'; const location = useLocation(); const draftKey = `register:${location.pathname}${location.search}`; - const registrationUrlCode = useMemo(() => { - const value = new URLSearchParams(location.search).get('registration_url')?.trim(); - return value || undefined; - }, [location.search]); + const registrationUrlCode = useMemo( + () => readRegistrationUrlCode(new URLSearchParams(location.search)), + [location.search], + ); const registrationTarget = useMemo(() => instanceTargetFromSnapshot(runtimeSnapshot), [runtimeSnapshot]); const isPublicRegistrationClosed = runtimeSnapshot.registration.mode === 'closed' && !registrationUrlCode; const collectDateOfBirth = runtimeSnapshot.appPublic.registration.collect_date_of_birth; diff --git a/fluxer_app/src/features/auth/flow/RegisterFormFields.test.ts b/fluxer_app/src/features/auth/flow/RegisterFormFields.test.ts new file mode 100644 index 000000000..75f596045 --- /dev/null +++ b/fluxer_app/src/features/auth/flow/RegisterFormFields.test.ts @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import type {RuntimeConfigSnapshot} from '@app/features/app/state/InstanceSnapshotStore'; +import { + credentialRegisterFields, + linkRegisterFields, + readRegistrationUrlCode, +} from '@app/features/auth/flow/RegisterFormFields'; +import {AccountIdentityModes} from '@fluxer/constants/src/AccountIdentityConstants'; +import type {InstanceRegistration} from '@fluxer/schema/src/domains/instance/InstanceSchemas'; +import {describe, expect, it} from 'vitest'; + +function snapshot(usernameSignIn: boolean, mode: InstanceRegistration['mode']): RuntimeConfigSnapshot { + return { + features: {account_identity: usernameSignIn ? AccountIdentityModes.USERNAME : AccountIdentityModes.EMAIL}, + registration: {mode, admin_registration_urls_enabled: false}, + } as RuntimeConfigSnapshot; +} + +const EMAIL_CREDENTIALS = { + showEmail: true, + showPassword: true, + showPasswordConfirmation: true, + showUsernameValidation: true, + requireUsername: false, +}; +const USERNAME_CREDENTIALS = { + showEmail: false, + showPassword: true, + showPasswordConfirmation: true, + showUsernameValidation: true, + requireUsername: true, +}; + +describe('register form fields', () => { + it('asks for credentials on the full register form in both sign-in modes', () => { + expect(credentialRegisterFields(false)).toEqual(EMAIL_CREDENTIALS); + expect(credentialRegisterFields(true)).toEqual(USERNAME_CREDENTIALS); + }); + + it('keeps link sign-ups minimal only where the account can sign in right away', () => { + expect(linkRegisterFields(snapshot(false, 'open'), undefined)).toEqual({showUsername: false}); + expect(linkRegisterFields(snapshot(false, 'closed'), undefined)).toEqual({showUsername: false}); + expect(linkRegisterFields(snapshot(false, 'approval'), undefined)).toEqual(EMAIL_CREDENTIALS); + }); + + it('asks for credentials on link sign-ups through a registration URL, which can require approval', () => { + const code = readRegistrationUrlCode(new URLSearchParams('?registration_url=%20abc%20')); + expect(code).toBe('abc'); + for (const mode of ['open', 'approval', 'closed'] as const) { + expect(linkRegisterFields(snapshot(false, mode), code)).toEqual(EMAIL_CREDENTIALS); + } + expect(readRegistrationUrlCode(new URLSearchParams('?registration_url=%20'))).toBeUndefined(); + }); + + it('always asks for a username and password on link sign-ups of a username instance', () => { + for (const mode of ['open', 'approval', 'closed'] as const) { + expect(linkRegisterFields(snapshot(true, mode), undefined)).toEqual(USERNAME_CREDENTIALS); + } + }); +}); diff --git a/fluxer_app/src/features/auth/flow/RegisterFormFields.ts b/fluxer_app/src/features/auth/flow/RegisterFormFields.ts new file mode 100644 index 000000000..30b4f6f4e --- /dev/null +++ b/fluxer_app/src/features/auth/flow/RegisterFormFields.ts @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import type {RuntimeConfigSnapshot} from '@app/features/app/state/InstanceSnapshotStore'; +import {usesUsernameSignIn} from '@app/features/app/utils/AccountIdentityFeatures'; + +export interface RegisterFormFields { + showEmail?: boolean; + showPassword?: boolean; + showPasswordConfirmation?: boolean; + showUsername?: boolean; + showUsernameValidation?: boolean; + requireUsername?: boolean; +} + +export function credentialRegisterFields(usernameSignIn: boolean): RegisterFormFields { + return { + showEmail: !usernameSignIn, + showPassword: true, + showPasswordConfirmation: true, + showUsernameValidation: true, + requireUsername: usernameSignIn, + }; +} + +export function readRegistrationUrlCode(params: URLSearchParams): string | undefined { + return params.get('registration_url')?.trim() || undefined; +} + +export function linkRegisterFields( + snapshot: RuntimeConfigSnapshot, + registrationUrlCode: string | undefined, +): RegisterFormFields { + const usernameSignIn = usesUsernameSignIn(snapshot.features); + if (usernameSignIn || registrationUrlCode || snapshot.registration.mode === 'approval') { + return credentialRegisterFields(usernameSignIn); + } + return {showUsername: false}; +} diff --git a/fluxer_app/src/features/auth/flow/ResourceAuthPages.test.tsx b/fluxer_app/src/features/auth/flow/ResourceAuthPages.test.tsx index 2d563e48b..80c463e00 100644 --- a/fluxer_app/src/features/auth/flow/ResourceAuthPages.test.tsx +++ b/fluxer_app/src/features/auth/flow/ResourceAuthPages.test.tsx @@ -13,7 +13,12 @@ import {renderToStaticMarkup} from 'react-dom/server'; import {beforeEach, describe, expect, it, vi} from 'vitest'; const mocks = vi.hoisted(() => ({ - snapshot: {apiEndpoint: 'https://instance.example/api', apiCodeVersion: 7, features: {}} as RuntimeConfigSnapshot, + snapshot: { + apiEndpoint: 'https://instance.example/api', + apiCodeVersion: 7, + features: {}, + registration: {mode: 'open', admin_registration_urls_enabled: false}, + } as RuntimeConfigSnapshot, reset: vi.fn(), completeLogin: vi.fn(), search: '', @@ -110,6 +115,7 @@ beforeEach(() => { mocks.search = ''; mocks.loginProps = mocks.registerProps = mocks.ssoProps = mocks.carouselProps = null; delete (mocks.snapshot as any).sso; + (mocks.snapshot as any).registration = {mode: 'open', admin_registration_urls_enabled: false}; }); describe('resource auth guards', () => { it('blocks redeemed resources before login, registration or enforced SSO', () => { @@ -207,4 +213,21 @@ describe('resource auth guards', () => { expect(mocks.ssoProps.redirectPath).toBe('/gift/code/register'); expect(mocks.registerProps).toBeNull(); }); + it('asks for an email and password on an approval instance', () => { + (mocks.snapshot as any).registration = {mode: 'approval', admin_registration_urls_enabled: false}; + renderToStaticMarkup( + , + ); + expect(mocks.registerProps.fields).toMatchObject({ + showEmail: true, + showPassword: true, + showPasswordConfirmation: true, + }); + }); + it('keeps the minimal form on an open email instance', () => { + renderToStaticMarkup( + , + ); + expect(mocks.registerProps.fields).toEqual({showUsername: false}); + }); }); diff --git a/fluxer_app/src/features/auth/flow/ResourceAuthPages.tsx b/fluxer_app/src/features/auth/flow/ResourceAuthPages.tsx index 0c7c2b4de..9e10ef412 100644 --- a/fluxer_app/src/features/auth/flow/ResourceAuthPages.tsx +++ b/fluxer_app/src/features/auth/flow/ResourceAuthPages.tsx @@ -17,6 +17,7 @@ import {AuthRuntimeTargetResetAction} from '@app/features/auth/flow/AuthRuntimeT import {AuthSsoPanel, resolveAuthPanelSso} from '@app/features/auth/flow/AuthSsoPanel'; import {isHandoffRequest} from '@app/features/auth/flow/auth_login_core/useDesktopHandoffFlow'; import {DesktopDeepLinkPrompt} from '@app/features/auth/flow/DesktopDeepLinkPrompt'; +import {linkRegisterFields, readRegistrationUrlCode} from '@app/features/auth/flow/RegisterFormFields'; import {useAuthPresentation} from '@app/features/auth/flow/useAuthPresentation'; import {AuthCardVariant} from '@app/features/auth/state/AuthLayoutContext'; import {useAuthRuntimeTarget} from '@app/features/auth/state/AuthRuntimeTarget'; @@ -188,16 +189,7 @@ const ResourceAuthContent = observer(function ResourceAuthContent({ /> ) : (
1 | ?string | IP address the account registered from, or null when none was recorded | +| invite_code2 | ?string | Invite the account joins on approval (0-256 characters), or null when it has none | 1 Stored at registration time and never refreshed, so it can be stale by the time an Admin reads it +2 The invite code sent with the registration, or the instance's configured auto-join invite when it sent none. An account that registered through SSO sends no invite code, so it stores the auto-join invite or null + ## Pending registration list object Every account still waiting for an Admin decision. @@ -940,7 +943,7 @@ Both decisions remove the pending registration, but the endpoint does not requir ### Side effects -Approval removes both the `registration_pending_approval` trait and the `registration_rejected` trait, and joins the account to the single community when that mode is enabled and a guild is designated. A join that fails is logged and does not fail the request. Rejection removes the `registration_pending_approval` trait and adds the `registration_rejected` trait, which blocks login and every later session creation. A session issued before the decision stays valid. The pending registration is removed either way. +Approval removes both the `registration_pending_approval` trait and the `registration_rejected` trait. When the account still had the `registration_pending_approval` trait, approval then accepts the stored `invite_code` when there is one and joins the account to the single community when that mode is enabled and a guild is designated. A join that fails, such as one through an invite that was deleted, has expired or needs the account to be online, is logged and does not fail the request. Rejection removes the `registration_pending_approval` trait and adds the `registration_rejected` trait, which blocks login and every later session creation. A session issued before the decision stays valid. The pending registration is removed either way. When the stored pending registration list fails validation, the request fails before Fluxer changes the account. A failure after Fluxer writes the account traits leaves those traits written and can leave the entry in the pending list. Check the account traits and the pending list before retrying. diff --git a/fluxer_docs/src/content/docs/http-api/authentication.mdx b/fluxer_docs/src/content/docs/http-api/authentication.mdx index 1072d5779..f5a215e25 100644 --- a/fluxer_docs/src/content/docs/http-api/authentication.mdx +++ b/fluxer_docs/src/content/docs/http-api/authentication.mdx @@ -590,7 +590,7 @@ Registration verifies a [CAPTCHA](/topics/captcha/). It permits 3 attempts per h Fluxer resolves the region from the client IP address, and an age below the minimum for that region returns the field code `MUST_BE_MINIMUM_AGE`. That minimum is 13 years unless the region sets a different minimum, and the applied minimum appears only in the localised message. -Registration returns 403 `REGISTRATION_CLOSED` when the instance is closed and no valid registration URL was supplied. A username whose discriminator space is exhausted returns the field code `TOO_MANY_USERS_WITH_THIS_USERNAME`. A username or display name containing a blocked substring returns 403 `CONTENT_BLOCKED`. +Registration returns 403 `REGISTRATION_CLOSED` when the instance is closed and no valid registration URL was supplied. A registration that would wait for approval, through the approval mode or a registration URL that requires approval, returns 400 `REGISTRATION_CREDENTIALS_REQUIRED` unless it has a `password`, and on an `email` instance also an `email`, because an account without them could never sign in once approved. That refusal counts against no registration allowance and uses no registration URL seat. A username whose discriminator space is exhausted returns the field code `TOO_MANY_USERS_WITH_THIS_USERNAME`. A username or display name containing a blocked substring returns 403 `CONTENT_BLOCKED`. After the route rate limit, the CAPTCHA and schema validation, the setup token is checked before every other registration rule. A registration that presents the correct token is admitted whatever the registration mode is, is exempt from the registration allowances above, and receives the wildcard [Admin ACL](/admin-api/#acl-registry) when it creates a claimed account, which needs `email` on an `email` instance and `password` on a `username` instance. A client IP address is permitted 10 wrong tokens per 30 minutes, and further requests that present a token from that address return 429 until the window passes, the correct token included. A request with no token does not count. @@ -607,7 +607,7 @@ There is no retry key. A repeated request with the same values creates a second | Status | Body | Condition | | --- | --- | --- | | 200 | [authentication token response](#authentication-token-response) \| [registration pending approval response](#registration-pending-approval-response-object) | Registration completed or entered the approval state | -| 400 | [error response](/http-api/#error-response) | The body, CAPTCHA, account fields, registration URL code, or setup token is invalid | +| 400 | [error response](/http-api/#error-response) | The body, CAPTCHA, account fields, sign-in credentials, registration URL code, or setup token is invalid | | 403 | [error response](/http-api/#error-response) | SSO enforcement, registration policy, or content moderation rejects the registration | | 423 | [error response](/http-api/#error-response) | `GENERAL_ERROR`, because a self-hosted instance with no account yet is still settling its [sign-in method](/http-api/instance/#set-the-setup-sign-in-method) after 15 seconds | | 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A route, global, email, IP, or subnet bucket denies the request, or the address has used its wrong setup token allowance | @@ -620,7 +620,7 @@ The operation applies the instance's registration, email-domain, breached-passwo Registration accepts the `invite_code` from the body, or the instance's configured auto-join invite when the body has none. On an instance with single-community mode enabled, the account also joins the community guild. Each join emits [Guild Member Add](/gateway/events/#guild-member-add) to the affected guild's sessions. -Approval mode registration creates no guild membership and no authentication session, and the account cannot sign in until an administrator approves it. Every other successful registration creates one session and returns its token. +Approval mode registration creates no guild membership and no authentication session, and the account cannot sign in until an administrator approves it. The pending registration keeps that invite, and [approving it](/admin-api/instance/#update-pending-registration) joins the account to that guild and to the single community guild. Every other successful registration creates one session and returns its token. ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/errors.md b/fluxer_docs/src/content/docs/http-api/errors.md index 57761a90f..d88ee9f32 100644 --- a/fluxer_docs/src/content/docs/http-api/errors.md +++ b/fluxer_docs/src/content/docs/http-api/errors.md @@ -890,6 +890,10 @@ Email verification is required for this action Registration is closed on this instance +### `REGISTRATION_CREDENTIALS_REQUIRED` + +This instance reviews new accounts. Sign up from the sign-up page with a password so you can sign in once approved + ### `REGISTRATION_PENDING_APPROVAL` This registration is waiting for admin approval diff --git a/packages/constants/src/ApiErrorCodes.ts b/packages/constants/src/ApiErrorCodes.ts index 7ae4b0d01..15b2ec65e 100644 --- a/packages/constants/src/ApiErrorCodes.ts +++ b/packages/constants/src/ApiErrorCodes.ts @@ -84,6 +84,7 @@ export const APIErrorCodes = { REACTION_EMAIL_VERIFICATION_REQUIRED: 'REACTION_EMAIL_VERIFICATION_REQUIRED', REPORT_EMAIL_VERIFICATION_REQUIRED: 'REPORT_EMAIL_VERIFICATION_REQUIRED', REGISTRATION_CLOSED: 'REGISTRATION_CLOSED', + REGISTRATION_CREDENTIALS_REQUIRED: 'REGISTRATION_CREDENTIALS_REQUIRED', REGISTRATION_PENDING_APPROVAL: 'REGISTRATION_PENDING_APPROVAL', REGISTRATION_REJECTED: 'REGISTRATION_REJECTED', REGISTRATION_URL_INVALID: 'REGISTRATION_URL_INVALID', diff --git a/packages/errors/src/domains/auth/RegistrationCredentialsRequiredError.ts b/packages/errors/src/domains/auth/RegistrationCredentialsRequiredError.ts new file mode 100644 index 000000000..773d044ad --- /dev/null +++ b/packages/errors/src/domains/auth/RegistrationCredentialsRequiredError.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; +import {BadRequestError} from '@fluxer/errors/src/domains/core/BadRequestError'; + +export class RegistrationCredentialsRequiredError extends BadRequestError { + constructor() { + super({code: APIErrorCodes.REGISTRATION_CREDENTIALS_REQUIRED}); + this.name = 'RegistrationCredentialsRequiredError'; + } +} diff --git a/packages/errors/src/i18n/ErrorCodeMappings.ts b/packages/errors/src/i18n/ErrorCodeMappings.ts index 5e0ae92a4..741fda627 100644 --- a/packages/errors/src/i18n/ErrorCodeMappings.ts +++ b/packages/errors/src/i18n/ErrorCodeMappings.ts @@ -232,6 +232,7 @@ export const ErrorCodeToI18nKey = { [APIErrorCodes.PROCESSING_FAILED]: 'admin_and_system.processing_failed', [APIErrorCodes.RATE_LIMITED]: 'rate_limits.rate_limited', [APIErrorCodes.REGISTRATION_CLOSED]: 'auth_and_oauth.registration_closed', + [APIErrorCodes.REGISTRATION_CREDENTIALS_REQUIRED]: 'auth_and_oauth.registration_credentials_required', [APIErrorCodes.REGISTRATION_PENDING_APPROVAL]: 'auth_and_oauth.registration_pending_approval', [APIErrorCodes.REGISTRATION_REJECTED]: 'auth_and_oauth.registration_rejected', [APIErrorCodes.REGISTRATION_URL_INVALID]: 'auth_and_oauth.registration_url_invalid', diff --git a/packages/errors/src/i18n/ErrorI18nMessages.ts b/packages/errors/src/i18n/ErrorI18nMessages.ts index d595b39af..bd635c6d0 100644 --- a/packages/errors/src/i18n/ErrorI18nMessages.ts +++ b/packages/errors/src/i18n/ErrorI18nMessages.ts @@ -106,6 +106,8 @@ export const ERROR_I18N_MESSAGES = { 'auth_and_oauth.origin_handoff_nonce_invalid': "This sign-in transfer doesn't match the one you started.", 'auth_and_oauth.passkey_bridge_nonce_invalid': 'This passkey request could not be confirmed. Try again.', 'auth_and_oauth.registration_closed': 'Registration is closed on this instance.', + 'auth_and_oauth.registration_credentials_required': + 'This instance reviews new accounts. Sign up from the sign-up page with a password so you can sign in once approved.', 'auth_and_oauth.registration_pending_approval': 'This registration is waiting for admin approval.', 'auth_and_oauth.registration_rejected': 'This registration request was rejected.', 'auth_and_oauth.registration_url_invalid': 'This registration link is invalid or has expired.', diff --git a/packages/errors/src/i18n/weblate/locales/ar.json b/packages/errors/src/i18n/weblate/locales/ar.json index d02a54450..6908b6f69 100644 --- a/packages/errors/src/i18n/weblate/locales/ar.json +++ b/packages/errors/src/i18n/weblate/locales/ar.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "عملية نقل تسجيل الدخول هذه لا تطابق العملية التي بدأتها.", "auth_and_oauth.passkey_bridge_nonce_invalid": "تعذّر تأكيد طلب مفتاح المرور هذا. حاول مرة أخرى.", "auth_and_oauth.registration_closed": "التسجيل مغلق في هذا المثيل.", + "auth_and_oauth.registration_credentials_required": "يراجع هذا المثيل الحسابات الجديدة. أنشئ حسابك من صفحة التسجيل باستخدام كلمة مرور حتى تتمكن من تسجيل الدخول بعد الموافقة عليه.", "auth_and_oauth.registration_pending_approval": "هذا التسجيل في انتظار موافقة المسؤول.", "auth_and_oauth.registration_rejected": "تم رفض طلب التسجيل هذا.", "auth_and_oauth.registration_url_invalid": "رابط التسجيل هذا غير صالح أو انتهت صلاحيته.", diff --git a/packages/errors/src/i18n/weblate/locales/bg.json b/packages/errors/src/i18n/weblate/locales/bg.json index 8fca6e8ab..bd7ce6fce 100644 --- a/packages/errors/src/i18n/weblate/locales/bg.json +++ b/packages/errors/src/i18n/weblate/locales/bg.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Това прехвърляне на влизането не съвпада с започнатото от теб.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Тази заявка за ключ за достъп не можа да бъде потвърдена. Опитай отново.", "auth_and_oauth.registration_closed": "Регистрацията е затворена на тази инстанция.", + "auth_and_oauth.registration_credentials_required": "Тази инстанция преглежда новите акаунти. Регистрирайте се от страницата за регистрация с парола, за да можете да влезете след одобрение.", "auth_and_oauth.registration_pending_approval": "Тази регистрация чака одобрение от администратор.", "auth_and_oauth.registration_rejected": "Тази заявка за регистрация беше отхвърлена.", "auth_and_oauth.registration_url_invalid": "Този линк за регистрация е невалиден или е изтекъл.", diff --git a/packages/errors/src/i18n/weblate/locales/cs.json b/packages/errors/src/i18n/weblate/locales/cs.json index d4ffc525f..45c094443 100644 --- a/packages/errors/src/i18n/weblate/locales/cs.json +++ b/packages/errors/src/i18n/weblate/locales/cs.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Tento přenos přihlášení neodpovídá tomu, který jste zahájili.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Tento požadavek na přístupový klíč se nepodařilo potvrdit. Zkuste to znovu.", "auth_and_oauth.registration_closed": "Registrace na této instanci je uzavřena.", + "auth_and_oauth.registration_credentials_required": "Tato instance kontroluje nové účty. Zaregistrujte se na registrační stránce s heslem, abyste se po schválení mohli přihlásit.", "auth_and_oauth.registration_pending_approval": "Tato registrace čeká na schválení administrátorem.", "auth_and_oauth.registration_rejected": "Tato žádost o registraci byla zamítnuta.", "auth_and_oauth.registration_url_invalid": "Tento odkaz k registraci je neplatný nebo vypršel.", diff --git a/packages/errors/src/i18n/weblate/locales/da.json b/packages/errors/src/i18n/weblate/locales/da.json index 237eae833..344c3144f 100644 --- a/packages/errors/src/i18n/weblate/locales/da.json +++ b/packages/errors/src/i18n/weblate/locales/da.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Denne overførsel af login matcher ikke den, du startede.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Denne anmodning om adgangsnøgle kunne ikke bekræftes. Prøv igen.", "auth_and_oauth.registration_closed": "Tilmelding er lukket på denne instans.", + "auth_and_oauth.registration_credentials_required": "Denne instans gennemgår nye konti. Tilmeld dig fra tilmeldingssiden med en adgangskode, så du kan logge ind, når kontoen er godkendt.", "auth_and_oauth.registration_pending_approval": "Denne tilmelding afventer godkendelse fra administratoren.", "auth_and_oauth.registration_rejected": "Denne anmodning om tilmelding blev afvist.", "auth_and_oauth.registration_url_invalid": "Dette tilmeldingslink er ugyldigt eller er udløbet.", diff --git a/packages/errors/src/i18n/weblate/locales/de.json b/packages/errors/src/i18n/weblate/locales/de.json index ca9ec4b75..8b93b14e0 100644 --- a/packages/errors/src/i18n/weblate/locales/de.json +++ b/packages/errors/src/i18n/weblate/locales/de.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Diese Anmeldeübertragung stimmt nicht mit der von dir gestarteten überein.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Diese Passkey-Anfrage konnte nicht bestätigt werden. Versuche es erneut.", "auth_and_oauth.registration_closed": "Die Registrierung ist auf dieser Instanz geschlossen.", + "auth_and_oauth.registration_credentials_required": "Diese Instanz prüft neue Konten. Registriere dich über die Registrierungsseite mit einem Passwort, damit du dich nach der Freigabe anmelden kannst.", "auth_and_oauth.registration_pending_approval": "Diese Registrierung wartet auf die Genehmigung durch einen Administrator.", "auth_and_oauth.registration_rejected": "Diese Registrierungsanfrage wurde abgelehnt.", "auth_and_oauth.registration_url_invalid": "Dieser Registrierungslink ist ungültig oder abgelaufen.", diff --git a/packages/errors/src/i18n/weblate/locales/el.json b/packages/errors/src/i18n/weblate/locales/el.json index 625ac82c0..934a8d1c8 100644 --- a/packages/errors/src/i18n/weblate/locales/el.json +++ b/packages/errors/src/i18n/weblate/locales/el.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Αυτή η μεταφορά σύνδεσης δεν αντιστοιχεί σε αυτήν που ξεκινήσατε.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Δεν ήταν δυνατή η επιβεβαίωση αυτού του αιτήματος κλειδιού πρόσβασης. Δοκίμασε ξανά.", "auth_and_oauth.registration_closed": "Η εγγραφή είναι κλειστή σε αυτήν την εγκατάσταση.", + "auth_and_oauth.registration_credentials_required": "Αυτή η παρουσία ελέγχει τους νέους λογαριασμούς. Εγγραφείτε από τη σελίδα εγγραφής με κωδικό πρόσβασης, ώστε να μπορείτε να συνδεθείτε μετά την έγκριση.", "auth_and_oauth.registration_pending_approval": "Αυτή η εγγραφή περιμένει έγκριση από τον διαχειριστή.", "auth_and_oauth.registration_rejected": "Αυτό το αίτημα εγγραφής απορρίφθηκε.", "auth_and_oauth.registration_url_invalid": "Αυτός ο σύνδεσμος εγγραφής είναι άκυρος ή έχει λήξει.", diff --git a/packages/errors/src/i18n/weblate/locales/en-GB.json b/packages/errors/src/i18n/weblate/locales/en-GB.json index e42b652ac..288614276 100644 --- a/packages/errors/src/i18n/weblate/locales/en-GB.json +++ b/packages/errors/src/i18n/weblate/locales/en-GB.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "This sign-in transfer doesn't match the one you started.", "auth_and_oauth.passkey_bridge_nonce_invalid": "This passkey request could not be confirmed. Try again.", "auth_and_oauth.registration_closed": "Registration is closed on this instance.", + "auth_and_oauth.registration_credentials_required": "This instance reviews new accounts. Sign up from the sign-up page with a password so you can sign in once approved.", "auth_and_oauth.registration_pending_approval": "This registration is waiting for admin approval.", "auth_and_oauth.registration_rejected": "This registration request was rejected.", "auth_and_oauth.registration_url_invalid": "This registration link is invalid or has expired.", diff --git a/packages/errors/src/i18n/weblate/locales/es-419.json b/packages/errors/src/i18n/weblate/locales/es-419.json index c9bc13719..033dd81b3 100644 --- a/packages/errors/src/i18n/weblate/locales/es-419.json +++ b/packages/errors/src/i18n/weblate/locales/es-419.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Esta transferencia de inicio de sesión no coincide con la que iniciaste.", "auth_and_oauth.passkey_bridge_nonce_invalid": "No se pudo confirmar esta solicitud de llave de acceso. Inténtalo de nuevo.", "auth_and_oauth.registration_closed": "El registro está cerrado en esta instancia.", + "auth_and_oauth.registration_credentials_required": "Esta instancia revisa las cuentas nuevas. Regístrate desde la página de registro con una contraseña para poder iniciar sesión cuando se apruebe.", "auth_and_oauth.registration_pending_approval": "Este registro está esperando la aprobación del administrador.", "auth_and_oauth.registration_rejected": "Esta solicitud de registro fue rechazada.", "auth_and_oauth.registration_url_invalid": "Este enlace de registro es inválido o ha expirado.", diff --git a/packages/errors/src/i18n/weblate/locales/es-ES.json b/packages/errors/src/i18n/weblate/locales/es-ES.json index 585c0e0ba..1d1d2d79a 100644 --- a/packages/errors/src/i18n/weblate/locales/es-ES.json +++ b/packages/errors/src/i18n/weblate/locales/es-ES.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Esta transferencia de inicio de sesión no coincide con la que iniciaste.", "auth_and_oauth.passkey_bridge_nonce_invalid": "No se ha podido confirmar esta solicitud de clave de acceso. Inténtalo de nuevo.", "auth_and_oauth.registration_closed": "El registro está cerrado en esta instancia.", + "auth_and_oauth.registration_credentials_required": "Esta instancia revisa las cuentas nuevas. Regístrate desde la página de registro con una contraseña para poder iniciar sesión cuando se apruebe.", "auth_and_oauth.registration_pending_approval": "Este registro está pendiente de aprobación por parte del administrador.", "auth_and_oauth.registration_rejected": "Esta solicitud de registro ha sido rechazada.", "auth_and_oauth.registration_url_invalid": "Este enlace de registro no es válido o ha caducado.", diff --git a/packages/errors/src/i18n/weblate/locales/fi.json b/packages/errors/src/i18n/weblate/locales/fi.json index 9e860d97a..b5ca25630 100644 --- a/packages/errors/src/i18n/weblate/locales/fi.json +++ b/packages/errors/src/i18n/weblate/locales/fi.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Tämä kirjautumisen siirto ei vastaa aloittamaasi siirtoa.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Tätä pääsyavainpyyntöä ei voitu vahvistaa. Yritä uudelleen.", "auth_and_oauth.registration_closed": "Rekisteröinti on suljettu tällä instanssilla.", + "auth_and_oauth.registration_credentials_required": "Tämä instanssi tarkistaa uudet tilit. Rekisteröidy rekisteröitymissivulla salasanan kanssa, jotta voit kirjautua sisään hyväksynnän jälkeen.", "auth_and_oauth.registration_pending_approval": "Tämä rekisteröinti odottaa järjestelmänvalvojan hyväksyntää.", "auth_and_oauth.registration_rejected": "Tämä rekisteröintipyyntö hylättiin.", "auth_and_oauth.registration_url_invalid": "Tämä rekisteröintilinkki on virheellinen tai vanhentunut.", diff --git a/packages/errors/src/i18n/weblate/locales/fr.json b/packages/errors/src/i18n/weblate/locales/fr.json index 6cc52afec..282de387d 100644 --- a/packages/errors/src/i18n/weblate/locales/fr.json +++ b/packages/errors/src/i18n/weblate/locales/fr.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Ce transfert de connexion ne correspond pas à celui que vous avez lancé.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Cette demande de clé d'accès n'a pas pu être confirmée. Réessayez.", "auth_and_oauth.registration_closed": "L'inscription est fermée sur cette instance.", + "auth_and_oauth.registration_credentials_required": "Cette instance vérifie les nouveaux comptes. Inscrivez-vous depuis la page d'inscription avec un mot de passe pour pouvoir vous connecter une fois le compte approuvé.", "auth_and_oauth.registration_pending_approval": "Cette inscription est en attente d'approbation par un administrateur.", "auth_and_oauth.registration_rejected": "Cette demande d'inscription a été rejetée.", "auth_and_oauth.registration_url_invalid": "Ce lien d'inscription est invalide ou a expiré.", diff --git a/packages/errors/src/i18n/weblate/locales/he.json b/packages/errors/src/i18n/weblate/locales/he.json index 6dd826d35..248682f6f 100644 --- a/packages/errors/src/i18n/weblate/locales/he.json +++ b/packages/errors/src/i18n/weblate/locales/he.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "העברת ההתחברות הזו אינה תואמת לזו שהתחלת.", "auth_and_oauth.passkey_bridge_nonce_invalid": "לא הצלחנו לאשר את בקשת מפתח הגישה הזו. ניתן לנסות שוב.", "auth_and_oauth.registration_closed": "ההרשמה סגורה במופע זה.", + "auth_and_oauth.registration_credentials_required": "מופע זה בודק חשבונות חדשים. הירשם מדף ההרשמה עם סיסמה כדי שתוכל להתחבר לאחר האישור.", "auth_and_oauth.registration_pending_approval": "הרשמה זו ממתינה לאישור מנהל.", "auth_and_oauth.registration_rejected": "בקשת הרשמה זו נדחתה.", "auth_and_oauth.registration_url_invalid": "קישור ההרשמה הזה שגוי או שפג תוקפו.", diff --git a/packages/errors/src/i18n/weblate/locales/hi.json b/packages/errors/src/i18n/weblate/locales/hi.json index ac2c61574..3a7bb86f0 100644 --- a/packages/errors/src/i18n/weblate/locales/hi.json +++ b/packages/errors/src/i18n/weblate/locales/hi.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "यह साइन-इन ट्रांसफ़र आपके द्वारा शुरू किए गए ट्रांसफ़र से मेल नहीं खाता।", "auth_and_oauth.passkey_bridge_nonce_invalid": "इस पासकी अनुरोध की पुष्टि नहीं हो सकी। फिर से कोशिश करें।", "auth_and_oauth.registration_closed": "इस इंस्टेंस पर रजिस्ट्रेशन बंद है।", + "auth_and_oauth.registration_credentials_required": "यह इंस्टेंस नए खातों की समीक्षा करता है। साइन-अप पेज से पासवर्ड के साथ साइन अप करें ताकि अप्रूवल के बाद आप साइन इन कर सकें।", "auth_and_oauth.registration_pending_approval": "यह रजिस्ट्रेशन एडमिन अप्रूवल का इंतज़ार कर रहा है।", "auth_and_oauth.registration_rejected": "यह रजिस्ट्रेशन रिक्वेस्ट रिजेक्ट कर दी गई है।", "auth_and_oauth.registration_url_invalid": "यह रजिस्ट्रेशन लिंक अमान्य है या एक्सपायर हो गया है।", diff --git a/packages/errors/src/i18n/weblate/locales/hr.json b/packages/errors/src/i18n/weblate/locales/hr.json index 7f79f12f8..3c1a2c69f 100644 --- a/packages/errors/src/i18n/weblate/locales/hr.json +++ b/packages/errors/src/i18n/weblate/locales/hr.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Ovaj prijenos prijave ne odgovara onome koji ste započeli.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Ovaj zahtjev za pristupni ključ nije moguće potvrditi. Pokušaj ponovno.", "auth_and_oauth.registration_closed": "Registracija je zatvorena na ovoj instanci.", + "auth_and_oauth.registration_credentials_required": "Ova instanca pregledava nove račune. Registrirajte se na stranici za registraciju s lozinkom kako biste se mogli prijaviti nakon odobrenja.", "auth_and_oauth.registration_pending_approval": "Ova registracija čeka odobrenje administratora.", "auth_and_oauth.registration_rejected": "Ovaj zahtjev za registraciju je odbijen.", "auth_and_oauth.registration_url_invalid": "Ova poveznica za registraciju je nevažeća ili je istekla.", diff --git a/packages/errors/src/i18n/weblate/locales/hu.json b/packages/errors/src/i18n/weblate/locales/hu.json index 99723c6c9..a0bf4229d 100644 --- a/packages/errors/src/i18n/weblate/locales/hu.json +++ b/packages/errors/src/i18n/weblate/locales/hu.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Ez a bejelentkezés-átvitel nem egyezik azzal, amelyet elindítottál.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Ezt a jelszókulcs-kérést nem sikerült megerősíteni. Próbáld újra.", "auth_and_oauth.registration_closed": "Ezen a példányon a regisztráció le van zárva.", + "auth_and_oauth.registration_credentials_required": "Ez a példány ellenőrzi az új fiókokat. Regisztrálj a regisztrációs oldalon jelszóval, hogy jóváhagyás után be tudj jelentkezni.", "auth_and_oauth.registration_pending_approval": "Ez a regisztráció rendszergazdai jóváhagyásra vár.", "auth_and_oauth.registration_rejected": "Ez a regisztrációs kérelem el lett utasítva.", "auth_and_oauth.registration_url_invalid": "Ez a regisztrációs link érvénytelen vagy lejárt.", diff --git a/packages/errors/src/i18n/weblate/locales/id.json b/packages/errors/src/i18n/weblate/locales/id.json index 4e8bca306..4bea8bce6 100644 --- a/packages/errors/src/i18n/weblate/locales/id.json +++ b/packages/errors/src/i18n/weblate/locales/id.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Transfer masuk ini tidak cocok dengan yang kamu mulai.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Permintaan passkey ini tidak dapat dikonfirmasi. Coba lagi.", "auth_and_oauth.registration_closed": "Pendaftaran ditutup di instance ini.", + "auth_and_oauth.registration_credentials_required": "Instans ini meninjau akun baru. Daftar dari halaman pendaftaran dengan kata sandi agar kamu bisa masuk setelah disetujui.", "auth_and_oauth.registration_pending_approval": "Pendaftaran ini menunggu persetujuan admin.", "auth_and_oauth.registration_rejected": "Permintaan pendaftaran ini ditolak.", "auth_and_oauth.registration_url_invalid": "Tautan pendaftaran ini tidak valid atau telah kedaluwarsa.", diff --git a/packages/errors/src/i18n/weblate/locales/it.json b/packages/errors/src/i18n/weblate/locales/it.json index ff6b62841..6eb5765f4 100644 --- a/packages/errors/src/i18n/weblate/locales/it.json +++ b/packages/errors/src/i18n/weblate/locales/it.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Questo trasferimento di accesso non corrisponde a quello che hai avviato.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Non è stato possibile confermare questa richiesta di passkey. Riprova.", "auth_and_oauth.registration_closed": "La registrazione è chiusa su questa istanza.", + "auth_and_oauth.registration_credentials_required": "Questa istanza verifica i nuovi account. Registrati dalla pagina di registrazione con una password per poter accedere dopo l'approvazione.", "auth_and_oauth.registration_pending_approval": "Questa registrazione è in attesa dell'approvazione dell'amministratore.", "auth_and_oauth.registration_rejected": "Questa richiesta di registrazione è stata rifiutata.", "auth_and_oauth.registration_url_invalid": "Questo link di registrazione non è valido o è scaduto.", diff --git a/packages/errors/src/i18n/weblate/locales/ja.json b/packages/errors/src/i18n/weblate/locales/ja.json index 3e851782c..ea9a80adb 100644 --- a/packages/errors/src/i18n/weblate/locales/ja.json +++ b/packages/errors/src/i18n/weblate/locales/ja.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "このサインインの引き継ぎは、開始したものと一致しません。", "auth_and_oauth.passkey_bridge_nonce_invalid": "このパスキーのリクエストを確認できませんでした。もう一度お試しください。", "auth_and_oauth.registration_closed": "このインスタンスでは新規登録を受け付けていません。", + "auth_and_oauth.registration_credentials_required": "このインスタンスでは新しいアカウントを審査しています。承認後にサインインできるよう、登録ページからパスワードを設定して登録してください。", "auth_and_oauth.registration_pending_approval": "この登録は管理者による承認待ちです。", "auth_and_oauth.registration_rejected": "この登録リクエストは拒否されました。", "auth_and_oauth.registration_url_invalid": "この登録リンクは無効または期限切れです。", diff --git a/packages/errors/src/i18n/weblate/locales/ko.json b/packages/errors/src/i18n/weblate/locales/ko.json index 05ad055d7..0c2a8702b 100644 --- a/packages/errors/src/i18n/weblate/locales/ko.json +++ b/packages/errors/src/i18n/weblate/locales/ko.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "이 로그인 이전 요청이 시작하신 요청과 일치하지 않습니다.", "auth_and_oauth.passkey_bridge_nonce_invalid": "이 패스키 요청을 확인하지 못했어요. 다시 시도해 주세요.", "auth_and_oauth.registration_closed": "이 인스턴스에서는 가입을 받고 있지 않아요.", + "auth_and_oauth.registration_credentials_required": "이 인스턴스는 새 계정을 검토해요. 승인 후 로그인할 수 있도록 가입 페이지에서 비밀번호를 설정해 가입하세요.", "auth_and_oauth.registration_pending_approval": "이 가입 신청은 관리자 승인을 기다리고 있어요.", "auth_and_oauth.registration_rejected": "이 가입 요청은 거부되었어요.", "auth_and_oauth.registration_url_invalid": "이 가입 링크는 유효하지 않거나 만료되었어요.", diff --git a/packages/errors/src/i18n/weblate/locales/lt.json b/packages/errors/src/i18n/weblate/locales/lt.json index 28fbbebd0..789f550f9 100644 --- a/packages/errors/src/i18n/weblate/locales/lt.json +++ b/packages/errors/src/i18n/weblate/locales/lt.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Šis prisijungimo perkėlimas nesutampa su tuo, kurį pradėjote.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Nepavyko patvirtinti šios prieigos rakto užklausos. Bandyk dar kartą.", "auth_and_oauth.registration_closed": "Registracija šioje instancijoje yra uždaryta.", + "auth_and_oauth.registration_credentials_required": "Ši instancija peržiūri naujas paskyras. Užsiregistruokite registracijos puslapyje su slaptažodžiu, kad po patvirtinimo galėtumėte prisijungti.", "auth_and_oauth.registration_pending_approval": "Ši registracija laukia administratoriaus patvirtinimo.", "auth_and_oauth.registration_rejected": "Šis registracijos prašymas buvo atmestas.", "auth_and_oauth.registration_url_invalid": "Ši registracijos nuoroda yra neteisinga arba baigėsi jos galiojimas.", diff --git a/packages/errors/src/i18n/weblate/locales/nl.json b/packages/errors/src/i18n/weblate/locales/nl.json index 1f3a5ebe2..0c61fe65b 100644 --- a/packages/errors/src/i18n/weblate/locales/nl.json +++ b/packages/errors/src/i18n/weblate/locales/nl.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Deze aanmeldoverdracht komt niet overeen met de overdracht die je hebt gestart.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Dit passkey-verzoek kon niet worden bevestigd. Probeer het opnieuw.", "auth_and_oauth.registration_closed": "Registratie is gesloten op deze instantie.", + "auth_and_oauth.registration_credentials_required": "Deze instantie beoordeelt nieuwe accounts. Registreer je via de registratiepagina met een wachtwoord, zodat je kunt inloggen zodra je account is goedgekeurd.", "auth_and_oauth.registration_pending_approval": "Deze registratie wacht op goedkeuring van de beheerder.", "auth_and_oauth.registration_rejected": "Dit registratieverzoek is afgewezen.", "auth_and_oauth.registration_url_invalid": "Deze registratielink is ongeldig of verlopen.", diff --git a/packages/errors/src/i18n/weblate/locales/no.json b/packages/errors/src/i18n/weblate/locales/no.json index 0ed350517..e2666cc77 100644 --- a/packages/errors/src/i18n/weblate/locales/no.json +++ b/packages/errors/src/i18n/weblate/locales/no.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Denne overføringen av påloggingen samsvarer ikke med den du startet.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Denne forespørselen om passnøkkel kunne ikke bekreftes. Prøv igjen.", "auth_and_oauth.registration_closed": "Registrering er stengt på denne instansen.", + "auth_and_oauth.registration_credentials_required": "Denne instansen går gjennom nye kontoer. Registrer deg fra registreringssiden med et passord, slik at du kan logge inn når kontoen er godkjent.", "auth_and_oauth.registration_pending_approval": "Denne registreringen venter på administratorgodkjenning.", "auth_and_oauth.registration_rejected": "Denne registreringsforespørselen ble avvist.", "auth_and_oauth.registration_url_invalid": "Denne registreringslenken er ugyldig eller har utløpt.", diff --git a/packages/errors/src/i18n/weblate/locales/pl.json b/packages/errors/src/i18n/weblate/locales/pl.json index 89e4b359d..b1f60f6e5 100644 --- a/packages/errors/src/i18n/weblate/locales/pl.json +++ b/packages/errors/src/i18n/weblate/locales/pl.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "To przeniesienie logowania nie pasuje do tego, które zostało rozpoczęte.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Nie udało się potwierdzić tego żądania klucza dostępu. Spróbuj ponownie.", "auth_and_oauth.registration_closed": "Rejestracja na tej instancji jest zamknięta.", + "auth_and_oauth.registration_credentials_required": "Ta instancja sprawdza nowe konta. Zarejestruj się na stronie rejestracji z hasłem, aby po zatwierdzeniu móc się zalogować.", "auth_and_oauth.registration_pending_approval": "Ta rejestracja czeka na zatwierdzenie przez administratora.", "auth_and_oauth.registration_rejected": "Ten wniosek o rejestrację został odrzucony.", "auth_and_oauth.registration_url_invalid": "Ten link rejestracyjny jest nieprawidłowy lub wygasł.", diff --git a/packages/errors/src/i18n/weblate/locales/pt-BR.json b/packages/errors/src/i18n/weblate/locales/pt-BR.json index 52bcb13db..1ed962b1c 100644 --- a/packages/errors/src/i18n/weblate/locales/pt-BR.json +++ b/packages/errors/src/i18n/weblate/locales/pt-BR.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Esta transferência de login não corresponde à que você iniciou.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Não foi possível confirmar esta solicitação de chave de acesso. Tente novamente.", "auth_and_oauth.registration_closed": "O cadastro está fechado nesta instância.", + "auth_and_oauth.registration_credentials_required": "Esta instância analisa novas contas. Cadastre-se pela página de cadastro com uma senha para poder entrar depois da aprovação.", "auth_and_oauth.registration_pending_approval": "Este cadastro está aguardando a aprovação de um administrador.", "auth_and_oauth.registration_rejected": "Esta solicitação de cadastro foi rejeitada.", "auth_and_oauth.registration_url_invalid": "Este link de cadastro é inválido ou expirou.", diff --git a/packages/errors/src/i18n/weblate/locales/ro.json b/packages/errors/src/i18n/weblate/locales/ro.json index d976f56eb..d638eb224 100644 --- a/packages/errors/src/i18n/weblate/locales/ro.json +++ b/packages/errors/src/i18n/weblate/locales/ro.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Acest transfer de autentificare nu corespunde celui pe care l-ai început.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Această cerere pentru cheia de acces nu a putut fi confirmată. Încearcă din nou.", "auth_and_oauth.registration_closed": "Înregistrarea este închisă pe această instanță.", + "auth_and_oauth.registration_credentials_required": "Această instanță verifică conturile noi. Înregistrează-te din pagina de înregistrare cu o parolă, ca să te poți conecta după aprobare.", "auth_and_oauth.registration_pending_approval": "Această înregistrare așteaptă aprobarea administratorului.", "auth_and_oauth.registration_rejected": "Această cerere de înregistrare a fost respinsă.", "auth_and_oauth.registration_url_invalid": "Acest link de înregistrare este invalid sau a expirat.", diff --git a/packages/errors/src/i18n/weblate/locales/ru.json b/packages/errors/src/i18n/weblate/locales/ru.json index 782e12008..1a9df2808 100644 --- a/packages/errors/src/i18n/weblate/locales/ru.json +++ b/packages/errors/src/i18n/weblate/locales/ru.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Этот перенос входа не совпадает с тем, который вы начали.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Не удалось подтвердить этот запрос ключа доступа. Повторите попытку.", "auth_and_oauth.registration_closed": "Регистрация в этом экземпляре закрыта.", + "auth_and_oauth.registration_credentials_required": "Этот экземпляр проверяет новые учетные записи. Зарегистрируйтесь на странице регистрации с паролем, чтобы войти после одобрения.", "auth_and_oauth.registration_pending_approval": "Эта регистрация ожидает одобрения администратора.", "auth_and_oauth.registration_rejected": "Заявка на регистрацию отклонена.", "auth_and_oauth.registration_url_invalid": "Эта ссылка для регистрации недействительна или её срок действия истёк.", diff --git a/packages/errors/src/i18n/weblate/locales/sv-SE.json b/packages/errors/src/i18n/weblate/locales/sv-SE.json index 4432f4332..b3ff5a8d4 100644 --- a/packages/errors/src/i18n/weblate/locales/sv-SE.json +++ b/packages/errors/src/i18n/weblate/locales/sv-SE.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Den här inloggningsöverföringen matchar inte den du startade.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Den här begäran om lösennyckel kunde inte bekräftas. Försök igen.", "auth_and_oauth.registration_closed": "Registrering är stängd på den här instansen.", + "auth_and_oauth.registration_credentials_required": "Den här instansen granskar nya konton. Registrera dig från registreringssidan med ett lösenord så att du kan logga in när kontot har godkänts.", "auth_and_oauth.registration_pending_approval": "Den här registreringen väntar på administratörsgodkännande.", "auth_and_oauth.registration_rejected": "Den här registreringsbegäran avslogs.", "auth_and_oauth.registration_url_invalid": "Den här registreringslänken är ogiltig eller har gått ut.", diff --git a/packages/errors/src/i18n/weblate/locales/th.json b/packages/errors/src/i18n/weblate/locales/th.json index 221581112..19a6387a4 100644 --- a/packages/errors/src/i18n/weblate/locales/th.json +++ b/packages/errors/src/i18n/weblate/locales/th.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "การโอนการลงชื่อเข้าใช้นี้ไม่ตรงกับรายการที่คุณเริ่มไว้", "auth_and_oauth.passkey_bridge_nonce_invalid": "ไม่สามารถยืนยันคำขอพาสคีย์นี้ได้ ลองอีกครั้ง", "auth_and_oauth.registration_closed": "การลงทะเบียนปิดให้บริการในอินสแตนซ์นี้", + "auth_and_oauth.registration_credentials_required": "อินสแตนซ์นี้ตรวจสอบบัญชีใหม่ โปรดลงทะเบียนจากหน้าลงทะเบียนพร้อมรหัสผ่าน เพื่อให้คุณเข้าสู่ระบบได้หลังจากได้รับการอนุมัติ", "auth_and_oauth.registration_pending_approval": "การลงทะเบียนนี้กำลังรอการอนุมัติจากผู้ดูแลระบบ", "auth_and_oauth.registration_rejected": "คำขอลงทะเบียนนี้ถูกปฏิเสธ", "auth_and_oauth.registration_url_invalid": "ลิงก์ลงทะเบียนนี้ไม่ถูกต้องหรือหมดอายุแล้ว", diff --git a/packages/errors/src/i18n/weblate/locales/tr.json b/packages/errors/src/i18n/weblate/locales/tr.json index 063d1741a..e14771cc2 100644 --- a/packages/errors/src/i18n/weblate/locales/tr.json +++ b/packages/errors/src/i18n/weblate/locales/tr.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Bu oturum açma aktarımı başlattığınız aktarımla eşleşmiyor.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Bu geçiş anahtarı isteği doğrulanamadı. Tekrar dene.", "auth_and_oauth.registration_closed": "Bu kurulumda kayıt kapalı.", + "auth_and_oauth.registration_credentials_required": "Bu örnek yeni hesapları inceliyor. Onaylandıktan sonra giriş yapabilmek için kayıt sayfasından bir parolayla kaydolun.", "auth_and_oauth.registration_pending_approval": "Bu kayıt yönetici onayını bekliyor.", "auth_and_oauth.registration_rejected": "Bu kayıt isteği reddedildi.", "auth_and_oauth.registration_url_invalid": "Bu kayıt bağlantısı geçersiz veya süresi dolmuş.", diff --git a/packages/errors/src/i18n/weblate/locales/uk.json b/packages/errors/src/i18n/weblate/locales/uk.json index 0424faa37..8be683f16 100644 --- a/packages/errors/src/i18n/weblate/locales/uk.json +++ b/packages/errors/src/i18n/weblate/locales/uk.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Це перенесення входу не збігається з тим, яке ви розпочали.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Не вдалося підтвердити цей запит ключа доступу. Спробуй ще раз.", "auth_and_oauth.registration_closed": "Реєстрацію на цьому екземплярі закрито.", + "auth_and_oauth.registration_credentials_required": "Цей екземпляр перевіряє нові облікові записи. Зареєструйтеся на сторінці реєстрації з паролем, щоб увійти після схвалення.", "auth_and_oauth.registration_pending_approval": "Ця реєстрація очікує на схвалення адміністратором.", "auth_and_oauth.registration_rejected": "Цей запит на реєстрацію було відхилено.", "auth_and_oauth.registration_url_invalid": "Це посилання для реєстрації недійсне або термін його дії минув.", diff --git a/packages/errors/src/i18n/weblate/locales/vi.json b/packages/errors/src/i18n/weblate/locales/vi.json index e8552b06f..03620faa8 100644 --- a/packages/errors/src/i18n/weblate/locales/vi.json +++ b/packages/errors/src/i18n/weblate/locales/vi.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "Lần chuyển đăng nhập này không khớp với lần bạn đã bắt đầu.", "auth_and_oauth.passkey_bridge_nonce_invalid": "Không thể xác nhận yêu cầu khóa truy cập này. Hãy thử lại.", "auth_and_oauth.registration_closed": "Đăng ký đã bị đóng trên phiên bản này.", + "auth_and_oauth.registration_credentials_required": "Phiên bản này xét duyệt tài khoản mới. Hãy đăng ký từ trang đăng ký với mật khẩu để có thể đăng nhập sau khi được phê duyệt.", "auth_and_oauth.registration_pending_approval": "Đăng ký này đang chờ quản trị viên phê duyệt.", "auth_and_oauth.registration_rejected": "Yêu cầu đăng ký này đã bị từ chối.", "auth_and_oauth.registration_url_invalid": "Liên kết đăng ký này không hợp lệ hoặc đã hết hạn.", diff --git a/packages/errors/src/i18n/weblate/locales/zh-CN.json b/packages/errors/src/i18n/weblate/locales/zh-CN.json index 66287bf72..d2ff3cb82 100644 --- a/packages/errors/src/i18n/weblate/locales/zh-CN.json +++ b/packages/errors/src/i18n/weblate/locales/zh-CN.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "此登录转移与你发起的不一致。", "auth_and_oauth.passkey_bridge_nonce_invalid": "无法确认此通行密钥请求。请重试。", "auth_and_oauth.registration_closed": "此实例的注册已关闭。", + "auth_and_oauth.registration_credentials_required": "此实例会审核新帐户。请在注册页面使用密码注册,以便获批后登录。", "auth_and_oauth.registration_pending_approval": "此注册正在等待管理员批准。", "auth_and_oauth.registration_rejected": "此注册请求已被拒绝。", "auth_and_oauth.registration_url_invalid": "此注册链接无效或已过期。", diff --git a/packages/errors/src/i18n/weblate/locales/zh-TW.json b/packages/errors/src/i18n/weblate/locales/zh-TW.json index 249433f1d..873840029 100644 --- a/packages/errors/src/i18n/weblate/locales/zh-TW.json +++ b/packages/errors/src/i18n/weblate/locales/zh-TW.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "此登入轉移與您發起的不一致。", "auth_and_oauth.passkey_bridge_nonce_invalid": "無法確認此通行金鑰要求。請再試一次。", "auth_and_oauth.registration_closed": "此實例已關閉註冊。", + "auth_and_oauth.registration_credentials_required": "此執行個體會審核新帳號。請在註冊頁面使用密碼註冊,以便核准後登入。", "auth_and_oauth.registration_pending_approval": "此註冊正在等待管理員核准。", "auth_and_oauth.registration_rejected": "此註冊要求已被拒絕。", "auth_and_oauth.registration_url_invalid": "此註冊連結無效或已過期。", diff --git a/packages/errors/src/i18n/weblate/messages.json b/packages/errors/src/i18n/weblate/messages.json index 8a9ba0a33..3d52f9a9b 100644 --- a/packages/errors/src/i18n/weblate/messages.json +++ b/packages/errors/src/i18n/weblate/messages.json @@ -89,6 +89,7 @@ "auth_and_oauth.origin_handoff_nonce_invalid": "This sign-in transfer doesn't match the one you started.", "auth_and_oauth.passkey_bridge_nonce_invalid": "This passkey request could not be confirmed. Try again.", "auth_and_oauth.registration_closed": "Registration is closed on this instance.", + "auth_and_oauth.registration_credentials_required": "This instance reviews new accounts. Sign up from the sign-up page with a password so you can sign in once approved.", "auth_and_oauth.registration_pending_approval": "This registration is waiting for admin approval.", "auth_and_oauth.registration_rejected": "This registration request was rejected.", "auth_and_oauth.registration_url_invalid": "This registration link is invalid or has expired.", diff --git a/packages/schema/src/domains/admin/AdminSchemas.ts b/packages/schema/src/domains/admin/AdminSchemas.ts index ee53cc3a7..7f23c0fc6 100644 --- a/packages/schema/src/domains/admin/AdminSchemas.ts +++ b/packages/schema/src/domains/admin/AdminSchemas.ts @@ -521,6 +521,7 @@ export const PendingRegistrationResponse = z.object({ requested_at: z.iso.datetime(), registration_url_id: createStringType(1, 128).nullable(), client_ip: z.string().nullable(), + invite_code: createStringType(0, 256).nullable(), }); export type PendingRegistrationResponse = z.infer;