fix(auth): fix approval signups and auto-join invites (#3467)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-14 16:41:33 +02:00
1 parent e403fb6cce
commit d5ca5d8293
64 files changed
+622 -72

No files matched your search

@@ -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<PendingRegistrationResponse>(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<RegistrationTokenResponse>(harness)
.post('/auth/register')
.body({global_name: 'Open Unclaimed', date_of_birth: '2000-01-01', consent: true})
.execute();
expect(registration.token.length).toBeGreaterThan(0);
});
});
@@ -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<PendingRegistrationListResponse>(harness, admin.token)
.patch(`/admin/instance/pending-registrations/${account.userId}`)
+77 -1
View File
@@ -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<SsoStartResponse>(harness)
.post('/auth/sso/start')
.body({})
.execute();
const completeData = await createBuilderWithoutAuth<SsoCompleteResponse>(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<SsoStartResponse>(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<PendingRegistrationListResponse>(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;
@@ -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', () => {