Skip to content

Commit a2c706e

Browse files
author
prakash meena
committed
feat: resume upload with auto-parsing for student profiles
1 parent 6cff330 commit a2c706e

9 files changed

Lines changed: 1055 additions & 0 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { NextResponse } from 'next/server';
2+
import dbConnect from '@/lib/mongodb';
3+
import { StudentProfile } from '@/models/StudentProfile';
4+
import { RateLimiter } from '@/lib/rate-limit';
5+
import type { ParsedResume } from '@/types/student';
6+
7+
const confirmLimiter = new RateLimiter(10, 60000);
8+
9+
export async function POST(req: Request) {
10+
const ip = req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip') || 'unknown';
11+
12+
if (!(await confirmLimiter.check(ip))) {
13+
return NextResponse.json(
14+
{ success: false, error: 'Too many requests, please try again later.' },
15+
{ status: 429 }
16+
);
17+
}
18+
19+
let body: unknown;
20+
21+
try {
22+
body = await req.json();
23+
} catch {
24+
return NextResponse.json(
25+
{ success: false, error: 'Malformed JSON request body' },
26+
{ status: 400 }
27+
);
28+
}
29+
30+
const { githubUsername, data } = body as {
31+
githubUsername?: unknown;
32+
data?: unknown;
33+
};
34+
35+
if (!githubUsername || typeof githubUsername !== 'string') {
36+
return NextResponse.json(
37+
{ success: false, error: 'Invalid or missing githubUsername' },
38+
{ status: 400 }
39+
);
40+
}
41+
42+
if (!data || typeof data !== 'object') {
43+
return NextResponse.json(
44+
{ success: false, error: 'Invalid or missing profile data' },
45+
{ status: 400 }
46+
);
47+
}
48+
49+
const profileData = data as ParsedResume;
50+
51+
if (!profileData.name || !profileData.email) {
52+
return NextResponse.json(
53+
{ success: false, error: 'Name and email are required' },
54+
{ status: 400 }
55+
);
56+
}
57+
58+
try {
59+
if (!process.env.MONGODB_URI) {
60+
console.warn('MONGODB_URI is not set. Bypassing student profile save.');
61+
return NextResponse.json({ success: true, bypassed: true });
62+
}
63+
64+
await dbConnect();
65+
66+
await StudentProfile.findOneAndUpdate(
67+
{ githubUsername: githubUsername.trim().toLowerCase() },
68+
{
69+
$set: {
70+
name: profileData.name,
71+
email: profileData.email,
72+
skills: profileData.skills || [],
73+
education: profileData.education || [],
74+
experience: profileData.experience || [],
75+
updatedAt: new Date(),
76+
},
77+
},
78+
{ upsert: true, new: true }
79+
);
80+
81+
return NextResponse.json({ success: true });
82+
} catch (error) {
83+
console.error('Error saving student profile:', error);
84+
return NextResponse.json(
85+
{ success: false, error: 'Failed to save profile data' },
86+
{ status: 500 }
87+
);
88+
}
89+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { NextResponse } from 'next/server';
2+
import { parseResume, ALLOWED_MIME_TYPES, MAX_FILE_SIZE } from '@/lib/resume-parser';
3+
import { RateLimiter } from '@/lib/rate-limit';
4+
5+
const uploadLimiter = new RateLimiter(10, 60000);
6+
7+
export async function POST(req: Request) {
8+
const ip = req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip') || 'unknown';
9+
10+
if (!(await uploadLimiter.check(ip))) {
11+
return NextResponse.json(
12+
{ success: false, error: 'Too many requests, please try again later.' },
13+
{ status: 429 }
14+
);
15+
}
16+
17+
let formData: FormData;
18+
19+
try {
20+
formData = await req.formData();
21+
} catch {
22+
return NextResponse.json({ success: false, error: 'Invalid form data' }, { status: 400 });
23+
}
24+
25+
const file = formData.get('resume') as File | null;
26+
27+
if (!file) {
28+
return NextResponse.json({ success: false, error: 'No resume file provided' }, { status: 400 });
29+
}
30+
31+
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
32+
return NextResponse.json(
33+
{
34+
success: false,
35+
error: 'Invalid file type. Only PDF and DOCX files are accepted.',
36+
},
37+
{ status: 400 }
38+
);
39+
}
40+
41+
if (file.size > MAX_FILE_SIZE) {
42+
return NextResponse.json(
43+
{
44+
success: false,
45+
error: 'File size exceeds the 5MB limit.',
46+
},
47+
{ status: 400 }
48+
);
49+
}
50+
51+
try {
52+
const buffer = Buffer.from(await file.arrayBuffer());
53+
const parsed = await parseResume(buffer, file.type);
54+
55+
return NextResponse.json({
56+
success: true,
57+
data: parsed,
58+
fileName: file.name,
59+
});
60+
} catch (error) {
61+
console.error('Error parsing resume:', error);
62+
return NextResponse.json(
63+
{ success: false, error: 'Failed to parse resume. Please enter your details manually.' },
64+
{ status: 422 }
65+
);
66+
}
67+
}

components/dashboard/DashboardClient.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import RepositoryGraph from './RepositoryGraph';
2121
import ComparisonStatsCard from './ComparisonStatsCard';
2222
import RadarChart from './RadarChart';
2323
import GrowthTrendChart from './GrowthTrendChart';
24+
import ResumeProfileSection from './ResumeProfileSection';
2425

2526
// Define the dashboard data structure
2627
interface DashboardData {
@@ -531,6 +532,7 @@ export default function DashboardClient({ initialData, username }: DashboardClie
531532
}}
532533
/>
533534
<Achievements achievements={initialData.achievements} />
535+
<ResumeProfileSection githubUsername={username} />
534536
</aside>
535537

536538
{/* Main Content */}

0 commit comments

Comments
 (0)