Skip to content

Commit 7d72b40

Browse files
authored
fix: improve type safety in team.ts, remove any usages (closes #554) (#581)
* fix: improve type safety in team.ts, remove any usages (closes #554) * fix: resolve broken test suite from team.ts type-safety refactor - Add authenticate decorator inside buildApp() before app.register(teamRoutes), since Fastify executes plugin registration during app.ready() — decorator must exist before routes are registered, not after buildApp() returns - Remove redundant .bind(app) calls in team.ts; authenticate decorator doesn't use 'this', consistent with auth.ts pattern - Cast payload via 'as typeof request.user' to satisfy TS in test mock * fix: resolve lint/type errors blocking team.ts type-safety PR - Add authenticate decorator inside buildApp() before app.register(teamRoutes); Fastify executes plugin registration during app.ready(), so the decorator must exist before routes are registered - Remove redundant .bind(app) in team.ts; authenticate decorator doesn't use 'this' - Fix eslint errors in team.test.ts: type-only imports, import order, const over let, missing curly braces (mostly via --fix) - Add explicit return type to createTeam test helper - Add 'this: void' to authenticate's type signature in fastify.d.ts so @typescript-eslint/unbound-method allows referencing it without binding - Remove duplicate authenticate declaration in prisma.ts (method-shorthand syntax with implicit bound this) that was silently conflicting with the fastify.d.ts declaration — tsc merged them without error since the call signatures were structurally compatible, but eslint's unbound-method rule was still resolving to the unbound declaration * chore: remove unused import * Update team.ts This PR also refactor functions name. Signed-off-by: Yachika Sharma <shakuntalaramphalsharma@gmail.com> --------- Signed-off-by: Yachika Sharma <shakuntalaramphalsharma@gmail.com>
1 parent 4d74b67 commit 7d72b40

4 files changed

Lines changed: 42 additions & 45 deletions

File tree

apps/backend/src/__tests__/team.test.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { type PrismaClient, TeamRole } from '@prisma/client';
2+
import Fastify, { type FastifyInstance } from 'fastify';
13
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2-
import Fastify, { FastifyInstance } from 'fastify';
3-
import { PrismaClient, TeamRole } from '@prisma/client';
4+
45
import { teamRoutes } from '../routes/team';
56

67
// ─── Shared mock data ─────────────────────────────────────────────────────────
@@ -92,7 +93,7 @@ const prismaMock = {
9293

9394
// ─── App factory ──────────────────────────────────────────────────────────────
9495

95-
let mockJwtVerify = vi.fn();
96+
const mockJwtVerify = vi.fn();
9697

9798
async function buildApp(): Promise<FastifyInstance> {
9899
const app = Fastify({ logger: false });
@@ -102,7 +103,14 @@ async function buildApp(): Promise<FastifyInstance> {
102103
app.decorateRequest('jwtVerify', function () {
103104
return mockJwtVerify();
104105
});
105-
106+
app.decorate('authenticate', async function (request, reply) {
107+
try {
108+
const payload = await request.jwtVerify();
109+
if (payload) {request.user = payload as typeof request.user;}
110+
} catch {
111+
return reply.status(401).send({ error: 'Unauthorized' });
112+
}
113+
});
106114
await app.register(teamRoutes);
107115
await app.ready();
108116
return app;
@@ -118,7 +126,7 @@ async function createTeam(
118126
app: FastifyInstance,
119127
body: Record<string, unknown>,
120128
authenticated = true,
121-
) {
129+
): Promise<Awaited<ReturnType<typeof app.inject>>> {
122130
return app.inject({
123131
method: 'POST',
124132
url: '/',

apps/backend/src/plugins/prisma.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
import fp from 'fastify-plugin';
21
import { PrismaClient } from '@prisma/client';
3-
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2+
import fp from 'fastify-plugin';
3+
4+
import type { FastifyInstance } from 'fastify';
45

56
declare module 'fastify' {
67
interface FastifyInstance {
78
prisma: PrismaClient;
8-
authenticate(
9-
request: FastifyRequest,
10-
reply: FastifyReply
11-
): Promise<void>;
129
}
1310
}
1411

apps/backend/src/routes/team.ts

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {generateUniqueSlug} from '../utils/slug.js'
55
import { createTeamScehma,inviteMembers,updateTeam } from '../validations/team.validation.js';
66

77
import type {PlatformLink, PublicProfile} from '@devcard/shared/src/types.js'
8-
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
8+
import type { FastifyInstance } from 'fastify';
99

1010
type TeamMember = PublicProfile & {
1111
teamRole: TeamRole
@@ -24,16 +24,11 @@ type TeamProfile = {
2424
members: TeamMember[];
2525
}
2626

27-
export async function teamRoutes(app:FastifyInstance){
28-
app.post('/', { preHandler: [async (request, reply) => {
29-
const server = request.server as any;
30-
if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return }
31-
if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return }
32-
try { const payload = await request.jwtVerify(); if (payload) (request as any).user = payload; } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) }
33-
}] }, async(request:FastifyRequest<{
34-
Body: {name: string, description? : string, avatarUrl?: string }
35-
}>, reply: FastifyReply) => {
36-
const userId = (request.user as any).id;
27+
export async function teamRoutes(app: FastifyInstance): Promise<void> {
28+
app.post<{
29+
Body: {name: string, description? : string, avatarUrl?: string }
30+
}>('/',{ preHandler: [app.authenticate] }, async (request, reply): Promise<void> => {
31+
const userId = request.user.id;
3732
const parsed = createTeamScehma.safeParse(request.body);
3833
if(!parsed.success){
3934
return reply.status(400).send({error: 'Bad request'})
@@ -48,7 +43,7 @@ export async function teamRoutes(app:FastifyInstance){
4843

4944
try {
5045
const team = await app.prisma.$transaction(async (tx) => {
51-
const team = await tx.team.create({
46+
const createdTeam = await tx.team.create({
5247
data: {
5348
name,
5449
slug: finalSlug,
@@ -60,13 +55,13 @@ export async function teamRoutes(app:FastifyInstance){
6055

6156
await tx.teamMember.create({
6257
data: {
63-
teamId : team.id,
58+
teamId : createdTeam.id,
6459
userId,
6560
role: TeamRole.OWNER,
6661
joinedAt: new Date(),
6762
}
6863
})
69-
return team
64+
return createdTeam
7065
})
7166
return reply.status(201).send(team)
7267

@@ -91,7 +86,7 @@ export async function teamRoutes(app:FastifyInstance){
9186
}
9287
})
9388

94-
app.get('/:slug', async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => {
89+
app.get<{Params: {slug: string}}>('/:slug', async (request, reply): Promise<void> => {
9590
const paramsSlug = request.params.slug;
9691

9792
try {
@@ -149,22 +144,17 @@ export async function teamRoutes(app:FastifyInstance){
149144
members
150145
}
151146

152-
return response;
147+
return reply.status(200).send(response);
153148
} catch (error) {
154149
app.log.error(error);
155150
return reply.status(500).send('Database query failed')
156151
}
157152

158153
})
159154

160-
app.post('/:slug/members', { preHandler: [async (request, reply) => {
161-
const server = request.server as any;
162-
if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return }
163-
if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return }
164-
try { const payload = await request.jwtVerify(); if (payload) (request as any).user = payload; } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) }
165-
}] }, async(request: FastifyRequest<{Params: {slug:string}, Body:{username:string}}>, reply: FastifyReply) => {
155+
app.post<{Params: {slug:string}, Body:{username:string}}>('/:slug/members', { preHandler: [app.authenticate] }, async (request, reply): Promise<void> => {
166156
const paramsSlug = request.params.slug;
167-
const userId = (request.user as any).id;
157+
const userId = request.user.id;
168158
const parsed = inviteMembers.safeParse(request.body);
169159
if(!parsed.success){
170160
return reply.status(400).send({error: 'Bad request'})
@@ -224,10 +214,10 @@ export async function teamRoutes(app:FastifyInstance){
224214
}
225215
})
226216

227-
app.delete('/:slug/members/:userId', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string, userId: string}}>, reply: FastifyReply) => {
217+
app.delete<{Params: {slug: string, userId: string}}>('/:slug/members/:userId',{ preHandler: [app.authenticate] }, async (request, reply): Promise<void> => {
228218
const paramsSlug = request.params.slug
229219
const paramsUserId = request.params.userId
230-
const userID = (request.user as any).id;
220+
const userId = request.user.id;
231221
const teamDetails = await app.prisma.team.findUnique(
232222
{where: {slug: paramsSlug},
233223
include: {
@@ -251,8 +241,8 @@ export async function teamRoutes(app:FastifyInstance){
251241
});
252242
}
253243

254-
const isOwner = teamDetails.ownerId === userID;
255-
const isSelfRemove = paramsUserId === userID;
244+
const isOwner = teamDetails.ownerId === userId;
245+
const isSelfRemove = paramsUserId === userId;
256246

257247
if (!isOwner && !isSelfRemove) {
258248
return reply.status(403).send({
@@ -286,8 +276,10 @@ export async function teamRoutes(app:FastifyInstance){
286276
}
287277
})
288278

289-
app.patch('/:slug',{ preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string},Body: {description?:string, name?:string, avatarUrl?:string}}>, reply: FastifyReply) => {
290-
const userId = (request.user as any).id;
279+
app.patch<{Params: {slug: string},Body: {description?:string, name?:string, avatarUrl?:string}}>('/:slug',{ preHandler: [app.authenticate
280+
281+
] }, async (request, reply): Promise<void> => {
282+
const userId = request.user.id;
291283
const paramsSlug = request.params.slug;
292284
const parsed = updateTeam.safeParse(request.body);
293285
if(!parsed.success){
@@ -328,8 +320,8 @@ export async function teamRoutes(app:FastifyInstance){
328320

329321
})
330322

331-
app.delete('/:slug',{ preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request:FastifyRequest<{Params:{slug: string}}>, reply:FastifyReply) => {
332-
const userId = (request.user as any).id;
323+
app.delete<{Params:{slug: string}}>('/:slug',{ preHandler: [app.authenticate] }, async (request, reply): Promise<void> => {
324+
const userId = request.user.id;
333325
const paramsSlug = request.params.slug;
334326

335327

@@ -364,7 +356,7 @@ export async function teamRoutes(app:FastifyInstance){
364356
}
365357
})
366358

367-
app.get('/:slug/qr',async(request:FastifyRequest<{Params:{slug:string}}>, reply: FastifyReply) => {
359+
app.get<{Params:{slug:string}}>('/:slug/qr', async (request, reply): Promise<void> => {
368360
const paramsSlug = request.params.slug;
369361
try {
370362
const teamDetails = await app.prisma.team.findUnique({
@@ -386,4 +378,4 @@ export async function teamRoutes(app:FastifyInstance){
386378
}
387379

388380
})
389-
}
381+
}

apps/backend/src/types/fastify.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ declare module 'fastify' {
1919
}
2020

2121
interface FastifyInstance {
22-
authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
22+
authenticate: (this: void, request: FastifyRequest, reply: FastifyReply) => Promise<void>;
2323
}
2424
}

0 commit comments

Comments
 (0)