mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
feat(admin): move pending registrations to their own page (#1388)
This commit is contained in:
1 parent
97b2ac91ca
commit
abcb5cb814
31 files changed
+839
-328
No files matched your search
@@ -39,6 +39,7 @@ export const AdminAuditReadActions = {
|
||||
LIST_GUILD_MEMORY_STATS: 'list_guild_memory_stats',
|
||||
LIST_GUILD_STICKERS: 'list_guild_stickers',
|
||||
LIST_GUILD_THREADS: 'list_guild_threads',
|
||||
LIST_PENDING_REGISTRATIONS: 'list_pending_registrations',
|
||||
LIST_REPORT_REASONS: 'list_report_reasons',
|
||||
LIST_USER_APPLICATIONS: 'list_user_applications',
|
||||
LIST_USER_CHANGE_LOG: 'list_user_change_log',
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
InstanceEmailSmtpTestRequest,
|
||||
InstanceEmailSmtpTestResponse,
|
||||
PendingRegistrationActionRequest,
|
||||
PendingRegistrationListResponse,
|
||||
RegistrationUrlIdParam,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import {DomainMigrationConfigSchema} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas';
|
||||
@@ -98,7 +99,6 @@ async function buildInstanceConfigResponse(): Promise<InstanceConfigResponse> {
|
||||
experimentDelivery,
|
||||
registrationConfig,
|
||||
registrationUrls,
|
||||
pendingRegistrations,
|
||||
] = await Promise.all([
|
||||
instanceConfigRepository.getSsoConfig(),
|
||||
instanceConfigRepository.getGatewayRolloutConfig(),
|
||||
@@ -109,7 +109,6 @@ async function buildInstanceConfigResponse(): Promise<InstanceConfigResponse> {
|
||||
instanceConfigRepository.getExperimentDeliveryConfig(),
|
||||
instanceConfigRepository.getRegistrationConfig(),
|
||||
instanceConfigRepository.getRegistrationUrlsForAdmin(),
|
||||
instanceConfigRepository.getPendingRegistrations(),
|
||||
]);
|
||||
const [appPublic, policy, resolvedServices, integrations, media, billing, accountIdentity, accountIdentityLocked] =
|
||||
await Promise.all([
|
||||
@@ -148,7 +147,6 @@ async function buildInstanceConfigResponse(): Promise<InstanceConfigResponse> {
|
||||
registration: {
|
||||
...registrationConfig,
|
||||
urls: registrationUrls,
|
||||
pending_registrations: pendingRegistrations,
|
||||
},
|
||||
self_hosted: Config.instance.selfHosted,
|
||||
account_identity: {
|
||||
@@ -320,10 +318,7 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
targetType: 'instance_config',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.GET_INSTANCE_CONFIG,
|
||||
metadata: {
|
||||
registration_url_count: response.registration.urls.length,
|
||||
pending_registration_count: response.registration.pending_registrations.length,
|
||||
},
|
||||
metadata: {registration_url_count: response.registration.urls.length},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
@@ -738,18 +733,43 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
return ctx.json(await buildInstanceConfigResponse());
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
'/admin/instance/pending-registrations',
|
||||
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
|
||||
requireAdminACL(AdminACLs.REGISTRATION_REVIEW),
|
||||
OpenAPI({
|
||||
operationId: 'list_admin_pending_registrations',
|
||||
summary: 'List pending registrations',
|
||||
description:
|
||||
'Lists every registration waiting for manual review, oldest first. Requires REGISTRATION_REVIEW permission.',
|
||||
responseSchema: PendingRegistrationListResponse,
|
||||
statusCode: 200,
|
||||
security: 'adminApiKey',
|
||||
tags: 'Admin',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const pendingRegistrations = await instanceConfigRepository.getPendingRegistrations();
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'instance_config',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.LIST_PENDING_REGISTRATIONS,
|
||||
metadata: {pending_registration_count: pendingRegistrations.length},
|
||||
});
|
||||
return ctx.json({pending_registrations: pendingRegistrations});
|
||||
},
|
||||
);
|
||||
app.patch(
|
||||
'/admin/instance/pending-registrations/:user_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
|
||||
requireAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE),
|
||||
requireAdminACL(AdminACLs.REGISTRATION_REVIEW),
|
||||
Validator('param', UserIdParam),
|
||||
Validator('json', PendingRegistrationActionRequest),
|
||||
OpenAPI({
|
||||
operationId: 'update_admin_pending_registration',
|
||||
summary: 'Approve or reject a pending registration',
|
||||
description:
|
||||
'Decides a registration waiting for manual review. Approving removes its pending registration trait, rejecting also prevents the account from logging in. Requires INSTANCE_CONFIG_UPDATE permission.',
|
||||
responseSchema: InstanceConfigResponse,
|
||||
'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.',
|
||||
responseSchema: PendingRegistrationListResponse,
|
||||
statusCode: 200,
|
||||
security: 'adminApiKey',
|
||||
tags: 'Admin',
|
||||
@@ -759,7 +779,7 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
const decision = ctx.req.valid('json').status === 'approved' ? 'approve' : 'reject';
|
||||
await updatePendingRegistrationUser(ctx, userId, decision);
|
||||
await instanceConfigRepository.removePendingRegistration(userId);
|
||||
return ctx.json(await buildInstanceConfigResponse());
|
||||
return ctx.json({pending_registrations: await instanceConfigRepository.getPendingRegistrations()});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
RequireEmailAccountIdentity,
|
||||
RequireUsernameAccountIdentity,
|
||||
} from '@app/api/middleware/AccountIdentityMiddleware';
|
||||
import {requireAdminACL, requireGrantedAdminACLs} from '@app/api/middleware/AdminMiddleware';
|
||||
import {hasAdminACL, requireAdminACL, requireGrantedAdminACLs} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware';
|
||||
import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons';
|
||||
import {RateLimitConfigs} from '@app/api/RateLimitConfig';
|
||||
import type {HonoApp} from '@app/api/types/HonoEnv';
|
||||
import {inputValidationErrorFromZodIssues, Validator} from '@app/api/Validator';
|
||||
@@ -94,14 +95,15 @@ export function UserAdminController(app: HonoApp) {
|
||||
security: 'adminApiKey',
|
||||
tags: 'Admin',
|
||||
description:
|
||||
'Return the admin the request was authenticated as, with the admin permissions, roles, and metadata of the account. Requires AUTHENTICATE permission.',
|
||||
'Return the admin the request was authenticated as, with the admin permissions, roles, and metadata of the account, plus the number of pending registrations when the admin holds REGISTRATION_REVIEW. Requires AUTHENTICATE permission.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const adminUser = ctx.get('user');
|
||||
const cacheService = ctx.get('cacheService');
|
||||
return ctx.json({
|
||||
user: await mapUserToAdminResponse(adminUser, cacheService),
|
||||
});
|
||||
const canReviewRegistrations = hasAdminACL(ctx.get('adminUserAcls'), AdminACLs.REGISTRATION_REVIEW);
|
||||
const [user, pendingRegistrations] = await Promise.all([
|
||||
mapUserToAdminResponse(ctx.get('user'), ctx.get('cacheService')),
|
||||
canReviewRegistrations ? getInstanceConfigRepository().getPendingRegistrations() : null,
|
||||
]);
|
||||
return ctx.json({user, pending_registration_count: pendingRegistrations?.length ?? null});
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
|
||||
@@ -13,7 +13,9 @@ import {createApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@app/api/test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
|
||||
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
||||
import {afterAll, beforeAll, beforeEach, describe, it} from 'vitest';
|
||||
import type {PendingRegistrationListResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {AdminUsersMeResponse} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
interface PendingRegistrationResponse {
|
||||
user_id: string;
|
||||
@@ -37,7 +39,7 @@ describe('pending registration approval and the stock community', () => {
|
||||
it('keeps a banned user out of the stock community on approval', async () => {
|
||||
const admin = await setUserACLs(harness, await createTestAccount(harness), [
|
||||
AdminACLs.AUTHENTICATE,
|
||||
AdminACLs.INSTANCE_CONFIG_UPDATE,
|
||||
AdminACLs.REGISTRATION_REVIEW,
|
||||
]);
|
||||
const {owner, guild} = await setupTestGuildWithMembers(harness, 0);
|
||||
await getInstanceConfigRepository().setInstancePolicyConfig({
|
||||
@@ -79,7 +81,7 @@ describe('pending registration approval and the stock community', () => {
|
||||
it('does not add a user who was never pending to the stock community', async () => {
|
||||
const admin = await setUserACLs(harness, await createTestAccount(harness), [
|
||||
AdminACLs.AUTHENTICATE,
|
||||
AdminACLs.INSTANCE_CONFIG_UPDATE,
|
||||
AdminACLs.REGISTRATION_REVIEW,
|
||||
]);
|
||||
const {owner, guild} = await setupTestGuildWithMembers(harness, 0);
|
||||
const outsider = await createTestAccount(harness);
|
||||
@@ -99,4 +101,59 @@ describe('pending registration approval and the stock community', () => {
|
||||
.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,
|
||||
AdminACLs.REGISTRATION_REVIEW,
|
||||
]);
|
||||
const configAdmin = await setUserACLs(harness, await createTestAccount(harness), [
|
||||
AdminACLs.AUTHENTICATE,
|
||||
AdminACLs.INSTANCE_CONFIG_VIEW,
|
||||
AdminACLs.INSTANCE_CONFIG_UPDATE,
|
||||
]);
|
||||
await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'});
|
||||
const pending = await createBuilderWithoutAuth<PendingRegistrationResponse>(harness)
|
||||
.post('/auth/register')
|
||||
.body({
|
||||
email: createUniqueEmail('reviewonly'),
|
||||
username: createUniqueUsername('reviewonly'),
|
||||
global_name: 'Review only',
|
||||
password: 'reviewing-since-1999',
|
||||
date_of_birth: '2000-01-01',
|
||||
consent: true,
|
||||
})
|
||||
.execute();
|
||||
|
||||
const listed = await createBuilder<PendingRegistrationListResponse>(harness, reviewer.token)
|
||||
.get('/admin/instance/pending-registrations')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect(listed.pending_registrations.map((entry) => entry.user_id)).toEqual([pending.user_id]);
|
||||
const reviewerMe = await createBuilder<AdminUsersMeResponse>(harness, reviewer.token)
|
||||
.get('/admin/users/@me')
|
||||
.execute();
|
||||
expect(reviewerMe.pending_registration_count).toBe(1);
|
||||
const configAdminMe = await createBuilder<AdminUsersMeResponse>(harness, configAdmin.token)
|
||||
.get('/admin/users/@me')
|
||||
.execute();
|
||||
expect(configAdminMe.pending_registration_count).toBeNull();
|
||||
|
||||
await createBuilder(harness, configAdmin.token)
|
||||
.get('/admin/instance/pending-registrations')
|
||||
.expect(HTTP_STATUS.FORBIDDEN, 'MISSING_ACL')
|
||||
.execute();
|
||||
await createBuilder(harness, configAdmin.token)
|
||||
.patch(`/admin/instance/pending-registrations/${pending.user_id}`)
|
||||
.body({status: 'approved'})
|
||||
.expect(HTTP_STATUS.FORBIDDEN, 'MISSING_ACL')
|
||||
.execute();
|
||||
|
||||
const decided = await createBuilder<PendingRegistrationListResponse>(harness, reviewer.token)
|
||||
.patch(`/admin/instance/pending-registrations/${pending.user_id}`)
|
||||
.body({status: 'rejected'})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect(decided.pending_registrations).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,7 @@ export const InstanceConfigAdminAuditCases: ReadonlyArray<AdminAuditCoverageCase
|
||||
action: 'get_instance_config',
|
||||
targetType: 'instance_config',
|
||||
targetId: '0',
|
||||
metadata: {registration_url_count: '1', pending_registration_count: '0'},
|
||||
metadata: {registration_url_count: '1'},
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -129,6 +129,30 @@ export const InstanceConfigAdminAuditCases: ReadonlyArray<AdminAuditCoverageCase
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
route: '/admin/instance/pending-registrations',
|
||||
async prepare({harness}) {
|
||||
await getInstanceConfigRepository().setRegistrationConfig({mode: 'approval'});
|
||||
await registerUser(harness, {
|
||||
email: createUniqueEmail('coverage-listed'),
|
||||
username: createUniqueUsername('coveragelisted'),
|
||||
global_name: 'Coverage Listed',
|
||||
password: 'coverage-listed-password',
|
||||
date_of_birth: '2000-01-01',
|
||||
consent: true,
|
||||
});
|
||||
return {
|
||||
request: {path: '/admin/instance/pending-registrations'},
|
||||
expected: {
|
||||
action: 'list_pending_registrations',
|
||||
targetType: 'instance_config',
|
||||
targetId: '0',
|
||||
metadata: {pending_registration_count: '1'},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'PATCH',
|
||||
route: '/admin/instance/pending-registrations/:user_id',
|
||||
|
||||
Reference in new issue
Block a user