From 1e0fced19b20c306a334983e3a24d0937aea7ebb Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Mon, 29 Sep 2025 22:41:35 +0100 Subject: [PATCH 1/3] Add valibot dependency Signed-off-by: Olivier 'reivilibre --- server/package.json | 3 ++- server/pnpm-lock.yaml | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/server/package.json b/server/package.json index 1d91434..63fb17d 100644 --- a/server/package.json +++ b/server/package.json @@ -36,6 +36,7 @@ "express-session": "^1.18.0", "openapi-backend": "^5.15.0", "pg": "^8.16.3", - "postgres-migrations": "^5.3.0" + "postgres-migrations": "^5.3.0", + "valibot": "^1.1.0" } } diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index 9cebb3d..0462891 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: postgres-migrations: specifier: ^5.3.0 version: 5.3.0 + valibot: + specifier: ^1.1.0 + version: 1.1.0(typescript@5.9.2) devDependencies: '@biomejs/biome': specifier: 2.2.3 @@ -2063,6 +2066,14 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + valibot@1.1.0: + resolution: {integrity: sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -4348,6 +4359,10 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + valibot@1.1.0(typescript@5.9.2): + optionalDependencies: + typescript: 5.9.2 + vary@1.1.2: {} walker@1.0.8: -- 2.50.1 From bff741c7dfcf939a381351d7c93633de14a83efe Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Tue, 30 Sep 2025 22:44:53 +0100 Subject: [PATCH 2/3] Disable 'noNonNullAssertion' lint Signed-off-by: Olivier 'reivilibre --- server/biome.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/biome.json b/server/biome.json index deed4a7..8bf4430 100644 --- a/server/biome.json +++ b/server/biome.json @@ -16,7 +16,10 @@ "enabled": true, "includes": ["src/**", "!src/types/openapi.d.ts"], "rules": { - "recommended": true + "recommended": true, + "style": { + "noNonNullAssertion": "off" + } } }, "javascript": { -- 2.50.1 From 92b00934a2dcb02553bc9d111df6a2caaa401b87 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Mon, 29 Sep 2025 22:39:45 +0100 Subject: [PATCH 3/3] Replace config: use Valibot and allow multiple forges of one type Signed-off-by: Olivier 'reivilibre --- server/.env.sample | 11 +-- server/src/api/repositories.ts | 6 +- server/src/config/db.ts | 18 ++--- server/src/config/env.ts | 55 -------------- server/src/config/index.ts | 22 ++++++ server/src/config/schema.ts | 76 +++++++++++++++++++ server/src/config/transformer.ts | 47 ++++++++++++ server/src/db/repositories/user-repository.ts | 13 +--- server/src/routes/auth.ts | 52 +++++++------ server/src/server.ts | 13 ++-- server/src/services/forges/constants.ts | 5 -- server/src/services/forges/gitea.ts | 28 ++++--- server/src/services/forges/index.ts | 24 +++--- server/src/services/user.ts | 4 +- server/src/types/express-session.d.ts | 2 +- server/src/util/seedforges.ts | 27 ++----- 16 files changed, 234 insertions(+), 169 deletions(-) delete mode 100644 server/src/config/env.ts create mode 100644 server/src/config/index.ts create mode 100644 server/src/config/schema.ts create mode 100644 server/src/config/transformer.ts delete mode 100644 server/src/services/forges/constants.ts diff --git a/server/.env.sample b/server/.env.sample index b844c99..d6cd07a 100644 --- a/server/.env.sample +++ b/server/.env.sample @@ -8,9 +8,10 @@ DB_PASSWORD= DB_HOST=localhost DB_NAME=database -GITEA_URL=http://localhost:3001 -GITEA_CLIENT_ID= -GITEA_CLIENT_SECRET= SESSION_SECRET=your_session_secret_here -GITEA_REDIRECT_URI=http://localhost:3000/auth/callback -FORGE_TYPES=gitea + +FORGES_1_TYPE=gitea +FORGES_1_URL=http://localhost:3001 +FORGES_1_CLIENTID= +FORGES_1_CLIENTSECRET= +FORGES_1_REDIRECTURI=http://localhost:3002/auth/callback diff --git a/server/src/api/repositories.ts b/server/src/api/repositories.ts index a5118f6..f0c4eec 100644 --- a/server/src/api/repositories.ts +++ b/server/src/api/repositories.ts @@ -13,9 +13,9 @@ export async function listAvailableRepos( ): Promise { try { const userId = req.session?.userId; - const forgeType = req.session?.forgeType; + const forgeId = req.session?.forgeId; - if (!userId || !forgeType) { + if (!userId || forgeId === undefined) { return res.status(401).json({ error: "Not authenticated" }); } @@ -24,7 +24,7 @@ export async function listAvailableRepos( return res.status(401).json({ error: "Missing or expired access token" }); } - const forge = createForge(forgeType); + const forge = createForge(forgeId); const repos = await forge.listRepositories(accessToken); return res.json(repos); diff --git a/server/src/config/db.ts b/server/src/config/db.ts index c42767a..06aad71 100644 --- a/server/src/config/db.ts +++ b/server/src/config/db.ts @@ -1,20 +1,14 @@ import { Pool } from "pg"; -import { config } from "./env"; +import { config } from "./index"; let pool: Pool; -const { DB_USER, DB_PASSWORD, DB_NAME } = config; - -if (!DB_USER || !DB_PASSWORD || !DB_NAME) { - throw new Error("DB_USER, DB_PASSWORD, and DB_NAME must be set"); -} - pool = new Pool({ - user: config.DB_USER, - host: config.DB_HOST, - database: config.DB_NAME, - password: config.DB_PASSWORD, - port: config.DB_PORT, + user: config.db.user, + host: config.db.host, + database: config.db.name, + password: config.db.password, + port: config.db.port, }); const connectDB = async () => { diff --git a/server/src/config/env.ts b/server/src/config/env.ts deleted file mode 100644 index 763485d..0000000 --- a/server/src/config/env.ts +++ /dev/null @@ -1,55 +0,0 @@ -type EnvMode = "development" | "production" | "test"; - -interface ProcessEnv { - NODE_ENV?: EnvMode; - APP_PORT?: string; - DB_USER: string; - DB_PASSWORD: string; - DB_HOST?: string; - DB_NAME: string; - DB_PORT?: string; - - GITEA_URL?: string; - GITEA_CLIENT_ID?: string; - GITEA_CLIENT_SECRET?: string; - GITEA_REDIRECT_URI?: string; - - FORGE_TYPES?: string; // comma-separated list of enabled forges - SESSION_SECRET: string; -} - -const env = process.env as unknown as ProcessEnv; - -function requiredEnv(key: keyof ProcessEnv): string { - const value = env[key]; - if (!value) { - throw new Error(`Missing required environment variable: ${key}`); - } - return value; -} - -export const config = { - NODE_ENV: env.NODE_ENV ?? "development", - APP_PORT: env.APP_PORT ? Number(env.APP_PORT) : 3000, - DB_HOST: env.DB_HOST ?? "localhost", - DB_NAME: requiredEnv("DB_NAME"), - DB_USER: requiredEnv("DB_USER"), - DB_PASSWORD: requiredEnv("DB_PASSWORD"), - DB_PORT: env.DB_PORT ? Number(env.DB_PORT) : 5432, - - GITEA_URL: env.GITEA_URL ?? "http://localhost:3001", - GITEA_CLIENT_ID: env.GITEA_CLIENT_ID, - GITEA_CLIENT_SECRET: env.GITEA_CLIENT_SECRET, - GITEA_REDIRECT_URI: - env.GITEA_REDIRECT_URI ?? "http://localhost:3000/auth/callback", - - FORGE_TYPES: (env.FORGE_TYPES ?? "gitea") - .split(",") - .map((f) => f.trim().toLowerCase()), - - SESSION_SECRET: requiredEnv("SESSION_SECRET"), - - IS_PRODUCTION: env.NODE_ENV === "production", -}; - -export const isProduction = config.NODE_ENV === "production"; diff --git a/server/src/config/index.ts b/server/src/config/index.ts new file mode 100644 index 0000000..b495826 --- /dev/null +++ b/server/src/config/index.ts @@ -0,0 +1,22 @@ +import * as v from "valibot"; +import { CONFIG_SCHEMA_KEYS, type Config, ConfigSchema } from "./schema"; +import { transformEnvToConfig } from "./transformer"; + +function createConfig(): Config { + const rawConfig = transformEnvToConfig( + process.env as Record, + CONFIG_SCHEMA_KEYS, + ); + + const result = v.safeParse(ConfigSchema, rawConfig); + if (!result.success) { + const issueStrings = result.issues.map( + (issue) => `- ${v.getDotPath(issue)}: ${issue.message}`, + ); + throw new Error(`Invalid configuration:\n${issueStrings.join("\n")}`); + } + + return result.output; +} + +export const config = createConfig(); diff --git a/server/src/config/schema.ts b/server/src/config/schema.ts new file mode 100644 index 0000000..7514644 --- /dev/null +++ b/server/src/config/schema.ts @@ -0,0 +1,76 @@ +import * as v from "valibot"; + +const ForgeInstanceSchema = v.object({ + type: v.picklist(["gitea", "github", "gitlab"]), + url: v.string(), + clientid: v.string(), + clientsecret: v.string(), + redirecturi: v.string(), +}); + +// Convert `{"1": {...}, "2": {...}}` to a `Map` +const ForgesSchema = v.pipe( + v.record(v.pipe(v.string(), v.digits()), ForgeInstanceSchema), + v.transform( + (obj) => new Map(Object.entries(obj).map(([k, v]) => [Number(k), v])), + ), +); + +// All these database configuration fields are optional; +// the Postgres client driver has defaults. +const DatabaseConfigSchema = v.object({ + host: v.optional(v.string()), + port: v.optional( + v.pipe(v.string(), v.digits(), v.transform(Number), v.integer()), + ), + name: v.optional(v.string()), + user: v.optional(v.string()), + password: v.optional(v.string()), +}); + +const AppConfigSchema = v.object({ + port: v.pipe( + v.optional(v.string(), "3000"), + v.transform(Number), + v.integer(), + ), +}); + +const SessionConfigSchema = v.object({ + secret: v.string(), +}); + +export const ConfigSchema = v.object({ + app: optionalObject(AppConfigSchema), + db: optionalObject(DatabaseConfigSchema), + forges: v.optional(ForgesSchema, {}), + session: SessionConfigSchema, + node: v.object({ + env: v.picklist(["production", "development", "test"]), + }), +}); + +// Keys included in the config that should be parsed +// from environment variables. +export const CONFIG_SCHEMA_KEYS = [ + "app", + "db", + "forges", + "session", + "node", +] satisfies (keyof Config)[]; + +export type Config = v.InferOutput; +export type DatabaseConfig = v.InferOutput; +export type ForgesConfig = v.InferOutput; +export type ForgeInstanceConfig = v.InferOutput; + +// Wrap a `v.object` schema so that if the object isn't supplied, +// it gets filled in with defaults. +function optionalObject< + TEntries extends v.ObjectEntries, + TMessage extends v.ErrorMessage | undefined, +>(sch: v.ObjectSchema) { + const defaultObject = v.getDefaults(sch); + return v.optional(sch, defaultObject!); +} diff --git a/server/src/config/transformer.ts b/server/src/config/transformer.ts new file mode 100644 index 0000000..a032096 --- /dev/null +++ b/server/src/config/transformer.ts @@ -0,0 +1,47 @@ +/** + * Transforms environment variables into a hierarchical (nested) configuration object. + * Environment variables are expected to have underscore-delimited keys. + * Keys are lowercased. + * + * Only variables with root keys included in the allowedKeys array are processed. + * + * For example, the environment variable `DATABASE_HOST=localhost` would be + * transformed into `{ database: { host: "localhost" } }` + * as long as "database" is in the allowedKeys. + */ +export function transformEnvToConfig( + env: Record, + allowedKeys: Keys, +): Record<(typeof allowedKeys)[number], unknown> { + const config: Record = {}; + + for (const [keyPath, value] of Object.entries(env)) { + const keyPathParts = keyPath.toLowerCase().split("_"); + if (!allowedKeys.includes(keyPathParts[0]!) || keyPathParts.length < 2) { + // Not an env var we care about. + continue; + } + + // Start at the top-level config map, then + // descend into the deeper layers + // (following keyPath) + let current: Record = config; + for (const part of keyPathParts.slice(0, -1)) { + if (!(part in current)) { + current[part] = {}; + } + if (typeof current[part] !== "object") { + throw new Error( + `Mixed config types: can't descend into ${part} on ${keyPath}, have: ${current[part]}`, + ); + } + current = current[part] as Record; + } + + // then set the value. + const lastPart = keyPathParts[keyPathParts.length - 1]!; + current[lastPart] = value; + } + + return config; +} diff --git a/server/src/db/repositories/user-repository.ts b/server/src/db/repositories/user-repository.ts index 36645ea..3177ade 100644 --- a/server/src/db/repositories/user-repository.ts +++ b/server/src/db/repositories/user-repository.ts @@ -1,16 +1,7 @@ import { pool } from "../../config/db"; -import { FORGE_IDS } from "../../services/forges/constants"; import type { User } from "../models/user"; class UserRepository { - private resolveForgeId(forgeType: string): number { - const forgeId = FORGE_IDS[forgeType]; - if (forgeId === undefined) { - throw new Error(`Unsupported forge type: ${forgeType}`); - } - return forgeId; - } - /** * Find a user by internal Molci ID */ @@ -52,13 +43,11 @@ class UserRepository { } async getOrCreateUser( - forgeType: string, + forgeId: number, forgeUserId: string, access_token?: string, token_expires_at?: Date, ): Promise { - const forgeId = this.resolveForgeId(forgeType); - const result = await pool.query( `INSERT INTO users (forge_id, forge_user_id, access_token, token_expires_at) VALUES ($1, $2, $3, $4) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 49b9b43..2d41396 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -1,36 +1,42 @@ /** biome-ignore-all lint/complexity/useLiteralKeys: */ import { type Request, type Response, Router } from "express"; -import { config } from "../config/env"; -import { createForge, listAvailableForges } from "../services/forges"; +import { config } from "../config"; +import { createForge, listAvailableForgeIds } from "../services/forges"; import { getOrCreateUser } from "../services/user"; const authRouter = Router(); authRouter.get("/providers", (_req, res) => { - res.json({ providers: listAvailableForges() }); + res.json({ providers: listAvailableForgeIds() }); }); -authRouter.get("/login/:forgeType", (req: Request, res: Response) => { - const forgeType = req.params["forgeType"]; - if (!forgeType) { - res.status(400).json({ error: "Missing forgeType" }); +authRouter.get("/login/:forgeId", (req: Request, res: Response) => { + const forgeIdParam = req.params["forgeId"]; + if (!forgeIdParam) { + res.status(400).json({ error: "Missing forgeId" }); return; } + const forgeId = parseInt(forgeIdParam, 10); + if (Number.isNaN(forgeId)) { + res.status(400).json({ error: "Invalid forgeId" }); + return; + } + + const isProduction = config.node.env === "production"; + try { - const forge = createForge(forgeType); + const forge = createForge(forgeId); const authData = forge.getAuthorizationUrl(); req.session.codeVerifier = authData.codeVerifier; - req.session.forgeType = forgeType; + req.session.forgeId = forgeId; - const cookieName = config.IS_PRODUCTION - ? "__Host-oauth_state" - : "oauth_state"; + const cookieName = isProduction ? "__Host-oauth_state" : "oauth_state"; // Store OAuth state in a cookie res.cookie(cookieName, authData.state, { - secure: config.IS_PRODUCTION, + secure: isProduction, path: "/", httpOnly: true, maxAge: 10 * 60 * 1000, // 10 minutes @@ -45,18 +51,17 @@ authRouter.get("/login/:forgeType", (req: Request, res: Response) => { }); authRouter.get("/callback", async (req: Request, res: Response) => { - const forgeType = req.session.forgeType; + const forgeId = req.session.forgeId; const codeVerifier = req.session.codeVerifier; const state = req.query["state"] as string; const code = req.query["code"] as string; + const isProduction = config.node.env === "production"; const cookies = parseCookies(req); - const cookieName = config.IS_PRODUCTION - ? "__Host-oauth_state" - : "oauth_state"; + const cookieName = isProduction ? "__Host-oauth_state" : "oauth_state"; const stateFromCookie = cookies[cookieName]; - if (!forgeType || !state || !code || !codeVerifier) { + if (forgeId === undefined || !state || !code || !codeVerifier) { res.status(400).json({ error: "Missing OAuth callback parameters" }); return; } @@ -67,21 +72,21 @@ authRouter.get("/callback", async (req: Request, res: Response) => { } try { - const forge = createForge(forgeType); + const forge = createForge(forgeId); const { accessToken, accessTokenExpiresAt } = await forge.exchangeCodeForToken(code, codeVerifier); const forgeUser = await forge.getUserInfo(accessToken); const internalUser = await getOrCreateUser( - forgeType, + forgeId, forgeUser.id.toString(), accessToken, accessTokenExpiresAt, ); req.session.userId = internalUser.user_id; - req.session.forgeType = forgeType; + req.session.forgeId = forgeId; // Clear OAuth session data delete req.session.codeVerifier; @@ -110,9 +115,8 @@ authRouter.post("/logout", (req: Request, res: Response) => { return; } res.clearCookie("__Host-SessionID", { path: "/" }); - const cookieName = config.IS_PRODUCTION - ? "__Host-oauth_state" - : "oauth_state"; + const isProduction = config.node.env === "production"; + const cookieName = isProduction ? "__Host-oauth_state" : "oauth_state"; res.clearCookie(cookieName, { path: "/" }); res.json({ success: true }); }); diff --git a/server/src/server.ts b/server/src/server.ts index 358972f..a786656 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,8 +1,8 @@ import express from "express"; import session from "express-session"; import { createOpenAPIBackend, createOpenAPIMiddleware } from "./api/openapi"; +import { config } from "./config"; import { connectDB } from "./config/db"; -import { config, isProduction } from "./config/env"; import authRouter from "./routes/auth"; import { seedForges } from "./util/seedforges"; @@ -13,18 +13,17 @@ async function startServer() { const app = express(); app.use(express.json()); - const sessionCookieName = config.IS_PRODUCTION - ? "__Host-SessionID" - : "sessionID"; + const isProduction = config.node.env === "production"; + const sessionCookieName = isProduction ? "__Host-SessionID" : "sessionID"; app.use( session({ name: sessionCookieName, - secret: config.SESSION_SECRET, + secret: config.session.secret, resave: false, saveUninitialized: false, cookie: { - secure: config.IS_PRODUCTION, // false in dev + secure: isProduction, // false in dev httpOnly: true, sameSite: "lax", maxAge: 24 * 60 * 60 * 1000, @@ -37,7 +36,7 @@ async function startServer() { const openapi = createOpenAPIBackend(); app.use(createOpenAPIMiddleware(openapi)); - const PORT = config.APP_PORT; + const PORT = config.app.port; app.listen(PORT, () => { console.log( `Server running in ${isProduction ? "production" : "development"} mode on port ${PORT}`, diff --git a/server/src/services/forges/constants.ts b/server/src/services/forges/constants.ts deleted file mode 100644 index d5e953a..0000000 --- a/server/src/services/forges/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const FORGE_IDS: Record = { - gitea: 0, - github: 1, - gitlab: 2, -}; diff --git a/server/src/services/forges/gitea.ts b/server/src/services/forges/gitea.ts index f487303..1fe5314 100644 --- a/server/src/services/forges/gitea.ts +++ b/server/src/services/forges/gitea.ts @@ -1,26 +1,34 @@ import * as arctic from "arctic"; -import { config } from "../../config/env"; +import type { ForgeInstanceConfig } from "../../config/schema"; import type { Forge } from "../../types/forge"; import type { ForgeUser } from "../../types/forgeuser"; import type { GiteaRepo } from "../../types/gitearepo"; import type { Repository } from "../../types/openapi"; - export class GiteaForge implements Forge { private gitea: arctic.Gitea; + private forgeId: number; + private baseUrl: string; - constructor() { - if (!config.GITEA_CLIENT_ID || !config.GITEA_CLIENT_SECRET) { + constructor(forgeId: number, config: ForgeInstanceConfig) { + this.forgeId = forgeId; + this.baseUrl = config.url; + + if (!config.clientid || !config.clientsecret) { throw new Error("Gitea OAuth2 credentials not configured"); } this.gitea = new arctic.Gitea( - config.GITEA_URL, - config.GITEA_CLIENT_ID, - config.GITEA_CLIENT_SECRET, - config.GITEA_REDIRECT_URI, + config.url, + config.clientid, + config.clientsecret, + config.redirecturi, ); } + getForgeId(): number { + return this.forgeId; + } + getAuthorizationUrl() { const state = arctic.generateState(); const codeVerifier = arctic.generateCodeVerifier(); @@ -51,7 +59,7 @@ export class GiteaForge implements Forge { } async getUserInfo(accessToken: string): Promise { - const res = await fetch(`${config.GITEA_URL}/api/v1/user`, { + const res = await fetch(`${this.baseUrl}/api/v1/user`, { headers: { Authorization: `token ${accessToken}` }, }); if (!res.ok) throw new Error(`Failed to fetch user: ${res.status}`); @@ -60,7 +68,7 @@ export class GiteaForge implements Forge { } async listRepositories(accessToken: string): Promise { - const res = await fetch(`${config.GITEA_URL}/api/v1/user/repos`, { + const res = await fetch(`${this.baseUrl}/api/v1/user/repos`, { headers: { Authorization: `token ${accessToken}` }, }); if (!res.ok) { diff --git a/server/src/services/forges/index.ts b/server/src/services/forges/index.ts index 32776a5..eac7f46 100644 --- a/server/src/services/forges/index.ts +++ b/server/src/services/forges/index.ts @@ -1,31 +1,33 @@ -import { config } from "../../config/env"; +import { config } from "../../config"; import type { Forge } from "../../types/forge"; import { GiteaForge } from "./gitea"; /** - * Returns a Forge implementation for the given forgeType. + * Returns a Forge implementation for the given forge ID. * Throws an error if the forge is not enabled or not implemented. */ -export function createForge(forgeType: string): Forge { - if (!config.FORGE_TYPES.includes(forgeType)) { - throw new Error(`Unsupported forge type: ${forgeType}`); +export function createForge(forgeId: number): Forge { + const forgeConfig = config.forges.get(forgeId); + + if (!forgeConfig) { + throw new Error(`Forge with ID ${forgeId} not found`); } - switch (forgeType) { + switch (forgeConfig.type) { case "gitea": - return new GiteaForge(); + return new GiteaForge(forgeId, forgeConfig); case "github": throw new Error("GitHubForge not implemented yet"); case "gitlab": throw new Error("GitLabForge not implemented yet"); default: - throw new Error(`Unsupported forge type: ${forgeType}`); + throw new Error(`Unsupported forge type: ${forgeConfig.type}`); } } /** - * Returns the list of available forge types from config. + * Returns the list of available forge IDs from config. */ -export function listAvailableForges(): string[] { - return [...config.FORGE_TYPES]; +export function listAvailableForgeIds(): number[] { + return Array.from(config.forges.keys()); } diff --git a/server/src/services/user.ts b/server/src/services/user.ts index aee3f4e..ec727c1 100644 --- a/server/src/services/user.ts +++ b/server/src/services/user.ts @@ -27,13 +27,13 @@ export const insertUser = ( }; export const getOrCreateUser = ( - forgeType: string, + forgeId: number, forgeUserId: string, access_token?: string, token_expires_at?: Date, ): Promise => { return userRepository.getOrCreateUser( - forgeType, + forgeId, forgeUserId, access_token, token_expires_at, diff --git a/server/src/types/express-session.d.ts b/server/src/types/express-session.d.ts index 7c45fcc..69bd349 100644 --- a/server/src/types/express-session.d.ts +++ b/server/src/types/express-session.d.ts @@ -3,7 +3,7 @@ import "express-session"; declare module "express-session" { interface SessionData { userId?: number; // internal DB ID - forgeType?: string; // e.g., "gitea" or "github" + forgeId?: number; // numeric forge ID codeVerifier?: string; // PKCE code verifier for OAuth } } diff --git a/server/src/util/seedforges.ts b/server/src/util/seedforges.ts index d21e939..be81889 100644 --- a/server/src/util/seedforges.ts +++ b/server/src/util/seedforges.ts @@ -1,36 +1,19 @@ +import { config } from "../config"; import { pool } from "../config/db"; -import { config } from "../config/env"; -import { FORGE_IDS } from "../services/forges/constants"; export async function seedForges(): Promise { - for (const forgeType of config.FORGE_TYPES) { - const forgeId = FORGE_IDS[forgeType]; - if (forgeId === undefined) { - console.warn(`Skipping unknown forge type: ${forgeType}`); - continue; - } - - let baseUrl: string; - switch (forgeType) { - case "gitea": - baseUrl = config.GITEA_URL; - break; - default: - console.warn(`Forge type ${forgeType} not supported`); - continue; - } - + for (const [forgeId, forgeConfig] of config.forges) { try { await pool.query( `INSERT INTO forges (forge_id, display_name, base_url) VALUES ($1, $2, $3) ON CONFLICT (forge_id) DO UPDATE SET base_url = EXCLUDED.base_url`, - [forgeId, forgeType, baseUrl], + [forgeId, forgeConfig.type, forgeConfig.url], ); - console.log(`Forge seeded: ${forgeType} (${baseUrl})`); + console.log(`Forge seeded: ${forgeConfig.type} (${forgeConfig.url})`); } catch (err) { - console.error(`Failed to seed forge ${forgeType}:`, err); + console.error(`Failed to seed forge ${forgeConfig.type}:`, err); throw err; } } -- 2.50.1