From e1f3804c02e3e06c8eb9bc77925df581935814d3 Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 21 Jun 2026 23:02:53 +0530 Subject: [PATCH 1/5] feat(profile): store previous usernames and 301 redirect old public URLs to new username --- .../migration.sql | 19 +++ apps/backend/prisma/schema.prisma | 14 ++ apps/backend/src/__tests__/redirects.test.ts | 152 ++++++++++++++++++ apps/backend/src/routes/public.ts | 42 +++++ apps/backend/src/services/profileService.ts | 30 +++- apps/web/src/pages/ProfilePage.tsx | 6 +- 6 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 apps/backend/prisma/migrations/20260621223000_add_username_redirects/migration.sql create mode 100644 apps/backend/src/__tests__/redirects.test.ts diff --git a/apps/backend/prisma/migrations/20260621223000_add_username_redirects/migration.sql b/apps/backend/prisma/migrations/20260621223000_add_username_redirects/migration.sql new file mode 100644 index 00000000..fa874d84 --- /dev/null +++ b/apps/backend/prisma/migrations/20260621223000_add_username_redirects/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "username_redirects" ( + "id" TEXT NOT NULL, + "old_username" TEXT NOT NULL, + "new_username" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "username_redirects_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "username_redirects_old_username_key" ON "username_redirects"("old_username"); + +-- CreateIndex +CREATE INDEX "username_redirects_old_username_idx" ON "username_redirects"("old_username"); + +-- AddForeignKey +ALTER TABLE "username_redirects" ADD CONSTRAINT "username_redirects_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 44190c5d..a3fa57ac 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -43,6 +43,7 @@ model User { attendedEvents EventAttendee[] ownedTeams Team[] @relation("TeamOwner") teamMemberships TeamMember[] @relation("TeamMember") + usernameRedirects UsernameRedirect[] @@map("users") } @@ -260,4 +261,17 @@ model TeamMember{ @@unique([userId, teamId]) @@index([userId]) @@map("team_members") +} + +model UsernameRedirect { + id String @id @default(uuid()) + oldUsername String @unique @map("old_username") + newUsername String @map("new_username") + userId String @map("user_id") + createdAt DateTime @default(now()) @map("created_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([oldUsername]) + @@map("username_redirects") } \ No newline at end of file diff --git a/apps/backend/src/__tests__/redirects.test.ts b/apps/backend/src/__tests__/redirects.test.ts new file mode 100644 index 00000000..d6e26ef5 --- /dev/null +++ b/apps/backend/src/__tests__/redirects.test.ts @@ -0,0 +1,152 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import Fastify from 'fastify'; +import { publicRoutes } from '../routes/public.js'; +import type { PrismaClient } from '@prisma/client'; + +const mockPrisma = { + usernameRedirect: { + findUnique: vi.fn(), + }, + user: { + findUnique: vi.fn(), + }, + cardView: { + create: vi.fn().mockReturnValue({ catch: vi.fn() }), + }, + followLog: { + findMany: vi.fn().mockResolvedValue([]), + }, +}; + +async function buildApp() { + const app = Fastify(); + app.decorate('prisma', mockPrisma as unknown as PrismaClient); + app.register(publicRoutes, { prefix: '/api/public' }); + await app.ready(); + return app; +} + +describe('Username Redirects Routing', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('performs a 301 redirect to the new username for recently changed usernames', async () => { + const app = buildApp(); + mockPrisma.usernameRedirect.findUnique.mockImplementation(({ where }: any) => { + if (where.oldUsername === 'olduser') { + return Promise.resolve({ + oldUsername: 'olduser', + newUsername: 'newuser', + createdAt: new Date(), + }); + } + return Promise.resolve(null); + }); + + const appInstance = await app; + const res = await appInstance.inject({ + method: 'GET', + url: '/api/public/olduser', + }); + + expect(res.statusCode).toBe(301); + expect(res.headers.location).toBe('/api/public/newuser'); + }); + + it('does not redirect and returns 404/200 if username is not in redirects', async () => { + const app = buildApp(); + mockPrisma.usernameRedirect.findUnique.mockResolvedValue(null); + mockPrisma.user.findUnique.mockResolvedValue(null); + + const appInstance = await app; + const res = await appInstance.inject({ + method: 'GET', + url: '/api/public/nonexistent', + }); + + expect(res.statusCode).toBe(404); + }); + + it('does not redirect if the redirect is older than 90 days', async () => { + const app = buildApp(); + const ninetyOneDaysAgo = new Date(); + ninetyOneDaysAgo.setDate(ninetyOneDaysAgo.getDate() - 91); + + mockPrisma.usernameRedirect.findUnique.mockResolvedValue({ + oldUsername: 'olduser', + newUsername: 'newuser', + createdAt: ninetyOneDaysAgo, + }); + mockPrisma.user.findUnique.mockResolvedValue(null); + + const appInstance = await app; + const res = await appInstance.inject({ + method: 'GET', + url: '/api/public/olduser', + }); + + expect(res.statusCode).toBe(404); + }); + + it('resolves multi-step redirect chains recursively', async () => { + const app = buildApp(); + mockPrisma.usernameRedirect.findUnique.mockImplementation(({ where }: any) => { + if (where.oldUsername === 'userA') { + return Promise.resolve({ + oldUsername: 'userA', + newUsername: 'userB', + createdAt: new Date(), + }); + } + if (where.oldUsername === 'userB') { + return Promise.resolve({ + oldUsername: 'userB', + newUsername: 'userC', + createdAt: new Date(), + }); + } + return Promise.resolve(null); + }); + + const appInstance = await app; + const res = await appInstance.inject({ + method: 'GET', + url: '/api/public/userA/qr?size=300', + }); + + expect(res.statusCode).toBe(301); + expect(res.headers.location).toBe('/api/public/userC/qr?size=300'); + }); + + it('guards against infinite loops in redirect chains', async () => { + const app = buildApp(); + mockPrisma.usernameRedirect.findUnique.mockImplementation(({ where }: any) => { + if (where.oldUsername === 'userA') { + return Promise.resolve({ + oldUsername: 'userA', + newUsername: 'userB', + createdAt: new Date(), + }); + } + if (where.oldUsername === 'userB') { + return Promise.resolve({ + oldUsername: 'userB', + newUsername: 'userA', + createdAt: new Date(), + }); + } + return Promise.resolve(null); + }); + mockPrisma.user.findUnique.mockResolvedValue(null); + + const appInstance = await app; + const res = await appInstance.inject({ + method: 'GET', + url: '/api/public/userA', + }); + + expect(res.statusCode).toBe(301); + expect(res.headers.location).toBe('/api/public/userB'); + }); +}); diff --git a/apps/backend/src/routes/public.ts b/apps/backend/src/routes/public.ts index 4333b9cd..bcf1eb5e 100644 --- a/apps/backend/src/routes/public.ts +++ b/apps/backend/src/routes/public.ts @@ -11,6 +11,48 @@ const MAX_QR_SIZE = 2048; const CACHE_CONTROL_HEADER = 'public, max-age=300, stale-while-revalidate=60'; export async function publicRoutes(app: FastifyInstance): Promise { + // ─── Username Redirect Hook ─── + app.addHook('preHandler', async (request, reply) => { + const params = request.params as Record | undefined; + if (!params || !params.username) { + return; + } + + const { username } = params; + + const ninetyDaysAgo = new Date(); + ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90); + + let current = username; + let redirect = await app.prisma.usernameRedirect.findUnique({ + where: { oldUsername: current }, + }); + + const visited = new Set(); + + while (redirect && redirect.createdAt >= ninetyDaysAgo && !visited.has(current)) { + visited.add(current); + current = redirect.newUsername; + redirect = await app.prisma.usernameRedirect.findUnique({ + where: { oldUsername: current }, + }); + } + + if (current !== username) { + const urlParts = request.url.split('?'); + const path = urlParts[0]; + const query = urlParts[1] ? `?${urlParts[1]}` : ''; + + const pathSegments = path.split('/'); + const index = pathSegments.indexOf(username); + if (index !== -1) { + pathSegments[index] = current; + const newPath = pathSegments.join('/') + query; + return reply.status(301).redirect(newPath); + } + } + }); + // ─── Public Profile ─────────────────────────────────────────────────────── /** * GET /api/u/:username diff --git a/apps/backend/src/services/profileService.ts b/apps/backend/src/services/profileService.ts index 4d300091..6159402b 100644 --- a/apps/backend/src/services/profileService.ts +++ b/apps/backend/src/services/profileService.ts @@ -29,9 +29,33 @@ export async function updateProfile(app: FastifyInstance, userId: string, data: const currentUser = await app.prisma.user.findUnique({ where: { id: userId }, select: { username: true } }) try { - const response = await app.prisma.user.update({ where: { id: userId }, data, select: { - id: true, email: true, username: true, displayName: true, bio: true, pronouns: true, role: true, company: true, avatarUrl: true, accentColor: true - } }) + const isUsernameChanging = data.username && currentUser && data.username !== currentUser.username; + + const response = await app.prisma.$transaction(async (tx) => { + if (isUsernameChanging) { + // Delete any existing redirects where the oldUsername is the new username + await tx.usernameRedirect.deleteMany({ + where: { oldUsername: data.username }, + }); + + // Record the redirect from the old username to the new username + await tx.usernameRedirect.create({ + data: { + oldUsername: currentUser.username, + newUsername: data.username, + userId, + }, + }); + } + + return tx.user.update({ + where: { id: userId }, + data, + select: { + id: true, email: true, username: true, displayName: true, bio: true, pronouns: true, role: true, company: true, avatarUrl: true, accentColor: true + } + }); + }); if (app.redis && currentUser) { app.redis.del(`profile:${currentUser.username}`).catch((err: unknown) => diff --git a/apps/web/src/pages/ProfilePage.tsx b/apps/web/src/pages/ProfilePage.tsx index 94a84f54..7a0b3db9 100644 --- a/apps/web/src/pages/ProfilePage.tsx +++ b/apps/web/src/pages/ProfilePage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { useParams, Link } from 'react-router-dom'; +import { useParams, Link, useNavigate } from 'react-router-dom'; import { PLATFORMS, getProfileUrl } from '../shared'; import type { PublicProfile } from '../shared'; import { apiFetch } from '../lib/api'; @@ -15,6 +15,7 @@ const platformColors: Record = { export default function ProfilePage() { const { username } = useParams<{ username: string }>(); + const navigate = useNavigate(); const [profile, setProfile] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); @@ -33,6 +34,9 @@ export default function ProfilePage() { .then((data) => { setProfile(data); setError(null); + if (data.username && data.username !== username) { + navigate(`/u/${data.username}`, { replace: true }); + } }) .catch(() => { setProfile(null); From 1c9957887c3d069719633266cfcaa1909cac7386 Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 22 Jun 2026 22:27:13 +0530 Subject: [PATCH 2/5] fix(backend): correct recursive loop-guard check in username redirects --- apps/backend/src/routes/public.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/routes/public.ts b/apps/backend/src/routes/public.ts index bcf1eb5e..dcbcc2dd 100644 --- a/apps/backend/src/routes/public.ts +++ b/apps/backend/src/routes/public.ts @@ -30,7 +30,7 @@ export async function publicRoutes(app: FastifyInstance): Promise { const visited = new Set(); - while (redirect && redirect.createdAt >= ninetyDaysAgo && !visited.has(current)) { + while (redirect && redirect.createdAt >= ninetyDaysAgo && !visited.has(redirect.newUsername)) { visited.add(current); current = redirect.newUsername; redirect = await app.prisma.usernameRedirect.findUnique({ From 4152dbe6e4236ddda31097eeeb135d363acd72af Mon Sep 17 00:00:00 2001 From: Test User Date: Wed, 15 Jul 2026 18:11:23 +0530 Subject: [PATCH 3/5] docs: add contributing guide and fix event details organizer id --- CONTRIBUTING.md | 236 ++++++--- apps/backend/package-lock.json | 33 +- apps/backend/src/__tests__/analytics.test.ts | 31 +- apps/backend/src/__tests__/cards.test.ts | 18 +- apps/backend/src/__tests__/event.test.ts | 32 +- apps/backend/src/__tests__/follow.test.ts | 2 +- .../backend/src/__tests__/oauth-scope.test.ts | 2 + apps/backend/src/__tests__/profiles.test.ts | 10 + apps/backend/src/__tests__/public.test.ts | 10 +- apps/backend/src/__tests__/redirects.test.ts | 6 +- apps/backend/src/__tests__/team.test.ts | 16 +- apps/backend/src/env.ts | 2 +- apps/backend/src/plugins/prisma.ts | 3 +- apps/backend/src/plugins/redis.ts | 3 +- apps/backend/src/routes/cards.ts | 3 + apps/backend/src/routes/event.ts | 491 ++++++++++-------- apps/backend/src/routes/nfc.ts | 41 +- apps/backend/src/routes/profiles.ts | 4 +- apps/backend/src/routes/public.ts | 4 + apps/backend/src/routes/team.ts | 14 +- apps/backend/src/services/authService.ts | 2 +- apps/backend/src/services/cardService.ts | 6 +- apps/backend/src/services/profileService.ts | 16 +- apps/backend/src/services/publicService.ts | 2 +- apps/backend/src/utils/encryption.ts | 2 +- apps/backend/src/utils/error.util.ts | 9 +- apps/backend/src/utils/slug.ts | 4 +- 27 files changed, 580 insertions(+), 422 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bdc73b20..88c860ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,116 +1,192 @@ # Contributing to DevCard -

- - Discord Server - -

+Thank you for your interest in contributing to **DevCard**! DevCard is an open-source developer profile exchange platform that aggregates your developer profiles into a single shareable QR code. -**Join the community** — ask questions, get help, discuss ideas, and meet other contributors on our [Discord server](https://discord.gg/QueQN83wn). +By contributing, you help make networking easier and more accessible for developers around the world. Please take a moment to review this guide before getting started. -## Development Setup +--- -### Prerequisites +## Table of Contents +1. [Project Overview](#project-overview) +2. [Prerequisites](#prerequisites) +3. [Local Setup](#local-setup) +4. [Branch Naming Conventions](#branch-naming-conventions) +5. [Pull Request Process & Checklist](#pull-request-process--checklist) +6. [Issue Labels Guide](#issue-labels-guide) +7. [Coding Standards](#coding-standards) -- **Node.js** >= 20 -- **npm** >= 10 (bundled with Node.js) -- **Docker** & Docker Compose -- **React Native** dev environment — follow the [official setup guide](https://reactnative.dev/docs/environment-setup) +--- -### Getting Started +## Project Overview -```bash -# 1. Fork and clone the repo -git clone https://github.com/Dev-Card/DevCard.git -cd devcard - -# 2. Install dependencies -npm install # root (orchestrator) -npm --prefix packages/shared install # shared types/utils -npm --prefix apps/backend install # backend API -npm --prefix apps/web install # web app -npm --prefix apps/mobile install # mobile app (if working on mobile) - -# 3. Start PostgreSQL + Redis -docker compose up -d +DevCard is structured as a monorepo containing the web frontend, mobile frontend, backend API, and a shared packages library. -# 4. Configure environment -cp .env.example .env -# Edit .env with your OAuth credentials +```text +devcard/ +├── apps/ +│ ├── backend/ # Fastify API (TypeScript, Prisma ORM, Vitest) +│ ├── mobile/ # React Native mobile app (Bare Workflow, Jest) +│ └── web/ # React + Vite web app (TypeScript, ESLint) +├── packages/ +│ └── shared/ # Shared types, platform registry, and utility functions +├── docker/ # Docker files and configurations +├── docker-compose.yml # Runs PostgreSQL and Redis services +└── package.json # Root orchestrator (npm scripts to run workspace tasks) +``` -# 5. Run database migrations and seed -npm run db:migrate -npm run db:seed +--- -# 6. Start development -npm run dev:backend # Backend API on :3000 -npm run dev:mobile # React Native app -``` +## Prerequisites + +To run DevCard locally, you will need the following installed: + +* **Node.js**: `v20.x` or `v22.x` (Long Term Support recommended) +* **npm**: `v10.x` or higher (usually bundled with Node.js) +* **Docker & Docker Compose**: Used to run PostgreSQL 16 and Redis 7 databases locally. +* **React Native / Mobile Environment**: + * **React Native CLI** (Bare workflow environment setup) — follow the [official React Native setup guide](https://reactnative.dev/docs/environment-setup) for your OS (Android Studio / Xcode). + * **Expo CLI** (if doing secondary Expo testing or building). Note that the primary mobile app in `apps/mobile` is a bare React Native project. +* **Backend Runtime & Tools**: + * **PostgreSQL**: (Provided via Docker) + * **Redis**: (Provided via Docker) + +--- -### Running Tests +## Local Setup + +Follow these step-by-step instructions to get the project running on your local machine: + +### 1. Clone the Repository +Fork the repository on GitHub, then clone your fork: +```bash +git clone https://github.com/YOUR-USERNAME/DevCard.git +cd DevCard +``` -This project uses `npm` to run tests across different parts of the codebase. +### 2. Install Dependencies +Install all package dependencies from the root directory. This will install dependencies for all monorepo workspaces: +```bash +npm install +``` -#### Run all tests -To execute backend tests: +### 3. Start Database and Cache Services +Run Docker Compose to start PostgreSQL and Redis in the background: ```bash -npm run test +docker compose up -d ``` -#### apps/backend -The backend uses Vitest: +### 4. Configure Environment Variables +Copy the template `.env.example` in the root (or `apps/backend`) to `.env` inside `apps/backend/`: ```bash -npm --prefix apps/backend run test -npm --prefix apps/backend run test:watch +cp .env.example apps/backend/.env ``` -#### apps/mobile -The mobile app uses Jest: +Open `apps/backend/.env` and generate the required secure secrets: +* **JWT_SECRET**: Generate using: + ```bash + node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" + ``` +* **ENCRYPTION_KEY**: Generate using: + ```bash + node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" + ``` +Paste these values into `apps/backend/.env`. + +### 5. Run Database Migrations and Seed +Initialize your PostgreSQL database schemas and seed it with dummy developer profiles: ```bash -npm --prefix apps/mobile run test +# Run migrations +npm run db:migrate + +# Seed sample database data +npm run db:seed ``` -#### apps/web -Currently, the web app does not define a test script. -#### packages/shared -The shared package does not include test scripts. It only provides linting and type checking. +### 6. Run the Applications + +You can run individual parts of the project from the root directory using the following orchestrator scripts: + +* **Run Backend API**: + ```bash + npm run dev:backend + ``` + This starts the Fastify server (usually listening on `http://localhost:3000`). +* **Run Web App**: + ```bash + npm run dev:web + ``` + This starts the Vite-powered React web dashboard. +* **Run Mobile App**: + Make sure your Android Emulator or iOS Simulator is running, then execute: + ```bash + npm run dev:mobile + ``` + And in another terminal window to launch on Android: + ```bash + npm run android + ``` +--- -## Project Structure +## Branch Naming Conventions -``` -devcard/ -├── apps/backend/ # Fastify API (TypeScript) -├── apps/mobile/ # React Native mobile app -├── apps/web/ # SvelteKit web backup -└── packages/shared/ # Shared types, utils, platform registry -``` +We enforce prefix-based branch naming to keep the repository history organized. When creating a branch, use one of the following prefix structures: -## Coding Standards +* `feat/` — For new features or additions (e.g., `feat/add-github-oauth`) +* `fix/` — For bug fixes and patches (e.g., `fix/event-organizer-id`) +* `docs/` — For updates to documentation or guides (e.g., `docs/contributing-guide`) +* `chore/` — For build processes, dependency updates, or tool configurations (e.g., `chore/upgrade-prisma`) -- **TypeScript** for all new code -- **ESLint + Prettier** for formatting (run `npm run lint` before committing) -- **Conventional Commits** for commit messages (`feat:`, `fix:`, `docs:`, `chore:`) -- Write tests for new features and bug fixes +Use hyphens to separate words (kebab-case) and keep names concise. -## Pull Request Process +--- -1. Create a feature branch from `main`: `git checkout -b feat/your-feature` -2. Make your changes with clear, descriptive commits -3. Ensure all tests pass: `npm run test` -4. Ensure linting passes: `npm run lint` -5. Open a PR against `main` with a clear description of the change -6. Wait for review — maintainers will respond within 48 hours +## Pull Request Process & Checklist + +When you are ready to submit your changes, follow this process: + +### 1. PR Checklist +Before opening a Pull Request, please ensure you satisfy the following checklist: +- [ ] Code compiles and builds without errors. +- [ ] Linting passes: Run `npm run lint` from the root. +- [ ] Tests pass: Run `npm run test` (Vitest backend tests) and ensure zero failures. +- [ ] Your branch name matches our [Branch Naming Conventions](#branch-naming-conventions). +- [ ] Your commits use clear descriptions and follow [Conventional Commits](https://www.conventionalcommits.org/) format (e.g., `feat(auth): add GitHub login flow`). +- [ ] You have updated/added tests for any new features or bug fixes. +- [ ] Documentation has been updated if applicable. + +### 2. Submitting the PR +1. Push your branch to your GitHub fork: + ```bash + git push origin branch-name + ``` +2. Navigate to the main [DevCard Repository](https://github.com/Dev-Card/DevCard) and click **New Pull Request**. +3. Choose your fork and branch, write a clear title and description outlining: + * What problem does this PR solve? + * How was it resolved? + * Any testing steps or verification done. +4. Submit the PR and wait for a review from the maintainers. Reviews are usually conducted within 24–48 hours. + +--- -## Reporting Issues +## Issue Labels Guide -- Use GitHub Issues for bug reports and feature requests -- Include reproduction steps for bugs -- Search existing issues before creating a new one +We use specific labels to categorize and track issues. Here is a guide to what they mean: -## Code of Conduct +* `good-first-issue` — Welcoming issues for newcomers or first-time contributors. Usually has clear instructions. +* `bug` — A reproducible issue or error in the codebase. +* `enhancement` — A request for new features, optimizations, or enhancements. +* `documentation` — Work related to writing or updating READMEs, guides, or code docstrings. +* `help wanted` — Extra attention or specific expertise is requested to solve the issue. +* `gssoc24` / `hacktoberfest` — Labels indicating participation in open-source programs like GirlScript Summer of Code or Hacktoberfest. + +--- + +## Coding Standards -Be kind, inclusive, and constructive. We follow the [Contributor Covenant](https://www.contributor-covenant.org/). +* **TypeScript**: Use static typing wherever possible. Avoid using `any` and define proper interfaces/types. +* **Formatting**: We use ESLint and Prettier for code style consistency. Run `npm run lint` or format files directly in your IDE before committing. +* **Migrations**: Do not modify existing Prisma migrations. Create new migrations via `prisma migrate dev` if you modify `schema.prisma`. --- -Thank you for helping make DevCard better! 🎉 +Thank you for contributing to DevCard! 🚀 diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json index 832b4eee..73d0cd7c 100644 --- a/apps/backend/package-lock.json +++ b/apps/backend/package-lock.json @@ -65,29 +65,6 @@ "resolved": "../../packages/shared", "link": true }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -1651,6 +1628,7 @@ "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", @@ -1800,6 +1778,7 @@ "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.1", @@ -2288,6 +2267,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2496,6 +2476,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3089,6 +3070,7 @@ "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -3243,6 +3225,7 @@ "integrity": "sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@package-json/types": "^0.0.12", "@typescript-eslint/types": "^8.56.0", @@ -4666,6 +4649,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4812,6 +4796,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "6.19.3", "@prisma/engines": "6.19.3" @@ -5548,6 +5533,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5672,6 +5658,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/apps/backend/src/__tests__/analytics.test.ts b/apps/backend/src/__tests__/analytics.test.ts index 4f0d07ae..ff9525c7 100644 --- a/apps/backend/src/__tests__/analytics.test.ts +++ b/apps/backend/src/__tests__/analytics.test.ts @@ -1,3 +1,6 @@ +import Fastify, { + type FastifyInstance, +} from 'fastify'; import { describe, it, @@ -7,13 +10,11 @@ import { vi, } from 'vitest'; -import Fastify, { - type FastifyInstance, -} from 'fastify'; + +import { analyticsRoutes } from '../routes/analytics'; import type { PrismaClient } from '@prisma/client'; -import { analyticsRoutes } from '../routes/analytics'; // ─── Shared mock data ──────────────────────────────────────────────────────── @@ -30,11 +31,12 @@ const prismaMock = { followLog: { count: vi.fn(), }, + $queryRaw: vi.fn(), }; // ─── App factory ───────────────────────────────────────────────────────────── -let mockJwtVerify = vi.fn(); +const mockJwtVerify = vi.fn(); async function buildApp(): Promise { const app = Fastify({ @@ -157,22 +159,9 @@ describe( ] ); - prismaMock.cardView.groupBy.mockResolvedValue( - [ - { - viewerId: - 'u1', - viewerIp: - null, - }, - { - viewerId: - 'u2', - viewerIp: - null, - }, - ] - ); + prismaMock.$queryRaw.mockResolvedValue([ + { count: 2n } + ]); const res = await app.inject( diff --git a/apps/backend/src/__tests__/cards.test.ts b/apps/backend/src/__tests__/cards.test.ts index a8d78e9c..ad4f5012 100644 --- a/apps/backend/src/__tests__/cards.test.ts +++ b/apps/backend/src/__tests__/cards.test.ts @@ -262,7 +262,7 @@ describe('PUT /api/cards/:id/update — card metadata', () => { const app = await buildApp(); const res = await app.inject({ method: 'PUT', - url: `/api/cards/${CARD_ID}/update`, + url: `/api/cards/${CARD_ID}`, payload: { title: 'Renamed', visibility: 'UNLISTED', qrEnabled: false }, }); @@ -280,7 +280,7 @@ describe('PUT /api/cards/:id/update — card metadata', () => { const app = await buildApp(); const res = await app.inject({ method: 'PUT', - url: `/api/cards/${CARD_ID}/update`, + url: `/api/cards/${CARD_ID}`, payload: { title: 'Renamed' }, }); @@ -292,7 +292,7 @@ describe('PUT /api/cards/:id/update — card metadata', () => { const app = await buildApp(); const res = await app.inject({ method: 'PUT', - url: `/api/cards/${CARD_ID}/update`, + url: `/api/cards/${CARD_ID}`, payload: {}, }); @@ -308,7 +308,7 @@ describe('PUT /api/cards/:id/update — card metadata', () => { const app = await buildApp(); const res = await app.inject({ method: 'PUT', - url: `/api/cards/${CARD_ID}/update`, + url: `/api/cards/${CARD_ID}`, payload: { title: 'Renamed' }, }); @@ -420,7 +420,7 @@ describe('DELETE /api/cards/:id/delete', () => { mockPrisma.card.delete.mockResolvedValue(mockCard); const app = await buildApp(); - const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}/delete` }); + const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}` }); expect(res.statusCode).toBe(204); expect(mockPrisma.card.delete).toHaveBeenCalledWith({ where: { id: CARD_ID } }); @@ -439,7 +439,7 @@ describe('DELETE /api/cards/:id/delete', () => { mockPrisma.card.delete.mockResolvedValue(mockCard); const app = await buildApp(); - const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}/delete` }); + const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}` }); expect(res.statusCode).toBe(204); expect(mockPrisma.card.update).toHaveBeenCalledWith({ @@ -453,7 +453,7 @@ describe('DELETE /api/cards/:id/delete', () => { mockPrisma.card.findFirst.mockResolvedValue(null); const app = await buildApp(); - const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}/delete` }); + const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}` }); expect(res.statusCode).toBe(404); expect(mockPrisma.card.delete).not.toHaveBeenCalled(); @@ -464,7 +464,7 @@ describe('DELETE /api/cards/:id/delete', () => { mockPrisma.card.count.mockResolvedValue(1); const app = await buildApp(); - const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}/delete` }); + const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}` }); expect(res.statusCode).toBe(400); expect(res.json().error).toBe('Cannot delete the last remaining card. A user must have at least one card.'); @@ -477,7 +477,7 @@ describe('DELETE /api/cards/:id/delete', () => { mockPrisma.card.delete.mockRejectedValue(new Error('Deadlock detected')); const app = await buildApp(); - const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}/delete` }); + const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}` }); expect(res.statusCode).toBe(500); }); diff --git a/apps/backend/src/__tests__/event.test.ts b/apps/backend/src/__tests__/event.test.ts index 44806af1..4a9cfe4c 100644 --- a/apps/backend/src/__tests__/event.test.ts +++ b/apps/backend/src/__tests__/event.test.ts @@ -1,8 +1,10 @@ +import Fastify, { type FastifyInstance } from 'fastify'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import Fastify, { FastifyInstance } from 'fastify'; -import { PrismaClient } from '@prisma/client'; + import { eventRoutes } from '../routes/event'; +import type { PrismaClient } from '@prisma/client'; + // ─── Shared mock data ──────────────────────────────────────────────────────── const MOCK_USER_ID = 'user-uuid-001'; @@ -64,7 +66,7 @@ const prismaMock = { // // This mirrors the real app setup without touching a real DB or real JWT keys. -let mockJwtVerify = vi.fn(); +const mockJwtVerify = vi.fn(); async function buildApp(): Promise { const app = Fastify({ logger: false }); @@ -78,6 +80,15 @@ async function buildApp(): Promise { return mockJwtVerify(); }); + app.decorate('authenticate', async function (request: any, reply: any) { + try { + const user = await request.jwtVerify(); + request.user = user; + } catch (err: any) { + return reply.status(401).send({ error: err.message || 'Unauthorized' }); + } + }); + // Register with the same prefix used in production (app.ts) so that // tests exercise routes at their real paths — /api/events, /api/events/:slug, etc. await app.register(eventRoutes, { prefix: '/api/events' }); @@ -252,6 +263,10 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, _count: { attendees: 42 }, + organizer: { + username: 'johndoe', + displayName: 'John Doe', + }, }); const res = await app.inject({ @@ -286,6 +301,10 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, _count: { attendees: 0 }, + organizer: { + username: 'johndoe', + displayName: 'John Doe', + }, }); const res = await app.inject({ @@ -494,6 +513,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, + _count: { attendees: 2 }, attendees: attendeeRows, }); @@ -522,6 +542,7 @@ describe('Events API', () => { it('200 — respects custom page and limit query params', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, + _count: { attendees: 1 }, attendees: [makeAttendeeRow(MOCK_OTHER_USER_PROFILE)], }); @@ -544,6 +565,7 @@ describe('Events API', () => { it('200 — caps limit at 50 even if higher value is requested', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, + _count: { attendees: 0 }, attendees: [], }); @@ -560,6 +582,7 @@ describe('Events API', () => { it('200 — treats page < 1 as page 1', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, + _count: { attendees: 0 }, attendees: [], }); @@ -576,6 +599,7 @@ describe('Events API', () => { it('200 — returns empty attendees list for event with no attendees', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, + _count: { attendees: 0 }, attendees: [], }); @@ -593,6 +617,7 @@ describe('Events API', () => { it('200 — public profiles do not leak sensitive fields', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, + _count: { attendees: 1 }, attendees: [makeAttendeeRow(MOCK_USER_PROFILE)], }); @@ -631,6 +656,7 @@ describe('Events API', () => { it('200 — attendees are ordered by joinedAt desc (latest first)', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, + _count: { attendees: 0 }, attendees: [], }); diff --git a/apps/backend/src/__tests__/follow.test.ts b/apps/backend/src/__tests__/follow.test.ts index 41830018..d0a44008 100644 --- a/apps/backend/src/__tests__/follow.test.ts +++ b/apps/backend/src/__tests__/follow.test.ts @@ -1,4 +1,4 @@ -import Fastify, { FastifyInstance } from 'fastify'; +import Fastify, { type FastifyInstance } from 'fastify'; import { describe, expect, it, vi, beforeAll, beforeEach, afterAll } from 'vitest'; import { followRoutes } from '../routes/follow.js'; diff --git a/apps/backend/src/__tests__/oauth-scope.test.ts b/apps/backend/src/__tests__/oauth-scope.test.ts index 0985dfa7..9a3c5773 100644 --- a/apps/backend/src/__tests__/oauth-scope.test.ts +++ b/apps/backend/src/__tests__/oauth-scope.test.ts @@ -45,6 +45,7 @@ function makeConnectState(userId: string): string { function buildConnectApp(mockPrisma: Partial) { const app = Fastify({ logger: false }); app.decorate('prisma', mockPrisma as PrismaClient); + // eslint-disable-next-line no-param-reassign app.decorate('authenticate', async (req: any) => { req.user = { id: USER_ID }; }); app.register(connectRoutes, { prefix: '/api/connect' }); return app.ready().then(() => app); @@ -55,6 +56,7 @@ function buildConnectApp(mockPrisma: Partial) { function buildFollowApp(mockPrisma: Partial) { const app = Fastify({ logger: false }); app.decorate('prisma', mockPrisma as PrismaClient); + // eslint-disable-next-line no-param-reassign app.decorate('authenticate', async (req: any) => { req.user = { id: USER_ID }; }); app.register(followRoutes, { prefix: '/api/follow' }); return app.ready().then(() => app); diff --git a/apps/backend/src/__tests__/profiles.test.ts b/apps/backend/src/__tests__/profiles.test.ts index 0633b841..9f3af348 100644 --- a/apps/backend/src/__tests__/profiles.test.ts +++ b/apps/backend/src/__tests__/profiles.test.ts @@ -28,6 +28,16 @@ const mockPrisma = { findFirst: vi.fn(), update: vi.fn(), }, + usernameRedirect: { + create: vi.fn(), + deleteMany: vi.fn(), + }, + $transaction: vi.fn(async (cb: any) => { + if (typeof cb === 'function') { + return cb(mockPrisma); + } + return cb; + }), }; async function buildApp():Promise { diff --git a/apps/backend/src/__tests__/public.test.ts b/apps/backend/src/__tests__/public.test.ts index a767b25d..8e825782 100644 --- a/apps/backend/src/__tests__/public.test.ts +++ b/apps/backend/src/__tests__/public.test.ts @@ -1,9 +1,13 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import Fastify from 'fastify'; import jwt from '@fastify/jwt'; +import Fastify from 'fastify'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; + import { publicRoutes } from '../routes/public.js'; +import { generateQRBuffer, generateQRSvg } from '../utils/qr.js'; + import type { PrismaClient } from '@prisma/client'; + // ── Mock QR utilities ───────────────────────────────────────────────────────── // Prevents real QR rasterisation (and any native canvas/image deps) from running // during unit tests. The stubs return minimal valid values that satisfy the @@ -13,8 +17,6 @@ vi.mock('../utils/qr.js', () => ({ generateQRSvg: vi.fn().mockResolvedValue('fake'), })); -import { generateQRBuffer, generateQRSvg } from '../utils/qr.js'; - const mockUser = { id: 'user-123', username: 'testuser', diff --git a/apps/backend/src/__tests__/redirects.test.ts b/apps/backend/src/__tests__/redirects.test.ts index d6e26ef5..75eaffb2 100644 --- a/apps/backend/src/__tests__/redirects.test.ts +++ b/apps/backend/src/__tests__/redirects.test.ts @@ -1,6 +1,8 @@ +import Fastify, { type FastifyInstance } from 'fastify'; import { describe, it, expect, beforeEach, vi } from 'vitest'; -import Fastify from 'fastify'; + import { publicRoutes } from '../routes/public.js'; + import type { PrismaClient } from '@prisma/client'; const mockPrisma = { @@ -18,7 +20,7 @@ const mockPrisma = { }, }; -async function buildApp() { +async function buildApp(): Promise { const app = Fastify(); app.decorate('prisma', mockPrisma as unknown as PrismaClient); app.register(publicRoutes, { prefix: '/api/public' }); diff --git a/apps/backend/src/__tests__/team.test.ts b/apps/backend/src/__tests__/team.test.ts index 350298a1..c55e380d 100644 --- a/apps/backend/src/__tests__/team.test.ts +++ b/apps/backend/src/__tests__/team.test.ts @@ -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 ───────────────────────────────────────────────────────── @@ -92,7 +93,7 @@ const prismaMock = { // ─── App factory ────────────────────────────────────────────────────────────── -let mockJwtVerify = vi.fn(); +const mockJwtVerify = vi.fn(); async function buildApp(): Promise { const app = Fastify({ logger: false }); @@ -103,6 +104,15 @@ async function buildApp(): Promise { return mockJwtVerify(); }); + app.decorate('authenticate', async function (request: any, reply: any) { + try { + const user = await request.jwtVerify(); + request.user = user; + } catch (err: any) { + return reply.status(401).send({ error: err.message || 'Unauthorized' }); + } + }); + await app.register(teamRoutes); await app.ready(); return app; diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts index ceb9222d..de5ee982 100644 --- a/apps/backend/src/env.ts +++ b/apps/backend/src/env.ts @@ -1,6 +1,6 @@ -import process from 'node:process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; + import dotenv from 'dotenv'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/apps/backend/src/plugins/prisma.ts b/apps/backend/src/plugins/prisma.ts index f6ebede8..ec2d74aa 100644 --- a/apps/backend/src/plugins/prisma.ts +++ b/apps/backend/src/plugins/prisma.ts @@ -1,5 +1,6 @@ -import fp from 'fastify-plugin'; import { PrismaClient } from '@prisma/client'; +import fp from 'fastify-plugin'; + import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; declare module 'fastify' { diff --git a/apps/backend/src/plugins/redis.ts b/apps/backend/src/plugins/redis.ts index 864b112f..881c289b 100644 --- a/apps/backend/src/plugins/redis.ts +++ b/apps/backend/src/plugins/redis.ts @@ -1,5 +1,6 @@ import fp from 'fastify-plugin'; import Redis from 'ioredis'; + import type { FastifyInstance } from 'fastify'; declare module 'fastify' { @@ -17,7 +18,7 @@ export const redisPlugin = fp(async (app: FastifyInstance) => { try { await redis.connect(); app.log.info('🔴 Redis connected'); - } catch (error) { + } catch { app.log.warn('⚠️ Redis connection failed — running without cache'); } diff --git a/apps/backend/src/routes/cards.ts b/apps/backend/src/routes/cards.ts index 8b8d6ff2..e7eb6ed1 100644 --- a/apps/backend/src/routes/cards.ts +++ b/apps/backend/src/routes/cards.ts @@ -110,6 +110,9 @@ export async function cardRoutes(app: FastifyInstance): Promise { if (!updated) {return reply.status(404).send({ error: 'Card not found' })} return updated } catch (error) { + if (hasErrorCode(error, 'NOT_FOUND')) { + return reply.status(404).send({ error: 'Card not found' }); + } if (hasErrorCode(error, 'OWNERSHIP')) {return reply.status(403).send({ error: 'One or more links do not belong to your account' })} return handleDbError(error, request, reply) } diff --git a/apps/backend/src/routes/event.ts b/apps/backend/src/routes/event.ts index 8d7bc566..87e87e27 100644 --- a/apps/backend/src/routes/event.ts +++ b/apps/backend/src/routes/event.ts @@ -1,22 +1,22 @@ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { createEventSchema, joinEventSchema} from '../validations/event.validation.js'; - -import {generateUniqueSlug} from '../utils/slug.js' +import { createEventSchema } from '../validations/event.validation.js'; +import { generateUniqueSlug } from '../utils/slug.js'; type EventDetails = { - id: string; - name: string; - slug: string; - location: string; - description: string | null; - organizerUsername: string; - organizerDisplayName: string; - startDate: Date; - endDate: Date; - createdAt: Date; - attendeesCount: number -} + id: string; + name: string; + slug: string; + location: string; + description: string | null; + organizerId: string; + organizerUsername: string; + organizerDisplayName: string; + startDate: Date; + endDate: Date; + createdAt: Date; + attendeesCount: number; +}; type AttendeePublicProfile = { id: string; @@ -27,17 +27,16 @@ type AttendeePublicProfile = { company: string | null; avatarUrl: string | null; accentColor: string; -} - +}; type PaginatedAttendeesResponse = { attendees: AttendeePublicProfile[]; pagination: { page: number; limit: number; - total: number; + total: number; }; -} +}; type EventWithAttendees = { _count: { @@ -55,231 +54,275 @@ type EventWithAttendees = { accentColor: string; }; }[]; -} +}; -export async function eventRoutes(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 { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } - }] }, async (request: FastifyRequest<{ - Body: { - name: string, - description?: string, - startDate: string, - location: string, - endDate: string, - isPublic?: boolean - }}>, reply: FastifyReply) => { - const userId = (request.user as any).id; - const parsed = createEventSchema.safeParse(request.body); - if(!parsed.success){ - return reply.status(400).send({error: 'Bad request'}) - } - - const {name, description, startDate, endDate, isPublic ,location} = parsed.data +export async function eventRoutes(app: FastifyInstance): Promise { + 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 { + await request.jwtVerify(); + } catch { + reply.status(401).send({ error: 'Unauthorized' }); + } + }], + }, async (request: FastifyRequest<{ + Body: { + name: string; + description?: string; + startDate: string; + location: string; + endDate: string; + isPublic?: boolean; + }; + }>, reply: FastifyReply) => { + const userId = (request.user as any).id; + const parsed = createEventSchema.safeParse(request.body); + if (!parsed.success) { + return reply.status(400).send({ error: 'Bad request' }); + } - let finalSlug = await generateUniqueSlug(name, async(slug) => { - const existing = await app.prisma.event.findUnique({where: {slug : slug}}) - - return !!existing - }) + const { name, description, startDate, endDate, isPublic, location } = parsed.data; - const startDateObj = new Date(startDate); - const endDateObj = new Date(endDate); + const finalSlug = await generateUniqueSlug(name, async (slug) => { + const existing = await app.prisma.event.findUnique({ where: { slug } }); + return !!existing; + }); - try { - const newEvent = await app.prisma.event.create({ - data: { - name, - description, - slug: finalSlug, - location: location, - startDate: startDateObj, - endDate: endDateObj, - isPublic: isPublic ?? true, - organizerId: userId - } - }) + const startDateObj = new Date(startDate); + const endDateObj = new Date(endDate); - return reply.status(201).send(newEvent); - } catch (error) { - app.log.error('Failed to create event'); - return reply.status(500).send({error: 'Failed to create event'}) - } - - }) + try { + const newEvent = await app.prisma.event.create({ + data: { + name, + description, + slug: finalSlug, + location, + startDate: startDateObj, + endDate: endDateObj, + isPublic: isPublic ?? true, + organizerId: userId, + }, + }); - //Returns event details and attendees count - app.get('/:slug', async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { - const paramsSlug = request.params.slug; - const details = await app.prisma.event.findUnique({ - where: { - slug: paramsSlug, - }, - include: { - _count: { - select: { - attendees: true - } - }, - organizer: { - select: { - username: true, - displayName: true - } - } - } - }) - if(!details){ - return reply.status(404).send({error: 'Event not found'}) - } + return reply.status(201).send(newEvent); + } catch { + app.log.error('Failed to create event'); + return reply.status(500).send({ error: 'Failed to create event' }); + } + }); - const response: EventDetails = { - id: details.id, - name: details.name, - slug: details.slug, - description: details.description, - location: details.location, - organizerUsername: details.organizer.username, - organizerDisplayName: details.organizer.displayName, - startDate: details.startDate, - endDate: details.endDate, - createdAt: details.createdAt, - attendeesCount: details._count.attendees - } - - return response; - }) + // Returns event details and attendees count + app.get('/:slug', async (request: FastifyRequest<{ Params: { slug: string } }>, reply: FastifyReply) => { + const paramsSlug = request.params.slug; + const details = await app.prisma.event.findUnique({ + where: { + slug: paramsSlug, + }, + include: { + _count: { + select: { + attendees: true, + }, + }, + organizer: { + select: { + username: true, + displayName: true, + }, + }, + }, + }); + if (!details) { + return reply.status(404).send({ error: 'Event not found' }); + } - app.post('/:slug/join', { 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; - const paramsSlug = request.params.slug; + const response: EventDetails = { + id: details.id, + name: details.name, + slug: details.slug, + description: details.description, + location: details.location, + organizerId: details.organizerId, + organizerUsername: details.organizer.username, + organizerDisplayName: details.organizer.displayName, + startDate: details.startDate, + endDate: details.endDate, + createdAt: details.createdAt, + attendeesCount: details._count.attendees, + }; + + return response; + }); - const event = await app.prisma.event.findUnique({ - where: { - slug: paramsSlug - } - }) + app.post('/:slug/join', { + 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 { + reply.status(401).send({ error: 'Unauthorized' }); + } + }], + }, async (request: FastifyRequest<{ Params: { slug: string } }>, reply: FastifyReply) => { + const userId = (request.user as any).id; + const paramsSlug = request.params.slug; - if(!event){ - return reply.status(404).send({error: 'Event not found'}) - } + const event = await app.prisma.event.findUnique({ + where: { + slug: paramsSlug, + }, + }); - try { - await app.prisma.eventAttendee.create({ - data: { - eventId: event.id, - userId: userId, - joinedAt: new Date() - } - }) + if (!event) { + return reply.status(404).send({ error: 'Event not found' }); + } - return reply.status(201).send({message: 'User joined successfully'}) - } catch (error:any) { - if(error.code === "P2002" ){ - return reply.status(409).send({error: 'Already joined'}) - } - app.log.error((error as Error).message); - return reply.status(500).send({error: 'Failed to join'}) - } + try { + await app.prisma.eventAttendee.create({ + data: { + eventId: event.id, + userId, + joinedAt: new Date(), + }, + }); - }) + return reply.status(201).send({ message: 'User joined successfully' }); + } catch (error: any) { + if (error.code === 'P2002') { + return reply.status(409).send({ error: 'Already joined' }); + } + app.log.error((error as Error).message); + return reply.status(500).send({ error: 'Failed to join' }); + } + }); - app.delete('/:slug/leave', { 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; - const paramsSlug = request.params.slug; + app.delete('/:slug/leave', { + 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 { + reply.status(401).send({ error: 'Unauthorized' }); + } + }], + }, async (request: FastifyRequest<{ Params: { slug: string } }>, reply: FastifyReply) => { + const userId = (request.user as any).id; + const paramsSlug = request.params.slug; - const event = await app.prisma.event.findUnique({ - where: { - slug: paramsSlug - } - }) + const event = await app.prisma.event.findUnique({ + where: { + slug: paramsSlug, + }, + }); - if(!event){ - return reply.status(404).send({error: 'Event not found'}) - } + if (!event) { + return reply.status(404).send({ error: 'Event not found' }); + } - try { - await app.prisma.eventAttendee.delete({ - where: { - userId_eventId: { - userId: userId, - eventId: event.id - } - } - }) - return reply.status(204).send({message: 'User left'}) - } catch (error:any) { - if(error.code === 'P2025'){ - return reply.status(404).send({error: 'User not found'}) - } - app.log.error((error as Error).message) - return reply.status(500).send({error: 'Failed to leave'}) - } - }) + try { + await app.prisma.eventAttendee.delete({ + where: { + userId_eventId: { + userId, + eventId: event.id, + }, + }, + }); + return reply.status(204).send({ message: 'User left' }); + } catch (error: any) { + if (error.code === 'P2025') { + return reply.status(404).send({ error: 'User not found' }); + } + app.log.error((error as Error).message); + return reply.status(500).send({ error: 'Failed to leave' }); + } + }); - app.get('/:slug/attendees', async(request: FastifyRequest<{Params: {slug: string}, Querystring: {page?:string; limit?: string}}>, reply: FastifyReply) => { - const paramsSlug = request.params.slug; - const page = Math.max(1, Number(request.query.page) || 1); - const limit = Math.min(50, Number(request.query.limit) || 10); - const skip = (page - 1) * limit - const event = await app.prisma.event.findUnique({ - where: { - slug: paramsSlug - }, - include: { - _count: { - select: { attendees: true } - }, - attendees : { - include: { - user: { - select: { - id: true, - username: true, - displayName:true, - bio: true, - pronouns: true, - company: true, - avatarUrl: true, - accentColor: true - } - } - }, - skip, - take: limit, - orderBy: {joinedAt: 'desc'} - } - }, - })as EventWithAttendees | null; + app.get('/:slug/attendees', async (request: FastifyRequest<{ Params: { slug: string }; Querystring: { page?: string; limit?: string } }>, reply: FastifyReply) => { + const paramsSlug = request.params.slug; + const page = Math.max(1, Number(request.query.page) || 1); + const limit = Math.min(50, Number(request.query.limit) || 10); + const skip = (page - 1) * limit; + const event = await app.prisma.event.findUnique({ + where: { + slug: paramsSlug, + }, + include: { + _count: { + select: { attendees: true }, + }, + attendees: { + include: { + user: { + select: { + id: true, + username: true, + displayName: true, + bio: true, + pronouns: true, + company: true, + avatarUrl: true, + accentColor: true, + }, + }, + }, + skip, + take: limit, + orderBy: { joinedAt: 'desc' }, + }, + }, + }) as EventWithAttendees | null; - if(!event){ - return reply.status(404).send({error: 'Event not found'}) - } + if (!event) { + return reply.status(404).send({ error: 'Event not found' }); + } - - const attendees = event.attendees.map((attendee: EventWithAttendees['attendees'][number]) => ({ - id: attendee.user.id, - username: attendee.user.username, - displayName: attendee.user.displayName, - bio: attendee.user.bio, - pronouns: attendee.user.pronouns, - company: attendee.user.company, - avatarUrl: attendee.user.avatarUrl, - accentColor: attendee.user.accentColor, - })); + const attendees = event.attendees.map((attendee: EventWithAttendees['attendees'][number]) => ({ + id: attendee.user.id, + username: attendee.user.username, + displayName: attendee.user.displayName, + bio: attendee.user.bio, + pronouns: attendee.user.pronouns, + company: attendee.user.company, + avatarUrl: attendee.user.avatarUrl, + accentColor: attendee.user.accentColor, + })); - const response: PaginatedAttendeesResponse = { - attendees, - pagination: { - page, - limit, - total : event._count.attendees, - } - } + const response: PaginatedAttendeesResponse = { + attendees, + pagination: { + page, + limit, + total: event._count.attendees, + }, + }; - return response; - }) + return response; + }); } \ No newline at end of file diff --git a/apps/backend/src/routes/nfc.ts b/apps/backend/src/routes/nfc.ts index 5cf13f0c..fee28090 100644 --- a/apps/backend/src/routes/nfc.ts +++ b/apps/backend/src/routes/nfc.ts @@ -1,6 +1,7 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import { z } from 'zod'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + type NfcPayloadResponse = { type: 'URI'; payload: string; @@ -10,22 +11,22 @@ const nfcQuerySchema = z.object({ card: z.string().uuid('Invalid card ID format').optional(), }); -export async function nfcRoutes(app: FastifyInstance) { +export async function nfcRoutes(app: FastifyInstance): Promise { app.addHook('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' }); - } + 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 { + reply.status(401).send({ error: 'Unauthorized' }); + } }); // GET /api/nfc/payload — returns NDEF URI payload for user's default DevCard URL @@ -99,10 +100,10 @@ export async function nfcRoutes(app: FastifyInstance) { } } -const safeUsername = encodeURIComponent(username); -const payloadUrl = `${process.env.PUBLIC_APP_URL}/${safeUsername}${ - cardId ? `?card=${encodeURIComponent(cardId)}` : '' -}`; + const safeUsername = encodeURIComponent(username); + const payloadUrl = `${process.env.PUBLIC_APP_URL}/${safeUsername}${ + cardId ? `?card=${encodeURIComponent(cardId)}` : '' + }`; const response: NfcPayloadResponse = { type: 'URI', payload: payloadUrl, diff --git a/apps/backend/src/routes/profiles.ts b/apps/backend/src/routes/profiles.ts index 388d3e02..3e3d9b89 100644 --- a/apps/backend/src/routes/profiles.ts +++ b/apps/backend/src/routes/profiles.ts @@ -1,5 +1,3 @@ -import { Prisma } from '@prisma/client'; - import * as profileService from '../services/profileService'; import { updateProfileSchema, createLinkSchema, reorderLinksSchema } from '../utils/validators.js'; @@ -79,7 +77,7 @@ export async function profileRoutes(app: FastifyInstance): Promise { const response = await profileService.updateProfile(app, userId, parsed.data) return response } catch (err: unknown) { - if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') { + if (err && typeof err === 'object' && 'code' in err && (err as any).code === 'P2002') { return reply.status(409).send({ error: 'Username already taken' }); } app.log.error({ err }, 'DB error in PUT /profiles/me') diff --git a/apps/backend/src/routes/public.ts b/apps/backend/src/routes/public.ts index dcbcc2dd..2b5c3fc4 100644 --- a/apps/backend/src/routes/public.ts +++ b/apps/backend/src/routes/public.ts @@ -20,6 +20,10 @@ export async function publicRoutes(app: FastifyInstance): Promise { const { username } = params; + if (!app.prisma.usernameRedirect) { + return; + } + const ninetyDaysAgo = new Date(); ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90); diff --git a/apps/backend/src/routes/team.ts b/apps/backend/src/routes/team.ts index 3ee44876..ec1c604d 100644 --- a/apps/backend/src/routes/team.ts +++ b/apps/backend/src/routes/team.ts @@ -29,7 +29,7 @@ export async function teamRoutes(app:FastifyInstance){ 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' }) } + 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) => { @@ -47,7 +47,7 @@ export async function teamRoutes(app:FastifyInstance){ }) try { - const team = await app.prisma.$transaction(async (tx) => { + const team = await app.prisma.$transaction(async (tx: any) => { const team = await tx.team.create({ data: { name, @@ -70,7 +70,7 @@ export async function teamRoutes(app:FastifyInstance){ }) return reply.status(201).send(team) - }catch (error) { + }catch (error: any) { if (error instanceof Prisma.PrismaClientKnownRequestError) { switch (error.code) { case 'P2002': @@ -116,7 +116,7 @@ export async function teamRoutes(app:FastifyInstance){ return reply.status(404).send({error: 'Team not found'}) } - const members = details.members.map((tm): TeamMember => ({ + const members = details.members.map((tm: any): TeamMember => ({ username: tm.user.username, displayName: tm.user.displayName, bio: tm.user.bio, @@ -161,7 +161,7 @@ export async function teamRoutes(app:FastifyInstance){ 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' }) } + 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) => { const paramsSlug = request.params.slug; const userId = (request.user as any).id; @@ -191,7 +191,7 @@ export async function teamRoutes(app:FastifyInstance){ return reply.status(403).send('Forbidden') } - const alreadyMember = teamDetails.members.find((u) => u.user.username === username) + const alreadyMember = teamDetails.members.find((u: any) => u.user.username === username) //Check invited username is not a member and owner; if(alreadyMember || teamDetails.owner.username === username){ @@ -243,7 +243,7 @@ export async function teamRoutes(app:FastifyInstance){ return reply.status(404).send({error: 'Team not found'}) } - const isMember = teamDetails.members.find((m) => paramsUserId === m.user.id) + const isMember = teamDetails.members.find((m: any) => paramsUserId === m.user.id) if(!isMember){ return reply.status(404).send({ diff --git a/apps/backend/src/services/authService.ts b/apps/backend/src/services/authService.ts index 9af718c5..c9b839bb 100644 --- a/apps/backend/src/services/authService.ts +++ b/apps/backend/src/services/authService.ts @@ -1,4 +1,4 @@ -import { randomBytes } from 'crypto'; +import { randomBytes } from 'node:crypto'; export function generateState(): string { return randomBytes(32).toString('hex'); diff --git a/apps/backend/src/services/cardService.ts b/apps/backend/src/services/cardService.ts index 4c83d5b2..a937f974 100644 --- a/apps/backend/src/services/cardService.ts +++ b/apps/backend/src/services/cardService.ts @@ -72,7 +72,7 @@ export async function createCard(app: FastifyInstance, userId: string, body: Cre for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const card = (await app.prisma.$transaction( - async (tx: Prisma.TransactionClient) => { + async (tx: any) => { const cardCount = await tx.card.count({ where: { userId } }); return tx.card.create({ @@ -145,7 +145,7 @@ export async function updateCard( //Delete card service export async function deleteCard(app: FastifyInstance, userId: string, id: string): Promise { - return await app.prisma.$transaction(async (tx: Prisma.TransactionClient) => { + return await app.prisma.$transaction(async (tx: any) => { const existing = await tx.card.findFirst({ where: { id, userId } }); if (!existing) { throw Object.assign(new Error('NotFound'), { code: 'NOT_FOUND' }); @@ -180,7 +180,7 @@ export async function setDefaultCard(app: FastifyInstance, userId: string, id: s throw Object.assign(new Error('NotFound'), { code: 'NOT_FOUND' }); } - await app.prisma.$transaction(async (tx: Prisma.TransactionClient) => { + await app.prisma.$transaction(async (tx: any) => { await tx.card.updateMany({ where: { userId }, data: { isDefault: false } }); await tx.card.update({ where: { id }, data: { isDefault: true } }); }); diff --git a/apps/backend/src/services/profileService.ts b/apps/backend/src/services/profileService.ts index 6159402b..d2d17209 100644 --- a/apps/backend/src/services/profileService.ts +++ b/apps/backend/src/services/profileService.ts @@ -1,7 +1,9 @@ -import type { FastifyInstance } from 'fastify' import { getProfileUrl } from '@devcard/shared/src/platforms.js' + import { getErrorMessage } from '../utils/error.util.js' +import type { FastifyInstance } from 'fastify' + export async function getOwnProfile(app: FastifyInstance, userId: string) { const user = await app.prisma.user.findUnique({ where: { id: userId }, @@ -11,9 +13,9 @@ export async function getOwnProfile(app: FastifyInstance, userId: string) { }, }) - if (!user) return null + if (!user) {return null} - const { provider, providerId, ...profileData } = user as any + const { provider: _provider, providerId: _providerId, ...profileData } = user as any return { ...profileData, defaultCardId: user.cards[0]?.id || null } } @@ -23,7 +25,7 @@ export async function updateProfile(app: FastifyInstance, userId: string, data: const existing = await app.prisma.user.findFirst({ where: { username: data.username, NOT: { id: userId } }, }) - if (existing) throw Object.assign(new Error('Username taken'), { code: 'P2002' }) + if (existing) {throw Object.assign(new Error('Username taken'), { code: 'P2002' })} } const currentUser = await app.prisma.user.findUnique({ where: { id: userId }, select: { username: true } }) @@ -65,7 +67,7 @@ export async function updateProfile(app: FastifyInstance, userId: string, data: return response } catch (err: any) { - if (err?.code === 'P2002') throw err + if (err?.code === 'P2002') {throw err} app.log.error({ err }, 'DB error in updateProfile') throw err } @@ -79,14 +81,14 @@ export async function createPlatformLink(app: FastifyInstance, userId: string, l export async function updatePlatformLink(app: FastifyInstance, userId: string, id: string, linkData: any) { const existing = await app.prisma.platformLink.findFirst({ where: { id, userId } }) - if (!existing) return null + if (!existing) {return null} const url = linkData.url || getProfileUrl(linkData.platform, linkData.username) return app.prisma.platformLink.update({ where: { id }, data: { platform: linkData.platform, username: linkData.username, url } }) } export async function deletePlatformLink(app: FastifyInstance, userId: string, id: string) { const existing = await app.prisma.platformLink.findFirst({ where: { id, userId } }) - if (!existing) return false + if (!existing) {return false} await app.prisma.platformLink.delete({ where: { id } }) return true } diff --git a/apps/backend/src/services/publicService.ts b/apps/backend/src/services/publicService.ts index 734686bb..f0811450 100644 --- a/apps/backend/src/services/publicService.ts +++ b/apps/backend/src/services/publicService.ts @@ -52,7 +52,7 @@ export async function getPublicProfile( app.redis.set(cacheKey, JSON.stringify(entry), 'EX', PROFILE_CACHE_TTL).catch((err: unknown) => app.log.warn(`Redis cache write failed for ${cacheKey}: ${getErrorMessage(err)}`)) } - const response = { username: user.username, displayName: user.displayName, bio: user.bio, pronouns: user.pronouns, role: user.role, company: user.company, avatarUrl: user.avatarUrl, accentColor: user.accentColor, links: baseLinks.map((link) => ({ ...link, followed: followedLinkIds.includes(link.id) })) } + const response = { username: user.username, displayName: user.displayName, bio: user.bio, pronouns: user.pronouns, role: user.role, company: user.company, avatarUrl: user.avatarUrl, accentColor: user.accentColor, links: baseLinks.map((link: any) => ({ ...link, followed: followedLinkIds.includes(link.id) })) } return { cached: false, data: response, cacheKey } } diff --git a/apps/backend/src/utils/encryption.ts b/apps/backend/src/utils/encryption.ts index b9105992..adfb3172 100644 --- a/apps/backend/src/utils/encryption.ts +++ b/apps/backend/src/utils/encryption.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto from 'node:crypto'; const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 16; diff --git a/apps/backend/src/utils/error.util.ts b/apps/backend/src/utils/error.util.ts index d429f1fb..48a0670e 100644 --- a/apps/backend/src/utils/error.util.ts +++ b/apps/backend/src/utils/error.util.ts @@ -36,19 +36,20 @@ export function handleDbError(error: unknown, request: FastifyRequest, reply: Fa request.log.error(error); if (error instanceof Prisma.PrismaClientKnownRequestError) { + const dbErr = error as Prisma.PrismaClientKnownRequestError; // P2002: Unique constraint failed - if (error.code === 'P2002') { + if (dbErr.code === 'P2002') { return reply.status(409).send({ error: 'Conflict: Record already exists or violates unique constraint' }); } // P2025: Record to update not found - if (error.code === 'P2025') { + if (dbErr.code === 'P2025') { return reply.status(404).send({ error: 'Not Found: Record does not exist' }); } // P2003: Foreign key constraint failed - if (error.code === 'P2003') { + if (dbErr.code === 'P2003') { return reply.status(400).send({ error: 'Constraint failed: Related record not found or invalid' }); } - return reply.status(400).send({ error: `Database error: ${error.message}` }); + return reply.status(400).send({ error: `Database error: ${dbErr.message}` }); } if (error instanceof Prisma.PrismaClientValidationError) { diff --git a/apps/backend/src/utils/slug.ts b/apps/backend/src/utils/slug.ts index 24b772f3..4f0d0fcd 100644 --- a/apps/backend/src/utils/slug.ts +++ b/apps/backend/src/utils/slug.ts @@ -10,9 +10,9 @@ export async function generateUniqueSlug(name: string, while(true){ const exists = await slugExists(finalSlug) - if(!exists) break; + if(!exists) {break;} - const randomSuffix = Math.random().toString(36).substring(2,6); + const randomSuffix = Math.random().toString(36).slice(2,6); finalSlug = `${cleanSlug}-${randomSuffix}` } return finalSlug; From 8d5a966702ffef9c42779b17b333cadd6a007e8f Mon Sep 17 00:00:00 2001 From: Test User Date: Wed, 15 Jul 2026 21:26:11 +0530 Subject: [PATCH 4/5] fix(backend): fix Prisma relation validation error, add missing card preHandlers, and typecast profile tx --- apps/backend/prisma/schema.prisma | 1 + apps/backend/src/__tests__/event.test.ts | 32 +++++-------------- apps/backend/src/routes/cards.ts | 13 +++++--- apps/backend/src/routes/team.ts | 34 ++++++++------------- apps/backend/src/services/profileService.ts | 2 +- 5 files changed, 30 insertions(+), 52 deletions(-) diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 36314610..dc11319c 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -45,6 +45,7 @@ model User { ownedTeams Team[] @relation("TeamOwner") teamMemberships TeamMember[] @relation("TeamMember") usernameRedirects UsernameRedirect[] + webhookEndpoints WebhookEndpoint[] @@map("users") } diff --git a/apps/backend/src/__tests__/event.test.ts b/apps/backend/src/__tests__/event.test.ts index 39ffc98e..1931223b 100644 --- a/apps/backend/src/__tests__/event.test.ts +++ b/apps/backend/src/__tests__/event.test.ts @@ -1,9 +1,10 @@ -import Fastify, { type FastifyInstance } from 'fastify'; +import Fastify from 'fastify'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { eventRoutes } from '../routes/event'; import type { PrismaClient } from '@prisma/client'; +import type { FastifyInstance,LightMyRequestResponse } from 'fastify'; // ─── Shared mock data ──────────────────────────────────────────────────────── @@ -79,16 +80,14 @@ async function buildApp(): Promise { app.decorateRequest('jwtVerify', function () { return mockJwtVerify(); }); - - app.decorate('authenticate', async function (request: any, reply: any) { - try { - const user = await request.jwtVerify(); - request.user = user; - } catch (err: any) { - return reply.status(401).send({ error: err.message || 'Unauthorized' }); + 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' }); } }); - // Register with the same prefix used in production (app.ts) so that // tests exercise routes at their real paths — /api/events, /api/events/:slug, etc. await app.register(eventRoutes, { prefix: '/api/events' }); @@ -264,10 +263,6 @@ describe('Events API', () => { ...MOCK_EVENT, organizer: { username: 'johndoe', displayName: 'John Doe' }, _count: { attendees: 42 }, - organizer: { - username: 'johndoe', - displayName: 'John Doe', - }, }); const res = await app.inject({ @@ -303,10 +298,6 @@ describe('Events API', () => { ...MOCK_EVENT, organizer: { username: 'johndoe', displayName: 'John Doe' }, _count: { attendees: 0 }, - organizer: { - username: 'johndoe', - displayName: 'John Doe', - }, }); const res = await app.inject({ @@ -521,7 +512,6 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, - _count: { attendees: 2 }, attendees: attendeeRows, _count: { attendees: 2 }, }); @@ -551,7 +541,6 @@ describe('Events API', () => { it('200 — respects custom page and limit query params', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, - _count: { attendees: 1 }, attendees: [makeAttendeeRow(MOCK_OTHER_USER_PROFILE)], _count: { attendees: 1 }, }); @@ -575,7 +564,6 @@ describe('Events API', () => { it('200 — caps limit at 50 even if higher value is requested', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, - _count: { attendees: 0 }, attendees: [], _count: { attendees: 0 }, }); @@ -593,7 +581,6 @@ describe('Events API', () => { it('200 — treats page < 1 as page 1', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, - _count: { attendees: 0 }, attendees: [], _count: { attendees: 0 }, }); @@ -611,7 +598,6 @@ describe('Events API', () => { it('200 — returns empty attendees list for event with no attendees', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, - _count: { attendees: 0 }, attendees: [], _count: { attendees: 0 }, }); @@ -630,7 +616,6 @@ describe('Events API', () => { it('200 — public profiles do not leak sensitive fields', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, - _count: { attendees: 1 }, attendees: [makeAttendeeRow(MOCK_USER_PROFILE)], _count: { attendees: 1 }, }); @@ -670,7 +655,6 @@ describe('Events API', () => { it('200 — attendees are ordered by joinedAt desc (latest first)', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, - _count: { attendees: 0 }, attendees: [], }); diff --git a/apps/backend/src/routes/cards.ts b/apps/backend/src/routes/cards.ts index bfa84348..860c9232 100644 --- a/apps/backend/src/routes/cards.ts +++ b/apps/backend/src/routes/cards.ts @@ -163,7 +163,7 @@ export async function cardRoutes(app: FastifyInstance): Promise { }); //Add platform-link - app.put('/:id/platform-link', async(request: FastifyRequest<{Params:{id: string}, Body: {platformLinkId: string}}>, reply: FastifyReply) => { + app.put('/:id/platform-link', { preHandler: [(req, reply) => app.authenticate(req, reply)] }, async(request: FastifyRequest<{Params:{id: string}, Body: {platformLinkId: string}}>, reply: FastifyReply) => { const cardId = request.params.id; const userId = request.user.id; const parsed = addPlatformLinkSchema.safeParse(request.body); @@ -203,7 +203,7 @@ export async function cardRoutes(app: FastifyInstance): Promise { }) //Share card - app.post('/:id/share',async(request: FastifyRequest<{Params: {id: string}}>, reply:FastifyReply) => { + app.post('/:id/share', { preHandler: [(req, reply) => app.authenticate(req, reply)] }, async(request: FastifyRequest<{Params: {id: string}}>, reply:FastifyReply) => { const cardId = request.params.id; const userId = request.user.id; @@ -233,8 +233,11 @@ export async function cardRoutes(app: FastifyInstance): Promise { // so source should not be hardcoded to "link". //Get shared card app.get('/share/:slug', async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { + try { + await request.jwtVerify(); + } catch (_e) {} const paramsSlug = request.params.slug; - const userId = request.user.id + const userId = request.user?.id const ip = hashIp(request.ip); const userAgent = request.headers['user-agent'] ?? 'unknown'; @@ -279,7 +282,7 @@ export async function cardRoutes(app: FastifyInstance): Promise { }) //Generates qr - app.get('/:id/qr', async(request: FastifyRequest<{Params: {id: string}}>, reply:FastifyReply) => { + app.get('/:id/qr', { preHandler: [(req, reply) => app.authenticate(req, reply)] }, async(request: FastifyRequest<{Params: {id: string}}>, reply:FastifyReply) => { const cardId = request.params.id const userId = request.user.id @@ -313,7 +316,7 @@ export async function cardRoutes(app: FastifyInstance): Promise { }) //Get analytics - app.get('/:id/analytics', async(request:FastifyRequest<{Params: {id:string}}>, reply: FastifyReply) => { + app.get('/:id/analytics', { preHandler: [(req, reply) => app.authenticate(req, reply)] }, async(request:FastifyRequest<{Params: {id:string}}>, reply: FastifyReply) => { const cardId = request.params.id const userId = request.user.id diff --git a/apps/backend/src/routes/team.ts b/apps/backend/src/routes/team.ts index d3883991..7a372228 100644 --- a/apps/backend/src/routes/team.ts +++ b/apps/backend/src/routes/team.ts @@ -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 { + app.post<{ + Body: {name: string, description? : string, avatarUrl?: string } + }>('/',{ preHandler: [app.authenticate] }, async (request, reply): Promise => { + const userId = request.user.id; const parsed = createTeamScehma.safeParse(request.body); if(!parsed.success){ return reply.status(400).send({error: 'Bad request'}) @@ -47,8 +42,8 @@ export async function teamRoutes(app:FastifyInstance){ }) try { - const team = await app.prisma.$transaction(async (tx: any) => { - const team = await tx.team.create({ + const team = await app.prisma.$transaction(async (tx) => { + const createdTeam = await tx.team.create({ data: { name, slug: finalSlug, @@ -70,7 +65,7 @@ export async function teamRoutes(app:FastifyInstance){ }) return reply.status(201).send(team) - }catch (error: any) { + }catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { switch (error.code) { case 'P2002': @@ -116,7 +111,7 @@ export async function teamRoutes(app:FastifyInstance){ return reply.status(404).send({error: 'Team not found'}) } - const members = details.members.map((tm: any): TeamMember => ({ + const members = details.members.map((tm): TeamMember => ({ username: tm.user.username, displayName: tm.user.displayName, bio: tm.user.bio, @@ -157,12 +152,7 @@ export async function teamRoutes(app:FastifyInstance){ }) - 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 => { const paramsSlug = request.params.slug; const userId = request.user.id; const parsed = inviteMembers.safeParse(request.body); @@ -191,7 +181,7 @@ export async function teamRoutes(app:FastifyInstance){ return reply.status(403).send('Forbidden') } - const alreadyMember = teamDetails.members.find((u: any) => u.user.username === username) + const alreadyMember = teamDetails.members.find((u) => u.user.username === username) //Check invited username is not a member and owner; if(alreadyMember || teamDetails.owner.username === username){ @@ -243,7 +233,7 @@ export async function teamRoutes(app:FastifyInstance){ return reply.status(404).send({error: 'Team not found'}) } - const isMember = teamDetails.members.find((m: any) => paramsUserId === m.user.id) + const isMember = teamDetails.members.find((m) => paramsUserId === m.user.id) if(!isMember){ return reply.status(404).send({ diff --git a/apps/backend/src/services/profileService.ts b/apps/backend/src/services/profileService.ts index d2d17209..86745045 100644 --- a/apps/backend/src/services/profileService.ts +++ b/apps/backend/src/services/profileService.ts @@ -33,7 +33,7 @@ export async function updateProfile(app: FastifyInstance, userId: string, data: try { const isUsernameChanging = data.username && currentUser && data.username !== currentUser.username; - const response = await app.prisma.$transaction(async (tx) => { + const response = await app.prisma.$transaction(async (tx: any) => { if (isUsernameChanging) { // Delete any existing redirects where the oldUsername is the new username await tx.usernameRedirect.deleteMany({ From 6c098c3b4dbb9eb3b26b95d0b5812321aa63911a Mon Sep 17 00:00:00 2001 From: Test User Date: Wed, 15 Jul 2026 21:36:47 +0530 Subject: [PATCH 5/5] style(backend): resolve import ordering and unused import lint errors --- apps/backend/src/__tests__/oauth-scope.test.ts | 4 +++- apps/backend/src/plugins/prisma.ts | 2 +- apps/backend/src/routes/event.ts | 5 +++-- apps/backend/src/routes/nfc.ts | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/backend/src/__tests__/oauth-scope.test.ts b/apps/backend/src/__tests__/oauth-scope.test.ts index 9a3c5773..150e779e 100644 --- a/apps/backend/src/__tests__/oauth-scope.test.ts +++ b/apps/backend/src/__tests__/oauth-scope.test.ts @@ -11,10 +11,12 @@ * flow so the two records are independent and can never overwrite each other. */ -import { describe, it, expect, beforeEach, vi } from 'vitest'; import Fastify from 'fastify'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; + import { connectRoutes } from '../routes/connect.js'; import { followRoutes } from '../routes/follow.js'; + import type { PrismaClient } from '@prisma/client'; // ── Mocks ───────────────────────────────────────────────────────────────────── diff --git a/apps/backend/src/plugins/prisma.ts b/apps/backend/src/plugins/prisma.ts index 53f4c33c..abe40961 100644 --- a/apps/backend/src/plugins/prisma.ts +++ b/apps/backend/src/plugins/prisma.ts @@ -1,7 +1,7 @@ import { PrismaClient } from '@prisma/client'; import fp from 'fastify-plugin'; -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import type { FastifyInstance } from 'fastify'; declare module 'fastify' { interface FastifyInstance { diff --git a/apps/backend/src/routes/event.ts b/apps/backend/src/routes/event.ts index 87e87e27..090031d0 100644 --- a/apps/backend/src/routes/event.ts +++ b/apps/backend/src/routes/event.ts @@ -1,7 +1,8 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { generateUniqueSlug } from '../utils/slug.js'; import { createEventSchema } from '../validations/event.validation.js'; -import { generateUniqueSlug } from '../utils/slug.js'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + type EventDetails = { id: string; diff --git a/apps/backend/src/routes/nfc.ts b/apps/backend/src/routes/nfc.ts index d3efc1e9..5cf48f66 100644 --- a/apps/backend/src/routes/nfc.ts +++ b/apps/backend/src/routes/nfc.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import type { FastifyInstance } from 'fastify'; type NfcPayloadResponse = { type: 'URI';