refactor: centralize configuration

This commit is contained in:
2025-11-23 19:54:32 +02:00
parent 57a8501c88
commit 72dfa53f80
78 changed files with 2252 additions and 1385 deletions
-7
View File
@@ -1,7 +0,0 @@
NODE_ENV=development
BASANGO_API_HOST=localhost
BASANGO_API_PORT=3080
BASANGO_API_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
BASANGO_API_KEY=your_api_key_here
BASANGO_CRAWLER_TOKEN=dev
BASANGO_JWT_SECRET=your_jwt_secret_here
-16
View File
@@ -1,16 +0,0 @@
{
"cors": {
"allowedHeaders": [
"Authorization",
"Content-Type",
"accept-language",
"x-trpc-source",
"x-user-locale",
"x-user-timezone",
"x-user-country"
],
"allowMethods": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
"exposeHeaders": ["Content-Length"],
"maxAge": 86400
}
}
-7
View File
@@ -1,7 +0,0 @@
{
"server": {
"host": "%env(BASANGO_API_HOST)%",
"port": "%env(number:BASANGO_API_PORT)%",
"version": "1.0.0"
}
}
-3
View File
@@ -4,13 +4,10 @@
"@basango/domain": "workspace:*",
"@basango/encryption": "workspace:*",
"@basango/logger": "workspace:*",
"@devscast/config": "catalog:",
"@hono/node-server": "^1.19.6",
"@hono/trpc-server": "^0.4.0",
"@hono/zod-openapi": "^1.1.4",
"@scalar/hono-api-reference": "^0.9.24",
"@trpc/server": "^11.7.1",
"ai": "^5.0.89",
"camelcase-keys": "^10.0.1",
"date-fns": "catalog:",
"hono-rate-limiter": "^0.4.2",
-45
View File
@@ -1,45 +0,0 @@
import path from "node:path";
import { loadConfig as defineConfig } from "@devscast/config";
import { z } from "zod";
export const PROJECT_DIR = path.resolve(__dirname, "../");
const ServerConfigurationSchema = z.object({
cors: z.object({
allowedHeaders: z.array(z.string()).optional(),
allowMethods: z.array(z.string()).optional(),
exposeHeaders: z.array(z.string()).optional(),
maxAge: z.number().int().min(0).optional(),
origin: z
.array(z.string())
.optional()
.default(["http://localhost:3000", "http://127.0.0.1:3000", "https://dashboard.basango.io"]),
}),
server: z.object({
host: z.string().default("localhost"),
port: z.number().int().min(1).max(65535).default(4000),
version: z.string().default("1.0.0"),
}),
});
export const { env, config } = defineConfig({
env: {
knownKeys: [
"BASANGO_API_HOST",
"BASANGO_API_PORT",
"BASANGO_API_ALLOWED_ORIGINS",
"BASANGO_API_KEY",
"BASANGO_CRAWLER_TOKEN",
"BASANGO_JWT_SECRET",
],
path: path.join(PROJECT_DIR, ".env"),
},
schema: ServerConfigurationSchema,
sources: [
path.join(PROJECT_DIR, "config", "server.json"),
path.join(PROJECT_DIR, "config", "cors.json"),
],
});
export type ServerConfiguration = z.infer<typeof ServerConfigurationSchema>;
+8 -48
View File
@@ -1,11 +1,10 @@
import { config } from "@basango/domain/config";
import { trpcServer } from "@hono/trpc-server";
import { OpenAPIHono } from "@hono/zod-openapi";
import { Scalar } from "@scalar/hono-api-reference";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { secureHeaders } from "hono/secure-headers";
import { config, env } from "#api/config";
import { routers } from "#api/rest/routers";
import { createTRPCContext } from "#api/trpc/init";
import { appRouter } from "#api/trpc/routers/_app";
@@ -18,11 +17,11 @@ app.use(secureHeaders());
app.use(
"*",
cors({
allowHeaders: config.cors.allowedHeaders,
allowMethods: config.cors.allowMethods,
exposeHeaders: config.cors.exposeHeaders,
maxAge: config.cors.maxAge,
origin: ["http://localhost:3000", "http://127.0.0.1:3000", "https://dashboard.basango.io"],
allowHeaders: config.api.cors.allowedHeaders,
allowMethods: config.api.cors.allowMethods,
exposeHeaders: config.api.cors.exposeHeaders,
maxAge: config.api.cors.maxAge,
origin: config.api.cors.origin,
}),
);
@@ -34,49 +33,10 @@ app.use(
}),
);
app.doc("/openapi", {
info: {
contact: {
email: "engineering@basango.io",
name: "Basango",
url: "https://basango.io",
},
description: "Basango is a platform that leverages AI to revolutionize news curation.",
license: {
name: "AGPL-3.0 license",
url: "https://github.com/bernard-ng/basango/blob/main/LICENSE",
},
title: "Basango API",
version: "0.0.1",
},
openapi: "3.1.0",
security: [
{
oauth2: [],
},
{ token: [] },
],
servers: [
{
description: "Production API",
url: "https://api.basango.io",
},
],
});
// Register security scheme
app.openAPIRegistry.registerComponent("securitySchemes", "token", {
description: "Default authentication mechanism",
scheme: "bearer",
type: "http",
"x-speakeasy-example": env("BASANGO_API_KEY"),
});
app.get("/", Scalar({ pageTitle: "Basango API", theme: "saturn", url: "/openapi" }));
app.route("/", routers);
export default {
fetch: app.fetch,
hostname: config.server.host,
port: config.server.port,
hostname: config.api.server.host,
port: config.api.server.port,
};
+2 -3
View File
@@ -1,8 +1,7 @@
import { config } from "@basango/domain/config";
import type { MiddlewareHandler } from "hono";
import { HTTPException } from "hono/http-exception";
import { env } from "#api/config";
export const withCrawlerAuth: MiddlewareHandler = async (c, next) => {
const token = c.req.header("Authorization");
@@ -10,7 +9,7 @@ export const withCrawlerAuth: MiddlewareHandler = async (c, next) => {
throw new HTTPException(401, { message: "Authorization header required" });
}
if (token !== env("BASANGO_CRAWLER_TOKEN")) {
if (token !== config.api.security.crawlerToken) {
throw new HTTPException(403, { message: "Invalid token" });
}
+1 -1
View File
@@ -13,7 +13,7 @@ export const authRouter = createTRPCRouter({
if (!user || user.isLocked) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid credentials.",
message: "Account is locked",
});
}
+10 -17
View File
@@ -1,15 +1,8 @@
import { Database } from "@basango/db/client";
import { getUserById } from "@basango/db/queries";
import {
DEFAULT_ACCESS_TOKEN_TTL,
DEFAULT_REFRESH_TOKEN_TTL,
DEFAULT_TOKEN_AUDIENCE,
DEFAULT_TOKEN_ISSUER,
} from "@basango/domain/constants";
import { config } from "@basango/domain/config";
import { type JWTPayload, SignJWT, jwtVerify } from "jose";
import { env } from "#api/config";
export type Session = {
user: {
id: string;
@@ -39,7 +32,7 @@ export type SessionTokens = {
const encoder = new TextEncoder();
function getSecretKey() {
return encoder.encode(env("BASANGO_JWT_SECRET"));
return encoder.encode(config.api.security.jwtSecret);
}
export async function getSession(db: Database, accessToken?: string): Promise<Session | null> {
@@ -74,24 +67,24 @@ async function createToken(session: Session, tokenType: TokenType, expiresIn: st
})
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setAudience(DEFAULT_TOKEN_AUDIENCE)
.setIssuer(DEFAULT_TOKEN_ISSUER)
.setAudience(config.api.security.audience)
.setIssuer(config.api.security.issuer)
.setExpirationTime(expiresIn)
.sign(getSecretKey());
}
export async function createSessionTokens(session: Session): Promise<SessionTokens> {
const [accessToken, refreshToken] = await Promise.all([
createToken(session, "access", DEFAULT_ACCESS_TOKEN_TTL),
createToken(session, "refresh", DEFAULT_REFRESH_TOKEN_TTL),
createToken(session, "access", config.api.security.accessTokenTtl),
createToken(session, "refresh", config.api.security.refreshTokenTtl),
]);
const issuedAt = Date.now();
const accessTokenExpiresAt = new Date(
issuedAt + formatTTL(DEFAULT_ACCESS_TOKEN_TTL),
issuedAt + formatTTL(config.api.security.accessTokenTtl),
).toISOString();
const refreshTokenExpiresAt = new Date(
issuedAt + formatTTL(DEFAULT_REFRESH_TOKEN_TTL),
issuedAt + formatTTL(config.api.security.refreshTokenTtl),
).toISOString();
return {
@@ -118,8 +111,8 @@ async function verifyToken(
try {
const { payload } = await jwtVerify<VerifiedJWTPayload>(token, getSecretKey(), {
audience: DEFAULT_TOKEN_AUDIENCE,
issuer: DEFAULT_TOKEN_ISSUER,
audience: config.api.security.audience,
issuer: config.api.security.issuer,
});
if (payload.tokenType !== expectedType) {