refactor progress
This commit is contained in:
149
packages/api/src/app/APILifecycle.tsx
Normal file
149
packages/api/src/app/APILifecycle.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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 {randomUUID} from 'node:crypto';
|
||||
import type {APIConfig} from '@fluxer/api/src/config/APIConfig';
|
||||
import {GuildDataRepository} from '@fluxer/api/src/guild/repositories/GuildDataRepository';
|
||||
import type {ILogger} from '@fluxer/api/src/ILogger';
|
||||
import {KVAccountDeletionQueueService} from '@fluxer/api/src/infrastructure/KVAccountDeletionQueueService';
|
||||
import {initializeMetricsService} from '@fluxer/api/src/infrastructure/MetricsService';
|
||||
import {InstanceConfigRepository} from '@fluxer/api/src/instance/InstanceConfigRepository';
|
||||
import {ipBanCache} from '@fluxer/api/src/middleware/IpBanMiddleware';
|
||||
import {initializeServiceSingletons} from '@fluxer/api/src/middleware/ServiceMiddleware';
|
||||
import {ensureVoiceResourcesInitialized, getKVClient} from '@fluxer/api/src/middleware/ServiceRegistry';
|
||||
import {ReportRepository} from '@fluxer/api/src/report/ReportRepository';
|
||||
import {initializeSearch, shutdownSearch} from '@fluxer/api/src/SearchFactory';
|
||||
import {warmupAdminSearchIndexes} from '@fluxer/api/src/search/SearchWarmup';
|
||||
import {VisionarySlotInitializer} from '@fluxer/api/src/stripe/VisionarySlotInitializer';
|
||||
import {UserRepository} from '@fluxer/api/src/user/repositories/UserRepository';
|
||||
import {VoiceDataInitializer} from '@fluxer/api/src/voice/VoiceDataInitializer';
|
||||
|
||||
export function createInitializer(config: APIConfig, logger: ILogger): () => Promise<void> {
|
||||
return async (): Promise<void> => {
|
||||
logger.info('Initializing API service...');
|
||||
|
||||
const kvClient = getKVClient();
|
||||
ipBanCache.setRefreshSubscriber(kvClient);
|
||||
await ipBanCache.initialize();
|
||||
logger.info('IP ban cache initialized');
|
||||
|
||||
initializeMetricsService();
|
||||
logger.info('Metrics service initialized');
|
||||
|
||||
await initializeServiceSingletons();
|
||||
logger.info('Service singletons initialized');
|
||||
|
||||
try {
|
||||
const userRepository = new UserRepository();
|
||||
const kvDeletionQueue = new KVAccountDeletionQueueService(kvClient, userRepository);
|
||||
|
||||
if (await kvDeletionQueue.needsRebuild()) {
|
||||
logger.warn('KV deletion queue needs rebuild, rebuilding...');
|
||||
await kvDeletionQueue.rebuildState();
|
||||
} else {
|
||||
logger.info('KV deletion queue state is healthy');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Failed to verify KV deletion queue state');
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.info({search_url: config.search.url}, 'Initializing search...');
|
||||
await initializeSearch();
|
||||
logger.info('Search initialized');
|
||||
|
||||
// All API replicas share the same Meilisearch cluster, so only one should warm it.
|
||||
const warmupLockKey = 'fluxer:search:warmup:admin';
|
||||
const warmupLockToken = randomUUID();
|
||||
const warmupLockTtlSeconds = 60 * 60;
|
||||
const acquiredWarmupLock = await kvClient.setnx(warmupLockKey, warmupLockToken, warmupLockTtlSeconds);
|
||||
if (!acquiredWarmupLock) {
|
||||
logger.info('Another API instance is warming search indexes, skipping warmup');
|
||||
} else {
|
||||
try {
|
||||
await warmupAdminSearchIndexes({
|
||||
userRepository: new UserRepository(),
|
||||
guildRepository: new GuildDataRepository(),
|
||||
reportRepository: new ReportRepository(),
|
||||
logger,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Admin search warmup failed (continuing startup)');
|
||||
} finally {
|
||||
try {
|
||||
await kvClient.releaseLock(warmupLockKey, warmupLockToken);
|
||||
} catch (error) {
|
||||
logger.warn({error}, 'Failed to release admin search warmup lock');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.voice.enabled && config.voice.defaultRegion) {
|
||||
const voiceDataInitializer = new VoiceDataInitializer();
|
||||
await voiceDataInitializer.initialize();
|
||||
await ensureVoiceResourcesInitialized();
|
||||
logger.info('Voice data initialized');
|
||||
}
|
||||
|
||||
if (config.dev.testModeEnabled && config.stripe.enabled) {
|
||||
const visionarySlotInitializer = new VisionarySlotInitializer();
|
||||
await visionarySlotInitializer.initialize();
|
||||
logger.info('Stripe visionary slots initialized');
|
||||
}
|
||||
|
||||
if (config.dev.testModeEnabled) {
|
||||
const instanceConfigRepository = new InstanceConfigRepository();
|
||||
try {
|
||||
await instanceConfigRepository.setSsoConfig({
|
||||
enabled: false,
|
||||
authorizationUrl: null,
|
||||
tokenUrl: null,
|
||||
clientId: null,
|
||||
});
|
||||
logger.info('Reset SSO config to disabled for test mode');
|
||||
} catch (error) {
|
||||
logger.warn({error}, 'Failed to reset SSO config for test mode');
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('API service initialization complete');
|
||||
};
|
||||
}
|
||||
|
||||
export function createShutdown(logger: ILogger): () => Promise<void> {
|
||||
return async (): Promise<void> => {
|
||||
logger.info('Shutting down API service...');
|
||||
|
||||
try {
|
||||
await shutdownSearch();
|
||||
logger.info('Search service shut down');
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Error shutting down search service');
|
||||
}
|
||||
|
||||
try {
|
||||
ipBanCache.shutdown();
|
||||
logger.info('IP ban cache shut down');
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Error shutting down IP ban cache');
|
||||
}
|
||||
|
||||
logger.info('API service shutdown complete');
|
||||
};
|
||||
}
|
||||
83
packages/api/src/app/ControllerRegistry.tsx
Normal file
83
packages/api/src/app/ControllerRegistry.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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 {registerAdminControllers} from '@fluxer/api/src/admin/controllers';
|
||||
import {AuthController} from '@fluxer/api/src/auth/AuthController';
|
||||
import {BlueskyOAuthController} from '@fluxer/api/src/bluesky/BlueskyOAuthController';
|
||||
import {ChannelController} from '@fluxer/api/src/channel/ChannelController';
|
||||
import type {APIConfig} from '@fluxer/api/src/config/APIConfig';
|
||||
import {ConnectionController} from '@fluxer/api/src/connection/ConnectionController';
|
||||
import {DonationController} from '@fluxer/api/src/donation/DonationController';
|
||||
import {DownloadController} from '@fluxer/api/src/download/DownloadController';
|
||||
import {FavoriteMemeController} from '@fluxer/api/src/favorite_meme/FavoriteMemeController';
|
||||
import {GatewayController} from '@fluxer/api/src/gateway/GatewayController';
|
||||
import {GuildController} from '@fluxer/api/src/guild/GuildController';
|
||||
import {InstanceController} from '@fluxer/api/src/instance/InstanceController';
|
||||
import {InviteController} from '@fluxer/api/src/invite/InviteController';
|
||||
import {KlipyController} from '@fluxer/api/src/klipy/KlipyController';
|
||||
import {OAuth2ApplicationsController} from '@fluxer/api/src/oauth/OAuth2ApplicationsController';
|
||||
import {OAuth2Controller} from '@fluxer/api/src/oauth/OAuth2Controller';
|
||||
import {registerPackControllers} from '@fluxer/api/src/pack/controllers';
|
||||
import {ReadStateController} from '@fluxer/api/src/read_state/ReadStateController';
|
||||
import {ReportController} from '@fluxer/api/src/report/ReportController';
|
||||
import {RpcController} from '@fluxer/api/src/rpc/RpcController';
|
||||
import {SearchController} from '@fluxer/api/src/search/controllers/SearchController';
|
||||
import {StripeController} from '@fluxer/api/src/stripe/StripeController';
|
||||
import {TenorController} from '@fluxer/api/src/tenor/TenorController';
|
||||
import {TestHarnessController} from '@fluxer/api/src/test/TestHarnessController';
|
||||
import {ThemeController} from '@fluxer/api/src/theme/ThemeController';
|
||||
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {UserController} from '@fluxer/api/src/user/controllers/UserController';
|
||||
import {WebhookController} from '@fluxer/api/src/webhook/WebhookController';
|
||||
|
||||
export function registerControllers(routes: HonoApp, config: APIConfig): void {
|
||||
GatewayController(routes);
|
||||
registerAdminControllers(routes);
|
||||
AuthController(routes);
|
||||
ChannelController(routes);
|
||||
ConnectionController(routes);
|
||||
BlueskyOAuthController(routes);
|
||||
InstanceController(routes);
|
||||
DownloadController(routes);
|
||||
FavoriteMemeController(routes);
|
||||
InviteController(routes);
|
||||
registerPackControllers(routes);
|
||||
ReadStateController(routes);
|
||||
ReportController(routes);
|
||||
RpcController(routes);
|
||||
GuildController(routes);
|
||||
SearchController(routes);
|
||||
KlipyController(routes);
|
||||
TenorController(routes);
|
||||
ThemeController(routes);
|
||||
|
||||
if (config.dev.testModeEnabled || config.nodeEnv === 'development') {
|
||||
TestHarnessController(routes);
|
||||
}
|
||||
|
||||
UserController(routes);
|
||||
WebhookController(routes);
|
||||
OAuth2Controller(routes);
|
||||
OAuth2ApplicationsController(routes);
|
||||
|
||||
if (!config.instance.selfHosted) {
|
||||
DonationController(routes);
|
||||
StripeController(routes);
|
||||
}
|
||||
}
|
||||
147
packages/api/src/app/MiddlewarePipeline.tsx
Normal file
147
packages/api/src/app/MiddlewarePipeline.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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 type {ILogger} from '@fluxer/api/src/ILogger';
|
||||
import {AuditLogMiddleware} from '@fluxer/api/src/middleware/AuditLogMiddleware';
|
||||
import {ConcurrencyLimitMiddleware} from '@fluxer/api/src/middleware/ConcurrencyLimitMiddleware';
|
||||
import {GuildAvailabilityMiddleware} from '@fluxer/api/src/middleware/GuildAvailabilityMiddleware';
|
||||
import {IpBanMiddleware} from '@fluxer/api/src/middleware/IpBanMiddleware';
|
||||
import {LocaleMiddleware} from '@fluxer/api/src/middleware/LocaleMiddleware';
|
||||
import {MetricsMiddleware} from '@fluxer/api/src/middleware/MetricsMiddleware';
|
||||
import {RequestCacheMiddleware} from '@fluxer/api/src/middleware/RequestCacheMiddleware';
|
||||
import {RequireXForwardedForMiddleware} from '@fluxer/api/src/middleware/RequireXForwardedForMiddleware';
|
||||
import {ServiceMiddleware} from '@fluxer/api/src/middleware/ServiceMiddleware';
|
||||
import {UserMiddleware} from '@fluxer/api/src/middleware/UserMiddleware';
|
||||
import type {HonoApp, HonoEnv} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {InvalidApiOriginError} from '@fluxer/errors/src/domains/core/InvalidApiOriginError';
|
||||
import {applyMiddlewareStack} from '@fluxer/hono/src/middleware/MiddlewareStack';
|
||||
import {createServiceTelemetry} from '@fluxer/hono/src/middleware/TelemetryAdapters';
|
||||
import {formatTraceparent, getActiveSpan} from '@fluxer/telemetry/src/Tracing';
|
||||
import type {Context as HonoContext} from 'hono';
|
||||
|
||||
export interface MiddlewarePipelineOptions {
|
||||
logger: ILogger;
|
||||
nodeEnv: string;
|
||||
setSentryUser?: (user: {id?: string; username?: string; email?: string; ip_address?: string}) => void;
|
||||
isTelemetryActive?: () => boolean;
|
||||
}
|
||||
|
||||
const TRACEPARENT_HEADER = 'traceparent';
|
||||
function attachTraceparentHeader(ctx: HonoContext<HonoEnv>): void {
|
||||
const span = getActiveSpan();
|
||||
if (!span) {
|
||||
return;
|
||||
}
|
||||
|
||||
const traceparent = formatTraceparent(span);
|
||||
if (traceparent) {
|
||||
ctx.header(TRACEPARENT_HEADER, traceparent);
|
||||
}
|
||||
}
|
||||
|
||||
export function configureMiddleware(routes: HonoApp, options: MiddlewarePipelineOptions): void {
|
||||
const {logger, nodeEnv, setSentryUser, isTelemetryActive} = options;
|
||||
|
||||
const requestTelemetry = createServiceTelemetry({
|
||||
serviceName: 'fluxer-api',
|
||||
skipPaths: ['/_health', '/internal/telemetry'],
|
||||
});
|
||||
|
||||
applyMiddlewareStack(routes, {
|
||||
requestId: {},
|
||||
tracing: requestTelemetry.tracing,
|
||||
metrics: {
|
||||
enabled: true,
|
||||
collector: requestTelemetry.metricsCollector,
|
||||
skipPaths: ['/_health', '/internal/telemetry'],
|
||||
},
|
||||
logger: {
|
||||
log: (data) => {
|
||||
logger.info(
|
||||
{
|
||||
method: data.method,
|
||||
path: data.path,
|
||||
status: data.status,
|
||||
durationMs: data.durationMs,
|
||||
},
|
||||
'Request completed',
|
||||
);
|
||||
},
|
||||
skip: ['/_health'],
|
||||
},
|
||||
skipErrorHandler: true,
|
||||
});
|
||||
|
||||
if (nodeEnv === 'production') {
|
||||
routes.use('*', async (ctx, next) => {
|
||||
const host = ctx.req.header('host');
|
||||
if (ctx.req.method !== 'GET' && (host === 'web.fluxer.app' || host === 'web.canary.fluxer.app')) {
|
||||
const origin = ctx.req.header('origin');
|
||||
if (!origin || origin !== `https://${host}`) {
|
||||
throw new InvalidApiOriginError();
|
||||
}
|
||||
}
|
||||
await next();
|
||||
});
|
||||
}
|
||||
|
||||
routes.use(IpBanMiddleware);
|
||||
routes.use(ConcurrencyLimitMiddleware);
|
||||
routes.use(MetricsMiddleware);
|
||||
routes.use(AuditLogMiddleware);
|
||||
routes.use(RequireXForwardedForMiddleware());
|
||||
routes.use(RequestCacheMiddleware);
|
||||
routes.use(ServiceMiddleware);
|
||||
routes.use(UserMiddleware);
|
||||
routes.use(GuildAvailabilityMiddleware);
|
||||
routes.use(LocaleMiddleware);
|
||||
|
||||
routes.use('*', async (ctx, next) => {
|
||||
attachTraceparentHeader(ctx);
|
||||
await next();
|
||||
});
|
||||
|
||||
if (setSentryUser) {
|
||||
routes.use('*', async (ctx, next) => {
|
||||
const user = ctx.get('user');
|
||||
const clientIp = ctx.req.header('X-Forwarded-For')?.split(',')[0]?.trim();
|
||||
|
||||
setSentryUser({
|
||||
id: user?.id.toString(),
|
||||
username: user?.username,
|
||||
email: user?.email ?? undefined,
|
||||
ip_address: clientIp,
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
}
|
||||
|
||||
routes.get('/_health', async (ctx) => ctx.text('OK'));
|
||||
|
||||
if (isTelemetryActive) {
|
||||
routes.get('/internal/telemetry', async (ctx) => {
|
||||
return ctx.json({
|
||||
telemetry_enabled: isTelemetryActive(),
|
||||
service: 'fluxer_api',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user