Files
waseel/lib/jwt.ts
T
Krikorios a0b297285a Add self-hosted auth, admin API, and owner web dashboard
- Replace Clerk with self-hosted JWT auth (register/login/verify, bcrypt
  passwords, Gmail OTP with console fallback)
- Add lib/db.ts pg pool + transaction helpers; seed script migrates legacy
  Clerk-era schema (drop clerk_id, enforce UUID ids and unique email)
- Add owner-gated admin API: stats, users, drivers CRUD, rides
- Add dashboard/ Vite React owner dashboard (login, overview, users,
  fleet, rides) with dev-server proxy to avoid Expo CORS middleware
- Add scripts/set-owner.mjs for role management
2026-08-23 16:38:41 +03:00

101 lines
2.5 KiB
TypeScript

import { createHmac, timingSafeEqual } from "crypto";
const base64Url = (input: Buffer | string): string =>
Buffer.from(input)
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
const fromBase64Url = (input: string): Buffer =>
Buffer.from(input.replace(/-/g, "+").replace(/_/g, "/"), "base64");
export type JwtPayload = {
sub: string;
email: string;
iat: number;
exp: number;
};
const secret = (): string => {
const value = process.env.AUTH_JWT_SECRET;
if (!value) throw new Error("Missing AUTH_JWT_SECRET.");
return value;
};
export const signJwt = (
payload: { sub: string; email: string },
expiresInSeconds = 30 * 24 * 60 * 60,
): string => {
const iat = Math.floor(Date.now() / 1000);
const body: JwtPayload = { ...payload, iat, exp: iat + expiresInSeconds };
const header = base64Url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
const claims = base64Url(JSON.stringify(body));
const signature = base64Url(
createHmac("sha256", secret()).update(`${header}.${claims}`).digest(),
);
return `${header}.${claims}.${signature}`;
};
export const verifyJwt = (token: string): JwtPayload | null => {
const parts = token.split(".");
if (parts.length !== 3) return null;
const [header, claims, signature] = parts;
const expected = createHmac("sha256", secret())
.update(`${header}.${claims}`)
.digest();
const received = fromBase64Url(signature);
if (
expected.length !== received.length ||
!timingSafeEqual(expected, received)
) {
return null;
}
try {
const payload = JSON.parse(fromBase64Url(claims).toString()) as JwtPayload;
if (payload.exp * 1000 < Date.now()) return null;
return payload;
} catch {
return null;
}
};
// Returns the authenticated principal or a ready-to-return error Response.
export const requireAuth = (
req: Request,
): { userId: string; email: string } | { error: Response } => {
const header = req.headers.get("authorization") ?? "";
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
const payload = token ? verifyJwt(token) : null;
if (!payload) {
return {
error: Response.json({ error: "Unauthorized." }, { status: 401 }),
};
}
return { userId: payload.sub, email: payload.email };
};
export const decodeJwtExp = (token: string): number | null => {
try {
const claims = JSON.parse(
fromBase64Url(token.split(".")[1]).toString(),
) as JwtPayload;
return claims.exp ?? null;
} catch {
return null;
}
};