Files
itinova-budget-si/server/_core/sdk.ts

99 lines
2.9 KiB
TypeScript

/**
* SDK Server — Auth locale Itinova
* Remplace le flow Manus OAuth par une authentification locale email/password + JWT.
* Le cookie de session contient un JWT signé avec JWT_SECRET.
*/
import { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
import { ForbiddenError } from "@shared/_core/errors";
import { parse as parseCookieHeader } from "cookie";
import type { Request } from "express";
import { SignJWT, jwtVerify } from "jose";
import type { User } from "../../drizzle/schema";
import * as db from "../db";
import { ENV } from "./env";
const isNonEmptyString = (value: unknown): value is string =>
typeof value === "string" && value.length > 0;
export type SessionPayload = {
userId: number;
login: string;
role: string;
};
/** Result of `sdk.authenticateRequest`. */
export type AuthenticatedUser = User & {
taskUid?: string;
isCron?: boolean;
};
class SDKServer {
private getSessionSecret() {
const secret = ENV.cookieSecret;
return new TextEncoder().encode(secret);
}
async createSessionToken(
userId: number,
login: string,
role: string,
options: { expiresInMs?: number } = {}
): Promise<string> {
const issuedAt = Date.now();
const expiresInMs = options.expiresInMs ?? ONE_YEAR_MS;
const expirationSeconds = Math.floor((issuedAt + expiresInMs) / 1000);
const secretKey = this.getSessionSecret();
return new SignJWT({ userId, login, role })
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setExpirationTime(expirationSeconds)
.sign(secretKey);
}
async verifySession(
cookieValue: string | undefined | null
): Promise<SessionPayload | null> {
if (!cookieValue) {
return null;
}
try {
const secretKey = this.getSessionSecret();
const { payload } = await jwtVerify(cookieValue, secretKey, {
algorithms: ["HS256"],
});
const { userId, login, role } = payload as Record<string, unknown>;
if (!userId || !isNonEmptyString(login) || !isNonEmptyString(role)) {
return null;
}
return { userId: userId as number, login, role };
} catch {
return null;
}
}
private parseCookies(cookieHeader: string | undefined) {
if (!cookieHeader) return new Map<string, string>();
const parsed = parseCookieHeader(cookieHeader);
return new Map(Object.entries(parsed));
}
async authenticateRequest(req: Request): Promise<AuthenticatedUser> {
const cookies = this.parseCookies(req.headers.cookie);
const sessionCookie = cookies.get(COOKIE_NAME);
const session = await this.verifySession(sessionCookie);
if (!session) {
throw ForbiddenError("Invalid session cookie");
}
const user = await db.getUserById(session.userId);
if (!user || !user.isActive) {
throw ForbiddenError("User not found or inactive");
}
return user as AuthenticatedUser;
}
}
export const sdk = new SDKServer();