import { Elysia, t } from "elysia"; import { jwt } from "@elysiajs/jwt"; import { bearer } from "@elysiajs/bearer"; import { authConfig } from "../config/auth"; import type { JWTPayload, AuthResponse, ApiScope } from "../types/auth"; import { authenticateUser, createUser, findUserById, } from "../services/user.service"; import { createApiToken, validateApiToken, listUserApiTokens, revokeApiToken, hasScope, } from "../services/token.service"; // Request/Response schemas const loginSchema = t.Object({ email: t.String({ format: "email", description: "User's email address" }), password: t.String({ minLength: 8, description: "User's password (min 8 characters)" }), }); const registerSchema = t.Object({ email: t.String({ format: "email", description: "User's email address" }), password: t.String({ minLength: 8, description: "Password (min 8 characters)" }), name: t.Optional(t.String({ description: "User's display name" })), }); const createTokenSchema = t.Object({ name: t.String({ minLength: 1, maxLength: 100, description: "Token name for identification" }), scopes: t.Optional( t.Array(t.Union([t.Literal("read"), t.Literal("write"), t.Literal("delete")]), { description: "Token permissions", }) ), expiresInDays: t.Optional(t.Number({ minimum: 1, maximum: 365, description: "Token expiration in days" })), }); const authResponseSchema = t.Object({ success: t.Boolean(), message: t.String(), user: t.Optional( t.Object({ id: t.String(), email: t.String(), role: t.String(), }) ), accessToken: t.Optional(t.String()), refreshToken: t.Optional(t.String()), }); const errorResponseSchema = t.Object({ success: t.Boolean(), error: t.String(), }); export const auth = new Elysia({ prefix: "/auth" }) .use( jwt({ name: "jwt", secret: authConfig.jwt.secret, exp: authConfig.jwt.expiry, }) ) .use(bearer()) // Registration .post( "/register", async ({ jwt, body, cookie: { accessToken, refreshToken } }): Promise => { const result = await createUser(body); if ("error" in result && result.error) { return { success: false, message: result.error, }; } const user = result.user!; // Generate tokens const accessTokenValue = await jwt.sign({ sub: user.id, email: user.email, role: user.role, type: "access", } satisfies JWTPayload); const refreshTokenValue = await jwt.sign({ sub: user.id, email: user.email, role: user.role, type: "refresh", } satisfies JWTPayload); // Set cookies accessToken.set({ value: accessTokenValue, ...authConfig.cookie, }); refreshToken.set({ value: refreshTokenValue, ...authConfig.cookie, maxAge: 30 * 24 * 60 * 60, // 30 days for refresh token }); return { success: true, message: "Registration successful", user: { id: user.id, email: user.email, role: user.role, }, accessToken: accessTokenValue, refreshToken: refreshTokenValue, }; }, { body: registerSchema, response: { 200: authResponseSchema, }, detail: { summary: "Register", description: "Create a new user account", tags: ["Authentication"], }, } ) // Login .post( "/login", async ({ jwt, body, cookie: { accessToken, refreshToken } }): Promise => { const result = await authenticateUser(body.email, body.password); if ("error" in result && result.error) { return { success: false, message: result.error, }; } const user = result.user!; // Generate tokens const accessTokenValue = await jwt.sign({ sub: user.id, email: user.email, role: user.role, type: "access", } satisfies JWTPayload); const refreshTokenValue = await jwt.sign({ sub: user.id, email: user.email, role: user.role, type: "refresh", } satisfies JWTPayload); // Set cookies accessToken.set({ value: accessTokenValue, ...authConfig.cookie, }); refreshToken.set({ value: refreshTokenValue, ...authConfig.cookie, maxAge: 30 * 24 * 60 * 60, // 30 days for refresh token }); return { success: true, message: "Login successful", user: { id: user.id, email: user.email, role: user.role, }, accessToken: accessTokenValue, refreshToken: refreshTokenValue, }; }, { body: loginSchema, response: { 200: authResponseSchema, }, detail: { summary: "Login", description: "Authenticate with email and password to receive access and refresh tokens", tags: ["Authentication"], }, } ) // Logout .post( "/logout", ({ cookie: { accessToken, refreshToken } }) => { accessToken.remove(); refreshToken.remove(); return { success: true, message: "Logged out successfully", }; }, { detail: { summary: "Logout", description: "Clear authentication cookies", tags: ["Authentication"], }, } ) // Refresh token .post( "/refresh", async ({ jwt, cookie: { accessToken, refreshToken }, set }) => { const token = refreshToken.value; if (!token || typeof token !== "string") { set.status = 401; return { success: false, error: "Refresh token required", }; } const payload = (await jwt.verify(token)) as JWTPayload | false; if (!payload || payload.type !== "refresh") { set.status = 401; return { success: false, error: "Invalid refresh token", }; } const user = await findUserById(payload.sub); if (!user) { set.status = 401; return { success: false, error: "User not found", }; } const newAccessToken = await jwt.sign({ sub: user.id, email: user.email, role: user.role, type: "access", } satisfies JWTPayload); accessToken.set({ value: newAccessToken, ...authConfig.cookie, }); return { success: true, message: "Token refreshed", accessToken: newAccessToken, }; }, { detail: { summary: "Refresh Token", description: "Exchange a valid refresh token for a new access token", tags: ["Authentication"], }, } ) // Get current user .get( "/me", async ({ jwt, cookie: { accessToken }, bearer, set }) => { const cookieToken = typeof accessToken.value === "string" ? accessToken.value : null; const bearerToken = bearer && bearer.length > 0 ? bearer : null; const token = cookieToken || bearerToken; if (!token) { set.status = 401; return { success: false, error: "Authentication required", }; } const payload = (await jwt.verify(token)) as JWTPayload | false; if (!payload) { set.status = 401; return { success: false, error: "Invalid or expired token", }; } const user = await findUserById(payload.sub); if (!user) { set.status = 401; return { success: false, error: "User not found", }; } return { success: true, user: { id: user.id, email: user.email, role: user.role, }, }; }, { detail: { summary: "Get Current User", description: "Get authenticated user profile", tags: ["Authentication"], }, } ) // Create API token (requires JWT auth - API tokens cannot create other tokens) .post( "/tokens", async ({ jwt, cookie: { accessToken }, bearer, body, set }) => { const cookieToken = typeof accessToken.value === "string" && accessToken.value.length > 0 ? accessToken.value : null; const bearerToken = typeof bearer === "string" && bearer.length > 0 ? bearer : null; const token = cookieToken || bearerToken; if (!token) { set.status = 401; return { success: false, error: "Authentication required", }; } const payload = (await jwt.verify(token)) as JWTPayload | false; if (!payload) { set.status = 401; return { success: false, error: "Invalid or expired token", }; } // Verify user still exists const user = await findUserById(payload.sub); if (!user) { set.status = 401; return { success: false, error: "User not found", }; } const result = await createApiToken( user.id, body.name, (body.scopes as ApiScope[]) || ["read"], body.expiresInDays ); return { success: true, message: "API token created - save it now, it won't be shown again", token: result.plainToken, tokenId: result.token.id, name: result.token.name, scopes: result.token.scopes, expiresAt: result.token.expiresAt, }; }, { body: createTokenSchema, detail: { summary: "Create API Token", description: "Generate a new API token", tags: ["API Tokens"], }, } ) // List API tokens (requires JWT auth) .get( "/tokens", async ({ jwt, cookie: { accessToken }, bearer, set }) => { const cookieToken = typeof accessToken.value === "string" && accessToken.value.length > 0 ? accessToken.value : null; const bearerToken = typeof bearer === "string" && bearer.length > 0 ? bearer : null; const token = cookieToken || bearerToken; if (!token) { set.status = 401; return { success: false, error: "Authentication required", }; } const payload = (await jwt.verify(token)) as JWTPayload | false; if (!payload) { set.status = 401; return { success: false, error: "Invalid or expired token", }; } const user = await findUserById(payload.sub); if (!user) { set.status = 401; return { success: false, error: "User not found", }; } const tokens = await listUserApiTokens(user.id); return { success: true, tokens, }; }, { detail: { summary: "List API Tokens", description: "List all API tokens for the user", tags: ["API Tokens"], }, } ) // Revoke API token (requires JWT auth) .delete( "/tokens/:id", async ({ jwt, cookie: { accessToken }, bearer, params, set }) => { const cookieToken = typeof accessToken.value === "string" && accessToken.value.length > 0 ? accessToken.value : null; const bearerToken = typeof bearer === "string" && bearer.length > 0 ? bearer : null; const token = cookieToken || bearerToken; if (!token) { set.status = 401; return { success: false, error: "Authentication required", }; } const payload = (await jwt.verify(token)) as JWTPayload | false; if (!payload) { set.status = 401; return { success: false, error: "Invalid or expired token", }; } const user = await findUserById(payload.sub); if (!user) { set.status = 401; return { success: false, error: "User not found", }; } const revoked = await revokeApiToken(params.id, user.id); if (!revoked) { set.status = 404; return { success: false, error: "Token not found", }; } return { success: true, message: "Token revoked successfully", }; }, { params: t.Object({ id: t.String({ description: "Token ID to revoke" }), }), detail: { summary: "Revoke API Token", description: "Revoke an API token", tags: ["API Tokens"], }, } ) // Verify API token .get( "/api/verify", async ({ bearer, set }) => { if (!bearer) { set.status = 400; set.headers["WWW-Authenticate"] = `Bearer realm='api', error="invalid_request"`; return { success: false, error: "Bearer token required", }; } const validation = await validateApiToken(bearer); if (!validation.valid || !validation.token) { set.status = 401; set.headers["WWW-Authenticate"] = `Bearer realm='api', error="invalid_token"`; return { success: false, error: validation.error || "Invalid token", }; } return { success: true, message: "API token is valid", tokenId: validation.token.id, scopes: validation.token.scopes, expiresAt: validation.token.expiresAt, }; }, { detail: { summary: "Verify API Token", description: "Validate an API token and return its metadata", tags: ["API Tokens"], }, } ); // Helper to validate JWT from request context export async function validateJWT( jwt: { verify: (token: string) => Promise }, accessTokenCookie: { value: unknown }, bearer: string | undefined ): Promise<{ user: JWTPayload; error?: never } | { user?: never; error: string }> { const token = (typeof accessTokenCookie.value === "string" && accessTokenCookie.value) || bearer; if (!token) { return { error: "Authentication required" }; } const payload = await jwt.verify(token); if (!payload) { return { error: "Invalid or expired token" }; } return { user: payload as JWTPayload }; } // Helper to validate API token with scope check export async function validateApiTokenWithScope( bearerToken: string | undefined, requiredScope: ApiScope ): Promise<{ valid: true; userId: string } | { valid: false; error: string }> { if (!bearerToken) { return { valid: false, error: "Bearer token required" }; } const validation = await validateApiToken(bearerToken); if (!validation.valid || !validation.token) { return { valid: false, error: validation.error || "Invalid token" }; } if (!hasScope(validation.token, requiredScope)) { return { valid: false, error: `Token missing required scope: ${requiredScope}` }; } return { valid: true, userId: validation.token.userId }; }