Skip to content
Open
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
18 changes: 13 additions & 5 deletions apps/backend/src/__tests__/team.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type PrismaClient, TeamRole } from '@prisma/client';
import Fastify, { type FastifyInstance } from 'fastify';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import { PrismaClient, TeamRole } from '@prisma/client';

import { teamRoutes } from '../routes/team';

// ─── Shared mock data ─────────────────────────────────────────────────────────
Expand Down Expand Up @@ -92,7 +93,7 @@ const prismaMock = {

// ─── App factory ──────────────────────────────────────────────────────────────

let mockJwtVerify = vi.fn();
const mockJwtVerify = vi.fn();

async function buildApp(): Promise<FastifyInstance> {
const app = Fastify({ logger: false });
Expand All @@ -102,7 +103,14 @@ async function buildApp(): Promise<FastifyInstance> {
app.decorateRequest('jwtVerify', function () {
return mockJwtVerify();
});

app.decorate('authenticate', async function (request, reply) {
try {
const payload = await request.jwtVerify();
if (payload) {request.user = payload as typeof request.user;}
} catch {
return reply.status(401).send({ error: 'Unauthorized' });
}
});
await app.register(teamRoutes);
await app.ready();
return app;
Expand All @@ -118,7 +126,7 @@ async function createTeam(
app: FastifyInstance,
body: Record<string, unknown>,
authenticated = true,
) {
): Promise<Awaited<ReturnType<typeof app.inject>>> {
return app.inject({
method: 'POST',
url: '/',
Expand Down
9 changes: 3 additions & 6 deletions apps/backend/src/plugins/prisma.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import fp from 'fastify-plugin';
import { PrismaClient } from '@prisma/client';
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import fp from 'fastify-plugin';

import type { FastifyInstance } from 'fastify';

declare module 'fastify' {
interface FastifyInstance {
prisma: PrismaClient;
authenticate(
request: FastifyRequest,
reply: FastifyReply
): Promise<void>;
}
}

Expand Down
58 changes: 25 additions & 33 deletions apps/backend/src/routes/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {generateUniqueSlug} from '../utils/slug'
import { createTeamScehma,inviteMembers,updateTeam } from '../validations/team.validation';

import type {PlatformLink, PublicProfile} from '@devcard/shared'
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import type { FastifyInstance } from 'fastify';

type TeamMember = PublicProfile & {
teamRole: TeamRole
Expand All @@ -24,16 +24,11 @@ type TeamProfile = {
members: TeamMember[];
}

export async function teamRoutes(app:FastifyInstance){
app.post('/', { 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 { const payload = await request.jwtVerify(); if (payload) (request as any).user = payload; } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) }
}] }, async(request:FastifyRequest<{
Body: {name: string, description? : string, avatarUrl?: string }
}>, reply: FastifyReply) => {
const userId = (request.user as any).id;
export async function teamRoutes(app: FastifyInstance): Promise<void> {
app.post<{
Body: {name: string, description? : string, avatarUrl?: string }
}>('/',{ preHandler: [app.authenticate] }, async (request, reply): Promise<void> => {
const userId = request.user.id;
const parsed = createTeamScehma.safeParse(request.body);
if(!parsed.success){
return reply.status(400).send({error: 'Bad request'})
Expand All @@ -48,7 +43,7 @@ export async function teamRoutes(app:FastifyInstance){

try {
const team = await app.prisma.$transaction(async (tx) => {
const team = await tx.team.create({
const createdTeam = await tx.team.create({
data: {
name,
slug: finalSlug,
Expand All @@ -60,13 +55,13 @@ export async function teamRoutes(app:FastifyInstance){

await tx.teamMember.create({
data: {
teamId : team.id,
teamId : createdTeam.id,
userId,
role: TeamRole.OWNER,
joinedAt: new Date(),
}
})
return team
return createdTeam
})
return reply.status(201).send(team)

Expand All @@ -91,7 +86,7 @@ export async function teamRoutes(app:FastifyInstance){
}
})

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

try {
Expand Down Expand Up @@ -149,22 +144,17 @@ export async function teamRoutes(app:FastifyInstance){
members
}

return response;
return reply.status(200).send(response);
} catch (error) {
app.log.error(error);
return reply.status(500).send('Database query failed')
}

})

app.post('/:slug/members', { 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 { const payload = await request.jwtVerify(); if (payload) (request as any).user = payload; } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) }
}] }, async(request: FastifyRequest<{Params: {slug:string}, Body:{username:string}}>, reply: FastifyReply) => {
app.post<{Params: {slug:string}, Body:{username:string}}>('/:slug/members', { preHandler: [app.authenticate] }, async (request, reply): Promise<void> => {
const paramsSlug = request.params.slug;
const userId = (request.user as any).id;
const userId = request.user.id;
const parsed = inviteMembers.safeParse(request.body);
if(!parsed.success){
return reply.status(400).send({error: 'Bad request'})
Expand Down Expand Up @@ -224,10 +214,10 @@ export async function teamRoutes(app:FastifyInstance){
}
})

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) => {
app.delete<{Params: {slug: string, userId: string}}>('/:slug/members/:userId',{ preHandler: [app.authenticate] }, async (request, reply): Promise<void> => {
const paramsSlug = request.params.slug
const paramsUserId = request.params.userId
const userID = (request.user as any).id;
const userId = request.user.id;
const teamDetails = await app.prisma.team.findUnique(
{where: {slug: paramsSlug},
include: {
Expand All @@ -251,8 +241,8 @@ export async function teamRoutes(app:FastifyInstance){
});
}

const isOwner = teamDetails.ownerId === userID;
const isSelfRemove = paramsUserId === userID;
const isOwner = teamDetails.ownerId === userId;
const isSelfRemove = paramsUserId === userId;

if (!isOwner && !isSelfRemove) {
return reply.status(403).send({
Expand Down Expand Up @@ -286,8 +276,10 @@ export async function teamRoutes(app:FastifyInstance){
}
})

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) => {
const userId = (request.user as any).id;
app.patch<{Params: {slug: string},Body: {description?:string, name?:string, avatarUrl?:string}}>('/:slug',{ preHandler: [app.authenticate

] }, async (request, reply): Promise<void> => {
const userId = request.user.id;
const paramsSlug = request.params.slug;
const parsed = updateTeam.safeParse(request.body);
if(!parsed.success){
Expand Down Expand Up @@ -328,8 +320,8 @@ export async function teamRoutes(app:FastifyInstance){

})

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) => {
const userId = (request.user as any).id;
app.delete<{Params:{slug: string}}>('/:slug',{ preHandler: [app.authenticate] }, async (request, reply): Promise<void> => {
const userId = request.user.id;
const paramsSlug = request.params.slug;


Expand Down Expand Up @@ -364,7 +356,7 @@ export async function teamRoutes(app:FastifyInstance){
}
})

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

})
}
}
2 changes: 1 addition & 1 deletion apps/backend/src/types/fastify.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ declare module 'fastify' {
}

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