diff --git a/api/AUTH_FLOW.md b/api/AUTH_FLOW.md new file mode 100644 index 0000000..0df4da6 --- /dev/null +++ b/api/AUTH_FLOW.md @@ -0,0 +1,53 @@ +# Authentication Flow + +## Overview +This API uses Discord OAuth for authentication and JWT tokens for session management. Only the admin user can perform create/update/delete operations, while public read access is available to everyone. + +## Environment Variables +Set up your `prod.env` file with 1Password references: +- `DATABASE_URL` - MongoDB connection string +- `JWT_SECRET` - Secret for signing JWT tokens +- `DISCORD_CLIENT_ID` - Discord OAuth app client ID +- `DISCORD_CLIENT_SECRET` - Discord OAuth app client secret +- `ADMIN_DISCORD_ID` - Your Discord user ID for admin access +- `API_URL` - API base URL (e.g., http://localhost:3000) +- `FRONTEND_URL` - Frontend URL to redirect after login + +## Running the API +```bash +# Start with 1Password secrets +op run --env-file=prod.env -- nx serve api +``` + +## Auth Endpoints + +### 1. Login +`GET /api/auth/login` - Redirects to Discord OAuth + +### 2. Callback +`GET /api/auth/callback` - Discord redirects here after auth +- Creates/updates user in database +- Generates JWT token +- Sets httpOnly cookie `auth-token` +- Redirects to frontend + +### 3. Get Current User +`GET /api/auth/me` - Returns authenticated user (requires auth) + +### 4. Logout +`POST /api/auth/logout` - Clears auth cookie + +## Protected Routes +Example: Games API +- `GET /api/games` - Public (list all games) +- `GET /api/games/:id` - Public (get single game) +- `POST /api/games` - Admin only (create game) +- `PUT /api/games/:id` - Admin only (update game) +- `DELETE /api/games/:id` - Admin only (delete game) + +## Testing +1. Set up Discord OAuth app at https://discord.com/developers/applications +2. Add redirect URI: `http://localhost:3000/api/auth/callback` +3. Copy client ID and secret to 1Password +4. Run the API and visit `http://localhost:3000/api/auth/login` +5. After Discord auth, you'll be redirected to frontend with auth cookie set \ No newline at end of file diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 2b84236..813f2e4 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -80,3 +80,14 @@ enum MusicStatus { COMPLETED WANT_TO_LISTEN } + +model User { + id String @id @default(auto()) @map("_id") @db.ObjectId + discordId String @unique + username String + email String @unique + avatar String? + isAdmin Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/api/prod.env b/api/prod.env new file mode 100644 index 0000000..9c21274 --- /dev/null +++ b/api/prod.env @@ -0,0 +1,16 @@ +# Database +DATABASE_URL=op://Personal/MongoDB Atlas - Library/connection string + +# JWT Secret +JWT_SECRET=op://Personal/Library API Secrets/jwt_secret + +# Discord OAuth +DISCORD_CLIENT_ID=op://Personal/Library Discord OAuth/client_id +DISCORD_CLIENT_SECRET=op://Personal/Library Discord OAuth/client_secret + +# Admin Configuration +ADMIN_DISCORD_ID=op://Personal/Library API Secrets/admin_discord_id + +# API Configuration +API_URL=op://Personal/Library API Secrets/api_url +FRONTEND_URL=op://Personal/Library API Secrets/frontend_url \ No newline at end of file diff --git a/api/src/app/middleware/admin-guard.ts b/api/src/app/middleware/admin-guard.ts new file mode 100644 index 0000000..f493308 --- /dev/null +++ b/api/src/app/middleware/admin-guard.ts @@ -0,0 +1,21 @@ +/** + * @copyright 2026 NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { FastifyReply, FastifyRequest } from "fastify"; + +/** + * Middleware to check if the authenticated user is an admin. + * Must be used after app.authenticate. + */ +export async function adminGuard( + request: FastifyRequest, + reply: FastifyReply +): Promise { + const user = request.user as any; + if (!user || !user.isAdmin) { + return reply.code(403).send({ error: "Forbidden: Admin access required" }); + } +} \ No newline at end of file diff --git a/api/src/app/plugins/auth.ts b/api/src/app/plugins/auth.ts new file mode 100644 index 0000000..13e6eb3 --- /dev/null +++ b/api/src/app/plugins/auth.ts @@ -0,0 +1,54 @@ +import { FastifyPluginAsync, FastifyRequest } from "fastify"; +import fastifyPlugin from "fastify-plugin"; +import fastifyJwt from "@fastify/jwt"; +import fastifyCookie from "@fastify/cookie"; +import fastifyOauth2 from "@fastify/oauth2"; + +declare module "fastify" { + interface FastifyInstance { + authenticate: (request: FastifyRequest) => Promise; + oauth2Discord: any; + } + interface FastifyRequest { + user?: any; + } +} + +const authPlugin: FastifyPluginAsync = async (app) => { + // Register JWT plugin + app.register(fastifyJwt, { + secret: process.env.JWT_SECRET || "your-secret-key", + cookie: { + cookieName: "auth-token", + signed: false, + }, + }); + + // Register cookie plugin + app.register(fastifyCookie); + + // Register Discord OAuth2 + app.register(fastifyOauth2, { + name: "oauth2Discord", + credentials: { + client: { + id: process.env.DISCORD_CLIENT_ID || "", + secret: process.env.DISCORD_CLIENT_SECRET || "", + }, + auth: fastifyOauth2.DISCORD_CONFIGURATION, + }, + startRedirectPath: "/api/auth/login", + callbackUri: `${process.env.API_URL || "http://localhost:3000"}/api/auth/callback`, + }); + + // Authentication decorator + app.decorate("authenticate", async (request: FastifyRequest) => { + try { + await request.jwtVerify(); + } catch (err) { + throw app.httpErrors.unauthorized("Invalid token"); + } + }); +}; + +export default fastifyPlugin(authPlugin); \ No newline at end of file diff --git a/api/src/app/plugins/sensible.ts b/api/src/app/plugins/sensible.ts index 7ad73ad..b71ea4d 100644 --- a/api/src/app/plugins/sensible.ts +++ b/api/src/app/plugins/sensible.ts @@ -1,3 +1,9 @@ +/** + * @copyright 2026 NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + import { FastifyInstance } from 'fastify'; import fp from 'fastify-plugin'; import sensible from '@fastify/sensible'; diff --git a/api/src/app/routes/auth.ts b/api/src/app/routes/auth.ts new file mode 100644 index 0000000..3f3b641 --- /dev/null +++ b/api/src/app/routes/auth.ts @@ -0,0 +1,86 @@ +import { FastifyPluginAsync } from "fastify"; +import { AuthService } from "../services/auth.service"; +import { AuthResponse } from "@library/shared-types"; + +const authRoutes: FastifyPluginAsync = async (app) => { + const authService = new AuthService(app); + + /** + * Initiate Discord OAuth login. + */ + app.get("/login", async (request, reply) => { + const authUrl = app.oauth2Discord.generateAuthorizationUri({ + scope: ["identify", "email"], + }); + + return reply.redirect(authUrl); + }); + + /** + * Discord OAuth callback. + */ + app.get("/callback", async (request, reply) => { + try { + const token = await app.oauth2Discord.getAccessTokenFromAuthorizationCodeFlow( + request + ); + + // Get user data from Discord + const userData = await app.oauth2Discord.userinfo(token.access_token); + + // Create or update user in database + const user = await authService.createOrUpdateUserFromDiscord(userData); + + // Generate JWT + const jwt = await authService.generateToken(user); + + // Set cookie and redirect to frontend + reply + .setCookie("auth-token", jwt, { + path: "/", + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 7 * 24 * 60 * 60, // 7 days + }) + .redirect(process.env.FRONTEND_URL || "http://localhost:4200"); + } catch (error) { + app.log.error(error); + reply + .code(401) + .send({ error: "Authentication failed" }); + } + }); + + /** + * Get current user. + */ + app.get<{ Reply: AuthResponse | { error: string } }>( + "/me", + { + preValidation: [app.authenticate], + }, + async (request) => { + const user = request.user as any; + const token = await authService.generateToken(user); + + return { + user, + accessToken: token, + }; + } + ); + + /** + * Logout. + */ + app.post("/logout", async (request, reply) => { + reply + .clearCookie("auth-token", { + path: "/", + }) + .send({ message: "Logged out successfully" }); + }); +}; + +export default authRoutes; \ No newline at end of file diff --git a/api/src/app/routes/games/index.ts b/api/src/app/routes/games/index.ts new file mode 100644 index 0000000..5b53e8f --- /dev/null +++ b/api/src/app/routes/games/index.ts @@ -0,0 +1,109 @@ +/** + * @copyright 2026 NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { FastifyPluginAsync } from "fastify"; +import { PrismaClient } from "../../../generated/prisma"; +import { Game, GameStatus } from "@library/shared-types"; +import { adminGuard } from "../../middleware/admin-guard"; + +const gamesRoutes: FastifyPluginAsync = async (app) => { + const prisma = new PrismaClient(); + + // Get all games (public route) + app.get<{ Reply: Game[] }>("/", async () => { + const games = await prisma.game.findMany({ + orderBy: { updatedAt: "desc" }, + }); + return games.map((game) => ({ + ...game, + status: game.status.toLowerCase() as GameStatus, + })); + }); + + // Get single game (public route) + app.get<{ Params: { id: string }; Reply: Game | null }>( + "/:id", + async (request) => { + const { id } = request.params; + const game = await prisma.game.findUnique({ + where: { id }, + }); + if (!game) return null; + return { + ...game, + status: game.status.toLowerCase() as GameStatus, + }; + } + ); + + // Create game (protected admin route) + app.post<{ Body: Omit; Reply: Game }>( + "/", + { + preValidation: [app.authenticate, adminGuard], + }, + async (request, reply) => { + + const game = await prisma.game.create({ + data: { + ...request.body, + status: request.body.status.toUpperCase() as any, + }, + }); + return { + ...game, + status: game.status.toLowerCase() as GameStatus, + }; + } + ); + + // Update game (protected admin route) + app.put<{ + Params: { id: string }; + Body: Partial>; + Reply: Game | null; + }>( + "/:id", + { + preValidation: [app.authenticate, adminGuard], + }, + async (request, reply) => { + + const { id } = request.params; + const updateData = { ...request.body }; + if (updateData.status) { + updateData.status = updateData.status.toUpperCase() as any; + } + + const game = await prisma.game.update({ + where: { id }, + data: updateData, + }); + return { + ...game, + status: game.status.toLowerCase() as GameStatus, + }; + } + ); + + // Delete game (protected admin route) + app.delete<{ Params: { id: string }; Reply: { success: boolean } }>( + "/:id", + { + preValidation: [app.authenticate, adminGuard], + }, + async (request, reply) => { + + const { id } = request.params; + await prisma.game.delete({ + where: { id }, + }); + return { success: true }; + } + ); +}; + +export default gamesRoutes; \ No newline at end of file diff --git a/api/src/app/services/auth.service.ts b/api/src/app/services/auth.service.ts new file mode 100644 index 0000000..641bdc1 --- /dev/null +++ b/api/src/app/services/auth.service.ts @@ -0,0 +1,78 @@ +import { FastifyInstance } from "fastify"; +import { JwtPayload, User } from "@library/shared-types"; +import { PrismaClient } from "../../generated/prisma"; + +export class AuthService { + private prisma: PrismaClient; + + constructor(private readonly app: FastifyInstance) { + this.prisma = new PrismaClient(); + } + + /** + * Generate JWT token for user. + */ + async generateToken(user: User): Promise { + const payload: JwtPayload = { + sub: user.id, + email: user.email, + username: user.username, + isAdmin: user.isAdmin, + }; + + return this.app.jwt.sign(payload, { + expiresIn: "7d", + }); + } + + /** + * Verify JWT token. + */ + async verifyToken(token: string): Promise { + return this.app.jwt.verify(token) as JwtPayload; + } + + /** + * Create or update user from Discord OAuth data. + */ + async createOrUpdateUserFromDiscord(discordData: DiscordUser): Promise { + const avatarUrl = discordData.avatar + ? `https://cdn.discordapp.com/avatars/${discordData.id}/${discordData.avatar}.png` + : undefined; + + // Upsert user in database + const dbUser = await this.prisma.user.upsert({ + where: { + discordId: discordData.id, + }, + create: { + discordId: discordData.id, + username: discordData.username, + email: discordData.email, + avatar: avatarUrl, + isAdmin: discordData.id === process.env.ADMIN_DISCORD_ID, + }, + update: { + username: discordData.username, + email: discordData.email, + avatar: avatarUrl, + }, + }); + + return { + id: dbUser.id, + discordId: dbUser.discordId, + username: dbUser.username, + email: dbUser.email, + avatarUrl: dbUser.avatar || undefined, + isAdmin: dbUser.isAdmin, + }; + } +} + +interface DiscordUser { + id: string; + username: string; + email: string; + avatar?: string; +} \ No newline at end of file diff --git a/package.json b/package.json index ad271a0..75f1bc0 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,10 @@ "@angular/platform-browser": "21.1.2", "@angular/router": "21.1.2", "@fastify/autoload": "6.0.3", - "@fastify/sensible": "6.0.4", + "@fastify/cookie": "^11.0.2", + "@fastify/jwt": "^10.0.0", + "@fastify/oauth2": "^8.1.2", + "@fastify/sensible": "5.6.0", "@prisma/client": "7.3.0", "fastify": "5.2.2", "fastify-plugin": "5.0.1", @@ -46,6 +49,7 @@ "@swc/core": "1.5.29", "@swc/helpers": "0.5.18", "@types/jest": "30.0.0", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "20.19.9", "@typescript-eslint/utils": "8.54.0", "angular-eslint": "21.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f5ad1a..f6794f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,9 +29,18 @@ importers: '@fastify/autoload': specifier: 6.0.3 version: 6.0.3 + '@fastify/cookie': + specifier: ^11.0.2 + version: 11.0.2 + '@fastify/jwt': + specifier: ^10.0.0 + version: 10.0.0 + '@fastify/oauth2': + specifier: ^8.1.2 + version: 8.1.2 '@fastify/sensible': - specifier: 6.0.4 - version: 6.0.4 + specifier: 5.6.0 + version: 5.6.0 '@prisma/client': specifier: 7.3.0 version: 7.3.0(prisma@7.3.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3) @@ -114,6 +123,9 @@ importers: '@types/jest': specifier: 30.0.0 version: 30.0.0 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 '@types/node': specifier: 20.19.9 version: 20.19.9 @@ -1537,6 +1549,9 @@ packages: '@fastify/autoload@6.0.3': resolution: {integrity: sha512-/wM2pmI7jP2fGah3YuP14i9vuaijpD4gQYLiyd+eD7gUxpA3B4R6/0QIXQS2eJaYD9aIX4ZFRzPmrZsaetfcWw==} + '@fastify/cookie@11.0.2': + resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} + '@fastify/error@4.2.0': resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} @@ -1546,14 +1561,38 @@ packages: '@fastify/forwarded@3.0.1': resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + '@fastify/jwt@10.0.0': + resolution: {integrity: sha512-2Qka3NiyNNcsfejMUvyzot1T4UYIzzcbkFGDdVyrl344fRZ/WkD6VFXOoXhxe2Pzf3LpJNkoSxUM4Ru4DVgkYA==} + '@fastify/merge-json-schemas@0.2.1': resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + '@fastify/oauth2@8.1.2': + resolution: {integrity: sha512-XZWFRWTZE2fkZ2pjuHNGtpFn1tOFgcJbU0205kHbfd16dn9xRc/6HmG0gHtN/g/BNkEL3EsQ54+pYEdh8dnBgA==} + '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} - '@fastify/sensible@6.0.4': - resolution: {integrity: sha512-1vxcCUlPMew6WroK8fq+LVOwbsLtX+lmuRuqpcp6eYqu6vmkLwbKTdBWAZwbeaSgCfW4tzUpTIHLLvTiQQ1BwQ==} + '@fastify/sensible@5.6.0': + resolution: {integrity: sha512-Vq6Z2ZQy10GDqON+hvLF52K99s9et5gVVxTul5n3SIAf0Kq5QjPRUKkAMT3zPAiiGvoHtS3APa/3uaxfDgCODQ==} + + '@hapi/boom@10.0.1': + resolution: {integrity: sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==} + + '@hapi/bourne@3.0.0': + resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} + + '@hapi/hoek@11.0.7': + resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==} + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@hapi/wreck@18.1.0': + resolution: {integrity: sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==} '@hono/node-server@1.19.9': resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} @@ -3050,6 +3089,15 @@ packages: resolution: {integrity: sha512-kxwxhCIUrj7DfzEtDSs/pi/w+aII/WQLpPfLgoQCWE8/95v60WnTfd1afmsXsFoxikKPxkwoPWtU2YbhSoX9MQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + '@sigstore/bundle@4.0.0': resolution: {integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==} engines: {node: ^20.17.0 || >=22.9.0} @@ -3294,9 +3342,15 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} @@ -3912,6 +3966,9 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} @@ -4104,6 +4161,9 @@ packages: bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + bn.js@4.12.2: + resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==} + body-parser@1.20.4: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -4835,6 +4895,9 @@ packages: ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -5251,6 +5314,10 @@ packages: fast-json-stringify@6.2.0: resolution: {integrity: sha512-Eaf/KNIDwHkzfyeQFNfLXJnQ7cl1XQI3+zRqmPlvtkMigbXnAcasTrvJQmquBSxKfFGeRA6PFog8t+hFmpDoWw==} + fast-jwt@6.1.0: + resolution: {integrity: sha512-cGK/TXlud8INL49Iv7yRtZy0PHzNJId1shfqNCqdF0gOlWiy+1FPgjxX+ZHp/CYxFYDaoNnxeYEGzcXSkahUEQ==} + engines: {node: '>=20'} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} @@ -5260,15 +5327,28 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastfall@1.5.1: + resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==} + engines: {node: '>=0.10.0'} + + fastify-plugin@4.5.1: + resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} + fastify-plugin@5.0.1: resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} fastify@5.2.2: resolution: {integrity: sha512-22T/PnhquWozuFXg3Ish4md5ipsF1Nx1mJ9ulLdZPXSk14WFj/wMlyNB/yll9sQOojKRgOIxT2inK3Xpjg5hyw==} + fastparallel@2.4.1: + resolution: {integrity: sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fastseries@1.7.2: + resolution: {integrity: sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==} + faye-websocket@0.11.4: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} engines: {node: '>=0.8.0'} @@ -6263,6 +6343,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} @@ -6742,6 +6825,9 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} + mnemonist@0.40.3: + resolution: {integrity: sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -6954,6 +7040,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obliterator@2.0.5: + resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} + obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} @@ -8109,6 +8198,9 @@ packages: resolution: {integrity: sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==} engines: {node: ^20.17.0 || >=22.9.0} + simple-oauth2@5.1.0: + resolution: {integrity: sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -8247,6 +8339,9 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} + steed@1.1.3: + resolution: {integrity: sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -9088,6 +9183,10 @@ packages: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -11317,6 +11416,11 @@ snapshots: '@fastify/autoload@6.0.3': {} + '@fastify/cookie@11.0.2': + dependencies: + cookie: 1.1.1 + fastify-plugin: 5.0.1 + '@fastify/error@4.2.0': {} '@fastify/fast-json-stringify-compiler@5.0.3': @@ -11325,25 +11429,61 @@ snapshots: '@fastify/forwarded@3.0.1': {} + '@fastify/jwt@10.0.0': + dependencies: + '@fastify/error': 4.2.0 + '@lukeed/ms': 2.0.2 + fast-jwt: 6.1.0 + fastify-plugin: 5.0.1 + steed: 1.1.3 + '@fastify/merge-json-schemas@0.2.1': dependencies: dequal: 2.0.3 + '@fastify/oauth2@8.1.2': + dependencies: + '@fastify/cookie': 11.0.2 + fastify-plugin: 5.0.1 + simple-oauth2: 5.1.0 + transitivePeerDependencies: + - supports-color + '@fastify/proxy-addr@5.1.0': dependencies: '@fastify/forwarded': 3.0.1 ipaddr.js: 2.3.0 - '@fastify/sensible@6.0.4': + '@fastify/sensible@5.6.0': dependencies: '@lukeed/ms': 2.0.2 - dequal: 2.0.3 - fastify-plugin: 5.0.1 + fast-deep-equal: 3.1.3 + fastify-plugin: 4.5.1 forwarded: 0.2.0 http-errors: 2.0.1 - type-is: 2.0.1 + type-is: 1.6.18 vary: 1.1.2 + '@hapi/boom@10.0.1': + dependencies: + '@hapi/hoek': 11.0.7 + + '@hapi/bourne@3.0.0': {} + + '@hapi/hoek@11.0.7': {} + + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@hapi/wreck@18.1.0': + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/bourne': 3.0.0 + '@hapi/hoek': 11.0.7 + '@hono/node-server@1.19.9(hono@4.11.4)': dependencies: hono: 4.11.4 @@ -13437,6 +13577,14 @@ snapshots: transitivePeerDependencies: - chokidar + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + '@sigstore/bundle@4.0.0': dependencies: '@sigstore/protobuf-specs': 0.5.0 @@ -13705,8 +13853,15 @@ snapshots: '@types/json5@0.0.29': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 20.19.9 + '@types/mime@1.3.5': {} + '@types/ms@2.1.0': {} + '@types/node-forge@1.3.14': dependencies: '@types/node': 20.19.9 @@ -14419,6 +14574,13 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.2 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + asn1@0.2.6: dependencies: safer-buffer: 2.1.2 @@ -14672,6 +14834,8 @@ snapshots: bluebird@3.7.2: {} + bn.js@4.12.2: {} + body-parser@1.20.4: dependencies: bytes: 3.1.2 @@ -15445,6 +15609,10 @@ snapshots: jsbn: 0.1.1 safer-buffer: 2.1.2 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} effect@3.18.4: @@ -16105,6 +16273,13 @@ snapshots: json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 + fast-jwt@6.1.0: + dependencies: + '@lukeed/ms': 2.0.2 + asn1.js: 5.4.1 + ecdsa-sig-formatter: 1.0.11 + mnemonist: 0.40.3 + fast-levenshtein@2.0.6: {} fast-querystring@1.1.2: @@ -16113,6 +16288,12 @@ snapshots: fast-uri@3.1.0: {} + fastfall@1.5.1: + dependencies: + reusify: 1.1.0 + + fastify-plugin@4.5.1: {} + fastify-plugin@5.0.1: {} fastify@5.2.2: @@ -16133,10 +16314,20 @@ snapshots: semver: 7.7.3 toad-cache: 3.7.0 + fastparallel@2.4.1: + dependencies: + reusify: 1.1.0 + xtend: 4.0.2 + fastq@1.20.1: dependencies: reusify: 1.1.0 + fastseries@1.7.2: + dependencies: + reusify: 1.1.0 + xtend: 4.0.2 + faye-websocket@0.11.4: dependencies: websocket-driver: 0.7.4 @@ -17349,6 +17540,14 @@ snapshots: jiti@2.6.1: {} + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + jose@6.1.3: {} js-tokens@4.0.0: {} @@ -17860,6 +18059,10 @@ snapshots: dependencies: minipass: 7.1.2 + mnemonist@0.40.3: + dependencies: + obliterator: 2.0.5 + mrmime@2.0.1: {} ms@2.0.0: {} @@ -18142,6 +18345,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obliterator@2.0.5: {} + obuf@1.1.2: {} obug@2.1.1: {} @@ -19403,6 +19608,15 @@ snapshots: transitivePeerDependencies: - supports-color + simple-oauth2@5.1.0: + dependencies: + '@hapi/hoek': 11.0.7 + '@hapi/wreck': 18.1.0 + debug: 4.4.3(supports-color@8.1.1) + joi: 17.13.3 + transitivePeerDependencies: + - supports-color + slash@3.0.0: {} slash@4.0.0: {} @@ -19564,6 +19778,14 @@ snapshots: stdin-discarder@0.2.2: {} + steed@1.1.3: + dependencies: + fastfall: 1.5.1 + fastparallel: 2.4.1 + fastq: 1.20.1 + fastseries: 1.7.2 + reusify: 1.1.0 + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -20579,6 +20801,8 @@ snapshots: is-wsl: 3.1.0 powershell-utils: 0.1.0 + xtend@4.0.2: {} + y18n@5.0.8: {} yallist@3.1.1: {} diff --git a/shared-types/src/lib/auth.types.ts b/shared-types/src/lib/auth.types.ts index 81f53d4..11a3517 100644 --- a/shared-types/src/lib/auth.types.ts +++ b/shared-types/src/lib/auth.types.ts @@ -10,6 +10,7 @@ export interface User { username: string; avatarUrl?: string; discordId: string; + isAdmin: boolean; } export interface JwtPayload { @@ -19,6 +20,7 @@ export interface JwtPayload { sub: string; email: string; username: string; + isAdmin: boolean; iat?: number; exp?: number; }