diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 6116b91b..44842088 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -7,7 +7,7 @@ import helmet from '@fastify/helmet'; import jwt from '@fastify/jwt'; import multipart from '@fastify/multipart'; import rateLimit from '@fastify/rate-limit'; -import Fastify, {type FastifyInstance} from 'fastify'; +import Fastify, {type FastifyInstance, type FastifyReply, type FastifyRequest} from 'fastify'; import { prismaPlugin } from './plugins/prisma.js'; import { redisPlugin } from './plugins/redis.js'; @@ -24,6 +24,8 @@ import { teamRoutes } from './routes/team.js'; import { extractRawJwt, blocklistKey } from './utils/jwt.js'; import { validateEnv } from './utils/validateEnv.js'; +import type { AuthenticatedUser } from './types/fastify.js'; + const __dirname = path.dirname(fileURLToPath(import.meta.url)); export async function buildApp():Promise { @@ -104,7 +106,7 @@ export async function buildApp():Promise { // Checks the Redis blocklist before calling jwtVerify so that a logged-out // token is rejected immediately even if it has not yet expired. // The blocklist check is skipped when Redis is not registered (test env). - app.decorate('authenticate', async function (request: any, reply: any) { + app.decorate('authenticate', async function (request: FastifyRequest, reply: FastifyReply) { try { if (app.hasDecorator('redis')) { const raw = extractRawJwt(request); @@ -122,7 +124,7 @@ export async function buildApp():Promise { } } // Assign verified payload to request.user (upstream addition). - const payload = await request.jwtVerify(); + const payload = await request.jwtVerify(); if (payload) { request.user = payload; } } catch (_err) { return reply.status(401).send({ error: 'Unauthorized' }); diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index 11351267..3bc39ad4 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -613,7 +613,7 @@ export async function authRoutes(app: FastifyInstance): Promise { // eslint-disable-next-line @typescript-eslint/unbound-method preHandler: [app.authenticate], }, async (request: FastifyRequest, reply: FastifyReply) => { - const userId = (request.user as any).id; + const userId = request.user.id; const user = await app.prisma.user.findUnique({ where: { id: userId }, select: { @@ -676,7 +676,7 @@ export async function authRoutes(app: FastifyInstance): Promise { await app.redis.set(blocklistKey(raw), '1', 'EX', ttl); } catch (err) { // Non-fatal: log and continue. The token will expire on its own. - app.log.warn({ err, userId: (request.user as any)?.id }, 'Redis blocklist write failed during logout — token will expire naturally'); + app.log.warn({ err, userId: request.user?.id }, 'Redis blocklist write failed during logout — token will expire naturally'); } } } else { diff --git a/apps/backend/src/types/fastify.d.ts b/apps/backend/src/types/fastify.d.ts index 8e7aee95..faeddd2a 100644 --- a/apps/backend/src/types/fastify.d.ts +++ b/apps/backend/src/types/fastify.d.ts @@ -1,8 +1,24 @@ import '@fastify/cookie'; -import { FastifyRequest } from 'fastify'; +import '@fastify/jwt'; +import { FastifyReply, FastifyRequest } from 'fastify'; + +export interface AuthenticatedUser { + id: string; + username: string; +} + +declare module '@fastify/jwt' { + interface FastifyJWT { + user: AuthenticatedUser; + } +} declare module 'fastify' { interface FastifyRequest { cookies: Record; } + + interface FastifyInstance { + authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise; + } }