refactor: centralize configuration
This commit is contained in:
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user