feat(auth): add enrollment check, role-based sessions, and player access
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,12 +6,16 @@
|
||||
import type { RouteContext, RouteModule } from "./types";
|
||||
import { jsonResponse, errorResponse } from "./utils";
|
||||
import { logger } from "@shared/lib/logger";
|
||||
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
||||
import { users } from "@shared/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
// In-memory session store: token → { discordId, username, avatar, expiresAt }
|
||||
// In-memory session store: token → { discordId, username, avatar, role, expiresAt }
|
||||
export interface Session {
|
||||
discordId: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
role: "admin" | "player";
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
@@ -144,26 +148,34 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
|
||||
|
||||
const user = await userRes.json() as { id: string; username: string; avatar: string | null };
|
||||
|
||||
// Check allowlist
|
||||
const adminIds = getAdminIds();
|
||||
if (adminIds.length > 0 && !adminIds.includes(user.id)) {
|
||||
logger.warn("auth", `Unauthorized login attempt by ${user.username} (${user.id})`);
|
||||
// Check enrollment — user must exist in the users table
|
||||
const dbUser = await DrizzleClient.query.users.findFirst({
|
||||
where: eq(users.id, BigInt(user.id)),
|
||||
});
|
||||
|
||||
if (!dbUser) {
|
||||
logger.info("auth", `Non-enrolled login attempt by ${user.username} (${user.id})`);
|
||||
return new Response(
|
||||
`<html><body><h1>Access Denied</h1><p>Your Discord account is not authorized.</p></body></html>`,
|
||||
`<html><body><h1>Not Enrolled</h1><p>You need to use the Aurora bot in Discord before you can access this panel.</p><a href="/">Go back</a></body></html>`,
|
||||
{ status: 403, headers: { "Content-Type": "text/html" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Determine role
|
||||
const adminIds = getAdminIds();
|
||||
const role: "admin" | "player" = adminIds.includes(user.id) ? "admin" : "player";
|
||||
|
||||
// Create session
|
||||
const token = generateToken();
|
||||
sessions.set(token, {
|
||||
discordId: user.id,
|
||||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
role,
|
||||
expiresAt: Date.now() + SESSION_MAX_AGE,
|
||||
});
|
||||
|
||||
logger.info("auth", `Admin login: ${user.username} (${user.id})`);
|
||||
logger.info("auth", `Login: ${user.username} (${user.id}) as ${role}`);
|
||||
|
||||
// Get return_to URL from redirect token cookie
|
||||
const cookies = parseCookies(ctx.req.headers.get("cookie"));
|
||||
@@ -213,13 +225,15 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
|
||||
// GET /auth/me — return current session info
|
||||
if (pathname === "/auth/me" && method === "GET") {
|
||||
const session = getSession(ctx.req);
|
||||
if (!session) return jsonResponse({ authenticated: false }, 401);
|
||||
if (!session) return jsonResponse({ authenticated: false, enrolled: false });
|
||||
return jsonResponse({
|
||||
authenticated: true,
|
||||
enrolled: true,
|
||||
user: {
|
||||
discordId: session.discordId,
|
||||
username: session.username,
|
||||
avatar: session.avatar,
|
||||
role: session.role,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { RouteContext, RouteModule } from "./types";
|
||||
import { authRoutes, isAuthenticated } from "./auth.routes";
|
||||
import { authRoutes, isAuthenticated, getSession } from "./auth.routes";
|
||||
import { healthRoutes } from "./health.routes";
|
||||
import { statsRoutes } from "./stats.routes";
|
||||
import { actionsRoutes } from "./actions.routes";
|
||||
@@ -70,9 +70,21 @@ export async function handleRequest(req: Request, url: URL): Promise<Response |
|
||||
|
||||
// For API routes, enforce authentication
|
||||
if (ctx.pathname.startsWith("/api/")) {
|
||||
if (!isAuthenticated(req)) {
|
||||
const session = getSession(req);
|
||||
if (!session) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
// Admin-only routes: everything except stats and own user data
|
||||
const playerAllowedPrefixes = ["/api/stats", "/api/health"];
|
||||
const isPlayerAllowed = playerAllowedPrefixes.some(p => ctx.pathname.startsWith(p));
|
||||
|
||||
// Players can access their own user data
|
||||
const isOwnUserRoute = ctx.pathname.match(/^\/api\/users\/\d+/) && session.role === "player";
|
||||
|
||||
if (session.role === "player" && !isPlayerAllowed && !isOwnUserRoute) {
|
||||
return errorResponse("Admin access required", 403);
|
||||
}
|
||||
}
|
||||
|
||||
// Try protected routes
|
||||
|
||||
Reference in New Issue
Block a user