Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions apps/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's an import type, so it should probably get stripped out at compile time and nothing actually tries to load that file at runtime.
and for typechecking, the backend is set to moduleResolution: bundler, which means a .js import just maps to the matching .ts or .d.ts file, so ./types/fastify.js points at fastify.d.ts fine

anddd the typecheck passing in CI backs that up.


const __dirname = path.dirname(fileURLToPath(import.meta.url));

export async function buildApp():Promise<FastifyInstance> {
Expand Down Expand Up @@ -104,7 +106,7 @@ export async function buildApp():Promise<FastifyInstance> {
// 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);
Expand All @@ -122,7 +124,7 @@ export async function buildApp():Promise<FastifyInstance> {
}
}
// Assign verified payload to request.user (upstream addition).
const payload = await request.jwtVerify();
const payload = await request.jwtVerify<AuthenticatedUser>();
if (payload) { request.user = payload; }
} catch (_err) {
return reply.status(401).send({ error: 'Unauthorized' });
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
// 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: {
Expand Down Expand Up @@ -676,7 +676,7 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
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 {
Expand Down
18 changes: 17 additions & 1 deletion apps/backend/src/types/fastify.d.ts
Original file line number Diff line number Diff line change
@@ -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<string, string | undefined>;
}

interface FastifyInstance {
authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
}
}