Skip to content
Merged
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
69 changes: 39 additions & 30 deletions apps/backend/src/routes/profiles.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { getProfileUrl } from '@devcard/shared';
import { updateProfileSchema, createLinkSchema, reorderLinksSchema } from '../utils/validators.js';
import { getErrorMessage } from '../utils/error.util.js';
import * as profileService from '../services/profileService'
import { Prisma } from '@prisma/client';

// ── Response types ────────────────────────────────────────────────────────────
// Declared explicitly so the API contract is visible without tracing through
// Prisma's generic return types. Follows the convention in public.ts.
import * as profileService from '../services/profileService';
import { updateProfileSchema, createLinkSchema, reorderLinksSchema } from '../utils/validators.js';

import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
// eslint-disable-next-line @typescript-eslint/no-unused-vars

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ProfileUpdateResponse is now suppressed with an eslint-disable for being unused rather than removed. The PR description says no behavior changes and this type isn't referenced anywhere — prefer deleting the dead type outright over silencing the linter. If it's intentionally kept as living API documentation, a one-line comment saying so would be clearer than the bare disable.

type ProfileUpdateResponse = {
id: string;
email: string;
Expand All @@ -21,38 +18,40 @@ type ProfileUpdateResponse = {
accentColor: string;
};

export async function profileRoutes(app: FastifyInstance) {
export async function profileRoutes(app: FastifyInstance): Promise<void> {
// All profile routes require auth
app.addHook('preHandler', async (request, reply) => {
const server = request.server as any;
const server = request.server;
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);
if (typeof app.authenticate === 'function') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Now that the casts are gone, the typing makes the redundancy here visible: request.server is the app instance, so the server?.authenticate branch (line 25) and this app.authenticate branch are the same decorator — and FastifyInstance.authenticate is now declared non-optional in fastify.d.ts, so the typeof === 'function' guards and the jwtVerify fallback are effectively dead code. Out of scope for a pure type-safety PR, but worth a follow-up to collapse this to a single await app.authenticate(request, reply).

await app.authenticate(request, reply);
return;
}
Comment on lines +24 to 32
try {
await request.jwtVerify();
} catch (e) {
} catch (_e) {
reply.status(401).send({ error: 'Unauthorized' });
}
});

// ─── Get Own Profile ───

app.get('/me', async (request: FastifyRequest, reply: FastifyReply) => {
const userId = (request.user as any).id;
const userId = request.user.id;
const user = await profileService.getOwnProfile(app, userId)
if (!user) return reply.status(404).send({ error: 'User not found' })
if (!user) {
return reply.status(404).send({ error: 'User not found' });
}
return user
});

// ─── Update Profile ───

app.put('/me', async (request: FastifyRequest, reply: FastifyReply) => {
const userId = (request.user as any).id;
const userId = request.user.id;
const parsed = updateProfileSchema.safeParse(request.body);

if (!parsed.success) {
Expand All @@ -79,8 +78,10 @@ export async function profileRoutes(app: FastifyInstance) {
try {
const response = await profileService.updateProfile(app, userId, parsed.data)
return response
} catch (err: any) {
if (err?.code === 'P2002') return reply.status(409).send({ error: 'Username already taken' })
} catch (err: unknown) {
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good change. Since Prisma is now imported solely for this guard, just confirm @prisma/client is generated in CI before typecheck/lint runs (the import resolves to generated types) — otherwise lint/typecheck could fail on a clean checkout.

return reply.status(409).send({ error: 'Username already taken' });
}
app.log.error({ err }, 'DB error in PUT /profiles/me')
return reply.status(500).send({ error: 'Internal server error' })
}
Expand All @@ -89,7 +90,7 @@ export async function profileRoutes(app: FastifyInstance) {
// ─── Add Platform Link ───

app.post('/me/links', async (request: FastifyRequest, reply: FastifyReply) => {
const userId = (request.user as any).id;
const userId = request.user.id;
const parsed = createLinkSchema.safeParse(request.body);

if (!parsed.success) {
Expand All @@ -99,7 +100,7 @@ export async function profileRoutes(app: FastifyInstance) {
try {
const link = await profileService.createPlatformLink(app, userId, parsed.data)
return reply.status(201).send(link)
} catch (err: any) {
} catch (err: unknown) {
app.log.error({ err }, 'Failed to create platform link')
return reply.status(500).send({ error: 'Internal server error' })
}
Expand All @@ -108,16 +109,20 @@ export async function profileRoutes(app: FastifyInstance) {
// ─── Update Platform Link ───

app.put('/me/links/:id', async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
const userId = (request.user as any).id;
const userId = request.user.id;
const { id } = request.params;

const parsedReq = createLinkSchema.safeParse(request.body)
if (!parsedReq.success) return reply.status(400).send({ error: 'Validation failed', details: parsedReq.error.flatten() })
if (!parsedReq.success) {
return reply.status(400).send({ error: 'Validation failed', details: parsedReq.error.flatten() });
}
try {
const updated = await profileService.updatePlatformLink(app, userId, id, parsedReq.data)
if (!updated) return reply.status(404).send({ error: 'Link not found' })
if (!updated) {
return reply.status(404).send({ error: 'Link not found' });
}
return updated
} catch (err: any) {
} catch (err: unknown) {
app.log.error({ err }, 'Failed to update platform link')
return reply.status(500).send({ error: 'Internal server error' })
}
Expand All @@ -126,14 +131,16 @@ export async function profileRoutes(app: FastifyInstance) {
// ─── Delete Platform Link ───

app.delete('/me/links/:id', async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
const userId = (request.user as any).id;
const userId = request.user.id;
const { id } = request.params;

try {
const deleted = await profileService.deletePlatformLink(app, userId, id)
if (!deleted) return reply.status(404).send({ error: 'Link not found' })
if (!deleted) {
return reply.status(404).send({ error: 'Link not found' });
}
return reply.status(204).send()
} catch (err: any) {
} catch (err: unknown) {
app.log.error({ err }, 'Failed to delete platform link')
return reply.status(500).send({ error: 'Internal server error' })
}
Expand All @@ -142,13 +149,15 @@ export async function profileRoutes(app: FastifyInstance) {
// ─── Reorder Links ───

app.put('/me/links/reorder', async (request: FastifyRequest, reply: FastifyReply) => {
const userId = (request.user as any).id;
const userId = request.user.id;
const parsedReq = reorderLinksSchema.safeParse(request.body)
if (!parsedReq.success) return reply.status(400).send({ error: 'Validation failed', details: parsedReq.error.flatten() })
if (!parsedReq.success) {
return reply.status(400).send({ error: 'Validation failed', details: parsedReq.error.flatten() });
}
try {
const resp = await profileService.reorderLinks(app, userId, parsedReq.data.links)
return resp
} catch (err: any) {
} catch (err: unknown) {
app.log.error({ err }, 'Failed to reorder links')
return reply.status(500).send({ error: 'Internal server error' })
}
Expand Down