From 1c97113c67f4976b5eda19427cdedc6fd601afc3 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Fri, 24 Apr 2026 15:31:34 +0200 Subject: [PATCH] feat(admin): add privileged admin session exchange --- .../__tests__/config/stripe-config.test.ts | 1 + .../__tests__/helpers/adminTokenTestUtils.ts | 24 +++ .../__tests__/middlewares/adminAuth.test.ts | 55 +++++ .../src/__tests__/routes/adminAuth.test.ts | 64 ++++++ apps/api/src/config.ts | 13 +- apps/api/src/helpers/adminTokens.ts | 201 ++++++++++++++++++ apps/api/src/middlewares/middleware.ts | 45 ++++ apps/api/src/routers/admin.ts | 18 ++ apps/api/src/routers/adminAuth.ts | 55 +++++ apps/api/src/routers/adminTables.ts | 19 ++ apps/api/src/routers/index.ts | 4 + apps/api/src/secrets.ts | 2 + apps/api/src/types/app.types.ts | 2 + packages/shared-types/src/admin.types.ts | 17 ++ packages/shared-types/src/index.ts | 4 + 15 files changed, 520 insertions(+), 4 deletions(-) create mode 100644 apps/api/src/__tests__/helpers/adminTokenTestUtils.ts create mode 100644 apps/api/src/__tests__/middlewares/adminAuth.test.ts create mode 100644 apps/api/src/__tests__/routes/adminAuth.test.ts create mode 100644 apps/api/src/helpers/adminTokens.ts create mode 100644 apps/api/src/routers/admin.ts create mode 100644 apps/api/src/routers/adminAuth.ts create mode 100644 apps/api/src/routers/adminTables.ts create mode 100644 packages/shared-types/src/admin.types.ts diff --git a/apps/api/src/__tests__/config/stripe-config.test.ts b/apps/api/src/__tests__/config/stripe-config.test.ts index fdc130b..0a29917 100644 --- a/apps/api/src/__tests__/config/stripe-config.test.ts +++ b/apps/api/src/__tests__/config/stripe-config.test.ts @@ -3,6 +3,7 @@ import { createConfig } from "../../config.js"; import type { Secrets } from "../../secrets.js"; const baseSecrets: Secrets = { + adminTokenSigningSecret: "admin-token-signing-secret", supabaseServiceRoleKey: "service-role-from-secret-manager", supabaseConnectionString: "postgres://secret-manager", supabaseCaCert: "ca-cert", diff --git a/apps/api/src/__tests__/helpers/adminTokenTestUtils.ts b/apps/api/src/__tests__/helpers/adminTokenTestUtils.ts new file mode 100644 index 0000000..4b024d0 --- /dev/null +++ b/apps/api/src/__tests__/helpers/adminTokenTestUtils.ts @@ -0,0 +1,24 @@ +import { createHmac } from "node:crypto"; + +type TestAdminTokenClaims = { + aud: string; + email: string; + exp: number; + role: "viewer" | "operator" | "superadmin"; + sub: string; + type: "admin_access" | "admin_session"; +}; + +function encodeSegment(value: unknown) { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +export function createSignedAdminToken(claims: TestAdminTokenClaims, secret: string) { + const header = encodeSegment({ alg: "HS256", typ: "JWT" }); + const payload = encodeSegment(claims); + const signature = createHmac("sha256", secret) + .update(`${header}.${payload}`) + .digest("base64url"); + + return `${header}.${payload}.${signature}`; +} diff --git a/apps/api/src/__tests__/middlewares/adminAuth.test.ts b/apps/api/src/__tests__/middlewares/adminAuth.test.ts new file mode 100644 index 0000000..1bc0393 --- /dev/null +++ b/apps/api/src/__tests__/middlewares/adminAuth.test.ts @@ -0,0 +1,55 @@ +import { createSignedAdminToken } from "../helpers/adminTokenTestUtils.js"; +import { describe, expect, it } from "vitest"; +import { createConfig } from "../../config.js"; +import { MiddlewareManager } from "../../middlewares/middleware.js"; +import { getMainRouter } from "../../routers/index.js"; + +const ADMIN_TOKEN_SIGNING_SECRET = "admin-test-secret"; +const ADMIN_TOKEN_AUDIENCE = "xtablo-admin"; + +describe("Admin Auth Middleware", () => { + process.env.ADMIN_TOKEN_SIGNING_SECRET = ADMIN_TOKEN_SIGNING_SECRET; + process.env.ADMIN_TOKEN_AUDIENCE = ADMIN_TOKEN_AUDIENCE; + process.env.ADMIN_APP_URL = "http://localhost:5176"; + + const config = createConfig(); + MiddlewareManager.initialize(config); + const app = getMainRouter(config); + + it("rejects admin routes without an admin session", async () => { + const res = await app.request("/admin/tables/profiles"); + + expect(res.status).toBe(401); + await expect(res.json()).resolves.toMatchObject({ + error: "Admin session required", + code: "ADMIN_SESSION_REQUIRED", + }); + }); + + it("returns the current admin session for a valid admin session token", async () => { + const sessionToken = createSignedAdminToken( + { + aud: ADMIN_TOKEN_AUDIENCE, + email: "ops@xtablo.com", + exp: Math.floor(Date.now() / 1000) + 900, + role: "operator", + sub: "operator-1", + type: "admin_session", + }, + ADMIN_TOKEN_SIGNING_SECRET + ); + + const res = await app.request("/admin/auth/session", { + headers: { + Authorization: `Bearer ${sessionToken}`, + }, + }); + + expect(res.status).toBe(200); + await expect(res.json()).resolves.toMatchObject({ + role: "operator", + operatorEmail: "ops@xtablo.com", + operatorId: "operator-1", + }); + }); +}); diff --git a/apps/api/src/__tests__/routes/adminAuth.test.ts b/apps/api/src/__tests__/routes/adminAuth.test.ts new file mode 100644 index 0000000..0ff3585 --- /dev/null +++ b/apps/api/src/__tests__/routes/adminAuth.test.ts @@ -0,0 +1,64 @@ +import { createSignedAdminToken } from "../helpers/adminTokenTestUtils.js"; +import { describe, expect, it } from "vitest"; +import { createConfig } from "../../config.js"; +import { MiddlewareManager } from "../../middlewares/middleware.js"; +import { getMainRouter } from "../../routers/index.js"; + +const ADMIN_TOKEN_SIGNING_SECRET = "admin-test-secret"; +const ADMIN_TOKEN_AUDIENCE = "xtablo-admin"; + +describe("Admin Auth Router", () => { + process.env.ADMIN_TOKEN_SIGNING_SECRET = ADMIN_TOKEN_SIGNING_SECRET; + process.env.ADMIN_TOKEN_AUDIENCE = ADMIN_TOKEN_AUDIENCE; + process.env.ADMIN_APP_URL = "http://localhost:5176"; + + const config = createConfig(); + MiddlewareManager.initialize(config); + const app = getMainRouter(config); + + it("rejects requests without a valid privileged token", async () => { + const res = await app.request("/admin/auth/exchange", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ accessToken: "bad-token" }), + }); + + expect(res.status).toBe(401); + await expect(res.json()).resolves.toMatchObject({ + error: "Invalid privileged access token", + code: "INVALID_ADMIN_ACCESS_TOKEN", + }); + }); + + it("exchanges a valid privileged token for an admin session", async () => { + const accessToken = createSignedAdminToken( + { + aud: ADMIN_TOKEN_AUDIENCE, + email: "ops@xtablo.com", + exp: Math.floor(Date.now() / 1000) + 3600, + role: "operator", + sub: "operator-1", + type: "admin_access", + }, + ADMIN_TOKEN_SIGNING_SECRET + ); + + const res = await app.request("/admin/auth/exchange", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ accessToken }), + }); + + expect(res.status).toBe(200); + await expect(res.json()).resolves.toMatchObject({ + role: "operator", + operatorEmail: "ops@xtablo.com", + sessionToken: expect.any(String), + expiresAt: expect.any(String), + }); + }); +}); diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts index 38f74f0..a6a4134 100644 --- a/apps/api/src/config.ts +++ b/apps/api/src/config.ts @@ -23,6 +23,9 @@ export interface AppConfig { R2_SECRET_ACCESS_KEY: string; LOG_LEVEL: "debug" | "info" | "warn" | "error"; TASKS_SECRET: string; + ADMIN_TOKEN_SIGNING_SECRET: string; + ADMIN_TOKEN_AUDIENCE: string; + ADMIN_APP_URL: string; /** * Test user @@ -85,10 +88,7 @@ export function createConfig(secrets?: Secrets): AppConfig { ? validateEnvVar("STRIPE_WEBHOOK_SECRET", process.env.STRIPE_WEBHOOK_SECRET) : getStripeWebhookSecretFromEnv() || getStripeWebhookSecret(isStagingMode), STRIPE_SOLO_PRICE_ID: validateEnvVar("STRIPE_SOLO_PRICE_ID", process.env.STRIPE_SOLO_PRICE_ID), - STRIPE_TEAM_PRICE_ID: validateEnvVar( - "STRIPE_TEAM_PRICE_ID", - process.env.STRIPE_TEAM_PRICE_ID - ), + STRIPE_TEAM_PRICE_ID: validateEnvVar("STRIPE_TEAM_PRICE_ID", process.env.STRIPE_TEAM_PRICE_ID), STRIPE_FOUNDER_PRICE_ID: validateEnvVar( "STRIPE_FOUNDER_PRICE_ID", process.env.STRIPE_FOUNDER_PRICE_ID @@ -110,6 +110,11 @@ export function createConfig(secrets?: Secrets): AppConfig { ? validateEnvVar("R2_SECRET_ACCESS_KEY", process.env.R2_SECRET_ACCESS_KEY) : secrets!.r2SecretAccessKey, TASKS_SECRET: process.env.TASKS_SECRET || "", + ADMIN_TOKEN_SIGNING_SECRET: isTestMode + ? validateEnvVar("ADMIN_TOKEN_SIGNING_SECRET", process.env.ADMIN_TOKEN_SIGNING_SECRET) + : secrets!.adminTokenSigningSecret, + ADMIN_TOKEN_AUDIENCE: process.env.ADMIN_TOKEN_AUDIENCE || "xtablo-admin", + ADMIN_APP_URL: process.env.ADMIN_APP_URL || "http://localhost:5176", LOG_LEVEL: "info", TEST_USER_DATA: { id: "test", diff --git a/apps/api/src/helpers/adminTokens.ts b/apps/api/src/helpers/adminTokens.ts new file mode 100644 index 0000000..b382e23 --- /dev/null +++ b/apps/api/src/helpers/adminTokens.ts @@ -0,0 +1,201 @@ +import { createHmac, timingSafeEqual } from "node:crypto"; +import type { AppConfig } from "../config.js"; + +export type AdminRole = "viewer" | "operator" | "superadmin"; + +type TokenKind = "admin_access" | "admin_session"; + +type AdminTokenClaims = { + aud: string; + email: string; + exp: number; + role: AdminRole; + sub: string; + type: TokenKind; +}; + +export type AdminSessionClaims = { + aud: string; + exp: number; + operatorEmail: string; + operatorId: string; + role: AdminRole; +}; + +type AdminTokenErrorCode = + | "ADMIN_SESSION_REQUIRED" + | "INVALID_ADMIN_ACCESS_TOKEN" + | "INVALID_ADMIN_SESSION"; + +type AdminTokenFailure = { + code: AdminTokenErrorCode; + error: string; + statusCode: 401; + success: false; +}; + +type AdminTokenSuccess = { + success: true; + value: T; +}; + +export type AdminTokenResult = AdminTokenFailure | AdminTokenSuccess; + +type ExchangeResult = { + expiresAt: string; + operatorEmail: string; + operatorId: string; + role: AdminRole; + sessionToken: string; +}; + +function encodeSegment(value: unknown) { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +function decodeSegment(segment: string): T | null { + try { + return JSON.parse(Buffer.from(segment, "base64url").toString("utf8")) as T; + } catch { + return null; + } +} + +function signToken(claims: AdminTokenClaims, secret: string) { + const header = encodeSegment({ alg: "HS256", typ: "JWT" }); + const payload = encodeSegment(claims); + const signature = createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url"); + + return `${header}.${payload}.${signature}`; +} + +function invalidToken(error: string, code: AdminTokenErrorCode): AdminTokenFailure { + return { + code, + error, + statusCode: 401, + success: false, + }; +} + +function isFailure(result: AdminTokenResult): result is AdminTokenFailure { + return !result.success; +} + +function verifyToken( + token: string, + config: AppConfig, + expectedType: TokenKind +): AdminTokenResult { + const segments = token.split("."); + if (segments.length !== 3) { + return invalidToken( + expectedType === "admin_access" ? "Invalid privileged access token" : "Invalid admin session", + expectedType === "admin_access" ? "INVALID_ADMIN_ACCESS_TOKEN" : "INVALID_ADMIN_SESSION" + ); + } + + const [header, payload, signature] = segments; + const expectedSignature = createHmac("sha256", config.ADMIN_TOKEN_SIGNING_SECRET) + .update(`${header}.${payload}`) + .digest(); + const receivedSignature = Buffer.from(signature, "base64url"); + + if ( + expectedSignature.length !== receivedSignature.length || + !timingSafeEqual(expectedSignature, receivedSignature) + ) { + return invalidToken( + expectedType === "admin_access" ? "Invalid privileged access token" : "Invalid admin session", + expectedType === "admin_access" ? "INVALID_ADMIN_ACCESS_TOKEN" : "INVALID_ADMIN_SESSION" + ); + } + + const claims = decodeSegment(payload); + if (!claims) { + return invalidToken( + expectedType === "admin_access" ? "Invalid privileged access token" : "Invalid admin session", + expectedType === "admin_access" ? "INVALID_ADMIN_ACCESS_TOKEN" : "INVALID_ADMIN_SESSION" + ); + } + + if (claims.type !== expectedType || claims.aud !== config.ADMIN_TOKEN_AUDIENCE) { + return invalidToken( + expectedType === "admin_access" ? "Invalid privileged access token" : "Invalid admin session", + expectedType === "admin_access" ? "INVALID_ADMIN_ACCESS_TOKEN" : "INVALID_ADMIN_SESSION" + ); + } + + if (claims.exp <= Math.floor(Date.now() / 1000)) { + return invalidToken( + expectedType === "admin_access" ? "Invalid privileged access token" : "Invalid admin session", + expectedType === "admin_access" ? "INVALID_ADMIN_ACCESS_TOKEN" : "INVALID_ADMIN_SESSION" + ); + } + + return { success: true, value: claims }; +} + +export function exchangePrivilegedToken( + token: string, + config: AppConfig +): AdminTokenResult { + const verifiedAccessToken = verifyToken(token, config, "admin_access"); + if (isFailure(verifiedAccessToken)) { + return verifiedAccessToken; + } + + const accessClaims = verifiedAccessToken.value; + const sessionExpiry = Math.floor(Date.now() / 1000) + 15 * 60; + const sessionToken = signToken( + { + aud: config.ADMIN_TOKEN_AUDIENCE, + email: accessClaims.email, + exp: sessionExpiry, + role: accessClaims.role, + sub: accessClaims.sub, + type: "admin_session", + }, + config.ADMIN_TOKEN_SIGNING_SECRET + ); + + return { + success: true, + value: { + expiresAt: new Date(sessionExpiry * 1000).toISOString(), + operatorEmail: accessClaims.email, + operatorId: accessClaims.sub, + role: accessClaims.role, + sessionToken, + }, + }; +} + +export function verifyAdminSession( + token: string | undefined, + config: AppConfig +): AdminTokenResult { + if (!token) { + return invalidToken("Admin session required", "ADMIN_SESSION_REQUIRED"); + } + + const verifiedSession = verifyToken(token, config, "admin_session"); + if (isFailure(verifiedSession)) { + return { + ...verifiedSession, + code: "ADMIN_SESSION_REQUIRED", + error: "Admin session required", + }; + } + + return { + success: true, + value: { + aud: verifiedSession.value.aud, + exp: verifiedSession.value.exp, + operatorEmail: verifiedSession.value.email, + operatorId: verifiedSession.value.sub, + role: verifiedSession.value.role, + }, + }; +} diff --git a/apps/api/src/middlewares/middleware.ts b/apps/api/src/middlewares/middleware.ts index 7c153e6..b53c15b 100644 --- a/apps/api/src/middlewares/middleware.ts +++ b/apps/api/src/middlewares/middleware.ts @@ -6,6 +6,7 @@ import { createMiddleware } from "hono/factory"; import type { Transporter } from "nodemailer"; import { Stripe } from "stripe"; import { type AppConfig } from "../config.js"; +import { type AdminTokenResult, verifyAdminSession } from "../helpers/adminTokens.js"; import { authenticateFromHeader } from "../helpers/auth.js"; import { createStripeSync } from "./stripeSync.js"; import { createTransporter } from "./transporter.js"; @@ -24,6 +25,9 @@ export type Middlewares = { Variables: { supabase: SupabaseClient; user: User }; Bindings: { user: User }; }>; + adminAuthMiddleware: MiddlewareHandler<{ + Variables: { adminSession: import("../helpers/adminTokens.js").AdminSessionClaims }; + }>; r2Middleware: MiddlewareHandler<{ Variables: { s3_client: S3Client }; }>; @@ -74,6 +78,10 @@ export class MiddlewareManager { } private initializeMiddlewares(config: AppConfig): Middlewares { + const isAdminTokenFailure = ( + result: AdminTokenResult + ): result is Extract, { success: false }> => !result.success; + const createProfileAccessMiddleware = (allowTemporaryUsers: boolean) => createMiddleware<{ Variables: { supabase: SupabaseClient; user: User }; @@ -141,6 +149,38 @@ export class MiddlewareManager { await next(); }); + const adminAuthMiddleware = createMiddleware<{ + Variables: { adminSession: import("../helpers/adminTokens.js").AdminSessionClaims }; + }>(async (c, next) => { + const authHeader = c.req.header("Authorization"); + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return c.json( + { + code: "ADMIN_SESSION_REQUIRED", + error: "Admin session required", + }, + 401 + ); + } + + const sessionToken = authHeader.substring(7); + const verifiedSession = verifyAdminSession(sessionToken, config); + + if (isAdminTokenFailure(verifiedSession)) { + return c.json( + { + code: verifiedSession.code, + error: verifiedSession.error, + }, + verifiedSession.statusCode + ); + } + + c.set("adminSession", verifiedSession.value); + await next(); + }); + const maybeAuthenticatedMiddleware = createMiddleware<{ Variables: { supabase: SupabaseClient; user: User | null }; }>(async (c, next) => { @@ -241,6 +281,7 @@ export class MiddlewareManager { supabaseMiddleware, basicAuthMiddleware, authMiddleware, + adminAuthMiddleware, maybeAuthenticatedMiddleware, r2Middleware, regularUserCheckMiddleware, @@ -264,6 +305,10 @@ export class MiddlewareManager { return this.middlewares.authMiddleware; } + get adminAuth() { + return this.middlewares.adminAuthMiddleware; + } + get maybeAuthenticated() { return this.middlewares.maybeAuthenticatedMiddleware; } diff --git a/apps/api/src/routers/admin.ts b/apps/api/src/routers/admin.ts new file mode 100644 index 0000000..1eb8f8b --- /dev/null +++ b/apps/api/src/routers/admin.ts @@ -0,0 +1,18 @@ +import { Hono } from "hono"; +import type { AppConfig } from "../config.js"; +import { MiddlewareManager } from "../middlewares/middleware.js"; +import type { BaseEnv } from "../types/app.types.js"; +import { getAdminAuthRouter } from "./adminAuth.js"; +import { getAdminTablesRouter } from "./adminTables.js"; + +export const getAdminRouter = (config: AppConfig) => { + const adminRouter = new Hono(); + const middlewareManager = MiddlewareManager.getInstance(); + + adminRouter.route("/auth", getAdminAuthRouter(config)); + + adminRouter.use("/tables/*", middlewareManager.adminAuth); + adminRouter.route("/tables", getAdminTablesRouter()); + + return adminRouter; +}; diff --git a/apps/api/src/routers/adminAuth.ts b/apps/api/src/routers/adminAuth.ts new file mode 100644 index 0000000..065d284 --- /dev/null +++ b/apps/api/src/routers/adminAuth.ts @@ -0,0 +1,55 @@ +import { Hono } from "hono"; +import type { AppConfig } from "../config.js"; +import { type AdminTokenResult, exchangePrivilegedToken } from "../helpers/adminTokens.js"; +import { MiddlewareManager } from "../middlewares/middleware.js"; +import type { BaseEnv } from "../types/app.types.js"; + +export const getAdminAuthRouter = (config: AppConfig) => { + const adminAuthRouter = new Hono(); + const middlewareManager = MiddlewareManager.getInstance(); + const isAdminTokenFailure = ( + result: AdminTokenResult + ): result is Extract, { success: false }> => !result.success; + + adminAuthRouter.post("/exchange", async (c) => { + const body = await c.req.json().catch(() => null); + const accessToken = + body && typeof body === "object" && "accessToken" in body ? body.accessToken : undefined; + + if (typeof accessToken !== "string" || accessToken.length === 0) { + return c.json( + { + code: "INVALID_ADMIN_ACCESS_TOKEN", + error: "Invalid privileged access token", + }, + 401 + ); + } + + const exchangeResult = exchangePrivilegedToken(accessToken, config); + + if (isAdminTokenFailure(exchangeResult)) { + return c.json( + { + code: exchangeResult.code, + error: exchangeResult.error, + }, + exchangeResult.statusCode + ); + } + + return c.json(exchangeResult.value, 200); + }); + + adminAuthRouter.use("/session", middlewareManager.adminAuth); + + adminAuthRouter.get("/session", async (c) => { + const adminSession = c.get("adminSession"); + + return c.json(adminSession, 200); + }); + + adminAuthRouter.post("/logout", async (c) => c.json({ success: true }, 200)); + + return adminAuthRouter; +}; diff --git a/apps/api/src/routers/adminTables.ts b/apps/api/src/routers/adminTables.ts new file mode 100644 index 0000000..29709f0 --- /dev/null +++ b/apps/api/src/routers/adminTables.ts @@ -0,0 +1,19 @@ +import { Hono } from "hono"; +import type { BaseEnv } from "../types/app.types.js"; + +export const getAdminTablesRouter = () => { + const adminTablesRouter = new Hono(); + + adminTablesRouter.get("/:tableId", async (c) => { + const tableId = c.req.param("tableId"); + + return c.json( + { + error: `Admin table '${tableId}' is not implemented yet`, + }, + 501 + ); + }); + + return adminTablesRouter; +}; diff --git a/apps/api/src/routers/index.ts b/apps/api/src/routers/index.ts index 1ca996e..93ccd34 100644 --- a/apps/api/src/routers/index.ts +++ b/apps/api/src/routers/index.ts @@ -1,6 +1,7 @@ import { Hono } from "hono"; import type { AppConfig } from "../config.js"; import { MiddlewareManager } from "../middlewares/middleware.js"; +import { getAdminRouter } from "./admin.js"; import type { BaseEnv } from "../types/app.types.js"; import { getAuthenticatedRouter } from "./authRouter.js"; import { getMaybeAuthenticatedRouter } from "./maybeAuthRouter.js"; @@ -31,6 +32,9 @@ export const getMainRouter = (config: AppConfig) => { // webhooks mainRouter.route("/stripe-webhook", getStripeWebhookRouter()); + // admin routes + mainRouter.route("/admin", getAdminRouter(config)); + // maybe authenticated routes (checked first to allow unauthenticated booking) mainRouter.route("/", getMaybeAuthenticatedRouter()); diff --git a/apps/api/src/secrets.ts b/apps/api/src/secrets.ts index 2126133..f3e917d 100644 --- a/apps/api/src/secrets.ts +++ b/apps/api/src/secrets.ts @@ -21,6 +21,7 @@ export type Secrets = { supabaseServiceRoleKey: string; supabaseConnectionString: string; supabaseCaCert: string; + adminTokenSigningSecret: string; emailClientSecret: string; emailRefreshToken: string; r2AccessKeyId: string; @@ -42,6 +43,7 @@ export async function loadSecrets(): Promise { supabaseServiceRoleKey: await fetchSecret("supabase-service-role-key"), supabaseConnectionString: await fetchSecret("supabase-connection-string"), supabaseCaCert: await fetchSecret("supabase-ca-cert"), + adminTokenSigningSecret: await fetchSecret("admin-token-signing-secret"), emailClientSecret: await fetchSecret("email-client-secret"), emailRefreshToken: await fetchSecret("email-refresh-token"), r2AccessKeyId: await fetchSecret("r2-access-key-id"), diff --git a/apps/api/src/types/app.types.ts b/apps/api/src/types/app.types.ts index 1f14da7..df9b18a 100644 --- a/apps/api/src/types/app.types.ts +++ b/apps/api/src/types/app.types.ts @@ -1,4 +1,5 @@ import type { S3Client } from "@aws-sdk/client-s3"; +import type { AdminSessionClaims } from "../helpers/adminTokens.js"; import type { StripeSync } from "@supabase/stripe-sync-engine"; import type { SupabaseClient, User } from "@supabase/supabase-js"; import type { Hono } from "hono"; @@ -10,6 +11,7 @@ import type Stripe from "stripe"; */ export type BaseEnv = { Variables: { + adminSession: AdminSessionClaims; supabase: SupabaseClient; s3_client: S3Client; transporter: Transporter; diff --git a/packages/shared-types/src/admin.types.ts b/packages/shared-types/src/admin.types.ts new file mode 100644 index 0000000..1a8859b --- /dev/null +++ b/packages/shared-types/src/admin.types.ts @@ -0,0 +1,17 @@ +export type AdminRole = "viewer" | "operator" | "superadmin"; + +export type AdminSessionResponse = { + expiresAt: string; + operatorEmail: string; + operatorId: string; + role: AdminRole; + sessionToken: string; +}; + +export type AdminSessionInfo = { + aud: string; + exp: number; + operatorEmail: string; + operatorId: string; + role: AdminRole; +}; diff --git a/packages/shared-types/src/index.ts b/packages/shared-types/src/index.ts index fd57e3a..78db254 100644 --- a/packages/shared-types/src/index.ts +++ b/packages/shared-types/src/index.ts @@ -1,4 +1,8 @@ // ============================================================================ +// Admin Types +// ============================================================================ +export type { AdminRole, AdminSessionInfo, AdminSessionResponse } from "./admin.types.js"; +// ============================================================================ // Database Types // ============================================================================ export type { Database, Json } from "./database.types.js";