fix(api): skip the mature gate when no birth date is collected (#2582)

This commit is contained in:
Hampus
2026-09-08 14:51:41 +02:00
committed by GitHub
parent d46c8d49c6
commit 2019909a5e
4 changed files with 88 additions and 11 deletions
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Config} from '../Config';
let cachedCollectDateOfBirth: boolean | null = null;
export function getDefaultDateOfBirthCollection(): boolean {
return !Config.instance.selfHosted;
}
export function instanceCollectsDateOfBirth(): boolean {
return cachedCollectDateOfBirth ?? getDefaultDateOfBirthCollection();
}
export function setCachedDateOfBirthCollection(collect: boolean): void {
cachedCollectDateOfBirth = collect;
}
@@ -17,6 +17,7 @@ import {resolveDeferredPhoneGateEnabled, setCachedDeferredPhoneGateEnabled} from
import {InstanceConfiguration} from '../Tables'; import {InstanceConfiguration} from '../Tables';
import {DEFAULT_DECAY_CONSTANTS, DEFAULT_RENEWAL_CONSTANTS} from '../utils/AttachmentDecay'; import {DEFAULT_DECAY_CONSTANTS, DEFAULT_RENEWAL_CONSTANTS} from '../utils/AttachmentDecay';
import {isJsonRecord, parseJsonArray, parseJsonRecord} from '../utils/JsonBoundaryUtils'; import {isJsonRecord, parseJsonArray, parseJsonRecord} from '../utils/JsonBoundaryUtils';
import {getDefaultDateOfBirthCollection, setCachedDateOfBirthCollection} from './DateOfBirthCollectionCache';
import {normalizeSsoAllowedEmailDomains} from './SsoConfigValidation'; import {normalizeSsoAllowedEmailDomains} from './SsoConfigValidation';
const GATEWAY_ROLLOUT_CONFIG_KEY = 'gateway_rollout_config'; const GATEWAY_ROLLOUT_CONFIG_KEY = 'gateway_rollout_config';
@@ -357,11 +358,21 @@ function getDefaultAppPublicConfig(): InstanceAppPublicConfig {
privacy_url: null, privacy_url: null,
}, },
registration: { registration: {
collect_date_of_birth: !Config.instance.selfHosted, collect_date_of_birth: getDefaultDateOfBirthCollection(),
}, },
}; };
} }
function parseAppPublicConfig(raw: string): InstanceAppPublicConfig {
try {
const parsed: unknown = JSON.parse(raw);
return normalizeAppPublicConfig(parsed);
} catch (error) {
Logger.warn({error}, 'Invalid app public config JSON, returning defaults');
return getDefaultAppPublicConfig();
}
}
function normalizeAppPublicConfig(value: unknown): InstanceAppPublicConfig { function normalizeAppPublicConfig(value: unknown): InstanceAppPublicConfig {
const defaults = getDefaultAppPublicConfig(); const defaults = getDefaultAppPublicConfig();
if (!isJsonRecord(value)) { if (!isJsonRecord(value)) {
@@ -939,6 +950,7 @@ export class InstanceConfigRepository {
this.configCache = await this.fetchAllConfigsFromDatabase(); this.configCache = await this.fetchAllConfigsFromDatabase();
} while (this.refreshRequested); } while (this.refreshRequested);
this.syncDeferredPhoneGateCache(this.configCache.get(INSTANCE_POLICY_CONFIG_KEY) ?? null); this.syncDeferredPhoneGateCache(this.configCache.get(INSTANCE_POLICY_CONFIG_KEY) ?? null);
this.syncDateOfBirthCollectionCache(this.configCache.get(APP_PUBLIC_CONFIG_KEY) ?? null);
})().finally(() => { })().finally(() => {
this.refreshPromise = null; this.refreshPromise = null;
}); });
@@ -950,6 +962,11 @@ export class InstanceConfigRepository {
setCachedDeferredPhoneGateEnabled(resolveDeferredPhoneGateEnabled(policy)); setCachedDeferredPhoneGateEnabled(resolveDeferredPhoneGateEnabled(policy));
} }
private syncDateOfBirthCollectionCache(raw: string | null): void {
const appPublic = raw ? normalizeAppPublicConfig(parseJsonRecord(raw)) : getDefaultAppPublicConfig();
setCachedDateOfBirthCollection(appPublic.registration.collect_date_of_birth);
}
private updateCachedConfigs(entries: Array<[string, string]>): void { private updateCachedConfigs(entries: Array<[string, string]>): void {
if (!this.configCache) { if (!this.configCache) {
return; return;
@@ -1067,16 +1084,9 @@ export class InstanceConfigRepository {
async getAppPublicConfig(): Promise<InstanceAppPublicConfig> { async getAppPublicConfig(): Promise<InstanceAppPublicConfig> {
const raw = await this.getConfig(APP_PUBLIC_CONFIG_KEY); const raw = await this.getConfig(APP_PUBLIC_CONFIG_KEY);
if (!raw) { const config = raw ? parseAppPublicConfig(raw) : getDefaultAppPublicConfig();
return getDefaultAppPublicConfig(); setCachedDateOfBirthCollection(config.registration.collect_date_of_birth);
} return config;
try {
const parsed: unknown = JSON.parse(raw);
return normalizeAppPublicConfig(parsed);
} catch (error) {
Logger.warn({error}, 'Invalid app public config JSON, returning defaults');
return getDefaultAppPublicConfig();
}
} }
async setAppPublicConfig(config: { async setAppPublicConfig(config: {
@@ -1105,6 +1115,7 @@ export class InstanceConfigRepository {
}, },
}); });
await this.setConfig(APP_PUBLIC_CONFIG_KEY, JSON.stringify(next)); await this.setConfig(APP_PUBLIC_CONFIG_KEY, JSON.stringify(next));
setCachedDateOfBirthCollection(next.registration.collect_date_of_birth);
return next; return next;
} }
+44
View File
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {afterEach, describe, expect, it} from 'vitest';
import {setCachedDateOfBirthCollection} from '../instance/DateOfBirthCollectionCache';
import {canUserAccessNsfwContent} from './AgeUtils';
const ADULT_DATE_OF_BIRTH = '1990-01-01';
const MINOR_DATE_OF_BIRTH = '2020-01-01';
describe('canUserAccessNsfwContent', () => {
afterEach(() => {
setCachedDateOfBirthCollection(true);
});
it('allows a bot whatever the instance collects', () => {
setCachedDateOfBirthCollection(true);
expect(canUserAccessNsfwContent({isBot: true, dateOfBirth: null})).toBe(true);
});
it('allows an adult when the instance collects a date of birth', () => {
setCachedDateOfBirthCollection(true);
expect(canUserAccessNsfwContent({isBot: false, dateOfBirth: ADULT_DATE_OF_BIRTH})).toBe(true);
});
it('blocks a minor when the instance collects a date of birth', () => {
setCachedDateOfBirthCollection(true);
expect(canUserAccessNsfwContent({isBot: false, dateOfBirth: MINOR_DATE_OF_BIRTH})).toBe(false);
});
it('blocks a missing date of birth when the instance collects one', () => {
setCachedDateOfBirthCollection(true);
expect(canUserAccessNsfwContent({isBot: false, dateOfBirth: null})).toBe(false);
});
it('allows a missing date of birth when the instance collects none', () => {
setCachedDateOfBirthCollection(false);
expect(canUserAccessNsfwContent({isBot: false, dateOfBirth: null})).toBe(true);
});
it('allows an account with a minor date of birth when the instance collects none', () => {
setCachedDateOfBirthCollection(false);
expect(canUserAccessNsfwContent({isBot: false, dateOfBirth: MINOR_DATE_OF_BIRTH})).toBe(true);
});
});
+5
View File
@@ -1,5 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import {instanceCollectsDateOfBirth} from '../instance/DateOfBirthCollectionCache';
export function calculateAge( export function calculateAge(
dateOfBirth: dateOfBirth:
| { | {
@@ -53,5 +55,8 @@ export function canUserAccessNsfwContent(user: NsfwEligibilityUser): boolean {
if (user.isBot) { if (user.isBot) {
return true; return true;
} }
if (!instanceCollectsDateOfBirth()) {
return true;
}
return isUserAdult(user.dateOfBirth); return isUserAdult(user.dateOfBirth);
} }