Codex 5.3 Refactor Note: Canonical refactor plan: docs/CODEX-5.3-REFACTOR-PLAN.md. This document is retained for historical and implementation context during the refactor.
W6 Code Structure & File Listing
Quick Reference
Total Files: 12
- 2 API endpoints
- 2 UI components + 1 layout
- 2 utility libraries
- 1 validation schema update
- 1 database schema update
- 3 documentation files
File Tree
projectweb-nextjs/
├── prisma/
│ └── schema.prisma [MODIFIED: +Candidate, CandidateDocument]
│
├── src/
│ ├── app/
│ │ ├── (app)/
│ │ │ ├── layout.tsx [NEW: 8 lines]
│ │ │ └── upload-resume/
│ │ │ ├── page.tsx [NEW: 380 lines]
│ │ │ └── _components/
│ │ │ └── Dropzone.tsx [NEW: 140 lines]
│ │ │
│ │ └── api/
│ │ ├── uploads/
│ │ │ └── presign/
│ │ │ └── route.ts [NEW: 50 lines]
│ │ │
│ │ └── candidates/
│ │ └── route.ts [NEW: 95 lines]
│ │
│ └── lib/
│ ├── validation/
│ │ └── schemas.ts [MODIFIED: +presignUploadSchema, createCandidateSchema]
│ │
│ ├── storage/
│ │ └── s3.ts [NEW: 35 lines]
│ │
│ └── candidates/
│ └── code.ts [NEW: 8 lines]
│
└── docs/
├── W6-UPLOAD-RESUME-IMPLEMENTATION.md [NEW: Full spec]
├── W6-IMPLEMENTATION-SUMMARY.md [NEW: Overview]
└── W6-SETUP-GUIDE.md [NEW: Deployment guide]
Schema Changes (Prisma)
Enums Added
enum CandidateStatus {
NEW
HR_SCREENED
MANAGER_EVAL_PENDING
MANAGER_EVAL_COMPLETED
TO_SMO
DECISION_MADE
COMPLETED
}
enum DocumentCategory {
RESUME
SCREENING_NOTES
EVALUATION
DECISION
OFFER
}
enum DocumentStatus {
AVAILABLE
PENDING_SCAN
}
enum AuditEventType {
// ... existing ...
RESUME_UPLOADED // [NEW]
}
Models Added
model Candidate {
id String @id @default(cuid())
candidateCode String @unique
fullName String?
applyingFor String
status CandidateStatus @default(NEW)
createdByUserId String
createdByUser User? @relation("CandidateCreatedBy", fields: [createdByUserId], references: [id], onDelete: SetNull)
documents CandidateDocument[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CandidateDocument {
id String @id @default(cuid())
candidateId String
candidate Candidate @relation(fields: [candidateId], references: [id], onDelete: Cascade)
category DocumentCategory
filename String
mimeType String
sizeBytes Int
storageKey String
version Int @default(1)
status DocumentStatus @default(AVAILABLE)
uploadedByUserId String
uploadedByUser User? @relation("DocumentUploadedBy", fields: [uploadedByUserId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
User Model Update
model User {
// ... existing fields ...
createdCandidates Candidate[] @relation("CandidateCreatedBy")
uploadedDocuments CandidateDocument[] @relation("DocumentUploadedBy")
}
API Endpoints
1. POST /api/uploads/presign
Request:
{
"filename": "resume.pdf",
"mimeType": "application/pdf",
"sizeBytes": 524288
}
Response (200):
{
"uploadUrl": "https://s3.amazonaws.com/...",
"storageKey": "candidates/temp-abc123/resume/1704974400000.pdf",
"expiresIn": 3600
}
Errors:
- 400: Invalid file metadata
- 401: Not authenticated
- 403: Not HR/ADMIN
- 500: Server error
2. POST /api/candidates
Request:
{
"applyingFor": "Software Engineer",
"fullName": "John Doe",
"notes": "Referred by Jane Smith",
"resume": {
"filename": "resume.pdf",
"mimeType": "application/pdf",
"sizeBytes": 524288,
"storageKey": "candidates/temp-abc123/resume/1704974400000.pdf"
}
}
Response (201):
{
"candidateId": "cuid123",
"candidateCode": "CAND-2026-45678",
"message": "Candidate created and resume uploaded successfully"
}
Errors:
- 400: Invalid candidate data
- 401: Not authenticated
- 403: Not HR/ADMIN
- 500: Database/transaction error
Key Functions
Storage Helper: src/lib/storage/s3.ts
async function generatePresignedUrl(
storageKey: string,
mimeType: string
): Promise<string>
Returns: S3 PUT presigned URL (expires in 1 hour)
function generateStorageKey(
candidateCode: string,
filename: string
): string
Returns: candidates/{candidateCode}/resume/{timestamp}.{ext}
Candidate Code Generator: src/lib/candidates/code.ts
function generateCandidateCode(): string
Returns: CAND-YYYY-##### (e.g., CAND-2026-45678)
Validation Schemas: src/lib/validation/schemas.ts
const presignUploadSchema = z.object({
filename: z.string().min(1).max(255),
mimeType: z.enum([...]),
sizeBytes: z.number().positive().max(10485760),
})
const createCandidateSchema = z.object({
applyingFor: z.string().min(1).max(255),
fullName: z.string().max(255).optional().or(z.literal('')),
notes: z.string().max(2000).optional().or(z.literal('')),
resume: z.object({...}),
})
UI Components
Main Page: src/app/(app)/upload-resume/page.tsx
Component: UploadResumePage (Client)
Features:
- Form with 4 fields (position, name, resume, notes)
- Dropzone integration
- 3-stage submission flow
- Progress indicator
- Error/success states
- Auto-redirect on success
State:
type UploadState = 'idle' | 'uploading' | 'success' | 'error'
Dropzone: src/app/(app)/upload-resume/_components/Dropzone.tsx
Component: Dropzone (Client)
Props:
interface DropzoneProps {
onFilesSelected: (files: File[]) => void
disabled?: boolean
}
Validation:
- Types: PDF, DOC, DOCX only
- Size: Max 10MB
- Returns first file only
Layout: src/app/(app)/layout.tsx
Component: AppLayout (Server)
Content: Minimal wrapper (passes children)
RBAC Implementation
All endpoints use:
const payload = await requireRole([Role.HR, Role.ADMIN])
Returns 403 Forbidden if user is MANAGER or SMO.
Audit Events
CANDIDATE_CREATED
{
"eventType": "CANDIDATE_CREATED",
"userId": "hr-user-id",
"details": {
"entity_type": "Candidate",
"entity_id": "cand-id",
"candidateCode": "CAND-2026-45678",
"status": "NEW",
"applyingFor": "Software Engineer",
"fullName": "John Doe"
}
}
RESUME_UPLOADED
{
"eventType": "RESUME_UPLOADED",
"userId": "hr-user-id",
"details": {
"entity_type": "CandidateDocument",
"entity_id": "doc-id",
"candidateId": "cand-id",
"candidateCode": "CAND-2026-45678",
"filename": "resume.pdf",
"sizeBytes": 524288,
"storageKey": "candidates/temp-abc/resume/1704974400000.pdf"
}
}
Configuration
Environment Variables
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/projectweb
JWT_SECRET=dev-secret-key
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
S3_BUCKET_NAME=offers-review
S3_ENDPOINT=https://s3.amazonaws.com # Optional
Dependencies
@prisma/client– ORMzod– Validation@aws-sdk/client-s3– S3 client@aws-sdk/s3-request-presigner– Presigned URLsjsonwebtoken– JWT verificationnext– Framework
Testing Checklist
- Database migration applied
- AWS credentials configured
- Dev server running without errors
- HR user can access
/upload-resume - MANAGER/SMO cannot access
/upload-resume(403) - Can upload PDF/DOC/DOCX successfully
- Cannot upload invalid type (error message)
- Cannot upload >10MB (error message)
- Candidate record created in DB
- Resume document record created in DB
- Audit logs written
- File uploaded to S3
- Auto-redirect to
/candidates/{id}works - Cancel button redirects to
/dashboard
Deployment Steps
-
Apply migration:
npx prisma migrate dev --name add_candidates_and_documents -
Install dependencies:
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner -
Configure environment:
# Update .env.local with AWS credentials -
Start dev server:
npm run dev -
Test end-to-end:
- Login as HR
- Navigate to
/upload-resume - Upload resume
- Verify redirect and database records
File Statistics
| File | Type | Lines |
|---|---|---|
schema.prisma | Schema | +60 |
s3.ts | Utility | 35 |
code.ts | Utility | 8 |
schemas.ts | Validation | +50 |
presign/route.ts | API | 50 |
candidates/route.ts | API | 95 |
upload-resume/page.tsx | UI | 380 |
Dropzone.tsx | Component | 140 |
(app)/layout.tsx | Layout | 8 |
| Total | Code | ~1100 |
Plus 3 documentation files (~1500 lines).
W6 Complete & Ready for Testing