-
Notifications
You must be signed in to change notification settings - Fork 24
feat: Complete Zplit Frontend with Dashboard, Auth, and Expense Management UI #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughThis PR restructures the project from a LaTeX-centric setup to a full-stack application featuring a Node.js/Express backend (PostgreSQL via Prisma) and React/Vite frontend. Introduces authentication, role-based access control, advertisement CRUD operations, and deep-link asset management with Docker containerization and environment-driven configuration. Changes
Sequence DiagramssequenceDiagram
participant Client
participant Auth Routes
participant Prisma
participant JWT Service
participant Database
Client->>Auth Routes: POST /register (email, password, clientName)
Auth Routes->>Prisma: upsert Client by email
Prisma->>Database: check/create Client
Database-->>Prisma: Client record
Auth Routes->>Auth Routes: hash password (bcrypt)
Auth Routes->>Prisma: create User (role: CLIENT)
Prisma->>Database: insert User
Database-->>Prisma: User record
Prisma-->>Auth Routes: User with id, email, role, clientId
Auth Routes-->>Client: { id, email, role, clientId }
rect rgba(100, 150, 200, 0.2)
Note over Client,Database: Login & Token Generation
Client->>Auth Routes: POST /login (email, password)
Auth Routes->>Prisma: find User by email
Prisma->>Database: query User
Database-->>Prisma: User with password hash
Auth Routes->>Auth Routes: compare password (bcrypt)
Auth Routes->>JWT Service: sign JWT { userId }
JWT Service-->>Auth Routes: token
Auth Routes-->>Client: { token, role, clientId }
end
rect rgba(100, 200, 100, 0.2)
Note over Client,Database: Protected Route Access
Client->>Auth Routes: GET /me (Authorization: Bearer token)
Auth Routes->>Auth Routes: verify JWT
Auth Routes->>Prisma: fetch User by userId
Prisma->>Database: query User with client relation
Database-->>Prisma: User + Client data
Prisma-->>Auth Routes: User record
Auth Routes-->>Client: { id, email, role, client }
end
sequenceDiagram
participant Admin
participant Admin Routes
participant Cache Service
participant Prisma
participant Database
participant Public Client
participant Deeplink Routes
rect rgba(100, 150, 200, 0.2)
Note over Admin,Database: Admin Creates/Updates Deep-Link Asset
Admin->>Admin Routes: POST /deeplink { platform: ANDROID, content }
Admin Routes->>Prisma: create DeepLinkAsset
Prisma->>Database: insert asset
Database-->>Prisma: created asset
Admin Routes->>Cache Service: invalidate ANDROID_ASSETLINKS
Cache Service-->>Admin Routes: invalidated
Admin Routes-->>Admin: { id, platform, content }
end
rect rgba(100, 200, 100, 0.2)
Note over Public Client,Deeplink Routes: Public Serves Cached Asset
Public Client->>Deeplink Routes: GET /.well-known/assetlinks.json
Deeplink Routes->>Cache Service: get ANDROID_ASSETLINKS
alt Cache Hit
Cache Service-->>Deeplink Routes: cached content
else Cache Miss
Deeplink Routes->>Prisma: fetch DeepLinkAsset (platform: ANDROID)
Prisma->>Database: query asset
Database-->>Prisma: asset
Deeplink Routes->>Cache Service: set ANDROID_ASSETLINKS
Cache Service-->>Deeplink Routes: cached
end
Deeplink Routes-->>Public Client: application/json { assetlinks }
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60–75 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🟠 Major comments (16)
backend/.env-1-1 (1)
1-1: Hardcoded database credentials pose a security risk.The DATABASE_URL contains default credentials (
postgres:postgres) that are commonly known and easily guessable. If this configuration is used in any non-local environment, it creates a significant security vulnerability.Consider:
- Using stronger credentials even for local development
- Documenting that these are development-only defaults
- Ensuring production environments use secure credential management (e.g., secrets managers, environment-specific configuration)
backend/Dockerfile-13-13 (1)
13-13: Suppressing Prisma generation errors can hide critical failures.The
|| truecauses the build to continue even if Prisma client generation fails. This can lead to runtime errors when the application tries to use the Prisma client, making issues harder to debug.Remove the error suppression to fail fast on Prisma issues:
-RUN npx prisma generate || true +RUN npx prisma generateIf there are legitimate cases where Prisma generation might not be needed, handle them explicitly with proper error messaging.
backend/package.json-20-20 (1)
20-20: MoveprismaCLI to devDependencies.The
prismapackage is a CLI tool used for migrations and schema management during development, not at runtime. Only@prisma/client(line 21) is needed in production dependencies.Apply this change:
"dependencies": { "bcrypt": "^5.1.0", "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.2", "jsonwebtoken": "^9.0.0", "node-cache": "^5.1.2", - "prisma": "^5.6.0", "@prisma/client": "^5.6.0", "zod": "^3.20.2" }, "devDependencies": { "@types/bcrypt": "^5.0.0", "@types/cors": "^2.8.12", "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.2", + "prisma": "^5.6.0", "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", "typescript": "^5.2.2" }README.md-1-9 (1)
1-9: README scope seems inconsistent with the PR description—please align.Line 3 says this is an app for “managing advertisements and hosting deep-linking assets”, and the documented endpoints are ads/deeplinks-focused—this conflicts with the PR objectives you provided (expense splitting features). Please confirm the intended scope and update either README (or PR description) so reviewers/users aren’t misled.
backend/src/server.ts-1-12 (1)
1-12: Load.envbefore importing route modules (current order can break config).
dotenv.config()is called after./routes/*imports (Line 4–7 vs Line 9). If any route/middleware readsprocess.envat module scope, it may run before.envis loaded.-import express from 'express'; -import cors from 'cors'; -import dotenv from 'dotenv'; +import 'dotenv/config'; +import express from 'express'; +import cors from 'cors'; import authRouter from './routes/auth'; import adsRouter from './routes/ads'; import deepLinksRouter from './routes/deeplinks'; import adminRouter from './routes/admin'; -dotenv.config(); const app = express(); const port = process.env.PORT || 4000;docker-compose.yml-3-26 (1)
3-26: DB readiness:depends_onwon’t prevent backend startup races—add a healthcheck/wait.As-is, backend can run migrations/generate/client before Postgres is ready, causing intermittent failures.
services: db: image: postgres:15 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d zplit_db"] + interval: 5s + timeout: 3s + retries: 20 @@ backend: @@ - depends_on: - - db + depends_on: + db: + condition: service_healthybackend/src/routes/deeplinks.ts-12-41 (1)
12-41: Handle async errors (today any prisma failure can bubble as an unhandled rejection).Both handlers are
async(Line 12, 29) but don’t guardawait prisma...(Line 21, 36). Wrap intry/catch(or use an async error middleware wrapper) and return a consistent 500.router.get('/assetlinks.json', async (req, res) => { - // Android assetlinks - const cacheKey = 'ANDROID_ASSETLINKS'; - const cached = cache.get(cacheKey); - if (cached) { - res.set('Content-Type', 'application/json'); - return res.json(cached); - } - - const record = await prisma.deepLinkAsset.findFirst({ where: { platform: 'ANDROID' }, orderBy: { updatedAt: 'desc' } }); - if (!record) return res.status(404).json({ message: 'Not configured' }); - - cache.set(cacheKey, record.content); - res.set('Content-Type', 'application/json'); - res.json(record.content); + try { + const cacheKey = 'ANDROID_ASSETLINKS'; + const cached = cache.get(cacheKey); + if (cached !== undefined) { + res.set('Content-Type', 'application/json'); + return res.json(cached); + } + + const record = await prisma.deepLinkAsset.findFirst({ + where: { platform: 'ANDROID' }, + orderBy: { updatedAt: 'desc' }, + }); + if (!record) return res.status(404).json({ message: 'Not configured' }); + + cache.set(cacheKey, record.content); + res.set('Content-Type', 'application/json'); + return res.json(record.content); + } catch (err) { + return res.status(500).json({ message: 'Internal error' }); + } });backend/prisma/seed.ts-17-25 (1)
17-25: Fix seed idempotency, remove error suppression, and correct Android content shape.
createMany(lines 17–20) has no uniqueness constraint and will create duplicate rows on every run.- DeepLinkAsset upserts by
id(lines 23–24), which works only if records already exist with those IDs. The ANDROID record (line 23) also has inconsistent content shapes:updateuses{ apps: [] }butcreateuses{ statements: [] }, while the IOS record correctly uses{ applinks: {} }for both paths.- Both upserts silently swallow errors via
.catch(()=>{}), which can mask failures and prevent detection of data issues.Replace
createManywith upserts keyed on stable fields (e.g.,ad_nameortarget_urlfor ads), add a unique constraint to DeepLinkAsset'splatformfield if it should be singleton per platform, fix the ANDROID content shape to match the update path, and remove error suppression to surface real issues.backend/src/routes/admin.ts-27-40 (1)
27-40: Return 400/404 forPUT /deeplink/:id(NaN + record-not-found shouldn’t become 500).
prisma.deepLinkAsset.update()throws when the record doesn’t exist; that should map to 404.router.put('/deeplink/:id', async (req, res) => { const id = parseInt(req.params.id, 10); + if (!Number.isFinite(id)) return res.status(400).json({ message: 'Invalid id' }); const { content } = req.body; try { const updated = await prisma.deepLinkAsset.update({ where: { id }, data: { content } }); // invalidate caches by detecting platform if (updated.platform === 'ANDROID') cache.del('ANDROID_ASSETLINKS'); if (updated.platform === 'IOS') cache.del('IOS_AASA'); res.json(updated); } catch (err) { console.error(err); + // Prisma: record not found => P2025 + if ((err as any)?.code === 'P2025') return res.status(404).json({ message: 'Deep link asset not found' }); res.status(500).json({ message: 'Error updating deep link asset' }); } });backend/src/routes/ads.ts-82-90 (1)
82-90:DELETE /ads/:id: add try/catch + validate:id(DB errors currently escape).
A failing delete (FK constraint, transient DB error) will currently bubble up.router.delete('/:id', async (req, res) => { const id = parseInt(req.params.id, 10); - const ad = await prisma.advertisement.findUnique({ where: { id } }); - if (!ad) return res.status(404).json({ message: 'Ad not found' }); - if (req.user.role === 'CLIENT' && ad.clientId !== req.user.clientId) return res.status(403).json({ message: 'Forbidden' }); - await prisma.advertisement.delete({ where: { id } }); - res.json({ ok: true }); + if (!Number.isFinite(id)) return res.status(400).json({ message: 'Invalid id' }); + try { + const ad = await prisma.advertisement.findUnique({ where: { id } }); + if (!ad) return res.status(404).json({ message: 'Ad not found' }); + if (req.user.role === 'CLIENT' && ad.clientId !== req.user.clientId) return res.status(403).json({ message: 'Forbidden' }); + await prisma.advertisement.delete({ where: { id } }); + res.json({ ok: true }); + } catch (err) { + console.error(err); + res.status(500).json({ message: 'Error deleting ad' }); + } });backend/src/routes/ads.ts-31-39 (1)
31-39: Validate:idbefore querying (NaN should be 400, not 500/undefined behavior).
parseInt()can produceNaN;findUnique({ id: NaN })may throw or behave unexpectedly, and there’s notry/catchhere.router.get('/:id', async (req, res) => { const id = parseInt(req.params.id, 10); + if (!Number.isFinite(id)) return res.status(400).json({ message: 'Invalid id' }); const ad = await prisma.advertisement.findUnique({ where: { id } }); if (!ad) return res.status(404).json({ message: 'Ad not found' }); // Client users can only access their own if (req.user.role === 'CLIENT' && ad.clientId !== req.user.clientId) return res.status(403).json({ message: 'Forbidden' }); res.json(ad); });backend/src/routes/admin.ts-12-25 (1)
12-25: Validateplatformas enum + don’t treat empty JSON as missing.
Right now any string forplatformwill reach Prisma (500 instead of 400), and!contentis a bit ambiguous for JSON payloads.router.post('/deeplink', async (req, res) => { const { platform, content } = req.body; // platform should be 'ANDROID' or 'IOS', content is JSON - if (!platform || !content) return res.status(400).json({ message: 'platform+content required' }); + if (typeof platform !== 'string') return res.status(400).json({ message: 'platform required' }); + if (platform !== 'ANDROID' && platform !== 'IOS') return res.status(400).json({ message: 'Invalid platform' }); + if (content === undefined) return res.status(400).json({ message: 'content required' }); try { const record = await prisma.deepLinkAsset.create({ data: { platform, content } }); // Invalidate caches for the platform if (platform === 'ANDROID') cache.del('ANDROID_ASSETLINKS'); if (platform === 'IOS') cache.del('IOS_AASA'); res.json(record); } catch (err) { console.error(err); res.status(500).json({ message: 'Error creating deep link asset' }); } });backend/src/routes/ads.ts-63-80 (1)
63-80:PUT /ads/:id: validate:id+ handle invalid dates similarly to create.
Same NaN/Invalid Date risks; plusstatuscan be invalid enum (causing 500).router.put('/:id', async (req, res) => { const id = parseInt(req.params.id, 10); + if (!Number.isFinite(id)) return res.status(400).json({ message: 'Invalid id' }); const ad = await prisma.advertisement.findUnique({ where: { id } }); if (!ad) return res.status(404).json({ message: 'Ad not found' }); if (req.user.role === 'CLIENT' && ad.clientId !== req.user.clientId) return res.status(403).json({ message: 'Forbidden' }); const { ad_name, target_url, start_date, end_date, status } = req.body; try { + const parsedStatus = typeof status === 'string' ? status.toUpperCase() : status; + if (parsedStatus && !['ACTIVE','PAUSED','ARCHIVED'].includes(parsedStatus)) { + return res.status(400).json({ message: 'Invalid status' }); + } + const sd = start_date ? new Date(start_date) : undefined; + const ed = end_date ? new Date(end_date) : undefined; + if (sd && !Number.isFinite(sd.getTime())) return res.status(400).json({ message: 'Invalid start_date' }); + if (ed && !Number.isFinite(ed.getTime())) return res.status(400).json({ message: 'Invalid end_date' }); const updated = await prisma.advertisement.update({ where: { id }, - data: { ad_name, target_url, start_date: start_date ? new Date(start_date) : undefined, end_date: end_date ? new Date(end_date) : undefined, status }, + data: { ad_name, target_url, start_date: sd, end_date: ed, status: parsedStatus }, }); res.json(updated); } catch (err) { console.error(err); res.status(500).json({ message: 'Error updating ad' }); } });backend/src/routes/auth.ts-25-44 (1)
25-44: Use a transaction for (Client create) + (User create) to avoid partial state.
If user creation fails after client creation, you’ll leave an orphanedClient.- let clientId: number | null = null; - // Create a client for role CLIENT if it doesn't exist - if (role === 'CLIENT') { - if (!clientName) return res.status(400).json({ message: 'clientName required for CLIENT role' }); - const existing = await prisma.client.findUnique({ where: { email } }); - const client = existing ?? (await prisma.client.create({ data: { name: clientName, email } })); - clientId = client.id; - } - - const hashed = await bcrypt.hash(password, 10); - const user = await prisma.user.create({ - data: { - email, - password: hashed, - role, - clientId, - }, - }); + const hashed = await bcrypt.hash(password, 10); + const user = await prisma.$transaction(async (tx) => { + let clientId: number | null = null; + if (role === 'CLIENT') { + if (!clientName) throw Object.assign(new Error('clientName required'), { httpStatus: 400 }); + const existing = await tx.client.findUnique({ where: { email } }); + const client = existing ?? (await tx.client.create({ data: { name: clientName, email } })); + clientId = client.id; + } + return tx.user.create({ data: { email, password: hashed, role, clientId } }); + }); res.json({ id: user.id, email: user.email, role: user.role, clientId: user.clientId }); } catch (err: any) { console.error(err); + if (err.httpStatus) return res.status(err.httpStatus).json({ message: err.message }); if (err.code === 'P2002') return res.status(400).json({ message: 'Email already exists' }); res.status(500).json({ message: 'Error registering user' }); }backend/src/routes/ads.ts-41-61 (1)
41-61: Reject invalid dates / invalid enumstatusup-front inPOST /ads(avoid storing “Invalid Date” / runtime 500s).
new Date(start_date)succeeds syntactically even for garbage input, yieldingInvalid Date.router.post('/', async (req, res) => { const { ad_name, target_url, start_date, end_date, status } = req.body; const role = req.user.role; const clientId = req.user.clientId; try { - const data: any = { ad_name, target_url, start_date: new Date(start_date), end_date: new Date(end_date), status }; + const parsedStatus = typeof status === 'string' ? status.toUpperCase() : status; + if (parsedStatus && !['ACTIVE','PAUSED','ARCHIVED'].includes(parsedStatus)) { + return res.status(400).json({ message: 'Invalid status' }); + } + const sd = new Date(start_date); + const ed = new Date(end_date); + if (!Number.isFinite(sd.getTime()) || !Number.isFinite(ed.getTime())) { + return res.status(400).json({ message: 'Invalid start_date/end_date' }); + } + const data: any = { ad_name, target_url, start_date: sd, end_date: ed, status: parsedStatus }; if (role === 'ADMIN' && req.body.clientId) { data.clientId = req.body.clientId; } else if (role === 'CLIENT') { data.clientId = clientId; } else { return res.status(400).json({ message: 'Client ID required for admin ad creation' }); } const ad = await prisma.advertisement.create({ data }); res.json(ad); } catch (err) { console.error(err); res.status(500).json({ message: 'Error creating ad' }); } });backend/src/routes/ads.ts-7-29 (1)
7-29: Add consistent async error handling + validatestatus/searchinputs (avoid unhandled rejections / accidental 500s).
Right now, theGET /adshandler has notry/catchand useswhere: any, so DB/query errors can escape Express’s normal error flow and crash/poison the request lifecycle.import express from 'express'; import prisma from '../prisma'; import { authMiddleware } from '../middlewares/auth'; const router = express.Router(); // All routes require auth router.use(authMiddleware); +function parseAdStatus(s: unknown) { + if (typeof s !== 'string') return undefined; + const v = s.toUpperCase(); + return (v === 'ACTIVE' || v === 'PAUSED' || v === 'ARCHIVED') ? v : null; +} + // GET /ads - list ads (admins see all, clients only their own) router.get('/', async (req, res) => { - const role = req.user.role; - const clientId = req.user.clientId; - const { status, search } = req.query; - const where: any = {}; - if (role === 'CLIENT') where.clientId = clientId; - if (status) where.status = { equals: (status as string).toUpperCase() }; - if (search) { - where.OR = [ - { ad_name: { contains: search as string, mode: 'insensitive' } }, - { target_url: { contains: search as string, mode: 'insensitive' } }, - ]; - } - const ads = await prisma.advertisement.findMany({ - where, - orderBy: { createdAt: 'desc' }, - }); - res.json(ads); + try { + const role = req.user.role; + const clientId = req.user.clientId; + const { status, search } = req.query; + + const where: any = {}; + if (role === 'CLIENT') where.clientId = clientId; + + const parsedStatus = parseAdStatus(status); + if (parsedStatus === null) return res.status(400).json({ message: 'Invalid status' }); + if (parsedStatus) where.status = parsedStatus; + + if (typeof search === 'string' && search.trim()) { + where.OR = [ + { ad_name: { contains: search, mode: 'insensitive' } }, + { target_url: { contains: search, mode: 'insensitive' } }, + ]; + } + + const ads = await prisma.advertisement.findMany({ where, orderBy: { createdAt: 'desc' } }); + res.json(ads); + } catch (err) { + console.error(err); + res.status(500).json({ message: 'Error fetching ads' }); + } });
🟡 Minor comments (6)
frontend/index.html-2-2 (1)
2-2: Add lang attribute for accessibility and SEO.The
<html>element is missing thelangattribute, which is important for screen readers, search engines, and browser translation features.Apply this fix:
-<html> +<html lang="en">backend/package.json-13-22 (1)
13-22: Update outdated dependencies for maintainability.Most dependencies are currently secure with no known vulnerabilities. However, several packages are significantly outdated: bcrypt (5.1.0 → 6.0.0), dotenv (16.0.3 → 17.2.3), express (4.18.2 → 5.2.1), jsonwebtoken (9.0.0 → 9.0.3), prisma (5.6.0 → 7.1.0), @prisma/client (5.6.0 → 7.1.0), and zod (3.20.2 → 4.1.13). Consider updating these packages to receive bug fixes, performance improvements, and new features, especially prisma and zod which are two major versions behind.
design/DesignGuidelines.md-66-71 (1)
66-71: Update/clarify the design deadline (currently in the past).This PR was created on 2025-12-13, but the document states Deadline: 1st December 2025 (Line 69). Either update the date or clarify it was a past milestone.
README.md-73-95 (1)
73-95: Update/clarify the design deadline (currently in the past).README says Design deadline: 1st December 2025 (Line 92), but the PR was created 2025-12-13. Please update or clarify.
backend/src/routes/deeplinks.ts-5-8 (1)
5-8: Remove duplicated comments and dead commented-out code.Line 5–8 contains duplicate comment lines and an obsolete commented-out cache export. Keep the module clean to avoid confusion.
design/DesignGuidelines.md-18-22 (1)
18-22: Complete the “4 modes for each screen” requirement (currently unfinished).Line 20–22 says “provide 4 modes” but doesn’t list them in this doc (even though README later mentions them). Add the explicit list here to make this doc standalone.
🧹 Nitpick comments (15)
backend/Dockerfile (2)
1-19: Consider multi-stage build to reduce production image size.The current Dockerfile installs all dependencies (including devDependencies) and keeps them in the final image. A multi-stage build can reduce the production image size and attack surface by excluding development dependencies.
Consider this multi-stage approach:
# Build stage FROM node:20-alpine AS builder WORKDIR /usr/src/app COPY package.json package-lock.json* ./ RUN npm ci COPY . . RUN npx prisma generate RUN npm run build # Production stage FROM node:20-alpine WORKDIR /usr/src/app COPY package.json package-lock.json* ./ RUN npm ci --only=production COPY --from=builder /usr/src/app/dist ./dist COPY --from=builder /usr/src/app/node_modules/.prisma ./node_modules/.prisma COPY --from=builder /usr/src/app/prisma ./prisma EXPOSE 4000 CMD ["node", "dist/server.js"]
1-19: Run container as non-root user for better security.The container runs as root by default, which violates security best practices and increases the potential impact of container breakout vulnerabilities.
Add a non-root user:
FROM node:20-alpine WORKDIR /usr/src/app # Create non-root user RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 COPY package.json package-lock.json* ./ RUN npm install COPY . . RUN npx prisma generate RUN npm run build # Change ownership RUN chown -R nodejs:nodejs /usr/src/app USER nodejs EXPOSE 4000 CMD ["node", "dist/server.js"]backend/tsconfig.json (1)
1-12: Consider adding explicit include/exclude patterns.While the current configuration works, explicitly specifying which files to compile can prevent accidental compilation of test files, build artifacts, or other non-source files.
"compilerOptions": { "target": "ES2020", "module": "CommonJS", "rootDir": "src", "outDir": "dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true - } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] }.gitignore (1)
1-1: Minor redundancy in node_modules patterns.Line 1 (
node_modules/) at the root already coversfrontend/node_modules/andbackend/node_modules/due to how .gitignore patterns work. The specific subdirectory entries on lines 10-11 are redundant.You can simplify by removing the redundant lines:
node_modules/ dist/ build/ .env .env.local *.log .DS_Store .vscode/ .idea/ -frontend/node_modules/ -backend/node_modules/Also applies to: 10-11
backend/src/prisma.ts (2)
1-4: Add graceful shutdown handling for Prisma client.The Prisma client should be properly disconnected when the application shuts down to ensure connections are cleaned up and prevent connection leaks.
Consider adding shutdown handling:
import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); // Graceful shutdown const shutdown = async () => { await prisma.$disconnect(); process.exit(0); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); export default prisma;Alternatively, if shutdown is handled centrally in
server.ts, document that expectation and ensureprisma.$disconnect()is called there.
3-3: Consider singleton pattern for development hot-reload.In development with hot module reloading (ts-node-dev), multiple PrismaClient instances can be created, leading to connection pool exhaustion and warnings.
Implement a development-safe singleton:
import { PrismaClient } from '@prisma/client'; declare global { var prisma: PrismaClient | undefined; } const prisma = global.prisma || new PrismaClient(); if (process.env.NODE_ENV !== 'production') { global.prisma = prisma; } export default prisma;frontend/Dockerfile (1)
5-5: Usenpm cifor more deterministic builds.
npm installcan update dependencies based on semver ranges, leading to inconsistent builds.npm ciuses the exact versions frompackage-lock.json, ensuring reproducible builds.-RUN npm install +RUN npm cibackend/src/server.ts (1)
13-15: Add minimal API hardening knobs (CORS scoping + JSON size limit).
app.use(cors())(Line 13) defaults to*andexpress.json()(Line 14) defaults can accept large payloads. Even for “example apps”, adding guardrails helps avoid surprises.-app.use(cors()); -app.use(express.json()); +app.use( + cors({ + origin: process.env.CORS_ORIGIN?.split(',') ?? true, + credentials: true, + }), +); +app.use(express.json({ limit: '1mb' }));docker-compose.yml (1)
15-26: Avoidnpm installon every container start; don’t clobber node_modules with bind mounts.Current pattern (Line 24–25) is slow and can produce odd host/container state. Consider a named volume for
/usr/src/app/node_modulesand/or move install/generate into the image build.backend/src/routes/deeplinks.ts (1)
12-41: Optional: addCache-Controlheaders to match the in-memory TTL.Since you’re caching for 60s, consider
res.set('Cache-Control', 'public, max-age=60')on both endpoints for better client/CDN behavior.backend/src/routes/admin.ts (1)
42-45: Consider pagination forGET /deeplinkif this can grow unbounded.
Admin endpoints still need guardrails to avoid accidental heavy reads.backend/src/middlewares/auth.ts (1)
5-13: Avoidreq.user?: any; define a concreteRequestUsershape (prevents accidental misuse).
This will help route code avoidany-driven bugs.declare global { namespace Express { interface Request { - user?: any; + user?: { id: number; role: 'ADMIN' | 'CLIENT'; clientId: number | null }; } } }backend/src/routes/auth.ts (1)
52-67: Consider basic brute-force protection on/login(rate limit / lockout).
Not required for functionality, but this is a common production hardening step.backend/prisma/schema.prisma (2)
10-40: Add indexes on FK columns used for filtering (clientId) to keep list endpoints fast.
Your routes frequently filter byclientId; indexing is low-effort and high-impact.model User { id Int @id @default(autoincrement()) email String @unique password String role UserRole client Client? @relation(fields: [clientId], references: [id]) clientId Int? createdAt DateTime @default(now()) + + @@index([clientId]) } model Advertisement { id Int @id @default(autoincrement()) ad_name String target_url String start_date DateTime end_date DateTime status AdStatus @default(ACTIVE) client Client @relation(fields: [clientId], references: [id]) clientId Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@index([clientId]) + @@index([status]) }
10-40: Consider explicit referential actions forClientrelations (avoid surprises on deletes).
Depending on your intended behavior, setonDelete: Restrict|Cascade|SetNullforUser.client/Advertisement.client.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (280)
frontend/node_modules/.bin/baseline-browser-mappingis excluded by!**/node_modules/**frontend/node_modules/.bin/baseline-browser-mapping.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/baseline-browser-mapping.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/browserslistis excluded by!**/node_modules/**frontend/node_modules/.bin/browserslist.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/browserslist.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/esbuildis excluded by!**/node_modules/**frontend/node_modules/.bin/esbuild.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/esbuild.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/jsescis excluded by!**/node_modules/**frontend/node_modules/.bin/jsesc.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/jsesc.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/json5is excluded by!**/node_modules/**frontend/node_modules/.bin/json5.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/json5.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/loose-envifyis excluded by!**/node_modules/**frontend/node_modules/.bin/loose-envify.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/loose-envify.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/nanoidis excluded by!**/node_modules/**frontend/node_modules/.bin/nanoid.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/nanoid.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/parseris excluded by!**/node_modules/**frontend/node_modules/.bin/parser.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/parser.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/rollupis excluded by!**/node_modules/**frontend/node_modules/.bin/rollup.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/rollup.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/semveris excluded by!**/node_modules/**frontend/node_modules/.bin/semver.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/semver.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/tscis excluded by!**/node_modules/**frontend/node_modules/.bin/tsc.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/tsc.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/tsserveris excluded by!**/node_modules/**frontend/node_modules/.bin/tsserver.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/tsserver.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/update-browserslist-dbis excluded by!**/node_modules/**frontend/node_modules/.bin/update-browserslist-db.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/update-browserslist-db.ps1is excluded by!**/node_modules/**frontend/node_modules/.bin/viteis excluded by!**/node_modules/**frontend/node_modules/.bin/vite.cmdis excluded by!**/node_modules/**frontend/node_modules/.bin/vite.ps1is excluded by!**/node_modules/**frontend/node_modules/.package-lock.jsonis excluded by!**/node_modules/**frontend/node_modules/.vite/deps/_metadata.jsonis excluded by!**/node_modules/**frontend/node_modules/.vite/deps/axios.jsis excluded by!**/node_modules/**frontend/node_modules/.vite/deps/axios.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/.vite/deps/chunk-6BKLQ22S.jsis excluded by!**/node_modules/**frontend/node_modules/.vite/deps/chunk-6BKLQ22S.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/.vite/deps/chunk-DRWLMN53.jsis excluded by!**/node_modules/**frontend/node_modules/.vite/deps/chunk-DRWLMN53.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/.vite/deps/chunk-G3PMV62Z.jsis excluded by!**/node_modules/**frontend/node_modules/.vite/deps/chunk-G3PMV62Z.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/.vite/deps/package.jsonis excluded by!**/node_modules/**frontend/node_modules/.vite/deps/react-dom_client.jsis excluded by!**/node_modules/**frontend/node_modules/.vite/deps/react-dom_client.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/.vite/deps/react-router-dom.jsis excluded by!**/node_modules/**frontend/node_modules/.vite/deps/react-router-dom.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/.vite/deps/react.jsis excluded by!**/node_modules/**frontend/node_modules/.vite/deps/react.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/code-frame/LICENSEis excluded by!**/node_modules/**frontend/node_modules/@babel/code-frame/README.mdis excluded by!**/node_modules/**frontend/node_modules/@babel/code-frame/lib/index.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/code-frame/lib/index.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/code-frame/package.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/LICENSEis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/README.mdis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/corejs2-built-ins.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/corejs3-shipped-proposals.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/data/corejs2-built-ins.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/data/corejs3-shipped-proposals.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/data/native-modules.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/data/overlapping-plugins.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/data/plugin-bugfixes.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/data/plugins.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/native-modules.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/overlapping-plugins.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/package.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/plugin-bugfixes.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/compat-data/plugins.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/LICENSEis excluded by!**/node_modules/**frontend/node_modules/@babel/core/README.mdis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/cache-contexts.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/cache-contexts.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/caching.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/caching.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/config-chain.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/config-chain.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/config-descriptors.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/config-descriptors.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/files/configuration.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/files/configuration.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/files/import.cjsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/files/import.cjs.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/files/index-browser.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/files/index-browser.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/files/index.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/files/index.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/files/module-types.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/files/module-types.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/files/package.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/files/package.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/files/plugins.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/files/plugins.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/files/types.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/files/types.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/files/utils.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/files/utils.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/full.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/full.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/helpers/config-api.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/helpers/config-api.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/helpers/deep-array.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/helpers/deep-array.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/helpers/environment.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/helpers/environment.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/index.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/index.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/item.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/item.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/partial.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/partial.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/pattern-to-regex.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/pattern-to-regex.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/plugin.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/plugin.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/printer.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/printer.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/resolve-targets-browser.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/resolve-targets-browser.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/resolve-targets.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/resolve-targets.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/util.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/util.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/validation/option-assertions.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/validation/option-assertions.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/validation/options.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/validation/options.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/validation/plugins.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/validation/plugins.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/config/validation/removed.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/config/validation/removed.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/errors/config-error.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/errors/config-error.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/errors/rewrite-stack-trace.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/errors/rewrite-stack-trace.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/gensync-utils/async.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/gensync-utils/async.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/gensync-utils/fs.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/gensync-utils/fs.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/gensync-utils/functional.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/gensync-utils/functional.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/index.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/index.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/parse.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/parse.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/parser/index.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/parser/index.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/parser/util/missing-plugin-helper.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/parser/util/missing-plugin-helper.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/tools/build-external-helpers.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/tools/build-external-helpers.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transform-ast.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transform-ast.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transform-file-browser.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transform-file-browser.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transform-file.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transform-file.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transform.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transform.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transformation/block-hoist-plugin.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transformation/block-hoist-plugin.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transformation/file/babel-7-helpers.cjsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transformation/file/babel-7-helpers.cjs.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transformation/file/file.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transformation/file/file.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transformation/file/generate.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transformation/file/generate.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transformation/file/merge-map.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transformation/file/merge-map.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transformation/index.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transformation/index.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transformation/normalize-file.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transformation/normalize-file.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transformation/normalize-opts.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transformation/normalize-opts.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transformation/plugin-pass.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transformation/plugin-pass.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/transformation/util/clone-deep.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/transformation/util/clone-deep.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/lib/vendor/import-meta-resolve.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/lib/vendor/import-meta-resolve.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/core/package.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/core/src/config/files/index-browser.tsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/src/config/files/index.tsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/src/config/resolve-targets-browser.tsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/src/config/resolve-targets.tsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/src/transform-file-browser.tsis excluded by!**/node_modules/**frontend/node_modules/@babel/core/src/transform-file.tsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/LICENSEis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/README.mdis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/buffer.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/buffer.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/base.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/base.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/classes.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/classes.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/deprecated.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/deprecated.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/expressions.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/expressions.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/flow.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/flow.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/index.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/index.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/jsx.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/jsx.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/methods.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/methods.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/modules.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/modules.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/statements.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/statements.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/template-literals.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/template-literals.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/types.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/types.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/generators/typescript.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/generators/typescript.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/index.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/index.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/node/index.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/node/index.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/node/parentheses.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/node/parentheses.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/node/whitespace.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/node/whitespace.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/printer.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/printer.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/source-map.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/source-map.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/lib/token-map.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/generator/lib/token-map.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/generator/package.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-compilation-targets/LICENSEis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-compilation-targets/README.mdis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-compilation-targets/lib/debug.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-compilation-targets/lib/debug.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/helper-compilation-targets/lib/filter-items.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-compilation-targets/lib/filter-items.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/helper-compilation-targets/lib/index.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-compilation-targets/lib/index.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/helper-compilation-targets/lib/options.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-compilation-targets/lib/options.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/helper-compilation-targets/lib/pretty.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-compilation-targets/lib/pretty.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/helper-compilation-targets/lib/targets.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-compilation-targets/lib/targets.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/helper-compilation-targets/lib/utils.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-compilation-targets/lib/utils.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/helper-compilation-targets/package.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-globals/LICENSEis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-globals/README.mdis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-globals/data/browser-upper.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-globals/data/builtin-lower.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-globals/data/builtin-upper.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-globals/package.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-module-imports/LICENSEis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-module-imports/README.mdis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-module-imports/lib/import-builder.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-module-imports/lib/import-builder.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/helper-module-imports/lib/import-injector.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-module-imports/lib/import-injector.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/helper-module-imports/lib/index.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-module-imports/lib/index.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/helper-module-imports/lib/is-module.jsis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-module-imports/lib/is-module.js.mapis excluded by!**/node_modules/**,!**/*.mapfrontend/node_modules/@babel/helper-module-imports/package.jsonis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-module-transforms/LICENSEis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-module-transforms/README.mdis excluded by!**/node_modules/**frontend/node_modules/@babel/helper-module-transforms/lib/dynamic-import.jsis excluded by!**/node_modules/**
📒 Files selected for processing (20)
.gitignore(1 hunks)README.md(1 hunks)backend/.env(1 hunks)backend/Dockerfile(1 hunks)backend/package.json(1 hunks)backend/prisma/schema.prisma(1 hunks)backend/prisma/seed.ts(1 hunks)backend/src/middlewares/auth.ts(1 hunks)backend/src/prisma.ts(1 hunks)backend/src/routes/admin.ts(1 hunks)backend/src/routes/ads.ts(1 hunks)backend/src/routes/auth.ts(1 hunks)backend/src/routes/deeplinks.ts(1 hunks)backend/src/server.ts(1 hunks)backend/src/services/cache.ts(1 hunks)backend/tsconfig.json(1 hunks)design/DesignGuidelines.md(1 hunks)docker-compose.yml(1 hunks)frontend/Dockerfile(1 hunks)frontend/index.html(1 hunks)
🧰 Additional context used
🪛 dotenv-linter (4.0.0)
backend/.env
[warning] 2-2: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 4-4: [UnorderedKey] The NODE_ENV key should go before the PORT key
(UnorderedKey)
🔇 Additional comments (8)
frontend/index.html (1)
7-7: Inline theme script is appropriate for preventing flash.The inline script that sets
data-themeanddata-orientationis correctly placed to prevent flash of unstyled content (FOUC) when the page loads. This is a valid pattern for theme initialization.backend/tsconfig.json (1)
1-12: TypeScript configuration is solid.The configuration appropriately targets ES2020 with CommonJS modules for the Node.js backend, enables strict type checking, and sets up correct input/output directories. The settings align well with the project structure.
.gitignore (1)
4-5: Excellent: .env files are now properly ignored.Adding
.envand.env.localto .gitignore is critical for preventing secrets from being committed to version control. This addresses the security concern with the committedbackend/.envfile in this PR.Important: The existing
backend/.envfile in this PR should be removed from the repository and replaced with a.env.exampletemplate file.frontend/Dockerfile (1)
1-13: Multi-stage build with nginx is well-structured.The Dockerfile effectively uses a multi-stage build to compile the frontend in a Node environment and serve it with nginx, resulting in a minimal production image. This is a solid approach for deploying React applications.
backend/src/services/cache.ts (1)
1-5: LGTM for a simple shared in-memory TTL cache.backend/src/server.ts (1)
16-20:/adminroute is properly protected by auth middleware.The
/adminroute inserver.ts(line 19) is mounted toadminRouter, which applies bothauthMiddlewareandadminOnlyat the router level (lines 8-9 inbackend/src/routes/admin.ts). Authentication is enforced before any admin endpoint executes.backend/src/routes/admin.ts (1)
8-10: Middleware stack is appropriately strict for admin-only routes.
Auth +adminOnlyat the router level is clean and hard to bypass.backend/src/middlewares/auth.ts (1)
35-45:adminOnly/clientOrAdminlook fine; consider centralizing role checks (optional).
If you add more roles later, a small helper avoids duplicated branching.
- Add backend/.env to .gitignore to prevent committing secrets - Create .env.example template with all required configuration placeholders - Update README with environment setup instructions - Prevents accidental exposure of DATABASE_URL, JWT_SECRET, ADMIN_INVITE_KEY
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (5)
frontend/nginx.conf (2)
20-20:X-XSS-Protectionis deprecated and can cause issues in modern browsers.Modern browsers have removed XSS Auditor, and this header can introduce vulnerabilities in some edge cases. Consider removing it or replacing with a Content-Security-Policy header for XSS protection.
- add_header X-XSS-Protection "1; mode=block" always; + # Consider adding CSP instead: + # add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;
37-39: 5xx errors routing to index.html may mask server issues.While routing 404 to index.html is correct for SPA client-side routing, redirecting 5xx errors might make debugging harder. Consider keeping default error pages for server errors or adding a dedicated error page.
# Error pages error_page 404 /index.html; - error_page 500 502 503 504 /index.html; + # Keep default 5xx handling or use a dedicated error page + # error_page 500 502 503 504 /50x.html;backend/prisma/seed.ts (1)
72-76:createManydoesn't handle re-runs gracefully.Unlike the
upsertoperations for users and clients,createManyfor advertisements will fail or create duplicates when the seed is run multiple times. Consider usingupsertor addingskipDuplicates: true:// Ads - await prisma.advertisement.createMany({ data: [ - { ad_name: 'Holiday Sale', target_url: 'https://example.com/holiday', start_date: new Date(), end_date: new Date(new Date().getTime() + 1000*60*60*24*30), status: 'ACTIVE', clientId: clientA.id }, - { ad_name: 'New Product', target_url: 'https://example.com/new', start_date: new Date(), end_date: new Date(new Date().getTime() + 1000*60*60*24*15), status: 'PAUSED', clientId: clientA.id } - ]}); + await prisma.advertisement.createMany({ + data: [ + { ad_name: 'Holiday Sale', target_url: 'https://example.com/holiday', start_date: new Date(), end_date: new Date(new Date().getTime() + 1000*60*60*24*30), status: 'ACTIVE', clientId: clientA.id }, + { ad_name: 'New Product', target_url: 'https://example.com/new', start_date: new Date(), end_date: new Date(new Date().getTime() + 1000*60*60*24*15), status: 'PAUSED', clientId: clientA.id } + ], + skipDuplicates: true, + });backend/src/middlewares/auth.ts (1)
6-12: Consider defining a typed interface forreq.user.Using
anyforreq.userloses type safety. A typed interface would provide better IntelliSense and catch errors at compile time.+interface AuthUser { + id: number; + role: string; + clientId: number | null; +} + declare global { namespace Express { interface Request { - user?: any; + user?: AuthUser; } } }backend/src/config.ts (1)
64-65: Consider validating PORT is a valid number.
Number(PORT)will produceNaNfor invalid inputs like"abc", which could cause silent failures when binding the server.+function parsePort(value: string | number): number { + const port = Number(value); + if (isNaN(port) || port < 1 || port > 65535) { + console.error(`❌ Invalid PORT value: ${value}`); + process.exit(1); + } + return port; +} + export const config = { // ... server: { - port: Number(PORT), + port: parsePort(PORT), nodeEnv: NODE_ENV, // ... }, };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
.gitignore(1 hunks)README.md(1 hunks)backend/.env.example(1 hunks)backend/prisma/seed.ts(1 hunks)backend/src/config.ts(1 hunks)backend/src/middlewares/auth.ts(1 hunks)backend/src/routes/auth.ts(1 hunks)backend/src/server.ts(1 hunks)frontend/Dockerfile(1 hunks)frontend/nginx.conf(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- frontend/Dockerfile
- backend/src/server.ts
- .gitignore
🧰 Additional context used
🧬 Code graph analysis (2)
backend/src/routes/auth.ts (2)
backend/src/config.ts (1)
config(53-70)backend/src/middlewares/auth.ts (1)
authMiddleware(26-73)
backend/src/middlewares/auth.ts (1)
backend/src/config.ts (1)
config(53-70)
🪛 dotenv-linter (4.0.0)
backend/.env.example
[warning] 7-7: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 13-13: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 17-17: [UnorderedKey] The NODE_ENV key should go before the PORT key
(UnorderedKey)
🪛 LanguageTool
README.md
[style] ~178-~178: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...tes CLIENT users - Admin accounts can ONLY be created via: - Database seed wit...
(ADVERB_REPETITION_PREMIUM)
🪛 markdownlint-cli2 (0.18.1)
README.md
26-26: Unordered list indentation
Expected: 0; Actual: 3
(MD007, ul-indent)
27-27: Unordered list indentation
Expected: 2; Actual: 5
(MD007, ul-indent)
28-28: Unordered list indentation
Expected: 2; Actual: 5
(MD007, ul-indent)
29-29: Unordered list indentation
Expected: 0; Actual: 3
(MD007, ul-indent)
30-30: Unordered list indentation
Expected: 2; Actual: 5
(MD007, ul-indent)
31-31: Unordered list indentation
Expected: 2; Actual: 5
(MD007, ul-indent)
32-32: Unordered list indentation
Expected: 2; Actual: 5
(MD007, ul-indent)
33-33: Unordered list indentation
Expected: 2; Actual: 5
(MD007, ul-indent)
34-34: Unordered list indentation
Expected: 0; Actual: 3
(MD007, ul-indent)
35-35: Unordered list indentation
Expected: 0; Actual: 3
(MD007, ul-indent)
36-36: Unordered list indentation
Expected: 0; Actual: 3
(MD007, ul-indent)
57-57: Unordered list indentation
Expected: 0; Actual: 3
(MD007, ul-indent)
58-58: Unordered list indentation
Expected: 0; Actual: 3
(MD007, ul-indent)
59-59: Unordered list indentation
Expected: 0; Actual: 3
(MD007, ul-indent)
65-65: Unordered list indentation
Expected: 0; Actual: 3
(MD007, ul-indent)
66-66: Unordered list indentation
Expected: 0; Actual: 3
(MD007, ul-indent)
67-67: Unordered list indentation
Expected: 0; Actual: 3
(MD007, ul-indent)
186-186: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
187-187: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
188-188: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
189-189: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
190-190: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
193-193: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
194-194: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
197-197: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
🔇 Additional comments (13)
backend/.env.example (1)
1-26: Well-documented example configuration file.Good practices observed:
- Clear instructions for generating secure secrets
- Appropriate documentation of optional vs required variables
- Security warnings about seed credentials
The static analysis warnings about quote characters are false positives for an example file, and the key ordering suggestion is negligible.
backend/prisma/seed.ts (1)
25-47: Good implementation of environment-driven password handling.The credential management now properly:
- Requires explicit passwords in production (fails fast)
- Generates secure random passwords in development
- Logs generated passwords for local testing convenience
This addresses the previous review concerns about hardcoded credentials.
backend/src/middlewares/auth.ts (2)
14-15: Good: JWT_SECRET now uses centralized, validated configuration.The secret is sourced from the validated config module, eliminating the insecure fallback that was flagged in the previous review.
26-72: Well-structured authentication middleware.The implementation properly:
- Validates Bearer token format strictly (lines 34-38)
- Catches JWT verification errors internally and returns generic messages (lines 44-50)
- Distinguishes auth failures (401) from server errors (500)
- Only sets
req.userafter all validations passThis addresses all concerns from the previous review.
backend/src/config.ts (1)
24-48: Solid fail-fast configuration validation.Good practices:
- Validates critical variables at startup
- Logs helpful error messages without exposing sensitive values
- Uses
process.exit(1)to prevent running with invalid configbackend/src/routes/auth.ts (5)
1-13: Previous issues have been addressed.The centralized config is now correctly used for
JWT_SECRETandADMIN_INVITE_KEY, and the public registration endpoint no longer allowsADMINrole creation.Note:
adminOnlyis imported on line 6 but not used in this file. Consider removing the unused import.
15-27: LGTM!The Zod schemas correctly enforce input validation:
registerSchemano longer accepts arolefield (addressing the previous security concern), andadminRegisterSchemaproperly requires aninviteKey.
34-73: LGTM!The public registration endpoint correctly enforces CLIENT-only role creation. Password hashing and error handling are properly implemented.
127-151: LGTM!The login endpoint follows security best practices: generic error messages prevent user enumeration,
bcrypt.compareis timing-safe, and JWT expiry is properly configured.
153-166: LGTM!The
/meendpoint is properly protected withauthMiddlewareand correctly fetches the authenticated user's information.README.md (3)
26-36: Documentation is accurate and comprehensive.The security notes about environment variables and fail-fast configuration validation are excellent. Static analysis flagged minor list indentation issues (lines 26-36 use 3-space indent instead of 0), but this renders correctly in most Markdown parsers.
162-209: Thorough security documentation.The security considerations section provides excellent guidance on configuration management, authentication flows, and production recommendations. The acknowledgment that localStorage tokens should be replaced with session cookies for production is particularly valuable.
211-222: LGTM!The roadmap appropriately identifies important future enhancements (tests, CI, refresh tokens, audit logs) that align with production-readiness requirements.
Overview
This PR introduces a fully-featured frontend application for Zplit - a modern expense splitting platform. The frontend includes comprehensive UI components for authentication, expense management, group handling, and visual expense analytics.
Features Implemented
Technical Stack
Key Components
NavBar.tsx- Navigation with theme and layout controlsLogin.tsx- Authentication interfaceDashboard.tsx- Main ad management systemAddExpenses.tsx- Expense tracking and splittingGroups.tsx- Group managementSplitButton.tsx&SplitModal.tsx- Bill splitting UIThemeProvider- Context-based theme managementUI/UX Improvements
Integration
http://localhost:4000VITE_API_URLenvironment variableFiles Changed
Testing
npm run devin the frontend directoryhttp://localhost:5173/loginrouteNotes
Summary by CodeRabbit
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.