Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions client/src/module/recruiter/profile/RecruiterProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
AlignLeft,
} from "lucide-react";
import api from "../../../lib/axios";
import { uploadDirectToS3 } from "../../../utils/upload";
import { useAuthStore } from "../../../lib/auth.store";
import { LoadingScreen } from "../../../components/LoadingScreen";
import toast from "@/components/ui/toast";
Expand Down Expand Up @@ -126,16 +127,15 @@ export default function RecruiterProfilePage() {
setCropSrc(null);
setUploadingPic(true);
try {
const fd = new FormData();
fd.append("file", blob, "cropped.jpg");
const res = await api.post("/upload/profile-pic", fd, {
headers: { "Content-Type": "multipart/form-data" },
const file = new File([blob], "cropped.jpg", { type: blob.type || "image/jpeg" });
const res = await uploadDirectToS3({
file,
folder: "profile-pics",
endpoint: "/profile-pic",
});

const u = res.data.user || res.data;

// Map path properly if relative string path is returned by localhost server
let imagePath = u.profilePic ?? "";

const u = res.user || res;
let imagePath = u.profilePic || u.fileUrl || u.url || "";
if (imagePath && !imagePath.startsWith("http")) {
imagePath = `${api.defaults.baseURL?.replace("/api", "") || "http://localhost:3000"}/${imagePath.replace(/^\//, "")}`;
}
Expand Down
11 changes: 6 additions & 5 deletions client/src/module/student/applications/ApplyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ArrowLeft, FileText, ExternalLink, Check, MapPin, IndianRupee, Clock, S
import { Navbar } from "../../../components/Navbar";
import { DynamicFieldRenderer } from "../../../components/DynamicFieldRenderer";
import api from "../../../lib/axios";
import { uploadDirectToS3 } from "../../../utils/upload";
import { queryKeys } from "../../../lib/query-keys";
import type { Job, CustomFieldDefinition, User } from "../../../lib/types";
import { LoadingScreen } from "../../../components/LoadingScreen";
Expand Down Expand Up @@ -56,12 +57,12 @@ export default function ApplyPage() {
try {
const finalAnswers = { ...customFieldAnswers };
for (const [fieldId, file] of Object.entries(fileUploads)) {
const formData = new FormData();
formData.append("file", file);
const uploadRes = await api.post("/upload/resume", formData, {
headers: { "Content-Type": "multipart/form-data" },
const uploadRes = await uploadDirectToS3({
file,
folder: "resumes",
endpoint: "/profile-resume",
});
finalAnswers[fieldId] = uploadRes.data.file.url;
finalAnswers[fieldId] = uploadRes.file?.url || uploadRes.fileUrl || uploadRes.url || "";
}

await api.post(`/student/jobs/${actualJobId}/apply`, {
Expand Down
11 changes: 6 additions & 5 deletions client/src/module/student/ats/AtsScorePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Link } from "react-router";
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
import toast from "@/components/ui/toast";
import { motion, AnimatePresence } from "framer-motion";
import { uploadDirectToS3 } from "../../../utils/upload";
import {
Upload,
FileText,
Expand Down Expand Up @@ -282,12 +283,12 @@ export default function AtsScorePage() {
}> => {
let url = resumeUrl;
if (file) {
const formData = new FormData();
formData.append("file", file);
const uploadRes = await api.post("/upload/profile-resume", formData, {
headers: { "Content-Type": "multipart/form-data" },
const uploadRes = await uploadDirectToS3({
file,
folder: "resumes",
endpoint: "/profile-resume",
});
url = uploadRes.data.file.url;
url = uploadRes.file?.url || uploadRes.fileUrl || uploadRes.url || url;
setResumeUrl(url);
}
if (!url) throw new Error("Please upload a resume PDF first.");
Expand Down
10 changes: 5 additions & 5 deletions client/src/module/student/companies/AddCompanyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from "react";
import { useNavigate } from "react-router";
import { Building2, Loader2, Plus, X } from "lucide-react";
import toast from "@/components/ui/toast";
import { uploadDirectToS3 } from "@/utils/upload";
import api from "../../../lib/axios";
import { Button } from "../../../components/ui/button";

Expand Down Expand Up @@ -51,12 +52,11 @@ export default function AddCompanyPage() {
let logoUrl: string | undefined;

if (logoFile) {
const formData = new FormData();
formData.append("file", logoFile);
const uploadRes = await api.post("/upload/resume", formData, {
headers: { "Content-Type": "multipart/form-data" },
const uploadRes = await uploadDirectToS3({
file: logoFile,
folder: "company-logos",
});
logoUrl = uploadRes.data.file.url;
logoUrl = uploadRes.fileUrl;
}

const body: Record<string, unknown> = {
Expand Down
48 changes: 28 additions & 20 deletions client/src/module/student/profile/StudentProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { Link } from "react-router";
import type { VerifiedSkill, ProjectItem, AchievementItem } from "../../../lib/types";
import api from "../../../lib/axios";
import { uploadDirectToS3 } from "../../../utils/upload";
import { useAuthStore } from "../../../lib/auth.store";
import { SEO } from "../../../components/SEO";
import { LoadingScreen } from "../../../components/LoadingScreen";
Expand Down Expand Up @@ -445,30 +446,31 @@ export default function StudentProfilePage() {
const handleCropComplete = async (blob: Blob) => {
const isProfile = cropType === "profile";
const setUploading = isProfile ? setUploadingPic : setUploadingCover;
const endpoint = isProfile ? "/upload/profile-pic" : "/upload/cover-image";
const field = isProfile ? "profilePic" : "coverImage";

setCropSrc(null);
setCropType(null);
setUploading(true);
try {
const fd = new FormData();
fd.append("file", blob, "cropped.jpg");
const res = await api.post(endpoint, fd, { headers: { "Content-Type": "multipart/form-data" } });

// Extract the updated user or look for a direct secure_url/filePath returned by your API
const u = res.data.user || res.data;
// Get the image path. If the backend returns a relative path, we map it to the backend server URL
let imagePath = u[field] ?? "";
const file = new File([blob], "cropped.jpg", { type: blob.type || "image/jpeg" });
const res = await uploadDirectToS3({
file,
folder: isProfile ? "profile-pics" : "cover-images",
endpoint: isProfile ? "/profile-pic" : "/cover-image",
});

const u = res.user || res;
let imagePath = u[field] || u.fileUrl || u.url || "";
if (imagePath && !imagePath.startsWith("http")) {
// Fallback for local backend uploads: prepend server address if not an external cloud URL (like Cloudinary)
imagePath = `${api.defaults.baseURL?.replace("/api", "") || "http://localhost:3000"}/${imagePath.replace(/^\//, "")}`;
}

setForm((prev) => ({ ...prev, [field]: imagePath }));
syncUser({ ...form, [field]: imagePath });

setForm((prev) => {
const next = { ...prev, [field]: imagePath };
syncUser(next);
return next;
});

toast.success(isProfile ? "Profile picture updated!" : "Cover image updated!");
} catch (error) {
console.error("Upload rendering error:", error);
Expand All @@ -483,12 +485,18 @@ export default function StudentProfilePage() {
if (!file) return;
setUploadingResume(true);
try {
const fd = new FormData();
fd.append("file", file);
const res = await api.post("/upload/profile-resume", fd, { headers: { "Content-Type": "multipart/form-data" } });
const u = res.data.user;
setForm((prev) => ({ ...prev, resumes: u.resumes ?? [] }));
syncUser({ ...form, resumes: u.resumes ?? [] });
const res = await uploadDirectToS3({
file,
folder: "resumes",
endpoint: "/profile-resume",
});
const u = res.user || res;
const resumes = u.resumes ?? [];
setForm((prev) => {
const next = { ...prev, resumes };
syncUser(next);
return next;
});
toast.success("Resume uploaded!");
} catch (err: unknown) {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message || "Failed to upload resume";
Expand Down
68 changes: 68 additions & 0 deletions client/src/utils/upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
interface UploadProfileParams {
file: File;
folder: 'resumes' | 'profile-pics' | 'cover-images' | 'company-logos';
endpoint?: '/profile-resume' | '/profile-pic' | '/cover-image';
}

export const uploadDirectToS3 = async ({ file, folder, endpoint }: UploadProfileParams) => {
try {
const apiUrl = (import.meta.env.VITE_API_URL as string | undefined) ?? "http://localhost:3000";

// Get the Pre-signed URL from your backend
const presignRes = await fetch(`${apiUrl}/api/upload/presigned-url`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileName: file.name,
fileType: file.type,
folder: folder,
}),
});

if (!presignRes.ok) throw new Error('Failed to get upload URL');
const { uploadUrl, fileUrl } = await presignRes.json();

// Upload the file DIRECTLY to AWS S3
const s3UploadRes = await fetch(uploadUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type,
},
body: file,
});

if (!s3UploadRes.ok) {
const bodyText = await s3UploadRes.text();
throw new Error(`S3 upload failed: ${s3UploadRes.status} ${s3UploadRes.statusText} ${bodyText}`);
}

if (endpoint) {
// Tell your backend to save the new file URL
const updateRes = await fetch(`${apiUrl}/api/upload${endpoint}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileUrl: fileUrl,
originalName: file.name,
size: file.size,
mimeType: file.type,
}),
});

if (!updateRes.ok) throw new Error('Failed to save file to profile');
const result = await updateRes.json();
return result;
}

return { fileUrl };
} catch (error) {
console.error('Upload Error:', error);
throw error;
}
};
3 changes: 1 addition & 2 deletions client/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@
"noUncheckedSideEffectImports": true,

/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
"@/*": ["./src/*"]
}
},
"include": ["src"],
Expand Down
74 changes: 0 additions & 74 deletions server/src/middleware/upload.middleware.ts

This file was deleted.

Loading