Files

108 lines
2.6 KiB
TypeScript

import { Context, Next } from "@oak/oak";
import { create, getNumericDate, verify } from "djwt";
import { config } from "../config/env.ts";
import type { JWTPayload, UserRole } from "../types/index.ts";
// Create crypto key from secret
const encoder = new TextEncoder();
const keyData = encoder.encode(config.JWT_SECRET);
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"],
);
// Generate JWT token
export async function generateToken(
payload: Omit<JWTPayload, "exp" | "iat">,
): Promise<string> {
const expiresIn = config.JWT_EXPIRES_IN;
let expSeconds = 7 * 24 * 60 * 60; // Default 7 days
if (expiresIn.endsWith("d")) {
expSeconds = parseInt(expiresIn) * 24 * 60 * 60;
} else if (expiresIn.endsWith("h")) {
expSeconds = parseInt(expiresIn) * 60 * 60;
} else if (expiresIn.endsWith("m")) {
expSeconds = parseInt(expiresIn) * 60;
}
const token = await create(
{ alg: "HS256", typ: "JWT" },
{
...payload,
exp: getNumericDate(expSeconds),
iat: getNumericDate(0),
},
cryptoKey,
);
return token;
}
// Verify JWT token
export async function verifyToken(token: string): Promise<JWTPayload | null> {
try {
const payload = await verify(token, cryptoKey);
return payload as unknown as JWTPayload;
} catch {
return null;
}
}
// Authentication middleware
export async function authenticateToken(
ctx: Context,
next: Next,
): Promise<void> {
const authHeader = ctx.request.headers.get("Authorization");
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
if (!token) {
ctx.response.status = 401;
ctx.response.body = { error: "Access token required" };
return;
}
const payload = await verifyToken(token);
if (!payload) {
ctx.response.status = 403;
ctx.response.body = { error: "Invalid or expired token" };
return;
}
// Attach user to context state
ctx.state.user = payload;
await next();
}
// Authorization middleware factory
export function authorize(...roles: UserRole[]) {
return async (ctx: Context, next: Next): Promise<void> => {
const user = ctx.state.user as JWTPayload | undefined;
if (!user) {
ctx.response.status = 401;
ctx.response.body = { error: "Unauthorized" };
return;
}
if (!roles.includes(user.role)) {
ctx.response.status = 403;
ctx.response.body = { error: "Insufficient permissions" };
return;
}
await next();
};
}
// Get current user from context
export function getCurrentUser(ctx: Context): JWTPayload {
return ctx.state.user as JWTPayload;
}