108 lines
2.6 KiB
TypeScript
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;
|
|
}
|