import { Elysia, t } from "elysia"; import config from "@/../config.toml"; import { jwt } from '@elysiajs/jwt'; import { AuditLog } from "@/models/audit"; import { USER_ROLES, User } from "@/models/user"; import { createAuditLog } from "@/lib/audit"; import { ulid } from "ulid"; const hardcodedOwners = new Set( Array.isArray(config.auth.hardcoded_owner) ? (config.auth.hardcoded_owner as Array).map((id: string | number) => String(id)) : [], ); export default new Elysia({ prefix: "/auth" }) .use( jwt({ name: 'jwt', secret: config.auth.jwt_secret, }) ) .get("/me", async ({ jwt, cookie: { mizuki }, status }) => { const rawToken = mizuki.value; if (typeof rawToken !== "string" || rawToken.length === 0) { return status(401, "Unauthorized"); } const payload = await jwt.verify(rawToken); if (!payload || typeof payload !== "object" || !("id" in payload) || typeof payload.id !== "string") { return status(401, "Unauthorized"); } const user = await User.findOne({ userId: payload.id }); if (!user) { return status(404, "User not found"); } return { id: user.userId, discordId: user.discordId, username: user.username, avatar: user.avatar, role: user.role, }; }) .get("/discord/login", ({ redirect }) => redirect(`https://discord.com/oauth2/authorize?client_id=${config.auth.discord_client_id}&response_type=code&redirect_uri=${encodeURIComponent(config.auth.discord_redirect_uri)}&scope=guilds+identify`)) .get("/roles", () => USER_ROLES) .post("/logout", ({ cookie: { mizuki } }) => { mizuki.set({ value: "", httpOnly: true, maxAge: 0, path: "/", }); return { ok: true }; }) .get("/discord/callback", async ({ jwt, query, status, cookie: { mizuki }, redirect }) => { const code = query.code; if (!code) { return status(400, "Missing code"); } const tokenResponse = await fetch("https://discord.com/api/oauth2/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ client_id: config.auth.discord_client_id, client_secret: config.auth.discord_client_secret, code, grant_type: "authorization_code", redirect_uri: config.auth.discord_redirect_uri, }), }); if (!tokenResponse.ok) { console.error("Failed to exchange code for token", await tokenResponse.text()); return status(500, "Failed to exchange code for token"); } const tokenData = await tokenResponse.json(); const accessToken = tokenData.access_token; const userResponse = await fetch("https://discord.com/api/users/@me", { headers: { Authorization: `Bearer ${accessToken}`, }, }); if (!userResponse.ok) { console.error("Failed to fetch user info", await userResponse.text()); return status(500, "Failed to fetch user info"); } const userData = await userResponse.json(); const isHardcodedOwner = hardcodedOwners.has(String(userData.id)); const updateSet: { username: string; avatar: string; role?: "admin" } = { username: userData.username, avatar: userData.avatar, }; if (isHardcodedOwner) { updateSet.role = "admin"; } const user = await User.findOneAndUpdate( { discordId: userData.id }, { $set: updateSet, $setOnInsert: { userId: ulid(), discordId: userData.id, }, }, { upsert: true, new: true }, ); const ensuredUser = user && !user.userId ? await User.findOneAndUpdate( { discordId: userData.id }, { $set: { userId: ulid() } }, { new: true }, ) : user; const token = await jwt.sign({ id: ensuredUser?.userId, discordId: userData.id, username: userData.username, avatar: userData.avatar, role: ensuredUser?.role ?? "reader", }); mizuki.set({ value: token, httpOnly: true, maxAge: 60 * 60 * 24 * 7, // 7 days path: "/", }); return redirect("/"); }, { query: t.Object({ code: t.String(), }) }) .post("/role", async ({ jwt, body, cookie: { mizuki }, status }) => { const rawToken = mizuki.value; if (typeof rawToken !== "string" || rawToken.length === 0) { return status(401, "Unauthorized"); } const payload = await jwt.verify(rawToken); if (!payload || typeof payload !== "object" || !("id" in payload) || typeof payload.id !== "string") { return status(401, "Unauthorized"); } const requester = await User.findOne({ userId: payload.id }); if (!requester) { return status(401, "Unauthorized"); } if (requester.role !== "admin") { return status(403, "Forbidden"); } const target = await User.findOne({ userId: body.userId }); if (!target) { return status(404, "User not found"); } if (hardcodedOwners.has(target.discordId) && body.role !== "admin") { return status(400, "Hardcoded owner role must remain admin"); } const updated = await User.findOneAndUpdate( { userId: body.userId }, { $set: { role: body.role } }, { new: true }, ); if (!updated) { return status(404, "User not found"); } if (requester.userId === updated.userId) { const nextToken = await jwt.sign({ id: updated.userId, discordId: updated.discordId, username: updated.username, avatar: updated.avatar, role: updated.role, }); mizuki.set({ value: nextToken, httpOnly: true, maxAge: 60 * 60 * 24 * 7, path: "/", }); } await createAuditLog({ actor: { userId: requester.userId, discordId: requester.discordId, username: requester.username, role: requester.role, }, action: "auth.role.update", targetType: "user", targetId: updated.userId, summary: `${requester.username} changed role of ${updated.username} to ${updated.role}`, detail: { role: updated.role, username: updated.username, }, }); return { id: updated.userId, discordId: updated.discordId, username: updated.username, avatar: updated.avatar, role: updated.role, }; }, { body: t.Object({ userId: t.String(), role: t.Union([ t.Literal("admin"), t.Literal("writer"), t.Literal("reader"), ]), }), }) .get("/users", async ({ jwt, cookie: { mizuki }, status }) => { const rawToken = mizuki.value; if (typeof rawToken !== "string" || rawToken.length === 0) { return status(401, "Unauthorized"); } const payload = await jwt.verify(rawToken); if (!payload || typeof payload !== "object" || !("id" in payload) || typeof payload.id !== "string") { return status(401, "Unauthorized"); } const requester = await User.findOne({ userId: payload.id }); if (!requester) { return status(401, "Unauthorized"); } if (requester.role !== "admin") { return status(403, "Forbidden"); } const users = await User.find().sort({ createdAt: -1 }); return users.map((user) => ({ id: user.userId, discordId: user.discordId, username: user.username, avatar: user.avatar, role: user.role, createdAt: user.createdAt, })); }) .post("/user", async ({ jwt, body, cookie: { mizuki }, status }) => { const rawToken = mizuki.value; if (typeof rawToken !== "string" || rawToken.length === 0) { return status(401, "Unauthorized"); } const payload = await jwt.verify(rawToken); if (!payload || typeof payload !== "object" || !("id" in payload) || typeof payload.id !== "string") { return status(401, "Unauthorized"); } const requester = await User.findOne({ userId: payload.id }); if (!requester) { return status(401, "Unauthorized"); } if (requester.role !== "admin") { return status(403, "Forbidden"); } const role = hardcodedOwners.has(body.discordId) ? "admin" : body.role; try { const created = await User.create({ userId: ulid(), discordId: body.discordId, username: body.username, avatar: body.avatar, role, }); await createAuditLog({ actor: { userId: requester.userId, discordId: requester.discordId, username: requester.username, role: requester.role, }, action: "auth.user.create", targetType: "user", targetId: created.userId, summary: `${requester.username} created user ${created.username}`, detail: { username: created.username, discordId: created.discordId, role: created.role, }, }); return { id: created.userId, discordId: created.discordId, username: created.username, avatar: created.avatar, role: created.role, }; } catch (error) { const message = error instanceof Error ? error.message : "Failed to create user"; return status(400, message); } }, { body: t.Object({ discordId: t.String({ minLength: 1 }), username: t.String({ minLength: 1 }), avatar: t.Optional(t.String()), role: t.Union([ t.Literal("admin"), t.Literal("writer"), t.Literal("reader"), ]), }), }) .get("/audit-logs", async ({ jwt, cookie: { mizuki }, query, status }) => { const rawToken = mizuki.value; if (typeof rawToken !== "string" || rawToken.length === 0) { return status(401, "Unauthorized"); } const payload = await jwt.verify(rawToken); if (!payload || typeof payload !== "object" || !("id" in payload) || typeof payload.id !== "string") { return status(401, "Unauthorized"); } const requester = await User.findOne({ userId: payload.id }); if (!requester) { return status(401, "Unauthorized"); } if (requester.role !== "admin") { return status(403, "Forbidden"); } const page = query.page; const size = query.size; const logs = await AuditLog.find() .sort({ createdAt: -1 }) .skip((page - 1) * size) .limit(size); return logs; }, { query: t.Object({ page: t.Number({ default: 1, minimum: 1 }), size: t.Number({ default: 30, minimum: 1, maximum: 200 }), }), })