From 45289b553198988ca303accaee96391b031e276f Mon Sep 17 00:00:00 2001 From: Hampus Date: Sun, 16 Aug 2026 22:14:59 +0200 Subject: [PATCH] fix(api): let single community setup work without email verification (#1683) --- .../guild/controllers/GuildBaseController.ts | 2 + .../services/data/GuildOperationsService.ts | 2 - .../GuildCreationEmailVerification.test.ts | 66 +++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 fluxer_api/src/api/guild/tests/GuildCreationEmailVerification.test.ts diff --git a/fluxer_api/src/api/guild/controllers/GuildBaseController.ts b/fluxer_api/src/api/guild/controllers/GuildBaseController.ts index 1b7f9faab..af3dfe531 100644 --- a/fluxer_api/src/api/guild/controllers/GuildBaseController.ts +++ b/fluxer_api/src/api/guild/controllers/GuildBaseController.ts @@ -16,6 +16,7 @@ import { } from '@fluxer/schema/src/domains/guild/GuildRequestSchemas'; import {GuildResponse, GuildVanityURLResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas'; import {z} from 'zod'; +import {requireEmailVerified} from '../../auth/EmailVerificationUtils'; import {requireSudoMode} from '../../auth/services/SudoVerificationService'; import {createGuildID} from '../../BrandedTypes'; import {LoginRequired} from '../../middleware/AuthMiddleware'; @@ -49,6 +50,7 @@ export function GuildBaseController(app: HonoApp) { if (policy.single_community_enabled) { throw new SingleCommunityCannotCreateGuildsError(); } + requireEmailVerified(user, 'guild_creation'); const auditLogReason = ctx.get('auditLogReason') ?? null; const locale = ctx.get('requestLocale') ?? null; return ctx.json(await ctx.get('guildService').data.createGuild({user, data, locale}, auditLogReason)); diff --git a/fluxer_api/src/api/guild/services/data/GuildOperationsService.ts b/fluxer_api/src/api/guild/services/data/GuildOperationsService.ts index 235905d27..b4eaed680 100644 --- a/fluxer_api/src/api/guild/services/data/GuildOperationsService.ts +++ b/fluxer_api/src/api/guild/services/data/GuildOperationsService.ts @@ -36,7 +36,6 @@ import type { TemplateSerializedGuild, } from '@fluxer/schema/src/domains/guild/GuildTemplateSchemas'; import {extractTimestamp} from '@fluxer/snowflake/src/SnowflakeUtils'; -import {requireEmailVerified} from '../../../auth/EmailVerificationUtils'; import type {ChannelID, GuildID, RoleID, UserID} from '../../../BrandedTypes'; import {createChannelID, createGuildID, createRoleID, guildIdToRoleId} from '../../../BrandedTypes'; import type {IChannelRepository} from '../../../channel/IChannelRepository'; @@ -257,7 +256,6 @@ export class GuildOperationsService { if (user.isUnclaimedAccount()) { throw new UnclaimedAccountCannotCreateGuildsError(); } - requireEmailVerified(user, 'guild_creation'); const currentGuildCount = await this.guildRepository.countUserGuilds(user.id); const ctx = createLimitMatchContext({user}); const maxGuilds = resolveLimitSafe( diff --git a/fluxer_api/src/api/guild/tests/GuildCreationEmailVerification.test.ts b/fluxer_api/src/api/guild/tests/GuildCreationEmailVerification.test.ts new file mode 100644 index 000000000..f0fd3bd20 --- /dev/null +++ b/fluxer_api/src/api/guild/tests/GuildCreationEmailVerification.test.ts @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; +import {describe, expect, it} from 'vitest'; +import type {User} from '../../models/User'; +import {GuildOperationsService} from '../services/data/GuildOperationsService'; + +const REACHED_GUILD_COUNT = new Error('reached guild count lookup'); + +function createServiceStoppingAtGuildCount(): GuildOperationsService { + const guildRepository = { + countUserGuilds: (): never => { + throw REACHED_GUILD_COUNT; + }, + }; + const unused = null as never; + return new GuildOperationsService( + guildRepository as never, + unused, + unused, + unused, + unused, + unused, + unused, + unused, + unused, + unused, + unused, + unused, + ); +} + +function createUser(overrides: {emailVerified: boolean; isBot?: boolean}): User { + return { + id: 1n, + isBot: overrides.isBot ?? false, + emailVerified: overrides.emailVerified, + isUnclaimedAccount: () => false, + } as unknown as User; +} + +describe('guild creation email verification', () => { + it('does not gate guild creation on email verification inside the domain operation', async () => { + const service = createServiceStoppingAtGuildCount(); + await expect( + service.createGuild({user: createUser({emailVerified: false}), data: {name: 'Stock Community'} as never}), + ).rejects.toBe(REACHED_GUILD_COUNT); + }); + + it('reaches the same point for a verified user', async () => { + const service = createServiceStoppingAtGuildCount(); + await expect( + service.createGuild({user: createUser({emailVerified: true}), data: {name: 'Stock Community'} as never}), + ).rejects.toBe(REACHED_GUILD_COUNT); + }); + + it('still rejects bots before any guild count lookup', async () => { + const service = createServiceStoppingAtGuildCount(); + await expect( + service.createGuild({ + user: createUser({emailVerified: true, isBot: true}), + data: {name: 'Stock Community'} as never, + }), + ).rejects.toMatchObject({code: APIErrorCodes.BOTS_CANNOT_CREATE_GUILDS}); + }); +});