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
This commit is contained in:
+100
@@ -0,0 +1,100 @@
|
||||
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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user