From 2019909a5eec3185f9a270a2b7fa484c054d30fc Mon Sep 17 00:00:00 2001 From: Hampus Date: Tue, 8 Sep 2026 14:51:41 +0200 Subject: [PATCH] fix(api): skip the mature gate when no birth date is collected (#2582) --- .../instance/DateOfBirthCollectionCache.ts | 17 +++++++ .../api/instance/InstanceConfigRepository.ts | 33 +++++++++----- fluxer_api/src/api/utils/AgeUtils.test.ts | 44 +++++++++++++++++++ fluxer_api/src/api/utils/AgeUtils.ts | 5 +++ 4 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 fluxer_api/src/api/instance/DateOfBirthCollectionCache.ts create mode 100644 fluxer_api/src/api/utils/AgeUtils.test.ts diff --git a/fluxer_api/src/api/instance/DateOfBirthCollectionCache.ts b/fluxer_api/src/api/instance/DateOfBirthCollectionCache.ts new file mode 100644 index 000000000..dc62e2cd9 --- /dev/null +++ b/fluxer_api/src/api/instance/DateOfBirthCollectionCache.ts @@ -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; +} diff --git a/fluxer_api/src/api/instance/InstanceConfigRepository.ts b/fluxer_api/src/api/instance/InstanceConfigRepository.ts index 3fa26bf78..a926f42c2 100644 --- a/fluxer_api/src/api/instance/InstanceConfigRepository.ts +++ b/fluxer_api/src/api/instance/InstanceConfigRepository.ts @@ -17,6 +17,7 @@ import {resolveDeferredPhoneGateEnabled, setCachedDeferredPhoneGateEnabled} from import {InstanceConfiguration} from '../Tables'; import {DEFAULT_DECAY_CONSTANTS, DEFAULT_RENEWAL_CONSTANTS} from '../utils/AttachmentDecay'; import {isJsonRecord, parseJsonArray, parseJsonRecord} from '../utils/JsonBoundaryUtils'; +import {getDefaultDateOfBirthCollection, setCachedDateOfBirthCollection} from './DateOfBirthCollectionCache'; import {normalizeSsoAllowedEmailDomains} from './SsoConfigValidation'; const GATEWAY_ROLLOUT_CONFIG_KEY = 'gateway_rollout_config'; @@ -357,11 +358,21 @@ function getDefaultAppPublicConfig(): InstanceAppPublicConfig { privacy_url: null, }, 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 { const defaults = getDefaultAppPublicConfig(); if (!isJsonRecord(value)) { @@ -939,6 +950,7 @@ export class InstanceConfigRepository { this.configCache = await this.fetchAllConfigsFromDatabase(); } while (this.refreshRequested); this.syncDeferredPhoneGateCache(this.configCache.get(INSTANCE_POLICY_CONFIG_KEY) ?? null); + this.syncDateOfBirthCollectionCache(this.configCache.get(APP_PUBLIC_CONFIG_KEY) ?? null); })().finally(() => { this.refreshPromise = null; }); @@ -950,6 +962,11 @@ export class InstanceConfigRepository { 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 { if (!this.configCache) { return; @@ -1067,16 +1084,9 @@ export class InstanceConfigRepository { async getAppPublicConfig(): Promise { const raw = await this.getConfig(APP_PUBLIC_CONFIG_KEY); - if (!raw) { - return getDefaultAppPublicConfig(); - } - try { - const parsed: unknown = JSON.parse(raw); - return normalizeAppPublicConfig(parsed); - } catch (error) { - Logger.warn({error}, 'Invalid app public config JSON, returning defaults'); - return getDefaultAppPublicConfig(); - } + const config = raw ? parseAppPublicConfig(raw) : getDefaultAppPublicConfig(); + setCachedDateOfBirthCollection(config.registration.collect_date_of_birth); + return config; } async setAppPublicConfig(config: { @@ -1105,6 +1115,7 @@ export class InstanceConfigRepository { }, }); await this.setConfig(APP_PUBLIC_CONFIG_KEY, JSON.stringify(next)); + setCachedDateOfBirthCollection(next.registration.collect_date_of_birth); return next; } diff --git a/fluxer_api/src/api/utils/AgeUtils.test.ts b/fluxer_api/src/api/utils/AgeUtils.test.ts new file mode 100644 index 000000000..db6dd8c39 --- /dev/null +++ b/fluxer_api/src/api/utils/AgeUtils.test.ts @@ -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); + }); +}); diff --git a/fluxer_api/src/api/utils/AgeUtils.ts b/fluxer_api/src/api/utils/AgeUtils.ts index 656d7377c..ecf68989c 100644 --- a/fluxer_api/src/api/utils/AgeUtils.ts +++ b/fluxer_api/src/api/utils/AgeUtils.ts @@ -1,5 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import {instanceCollectsDateOfBirth} from '../instance/DateOfBirthCollectionCache'; + export function calculateAge( dateOfBirth: | { @@ -53,5 +55,8 @@ export function canUserAccessNsfwContent(user: NsfwEligibilityUser): boolean { if (user.isBot) { return true; } + if (!instanceCollectsDateOfBirth()) { + return true; + } return isUserAdult(user.dateOfBirth); }