(Feat): Initial Commit
This commit is contained in:
102
backend-deno/middleware/auth.ts
Normal file
102
backend-deno/middleware/auth.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Context, Next } from "@oak/oak";
|
||||
import { verify, create, getNumericDate } 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;
|
||||
}
|
||||
Reference in New Issue
Block a user