b5c600ad81
Carve the platform super-admin API (/admin-api) and plaza operations console API (/api/ops/v1) out of the public server.mjs into their own process, so the user-facing fleet can no longer be taken down or scaled by back-office traffic. Architecture (split-ready, single process for now): - admin-routes.mjs: createAdminApi / createOpsApi route factories (DI, single source of truth; the route logic moved verbatim out of server.mjs). - admin-bootstrap.mjs: lean service container (user-auth, LLM providers, minimal plaza graph) with no user-facing daemons. - admin-server.mjs: standalone entry with a console registry. ADMIN_CONSOLES selects which consoles a process mounts (default both), so splitting into two processes later is a config change, not a code change. - admin-guard.mjs (+ tests): per-console host / IP-CIDR allowlists, letting the super-admin surface be locked down harder than moderation. server.mjs no longer serves or mounts either surface. user-auth.mjs gains pagination on listUsers/listUsageRecords/listBillingLedger to back the admin dashboards. dev.mjs launches memind_adm; the ops SPA proxies /api to it while /auth stays on the portal. Sessions are read from the shared cookie (H5_COOKIE_DOMAIN=.tkmind.cn); login stays on the main domain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
3.8 KiB
JavaScript
91 lines
3.8 KiB
JavaScript
// admin-bootstrap.mjs
|
|
//
|
|
// Service container for the standalone memind_adm process.
|
|
//
|
|
// Builds only the domain services the back-office routes need (user-auth, LLM
|
|
// providers, and the minimal plaza graph behind the ops console). It deliberately
|
|
// does NOT start any user-facing daemons — no thumbnail watcher, no asset-sync
|
|
// watcher, no plaza hot-score scheduler, no goosed auto-sync. Those belong to the
|
|
// public server. Side-effecting actions (e.g. push selected LLM key to goosed)
|
|
// stay behind explicit admin routes.
|
|
//
|
|
// The same factory functions imported here are the ones server.mjs uses, so the
|
|
// domain logic has a single source of truth; only the wiring differs.
|
|
import path from 'node:path';
|
|
import { createDbPool, isDatabaseConfigured } from './db.mjs';
|
|
import { createUserAuth } from './user-auth.mjs';
|
|
import { createLlmProviderService } from './llm-providers.mjs';
|
|
import { createPlazaPostService, formatPostRow } from './plaza-posts.mjs';
|
|
import { createPlazaInteractionService } from './plaza-interactions.mjs';
|
|
import { createPlazaOpsService } from './plaza-ops.mjs';
|
|
import { createNoopPlazaRedis } from './plaza-redis.mjs';
|
|
import { ensureAlgorithmConfig, loadAlgorithmConfig } from './plaza-algorithm.mjs';
|
|
|
|
const noop = () => {};
|
|
|
|
/**
|
|
* Build the admin service container.
|
|
* @param {object} [env]
|
|
* @param {string} [env.h5Root] Repo root used for workspace path resolution.
|
|
* @param {string} [env.usersRoot] User workspace root.
|
|
* @param {string} [env.apiTarget] Goosed/relay API target.
|
|
* @param {string} [env.apiSecret] Goosed/relay API secret.
|
|
* @param {number} [env.defaultSignupBalanceCents]
|
|
* @param {boolean} [env.ensureAdminUser] Ensure an admin account exists on boot (default true).
|
|
* @returns {Promise<{pool, userAuth, llmProviderService, plazaPosts, plazaOps}>}
|
|
*/
|
|
export async function createAdminServices(env = {}) {
|
|
if (!isDatabaseConfigured()) {
|
|
throw new Error('admin-server requires a database (set DATABASE_URL or MYSQL_* env)');
|
|
}
|
|
|
|
const h5Root = env.h5Root ?? process.cwd();
|
|
const usersRoot = env.usersRoot ?? process.env.H5_USERS_ROOT ?? path.join(h5Root, 'users');
|
|
const apiTarget = env.apiTarget ?? process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006';
|
|
const apiSecret = env.apiSecret ?? process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret';
|
|
const defaultSignupBalanceCents = Number(
|
|
env.defaultSignupBalanceCents ?? process.env.H5_SIGNUP_BALANCE_CENTS ?? 500,
|
|
);
|
|
|
|
const pool = createDbPool();
|
|
|
|
// --- plaza graph (review queue, reports, featured, analytics, creators) ---
|
|
const plazaRedis = createNoopPlazaRedis();
|
|
await ensureAlgorithmConfig(pool);
|
|
const algorithmConfig = await loadAlgorithmConfig(pool);
|
|
|
|
const plazaInteractions = createPlazaInteractionService(pool, { formatPostRow, plazaRedis });
|
|
|
|
let plazaOps = null;
|
|
const plazaPosts = createPlazaPostService(pool, {
|
|
loadViewerReactions: (viewerId, postIds) =>
|
|
plazaInteractions.loadViewerReactions(viewerId, postIds),
|
|
plazaRedis,
|
|
algorithmConfig,
|
|
onPostPublished: noop,
|
|
loadFeaturedPosts: async (viewerId) => {
|
|
if (!plazaOps) return { homepage_banner: [], trending: [], category_top: {} };
|
|
return plazaOps.loadActiveFeaturedPosts(viewerId);
|
|
},
|
|
});
|
|
plazaOps = createPlazaOpsService(pool, {
|
|
formatPostRow,
|
|
reviewPost: (...args) => plazaPosts.reviewPost(...args),
|
|
invalidateFeedCaches: () => plazaRedis?.invalidateFeedCaches?.(),
|
|
});
|
|
|
|
// --- platform super-admin services ---
|
|
const userAuth = createUserAuth(pool, {
|
|
usersRoot,
|
|
h5Root,
|
|
defaultSignupBalanceCents,
|
|
});
|
|
if (env.ensureAdminUser !== false) {
|
|
await userAuth.ensureAdminUser();
|
|
}
|
|
|
|
const llmProviderService = createLlmProviderService(pool, { apiTarget, apiSecret });
|
|
|
|
return { pool, userAuth, llmProviderService, plazaPosts, plazaOps };
|
|
}
|