refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,250 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Config} from '@fluxer/api/src/Config';
import {sanitizeLimitConfigForInstance} from '@fluxer/api/src/constants/LimitConfig';
import {fetchMany, fetchOne, upsertOne} from '@fluxer/api/src/database/Cassandra';
import type {InstanceConfigurationRow} from '@fluxer/api/src/database/types/InstanceConfigTypes';
import {Logger} from '@fluxer/api/src/Logger';
import {InstanceConfiguration} from '@fluxer/api/src/Tables';
import type {LimitConfigSnapshot} from '@fluxer/limits/src/LimitTypes';
const FETCH_CONFIG_QUERY = InstanceConfiguration.selectCql({
where: InstanceConfiguration.where.eq('key'),
limit: 1,
});
const FETCH_ALL_CONFIG_QUERY = InstanceConfiguration.selectCql();
export interface InstanceConfig {
manualReviewEnabled: boolean;
manualReviewScheduleEnabled: boolean;
manualReviewScheduleStartHourUtc: number;
manualReviewScheduleEndHourUtc: number;
registrationAlertsWebhookUrl: string | null;
systemAlertsWebhookUrl: string | null;
}
export interface InstanceSsoConfig {
enabled: boolean;
displayName: string | null;
issuer: string | null;
authorizationUrl: string | null;
tokenUrl: string | null;
userInfoUrl: string | null;
jwksUrl: string | null;
clientId: string | null;
clientSecret?: string | null;
clientSecretSet?: boolean;
scope: string | null;
allowedEmailDomains: Array<string>;
autoProvision: boolean;
redirectUri: string | null;
}
export class InstanceConfigRepository {
async getConfig(key: string): Promise<string | null> {
const row = await fetchOne<InstanceConfigurationRow>(FETCH_CONFIG_QUERY, {key});
return row?.value ?? null;
}
async getAllConfigs(): Promise<Map<string, string>> {
const rows = await fetchMany<InstanceConfigurationRow>(FETCH_ALL_CONFIG_QUERY, {});
const configs = new Map<string, string>();
for (const row of rows) {
if (row.value != null) {
configs.set(row.key, row.value);
}
}
return configs;
}
async setConfig(key: string, value: string): Promise<void> {
await upsertOne(
InstanceConfiguration.upsertAll({
key,
value,
updated_at: new Date(),
}),
);
}
async getInstanceConfig(): Promise<InstanceConfig> {
const configs = await this.getAllConfigs();
return {
manualReviewEnabled: configs.get('manual_review_enabled') === 'true',
manualReviewScheduleEnabled: configs.get('manual_review_schedule_enabled') === 'true',
manualReviewScheduleStartHourUtc: Number.parseInt(
configs.get('manual_review_schedule_start_hour_utc') ?? '0',
10,
),
manualReviewScheduleEndHourUtc: Number.parseInt(configs.get('manual_review_schedule_end_hour_utc') ?? '23', 10),
registrationAlertsWebhookUrl: configs.get('registration_alerts_webhook_url') ?? null,
systemAlertsWebhookUrl: configs.get('system_alerts_webhook_url') ?? null,
};
}
async setManualReviewEnabled(enabled: boolean): Promise<void> {
await this.setConfig('manual_review_enabled', enabled ? 'true' : 'false');
}
async setManualReviewSchedule(scheduleEnabled: boolean, startHourUtc: number, endHourUtc: number): Promise<void> {
await this.setConfig('manual_review_schedule_enabled', scheduleEnabled ? 'true' : 'false');
await this.setConfig('manual_review_schedule_start_hour_utc', String(startHourUtc));
await this.setConfig('manual_review_schedule_end_hour_utc', String(endHourUtc));
}
async setRegistrationAlertsWebhookUrl(url: string | null): Promise<void> {
if (url) {
await this.setConfig('registration_alerts_webhook_url', url);
} else {
await this.setConfig('registration_alerts_webhook_url', '');
}
}
async setSystemAlertsWebhookUrl(url: string | null): Promise<void> {
if (url) {
await this.setConfig('system_alerts_webhook_url', url);
} else {
await this.setConfig('system_alerts_webhook_url', '');
}
}
isManualReviewActiveNow(config: InstanceConfig): boolean {
if (!config.manualReviewEnabled) {
return false;
}
if (!config.manualReviewScheduleEnabled) {
return true;
}
const nowUtc = new Date();
const currentHour = nowUtc.getUTCHours();
const start = config.manualReviewScheduleStartHourUtc;
const end = config.manualReviewScheduleEndHourUtc;
if (start <= end) {
return currentHour >= start && currentHour <= end;
}
return currentHour >= start || currentHour <= end;
}
async hasLimitConfig(): Promise<boolean> {
const raw = await this.getConfig('limit_config');
return raw !== null;
}
async getLimitConfig(): Promise<LimitConfigSnapshot | null> {
const raw = await this.getConfig('limit_config');
if (!raw) {
return null;
}
try {
const parsed: LimitConfigSnapshot = JSON.parse(raw);
return sanitizeLimitConfigForInstance(parsed, {selfHosted: Config.instance.selfHosted});
} catch (error) {
Logger.warn({error}, 'Invalid limit config JSON, returning null');
return null;
}
}
async setLimitConfig(config: LimitConfigSnapshot): Promise<void> {
await this.setConfig('limit_config', JSON.stringify(config));
}
async getSsoConfig(options?: {includeSecret?: boolean}): Promise<InstanceSsoConfig> {
const configs = await this.getAllConfigs();
const read = (key: string): string | null => {
const v = configs.get(key);
if (!v) return null;
const trimmed = v.trim();
return trimmed.length === 0 ? null : trimmed;
};
const allowedDomainsRaw = configs.get('sso_allowed_domains');
let allowedDomains: Array<string> = [];
if (allowedDomainsRaw) {
try {
const parsed = JSON.parse(allowedDomainsRaw);
if (Array.isArray(parsed)) {
allowedDomains = parsed.map((item) => String(item)).filter((item) => item.length > 0);
}
} catch {
allowedDomains = allowedDomainsRaw
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
}
const clientSecret = read('sso_client_secret');
return {
enabled: configs.get('sso_enabled') === 'true',
displayName: read('sso_display_name'),
issuer: read('sso_issuer'),
authorizationUrl: read('sso_authorization_url'),
tokenUrl: read('sso_token_url'),
userInfoUrl: read('sso_userinfo_url'),
jwksUrl: read('sso_jwks_url'),
clientId: read('sso_client_id'),
clientSecret: options?.includeSecret ? clientSecret : undefined,
clientSecretSet: Boolean(clientSecret),
scope: read('sso_scope'),
allowedEmailDomains: allowedDomains,
autoProvision: configs.get('sso_auto_provision') !== 'false',
redirectUri: read('sso_redirect_uri'),
};
}
async setSsoConfig(config: Partial<InstanceSsoConfig>): Promise<InstanceSsoConfig> {
const current = await this.getSsoConfig({includeSecret: true});
const next: InstanceSsoConfig = {
...current,
...config,
clientSecret: config.clientSecret !== undefined ? config.clientSecret : current.clientSecret,
};
await Promise.all([
this.setConfig('sso_enabled', next.enabled ? 'true' : 'false'),
this.setConfig('sso_display_name', next.displayName ?? ''),
this.setConfig('sso_issuer', next.issuer ?? ''),
this.setConfig('sso_authorization_url', next.authorizationUrl ?? ''),
this.setConfig('sso_token_url', next.tokenUrl ?? ''),
this.setConfig('sso_userinfo_url', next.userInfoUrl ?? ''),
this.setConfig('sso_jwks_url', next.jwksUrl ?? ''),
this.setConfig('sso_client_id', next.clientId ?? ''),
this.setConfig('sso_scope', next.scope ?? ''),
this.setConfig('sso_allowed_domains', JSON.stringify(next.allowedEmailDomains ?? [])),
this.setConfig('sso_auto_provision', next.autoProvision ? 'true' : 'false'),
this.setConfig('sso_redirect_uri', next.redirectUri ?? ''),
]);
if (config.clientSecret !== undefined) {
await this.setConfig('sso_client_secret', config.clientSecret ?? '');
}
return this.getSsoConfig({includeSecret: true});
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Config} from '@fluxer/api/src/Config';
import {getKeyManager} from '@fluxer/api/src/federation/KeyManager';
import type {InstanceConfigRepository} from '@fluxer/api/src/instance/InstanceConfigRepository';
import type {LimitConfigService} from '@fluxer/api/src/limits/LimitConfigService';
import {RateLimitMiddleware} from '@fluxer/api/src/middleware/RateLimitMiddleware';
import {OpenAPI} from '@fluxer/api/src/middleware/ResponseTypeMiddleware';
import {RateLimitConfigs} from '@fluxer/api/src/RateLimitConfig';
import type {HonoEnv} from '@fluxer/api/src/types/HonoEnv';
import {API_CODE_VERSION} from '@fluxer/constants/src/AppConstants';
import {FEDERATION_PROTOCOL_VERSION} from '@fluxer/constants/src/Federation';
import {WellKnownFluxerResponse} from '@fluxer/schema/src/domains/instance/InstanceSchemas';
import type {Hono} from 'hono';
export function InstanceController(app: Hono<HonoEnv>) {
app.get(
'/.well-known/fluxer',
RateLimitMiddleware(RateLimitConfigs.INSTANCE_INFO),
OpenAPI({
operationId: 'get_well_known_fluxer',
summary: 'Get instance discovery document',
responseSchema: WellKnownFluxerResponse,
statusCode: 200,
security: [],
tags: ['Instance'],
description:
'Returns the instance discovery document including API endpoints, feature flags, limits, and federation capabilities. This is the canonical discovery endpoint for all Fluxer clients.',
}),
async (ctx) => {
ctx.header('Access-Control-Allow-Origin', '*');
const limitConfigService = ctx.get('limitConfigService') as LimitConfigService | undefined;
const limits = limitConfigService?.getConfigWireFormat();
const instanceConfigRepository = ctx.get('instanceConfigRepository') as InstanceConfigRepository | undefined;
const instanceConfig = await instanceConfigRepository?.getInstanceConfig();
const apiClientEndpoint = Config.endpoints.apiClient;
const apiPublicEndpoint = Config.endpoints.apiPublic;
const sso = await ctx.get('ssoService').getPublicStatus();
const response: Record<string, unknown> = {
api_code_version: API_CODE_VERSION,
endpoints: {
api: apiClientEndpoint,
api_client: apiClientEndpoint,
api_public: apiPublicEndpoint,
gateway: Config.endpoints.gateway,
media: Config.endpoints.media,
static_cdn: Config.endpoints.staticCdn,
marketing: Config.endpoints.marketing,
admin: Config.endpoints.admin,
invite: Config.endpoints.invite,
gift: Config.endpoints.gift,
webapp: Config.endpoints.webApp,
},
captcha: {
provider: Config.captcha.provider,
hcaptcha_site_key: Config.captcha.hcaptcha?.siteKey ?? null,
turnstile_site_key: Config.captcha.turnstile?.siteKey ?? null,
},
features: {
sms_mfa_enabled: Config.dev.testModeEnabled || Config.sms.enabled,
voice_enabled: Config.voice.enabled,
stripe_enabled: Config.stripe.enabled,
self_hosted: Config.instance.selfHosted,
manual_review_enabled: instanceConfig?.manualReviewEnabled ?? false,
},
gif: {
provider: Config.gif.provider,
},
sso,
limits,
push: {
public_vapid_key: Config.push.publicVapidKey ?? null,
},
app_public: {
sentry_dsn: Config.appPublic.sentryDsn,
sentry_proxy_path: Config.appPublic.sentryProxyPath,
sentry_report_host: Config.appPublic.sentryReportHost,
sentry_project_id: Config.appPublic.sentryProjectId,
sentry_public_key: Config.appPublic.sentryPublicKey,
},
};
if (Config.federation?.enabled) {
const keyManager = getKeyManager();
const instanceDomain = Config.domain.baseDomain;
response.federation = {
enabled: true,
version: FEDERATION_PROTOCOL_VERSION,
};
response.public_key = {
id: `https://${instanceDomain}/.well-known/fluxer#main-key`,
algorithm: 'x25519' as const,
public_key_base64: keyManager.getPublicKeyBase64(),
};
response.oauth2 = {
authorization_endpoint: `${apiClientEndpoint}/oauth2/authorize`,
token_endpoint: `${apiClientEndpoint}/oauth2/token`,
userinfo_endpoint: `${apiClientEndpoint}/oauth2/userinfo`,
scopes_supported: ['identify', 'guilds', 'guilds.join', 'messages.read', 'messages.write', 'voice'],
};
}
return ctx.json(response);
},
);
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {SNOWFLAKE_RESERVATION_KEY_PREFIX} from '@fluxer/api/src/constants/InstanceConfig';
import {deleteOneOrMany, fetchMany, upsertOne} from '@fluxer/api/src/database/Cassandra';
import type {InstanceConfigurationRow} from '@fluxer/api/src/database/types/InstanceConfigTypes';
import {Logger} from '@fluxer/api/src/Logger';
import {InstanceConfiguration} from '@fluxer/api/src/Tables';
const FETCH_ALL_CONFIG_QUERY = InstanceConfiguration.selectCql();
export interface SnowflakeReservationConfig {
emailKey: string;
snowflake: bigint;
updatedAt: Date | null;
}
export class SnowflakeReservationRepository {
async listReservations(): Promise<Array<SnowflakeReservationConfig>> {
const rows = await fetchMany<InstanceConfigurationRow>(FETCH_ALL_CONFIG_QUERY, {});
const reservations: Array<SnowflakeReservationConfig> = [];
for (const row of rows) {
if (!row.key.startsWith(SNOWFLAKE_RESERVATION_KEY_PREFIX) || row.value == null || row.value.trim().length === 0) {
continue;
}
const emailKey = row.key.slice(SNOWFLAKE_RESERVATION_KEY_PREFIX.length);
if (!emailKey) continue;
const snowflakeString = row.value.trim();
try {
const snowflake = BigInt(snowflakeString);
reservations.push({
emailKey,
snowflake,
updatedAt: row.updated_at ?? null,
});
} catch (error) {
Logger.warn({key: row.key, value: row.value, error}, 'Skipping invalid snowflake reservation value');
}
}
return reservations;
}
async setReservation(emailKey: string, snowflake: bigint): Promise<void> {
await upsertOne(
InstanceConfiguration.upsertAll({
key: `${SNOWFLAKE_RESERVATION_KEY_PREFIX}${emailKey}`,
value: snowflake.toString(),
updated_at: new Date(),
}),
);
}
async deleteReservation(emailKey: string): Promise<void> {
await deleteOneOrMany(
InstanceConfiguration.deleteCql({
where: InstanceConfiguration.where.eq('key'),
}),
{key: `${SNOWFLAKE_RESERVATION_KEY_PREFIX}${emailKey}`},
);
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {SNOWFLAKE_RESERVATION_REFRESH_CHANNEL} from '@fluxer/api/src/constants/InstanceConfig';
import type {
SnowflakeReservationConfig,
SnowflakeReservationRepository,
} from '@fluxer/api/src/instance/SnowflakeReservationRepository';
import {Logger} from '@fluxer/api/src/Logger';
import type {IKVProvider, IKVSubscription} from '@fluxer/kv_client/src/IKVProvider';
export class SnowflakeReservationService {
private reservations = new Map<string, bigint>();
private initialized = false;
private reloadPromise: Promise<void> | null = null;
private kvSubscription: IKVSubscription | null = null;
constructor(
private repository: SnowflakeReservationRepository,
private kvClient: IKVProvider | null,
) {}
async initialize(): Promise<void> {
if (this.initialized) {
return;
}
await this.reload();
this.initialized = true;
if (this.kvClient) {
try {
const subscription = this.kvClient.duplicate();
this.kvSubscription = subscription;
await subscription.connect();
await subscription.subscribe(SNOWFLAKE_RESERVATION_REFRESH_CHANNEL);
subscription.on('message', (channel) => {
if (channel === SNOWFLAKE_RESERVATION_REFRESH_CHANNEL) {
this.reload().catch((error) => {
Logger.error({error}, 'Failed to reload snowflake reservations');
});
}
});
} catch (error) {
Logger.error({error}, 'Failed to subscribe to snowflake reservation refresh channel');
}
}
}
async reload(): Promise<void> {
if (this.reloadPromise) {
return this.reloadPromise;
}
this.reloadPromise = (async () => {
const entries = await this.repository.listReservations();
this.reservations = this.buildLookup(entries);
})()
.catch((error) => {
Logger.error({error}, 'Failed to reload snowflake reservations from the database');
throw error;
})
.finally(() => {
this.reloadPromise = null;
});
return this.reloadPromise;
}
getReservedSnowflake(emailKey: string | null): bigint | null {
if (!emailKey) {
return null;
}
return this.reservations.get(emailKey) ?? null;
}
private buildLookup(entries: Array<SnowflakeReservationConfig>): Map<string, bigint> {
const lookup = new Map<string, bigint>();
for (const entry of entries) {
lookup.set(entry.emailKey, entry.snowflake);
}
return lookup;
}
shutdown(): void {
if (this.kvSubscription) {
this.kvSubscription.disconnect();
this.kvSubscription = null;
}
}
}