Compare commits
1 Commits
92b00934a2
...
698a6c7e19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
698a6c7e19 |
@ -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
|
||||
|
||||
@ -13,9 +13,9 @@ export async function listAvailableRepos(
|
||||
): Promise<ExpressResponse> {
|
||||
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);
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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";
|
||||
22
server/src/config/index.ts
Normal file
22
server/src/config/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ConfigSchema, CONFIG_SCHEMA_KEYS, type Config } from "./schema";
|
||||
import { transformEnvToConfig } from "./transformer";
|
||||
import * as v from "valibot";
|
||||
|
||||
function createConfig(): Config {
|
||||
const rawConfig = transformEnvToConfig(
|
||||
process.env as Record<string, string>,
|
||||
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();
|
||||
76
server/src/config/schema.ts
Normal file
76
server/src/config/schema.ts
Normal file
@ -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<number, { ... }>`
|
||||
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<typeof ConfigSchema>;
|
||||
export type DatabaseConfig = v.InferOutput<typeof DatabaseConfigSchema>;
|
||||
export type ForgesConfig = v.InferOutput<typeof ForgesSchema>;
|
||||
export type ForgeInstanceConfig = v.InferOutput<typeof ForgeInstanceSchema>;
|
||||
|
||||
// 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<v.ObjectIssue> | undefined,
|
||||
>(sch: v.ObjectSchema<TEntries, TMessage>) {
|
||||
const defaultObject = v.getDefaults(sch);
|
||||
return v.optional(sch, defaultObject!);
|
||||
}
|
||||
47
server/src/config/transformer.ts
Normal file
47
server/src/config/transformer.ts
Normal file
@ -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<Keys extends string[]>(
|
||||
env: Record<string, string>,
|
||||
allowedKeys: Keys,
|
||||
): Record<(typeof allowedKeys)[number], unknown> {
|
||||
const config: Record<string, unknown> = {};
|
||||
|
||||
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<string, unknown> = config;
|
||||
for (let 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<string, unknown>;
|
||||
}
|
||||
|
||||
// then set the value.
|
||||
const lastPart = keyPathParts[keyPathParts.length - 1]!;
|
||||
current[lastPart] = value;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@ -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<User> {
|
||||
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)
|
||||
|
||||
@ -1,36 +1,42 @@
|
||||
/** biome-ignore-all lint/complexity/useLiteralKeys: <oli said it was ok> */
|
||||
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 (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 });
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@ import express from "express";
|
||||
import session from "express-session";
|
||||
import { createOpenAPIBackend, createOpenAPIMiddleware } from "./api/openapi";
|
||||
import { connectDB } from "./config/db";
|
||||
import { config, isProduction } from "./config/env";
|
||||
import { config } from "./config";
|
||||
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}`,
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
export const FORGE_IDS: Record<string, number> = {
|
||||
gitea: 0,
|
||||
github: 1,
|
||||
gitlab: 2,
|
||||
};
|
||||
@ -1,26 +1,34 @@
|
||||
import * as arctic from "arctic";
|
||||
import { config } from "../../config/env";
|
||||
import type { Forge } from "../../types/forge";
|
||||
import type { ForgeUser } from "../../types/forgeuser";
|
||||
import type { GiteaRepo } from "../../types/gitearepo";
|
||||
import type { Repository } from "../../types/openapi";
|
||||
|
||||
import { ForgeInstanceConfig } from "../../config/schema";
|
||||
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<ForgeUser> {
|
||||
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<Repository[]> {
|
||||
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) {
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -27,13 +27,13 @@ export const insertUser = (
|
||||
};
|
||||
|
||||
export const getOrCreateUser = (
|
||||
forgeType: string,
|
||||
forgeId: number,
|
||||
forgeUserId: string,
|
||||
access_token?: string,
|
||||
token_expires_at?: Date,
|
||||
): Promise<User> => {
|
||||
return userRepository.getOrCreateUser(
|
||||
forgeType,
|
||||
forgeId,
|
||||
forgeUserId,
|
||||
access_token,
|
||||
token_expires_at,
|
||||
|
||||
2
server/src/types/express-session.d.ts
vendored
2
server/src/types/express-session.d.ts
vendored
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user