mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(guild): treat very high as high without phone verification (#3095)
This commit is contained in:
@@ -211,3 +211,41 @@ describe('buildAPIConfigFromMaster optional outbound lookups', () => {
|
||||
expect(config.breachedPasswordCheck.enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function withPhoneVerification(master: MasterConfig, selfHosted: boolean, enabled?: boolean): MasterConfig {
|
||||
return {
|
||||
...master,
|
||||
instance: {
|
||||
...master.instance,
|
||||
self_hosted: selfHosted,
|
||||
phone_verification_enabled: enabled,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('buildAPIConfigFromMaster phone verification', () => {
|
||||
let master: MasterConfig;
|
||||
beforeAll(async () => {
|
||||
master = await loadConfig();
|
||||
});
|
||||
|
||||
it('is on by default when the instance is not self-hosted', () => {
|
||||
expect(buildAPIConfigFromMaster(withPhoneVerification(master, false)).instance.phoneVerificationEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('is off by default on a self-hosted instance', () => {
|
||||
expect(buildAPIConfigFromMaster(withPhoneVerification(master, true)).instance.phoneVerificationEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('lets a self-hosted operator switch it on', () => {
|
||||
expect(buildAPIConfigFromMaster(withPhoneVerification(master, true, true)).instance.phoneVerificationEnabled).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('lets an operator switch it off when the instance is not self-hosted', () => {
|
||||
expect(
|
||||
buildAPIConfigFromMaster(withPhoneVerification(master, false, false)).instance.phoneVerificationEnabled,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -367,6 +367,7 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
},
|
||||
instance: {
|
||||
selfHosted: master.instance.self_hosted,
|
||||
phoneVerificationEnabled: master.instance.phone_verification_enabled ?? !master.instance.self_hosted,
|
||||
autoJoinInviteCode: master.instance.auto_join_invite_code,
|
||||
visionariesGuildId: master.instance.visionaries_guild_id,
|
||||
visionariesGuildVisionaryRoleId: master.instance.visionaries_guild_visionary_role_id,
|
||||
|
||||
@@ -262,6 +262,7 @@ export interface APIConfig {
|
||||
};
|
||||
instance: {
|
||||
selfHosted: boolean;
|
||||
phoneVerificationEnabled: boolean;
|
||||
autoJoinInviteCode?: string;
|
||||
visionariesGuildId?: string;
|
||||
visionariesGuildVisionaryRoleId?: string;
|
||||
|
||||
@@ -434,7 +434,11 @@ export class GuildDiscoveryService extends IGuildDiscoveryService {
|
||||
member_count: hit.memberCount,
|
||||
online_count: 0,
|
||||
features: hit.features,
|
||||
verification_level: getEffectiveGuildVerificationLevel(hit.verificationLevel, hit.isDiscoverable),
|
||||
verification_level: getEffectiveGuildVerificationLevel(
|
||||
hit.verificationLevel,
|
||||
hit.isDiscoverable,
|
||||
Config.instance.phoneVerificationEnabled,
|
||||
),
|
||||
}));
|
||||
const total = results.total;
|
||||
if (guilds.length > 0) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type {ChannelID, GuildID, RoleID, UserID} from '@app/api/BrandedTypes';
|
||||
import {createChannelID, createGuildID, createRoleID, guildIdToRoleId} from '@app/api/BrandedTypes';
|
||||
import {Config} from '@app/api/Config';
|
||||
import type {IChannelRepository} from '@app/api/channel/IChannelRepository';
|
||||
import {
|
||||
type ChannelFollowerRemovalCopyMode,
|
||||
@@ -481,6 +482,17 @@ export class GuildOperationsService {
|
||||
ValidationErrorCodes.DISCOVERABLE_GUILD_VERIFICATION_LEVEL_TOO_LOW,
|
||||
);
|
||||
}
|
||||
if (
|
||||
data.verification_level === GuildVerificationLevel.VERY_HIGH &&
|
||||
data.verification_level !== currentGuild.verificationLevel &&
|
||||
!Config.instance.phoneVerificationEnabled
|
||||
) {
|
||||
throw InputValidationError.fromCode('verification_level', ValidationErrorCodes.VALUE_MUST_BE_INTEGER_IN_RANGE, {
|
||||
name: 'verification_level',
|
||||
minValue: GuildVerificationLevel.NONE,
|
||||
maxValue: GuildVerificationLevel.HIGH,
|
||||
});
|
||||
}
|
||||
const isMfaLevelChange = data.mfa_level !== undefined && data.mfa_level !== currentGuild.mfaLevel;
|
||||
if (isMfaLevelChange) {
|
||||
const isOwner = guildData.owner_id === userId.toString();
|
||||
@@ -1177,7 +1189,12 @@ export class GuildOperationsService {
|
||||
|
||||
private sanitiseTemplateGuildSettings(template?: TemplateSerializedGuild): TemplateGuildSettings {
|
||||
return {
|
||||
verificationLevel: this.clampTemplateSetting(template?.verification_level, 0, 4, 0),
|
||||
verificationLevel: this.clampTemplateSetting(
|
||||
template?.verification_level,
|
||||
GuildVerificationLevel.NONE,
|
||||
Config.instance.phoneVerificationEnabled ? GuildVerificationLevel.VERY_HIGH : GuildVerificationLevel.HIGH,
|
||||
GuildVerificationLevel.NONE,
|
||||
),
|
||||
explicitContentFilter: this.clampTemplateSetting(template?.explicit_content_filter, 0, 2, 0),
|
||||
defaultMessageNotifications: this.clampTemplateSetting(template?.default_message_notifications, 0, 1, 0),
|
||||
systemChannelFlags: (template?.system_channel_flags ?? 0) & SUPPORTED_SYSTEM_CHANNEL_FLAGS,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils';
|
||||
import {Config} from '@app/api/Config';
|
||||
import {createGuild, updateGuild} from '@app/api/guild/tests/GuildTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@app/api/test/TestConstants';
|
||||
import {createBuilder} from '@app/api/test/TestRequestBuilder';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {GuildVerificationLevel} from '@fluxer/constants/src/GuildConstants';
|
||||
import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, test} from 'vitest';
|
||||
|
||||
describe('Guild verification level without phone verification', () => {
|
||||
let harness: ApiTestHarness;
|
||||
let savedPhoneVerificationEnabled: boolean;
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
savedPhoneVerificationEnabled = Config.instance.phoneVerificationEnabled;
|
||||
});
|
||||
afterEach(() => {
|
||||
Config.instance.phoneVerificationEnabled = savedPhoneVerificationEnabled;
|
||||
});
|
||||
|
||||
test('rejects very high when phone verification is unavailable', async () => {
|
||||
Config.instance.phoneVerificationEnabled = false;
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'No Phone Guild');
|
||||
await createBuilder(harness, account.token)
|
||||
.patch(`/guilds/${guild.id}`)
|
||||
.body({verification_level: GuildVerificationLevel.VERY_HIGH})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
|
||||
.execute();
|
||||
const updated = await updateGuild(harness, account.token, guild.id, {
|
||||
verification_level: GuildVerificationLevel.HIGH,
|
||||
});
|
||||
expect(updated.verification_level).toBe(GuildVerificationLevel.HIGH);
|
||||
});
|
||||
|
||||
test('accepts very high when phone verification is available', async () => {
|
||||
Config.instance.phoneVerificationEnabled = true;
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Phone Guild');
|
||||
const updated = await updateGuild(harness, account.token, guild.id, {
|
||||
verification_level: GuildVerificationLevel.VERY_HIGH,
|
||||
});
|
||||
expect(updated.verification_level).toBe(GuildVerificationLevel.VERY_HIGH);
|
||||
});
|
||||
|
||||
test('accepts an unchanged stored very high when phone verification is unavailable', async () => {
|
||||
Config.instance.phoneVerificationEnabled = true;
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Stored Phone Guild');
|
||||
await updateGuild(harness, account.token, guild.id, {verification_level: GuildVerificationLevel.VERY_HIGH});
|
||||
Config.instance.phoneVerificationEnabled = false;
|
||||
const updated = await updateGuild(harness, account.token, guild.id, {
|
||||
name: 'Renamed Phone Guild',
|
||||
verification_level: GuildVerificationLevel.VERY_HIGH,
|
||||
});
|
||||
expect(updated.name).toBe('Renamed Phone Guild');
|
||||
expect(updated.verification_level).toBe(GuildVerificationLevel.VERY_HIGH);
|
||||
});
|
||||
|
||||
test('clamps a template level to high when phone verification is unavailable', async () => {
|
||||
Config.instance.phoneVerificationEnabled = false;
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createBuilder<GuildResponse>(harness, account.token)
|
||||
.post('/guilds')
|
||||
.body({
|
||||
name: 'Template Guild',
|
||||
template: {
|
||||
name: 'Template Source',
|
||||
description: null,
|
||||
verification_level: GuildVerificationLevel.VERY_HIGH,
|
||||
default_message_notifications: 0,
|
||||
explicit_content_filter: 0,
|
||||
system_channel_id: 1001,
|
||||
afk_timeout: 300,
|
||||
system_channel_flags: 0,
|
||||
roles: [{id: 0, name: '@everyone', permissions: '0'}],
|
||||
channels: [{id: 1001, type: ChannelTypes.GUILD_TEXT, name: 'general', position: 0}],
|
||||
},
|
||||
})
|
||||
.execute();
|
||||
expect(guild.verification_level).toBe(GuildVerificationLevel.HIGH);
|
||||
});
|
||||
});
|
||||
@@ -71,6 +71,7 @@ function buildDiscoveryStaticInput(
|
||||
self_hosted: Config.instance.selfHosted,
|
||||
presigned_attachment_uploads: Config.presignedAttachmentUploadsEnabled,
|
||||
emails_enabled: runtime.emailEnabled,
|
||||
phone_verification_enabled: Config.instance.phoneVerificationEnabled,
|
||||
},
|
||||
gif: {
|
||||
provider: gifProviderName,
|
||||
|
||||
@@ -30849,6 +30849,10 @@
|
||||
"emails_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the instance sends emails (verification, password reset, etc.)"
|
||||
},
|
||||
"phone_verification_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether users can verify a phone number, so the very high guild verification level applies"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -30858,7 +30862,8 @@
|
||||
"stripe_serviceable",
|
||||
"self_hosted",
|
||||
"presigned_attachment_uploads",
|
||||
"emails_enabled"
|
||||
"emails_enabled",
|
||||
"phone_verification_enabled"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"description": "Feature flags for this instance"
|
||||
|
||||
@@ -34,7 +34,7 @@ export function convertToSearchableGuild(guild: Guild, discovery?: GuildDiscover
|
||||
bannerHash: guild.bannerHash,
|
||||
splashHash: guild.splashHash,
|
||||
features: Array.from(guild.features),
|
||||
verificationLevel: getEffectiveGuildVerificationLevel(guild.verificationLevel, isDiscoverable),
|
||||
verificationLevel: getEffectiveGuildVerificationLevel(guild.verificationLevel, isDiscoverable, true),
|
||||
mfaLevel: guild.mfaLevel,
|
||||
nsfwLevel: guild.nsfwLevel,
|
||||
createdAt,
|
||||
|
||||
@@ -76,11 +76,13 @@ export class UserAuthRequestService {
|
||||
if (user.suspiciousActivityFlags !== 0) {
|
||||
return;
|
||||
}
|
||||
const guildIds = await this.userRepository.getUserGuildIds(user.id);
|
||||
if (guildIds.length > 0) {
|
||||
const guilds = await this.guildRepository.listGuilds(guildIds);
|
||||
if (guilds.some((g) => g.verificationLevel >= GuildVerificationLevel.VERY_HIGH)) {
|
||||
return;
|
||||
if (this.apiContext.services.config.instance.phoneVerificationEnabled) {
|
||||
const guildIds = await this.userRepository.getUserGuildIds(user.id);
|
||||
if (guildIds.length > 0) {
|
||||
const guilds = await this.guildRepository.listGuilds(guildIds);
|
||||
if (guilds.some((g) => g.verificationLevel >= GuildVerificationLevel.VERY_HIGH)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new PhoneAddNotEligibleError();
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createUserID} from '@app/api/BrandedTypes';
|
||||
import {Config} from '@app/api/Config';
|
||||
import {User} from '@app/api/models/User';
|
||||
import {checkGuildVerificationWithResponse} from '@app/api/utils/GuildVerificationUtils';
|
||||
import {GuildFeatures, GuildVerificationLevel} from '@fluxer/constants/src/GuildConstants';
|
||||
import {ProfileFieldPrivacyFlags} from '@fluxer/constants/src/UserConstants';
|
||||
import {GuildEmailVerificationRequiredError} from '@fluxer/errors/src/domains/auth/EmailVerificationRequiredError';
|
||||
import {GuildPhoneVerificationRequiredError} from '@fluxer/errors/src/domains/auth/GuildPhoneVerificationRequiredError';
|
||||
import {GuildVerificationRequiredError} from '@fluxer/errors/src/domains/guild/GuildVerificationRequiredError';
|
||||
import type {GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
|
||||
import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
@@ -178,4 +180,28 @@ describe('GuildVerificationUtils', () => {
|
||||
}),
|
||||
).toThrow(GuildPhoneVerificationRequiredError);
|
||||
});
|
||||
it('treats very high as high when phone verification is unavailable', () => {
|
||||
const saved = Config.instance.phoneVerificationEnabled;
|
||||
Config.instance.phoneVerificationEnabled = false;
|
||||
try {
|
||||
const guild = createGuildResponse([]);
|
||||
guild.verification_level = GuildVerificationLevel.VERY_HIGH;
|
||||
expect(() =>
|
||||
checkGuildVerificationWithResponse({
|
||||
user: createUser({emailVerified: true, hasVerifiedPhone: false}),
|
||||
guild,
|
||||
member,
|
||||
}),
|
||||
).not.toThrow();
|
||||
expect(() =>
|
||||
checkGuildVerificationWithResponse({
|
||||
user: createUser({emailVerified: true, hasVerifiedPhone: false}),
|
||||
guild,
|
||||
member: createMemberResponse(new Date().toISOString()),
|
||||
}),
|
||||
).toThrow(GuildVerificationRequiredError);
|
||||
} finally {
|
||||
Config.instance.phoneVerificationEnabled = saved;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createRoleIDSet, createUserID, type RoleID, type UserID} from '@app/api/BrandedTypes';
|
||||
import {Config} from '@app/api/Config';
|
||||
import type {Guild} from '@app/api/models/Guild';
|
||||
import type {GuildMember} from '@app/api/models/GuildMember';
|
||||
import type {User} from '@app/api/models/User';
|
||||
@@ -90,6 +91,7 @@ export function checkGuildVerificationWithGuildModel({
|
||||
verificationLevel: getEffectiveGuildVerificationLevel(
|
||||
guild.verificationLevel ?? GuildVerificationLevel.NONE,
|
||||
guild.features.has(GuildFeatures.DISCOVERABLE),
|
||||
Config.instance.phoneVerificationEnabled,
|
||||
),
|
||||
memberJoinedAt: member.joinedAt,
|
||||
memberRoles: member.roleIds,
|
||||
@@ -115,6 +117,7 @@ export function checkGuildVerificationWithResponse({
|
||||
verificationLevel: getEffectiveGuildVerificationLevel(
|
||||
guild.verification_level ?? GuildVerificationLevel.NONE,
|
||||
(guild.features ?? []).includes(GuildFeatures.DISCOVERABLE),
|
||||
Config.instance.phoneVerificationEnabled,
|
||||
),
|
||||
memberJoinedAt: member.joined_at,
|
||||
memberRoles: createRoleIDSet(new Set(member.roles.map((roleId) => BigInt(roleId)))),
|
||||
|
||||
Reference in New Issue
Block a user