/** * @file Authentication routes for Discord OAuth. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable max-lines-per-function -- Auth callback requires many steps */ /* eslint-disable max-statements -- Auth callback requires many statements */ import { Hono } from "hono"; import { initialGameState } from "../data/initialState.js"; import { prisma } from "../db/client.js"; import { buildOAuthUrl, exchangeCode, fetchDiscordUser, } from "../services/discord.js"; import { signToken } from "../services/jwt.js"; import type { Player } from "@elysium/types"; const authRouter = new Hono(); authRouter.get("/url", (context) => { try { const url = buildOAuthUrl(); return context.json({ url }); } catch { return context.json({ error: "Failed to build OAuth URL" }, 500); } }); authRouter.get("/callback", async(context) => { const code = context.req.query("code"); if (code === undefined || code === "") { return context.json({ error: "Missing code parameter" }, 400); } try { const tokenData = await exchangeCode(code); const discordUser = await fetchDiscordUser(tokenData.access_token); const existing = await prisma.player.findUnique({ where: { discordId: discordUser.id }, }); const now = Date.now(); if (!existing) { const player = await prisma.player.create({ data: { avatar: discordUser.avatar, characterName: discordUser.username, createdAt: now, discordId: discordUser.id, discriminator: discordUser.discriminator, lastSavedAt: now, totalClicks: 0, totalGoldEarned: 0, username: discordUser.username, }, }); const playerShape: Player = { avatar: player.avatar ?? null, characterName: player.characterName, createdAt: player.createdAt, discordId: player.discordId, discriminator: player.discriminator, lastSavedAt: player.lastSavedAt, lifetimeAchievementsUnlocked: player.lifetimeAchievementsUnlocked, lifetimeAdventurersRecruited: player.lifetimeAdventurersRecruited, lifetimeBossesDefeated: player.lifetimeBossesDefeated, lifetimeClicks: player.lifetimeClicks, lifetimeGoldEarned: player.lifetimeGoldEarned, lifetimeQuestsCompleted: player.lifetimeQuestsCompleted, totalClicks: player.totalClicks, totalGoldEarned: player.totalGoldEarned, username: player.username, }; const freshState = initialGameState( playerShape, playerShape.characterName, ); await prisma.gameState.create({ data: { discordId: player.discordId, /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Prisma requires never type */ state: freshState as unknown as never, updatedAt: now, }, }); const jwtToken = signToken(player.discordId); // eslint-disable-next-line capitalized-comments -- v8 ignore /* v8 ignore next -- @preserve */ const clientUrl = process.env.CORS_ORIGIN ?? "http://localhost:5173"; return context.redirect( `${clientUrl}/auth/callback?token=${jwtToken}&isNew=true`, ); } const updated = await prisma.player.update({ data: { avatar: discordUser.avatar, discriminator: discordUser.discriminator, username: discordUser.username, }, where: { discordId: discordUser.id }, }); const jwtToken = signToken(updated.discordId); // eslint-disable-next-line capitalized-comments -- v8 ignore /* v8 ignore next -- @preserve */ const clientUrl = process.env.CORS_ORIGIN ?? "http://localhost:5173"; return context.redirect( `${clientUrl}/auth/callback?token=${jwtToken}&isNew=false`, ); } catch { // eslint-disable-next-line capitalized-comments -- v8 ignore /* v8 ignore next -- @preserve */ const clientUrl = process.env.CORS_ORIGIN ?? "http://localhost:5173"; return context.redirect(`${clientUrl}/auth/callback?error=auth_failed`); } }); export { authRouter };