Skip to content

Commit 0480c50

Browse files
authored
Merge pull request #88 from ComputerSocietyVITC/feat/fileUpload
MinIO connection, file upload
2 parents 0b01ce6 + b26ef83 commit 0480c50

File tree

37 files changed

+1216
-262
lines changed

37 files changed

+1216
-262
lines changed

app/admincontrols/page.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,25 @@ import React from "react";
99
const AdminControls = () => {
1010
const router = useRouter();
1111

12+
const handleBack = () => {
13+
if (typeof window !== "undefined") {
14+
const currentLocation = window.location.href;
15+
16+
router.back();
17+
18+
setTimeout(() => {
19+
if (window.location.href === currentLocation) {
20+
router.push("/");
21+
}
22+
}, 100);
23+
}
24+
};
25+
1226
return (
1327
<div className="bg-[#09090b] w-full h-screen flex flex-col text-white">
1428
<header className="w-full bg-[#121212] flex items-center justify-between px-6 py-3 border-b border-gray-700">
1529
<h1 className="text-lg font-bold">Admin Controls</h1>
16-
<DangerButton buttonText="Go Back" onClick={() => router.push("/")} />
30+
<DangerButton buttonText="Go Back" onClick={handleBack} />
1731
</header>
1832
<div className="flex-grow flex items-center justify-center">
1933
<div className="flex flex-col gap-4 items-center">

app/allprojects/page.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import DangerButton from "@/components/ui/DangerButton";
44
import FooterSection from "@/components/ui/FooterSection";
55
import React, { useEffect, useState } from "react";
66
import ProjectList from "@/components/project/ProjectList";
7-
import Link from "next/link";
87
import api from "@/api";
98
import axios from "axios";
109
import { Project } from "@/types";
1110
import { useAuth } from "@/context/AuthContext";
1211
import Error from "@/components/ui/Error";
1312
import Loading from "@/components/ui/Loading";
13+
import { useRouter } from "next/navigation";
1414

1515
const Page = () => {
1616
const [projects, setProjects] = useState<Project[]>([]);
@@ -21,6 +21,7 @@ const Page = () => {
2121
const [unauthorizedError, setUnauthorizedError] = useState(false);
2222

2323
const { user, getUser } = useAuth();
24+
const router = useRouter();
2425

2526
useEffect(() => {
2627
if (!user) {
@@ -29,6 +30,20 @@ const Page = () => {
2930
// eslint-disable-next-line react-hooks/exhaustive-deps
3031
}, []);
3132

33+
const handleBack = () => {
34+
if (typeof window !== "undefined") {
35+
const currentLocation = window.location.href;
36+
37+
router.back();
38+
39+
setTimeout(() => {
40+
if (window.location.href === currentLocation) {
41+
router.push("/");
42+
}
43+
}, 100);
44+
}
45+
};
46+
3247
const getAllProjects = async () => {
3348
try {
3449
const response = await api.get("/project/all");
@@ -102,11 +117,7 @@ const Page = () => {
102117
onChange={(e) => setSearchQuery(e.target.value)}
103118
className="px-3 py-1 rounded-md bg-gray-800 text-white border border-gray-600 focus:outline-none focus:ring focus:ring-gray-500"
104119
/>
105-
<Link
106-
href={`${user?.role === "EVALUATOR" ? "/evaluatorcontrols" : "/admincontrols"}`}
107-
>
108-
<DangerButton buttonText="Go Back" onClick={() => {}} />
109-
</Link>
120+
<DangerButton buttonText="Go Back" onClick={handleBack} />
110121
</header>
111122
<main className="flex-grow w-[95%] mx-auto py-8 bg-[#09090b]">
112123
<ProjectList

app/allteams/page.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,21 @@ import DangerButton from "@/components/ui/DangerButton";
66
import SelectedTeamInfo from "@/components/allteams/SelectedTeamInfo";
77
import axios from "axios";
88
import React, { useEffect, useState } from "react";
9-
import Link from "next/link";
109
import { Team } from "@/types";
1110
import { useAuth } from "@/context/AuthContext";
1211
import Loading from "@/components/ui/Loading";
1312
import Error from "@/components/ui/Error";
13+
import { useRouter } from "next/navigation";
1414

1515
export default function Page() {
1616
const [selectedTeamInfo, setSelectedTeamInfo] = useState<Team | null>(null);
17-
const [teams2, setTeams] = useState<Team[]>([]);
17+
const [teams, setTeams] = useState<Team[]>([]);
1818
const [error, setError] = useState("");
1919
const [searchTerm, setSearchTerm] = useState("");
2020
const [unauthorizedUser, setUnauthorizedUser] = useState(false);
2121

2222
const { user, getUser, loading } = useAuth();
23+
const router = useRouter();
2324

2425
useEffect(() => {
2526
if (!user) {
@@ -59,14 +60,26 @@ export default function Page() {
5960
fetchTeams();
6061
}, []);
6162

62-
const filteredTeams = teams2.filter((team) =>
63+
const filteredTeams = teams.filter((team) =>
6364
team.name.toLowerCase().includes(searchTerm.toLowerCase())
6465
);
6566

66-
const teams = filteredTeams.map(({ id, name }) => ({ id, name }));
67+
const handleBack = () => {
68+
if (typeof window !== "undefined") {
69+
const currentLocation = window.location.href;
70+
71+
router.back();
72+
73+
setTimeout(() => {
74+
if (window.location.href === currentLocation) {
75+
router.push("/");
76+
}
77+
}, 100);
78+
}
79+
};
6780

6881
const handleTeamClick = (teamId: string) => {
69-
const teamData = teams2.find((team) => team.id === teamId) || null;
82+
const teamData = teams.find((team) => team.id === teamId) || null;
7083
setSelectedTeamInfo(teamData);
7184
};
7285

@@ -81,9 +94,7 @@ export default function Page() {
8194
setSelectedTeamInfo((prevSelected) =>
8295
prevSelected?.id === deletedTeamId ? null : prevSelected
8396
);
84-
} catch (error) {
85-
console.error("Error deleting team:", error);
86-
}
97+
} catch {}
8798
};
8899

89100
if (loading) {
@@ -113,15 +124,11 @@ export default function Page() {
113124
}}
114125
className="px-3 py-1 rounded-md bg-gray-800 text-white border border-gray-600 focus:outline-none focus:ring focus:ring-gray-500"
115126
/>
116-
<Link
117-
href={`${user?.role === "EVALUATOR" ? "/evaluatorcontrols" : "/admincontrols"}`}
118-
>
119-
<DangerButton buttonText="Go Back" onClick={() => {}} />
120-
</Link>
127+
<DangerButton buttonText="Go Back" onClick={handleBack} />
121128
</header>
122129
<div className="flex-grow w-full flex flex-row gap-4 p-4 bg-[#09090b]">
123130
<AllTeams
124-
teams={teams}
131+
teams={filteredTeams}
125132
onClickUpdate={handleTeamClick}
126133
customStyle="flex-[1]"
127134
/>

app/allusers/page.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import FooterSection from "@/components/ui/FooterSection";
66
import { TeamMemberListItemModified } from "@/components/team/TeamMemberListItemModified";
77
import React, { useEffect, useState } from "react";
88
import axios from "axios";
9-
import Link from "next/link";
109
import { useAuth } from "@/context/AuthContext";
1110
import { User } from "@/types";
1211
import Error from "@/components/ui/Error";
1312
import Loading from "@/components/ui/Loading";
13+
import { getImageUrl } from "@/lib/utils";
14+
import { useRouter } from "next/navigation";
1415

1516
const Page = () => {
1617
const [users, setUsers] = useState<User[]>([]);
@@ -23,6 +24,7 @@ const Page = () => {
2324
const [error, setError] = useState<string | null>(null);
2425

2526
const { user, getUser } = useAuth();
27+
const router = useRouter();
2628

2729
useEffect(() => {
2830
if (!user) {
@@ -92,6 +94,20 @@ const Page = () => {
9294
);
9395
}, [searchQuery, users]);
9496

97+
const handleBack = () => {
98+
if (typeof window !== "undefined") {
99+
const currentLocation = window.location.href;
100+
101+
router.back();
102+
103+
setTimeout(() => {
104+
if (window.location.href === currentLocation) {
105+
router.push("/");
106+
}
107+
}, 100);
108+
}
109+
};
110+
95111
const handleUserDelete = (userId: string) => {
96112
setUsers((prevUsers) => prevUsers.filter((user) => user.id !== userId));
97113
};
@@ -115,9 +131,7 @@ const Page = () => {
115131
onChange={(e) => setSearchQuery(e.target.value)}
116132
className="px-3 py-1 rounded-md bg-gray-800 text-white border border-gray-600 focus:outline-none focus:ring focus:ring-gray-500"
117133
/>
118-
<Link href="/admincontrols">
119-
<DangerButton buttonText="Go Back" onClick={() => {}} />
120-
</Link>
134+
<DangerButton buttonText="Go Back" onClick={handleBack} />
121135
</header>
122136
<main className="flex-grow w-[95%] mx-auto py-8 bg-[#09090b]">
123137
<div className="flex flex-col">
@@ -128,7 +142,8 @@ const Page = () => {
128142
name={filteredUser.name}
129143
teamName={teamNames[filteredUser.teamId || ""] || "No Team"}
130144
avatarSrc={
131-
(filteredUser.github && `${filteredUser.github}.png`) || ""
145+
getImageUrl(filteredUser.imageId, filteredUser.mimeType) ||
146+
(filteredUser.github ? `${filteredUser.github}.png` : "")
132147
}
133148
userId={filteredUser.id}
134149
currentUserId={user?.id}

app/api/upload/route.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { Client } from "minio";
3+
import { writeFile, unlink } from "fs/promises";
4+
import { join } from "path";
5+
import { mkdir } from "fs/promises";
6+
import { v4 as uuidv4 } from "uuid";
7+
import { existsSync } from "fs";
8+
9+
const MAX_FILE_SIZE = 5 * 1024 * 1024;
10+
const ALLOWED_FILE_TYPES = [
11+
"image/jpeg",
12+
"image/png",
13+
"image/gif",
14+
"image/webp",
15+
"image/svg+xml",
16+
];
17+
18+
const minioClient = new Client({
19+
endPoint: process.env.NEXT_PUBLIC_MINIO_ENDPOINT || "localhost",
20+
port: parseInt(process.env.NEXT_PUBLIC_MINIO_PORT || "9000"),
21+
useSSL: process.env.NEXT_PUBLIC_MINIO_USE_SSL === "true",
22+
accessKey: process.env.NEXT_PUBLIC_MINIO_ACCESS_KEY || "your_access_key",
23+
secretKey: process.env.NEXT_PUBLIC_MINIO_SECRET_KEY || "your_secret_key",
24+
});
25+
26+
const bucketName =
27+
process.env.NEXT_PUBLIC_MINIO_BUCKET_NAME || "nextjs-uploads";
28+
29+
const ensureBucket = async () => {
30+
const exists = await minioClient.bucketExists(bucketName);
31+
if (!exists) {
32+
await minioClient.makeBucket(bucketName, "us-east-1");
33+
}
34+
};
35+
36+
const ensureTempDir = async () => {
37+
const tempDir = join(process.cwd(), "tmp", "uploads");
38+
if (!existsSync(tempDir)) {
39+
await mkdir(tempDir, { recursive: true });
40+
}
41+
return tempDir;
42+
};
43+
44+
const validateFile = (file: File) => {
45+
if (file.size > MAX_FILE_SIZE) {
46+
return {
47+
valid: false,
48+
error: `File size exceeds the maximum limit of 5MB`,
49+
};
50+
}
51+
52+
if (!ALLOWED_FILE_TYPES.includes(file.type)) {
53+
return {
54+
valid: false,
55+
error: `File type ${file.type} is not allowed. Allowed types: PNG, JPEG, GIF, WEBP, SVG`,
56+
};
57+
}
58+
59+
return { valid: true, error: null };
60+
};
61+
62+
export async function POST(request: NextRequest) {
63+
let filePath = "";
64+
65+
try {
66+
await ensureBucket();
67+
const tempDir = await ensureTempDir();
68+
69+
const formData = await request.formData();
70+
const file = formData.get("file") as File | null;
71+
72+
if (!file) {
73+
return NextResponse.json({ error: "No file provided" }, { status: 400 });
74+
}
75+
76+
const validation = validateFile(file);
77+
if (!validation.valid) {
78+
return NextResponse.json({ error: validation.error }, { status: 400 });
79+
}
80+
81+
const fileExt = file.name.split(".").pop() || "";
82+
const fileName = `${uuidv4()}.${fileExt}`;
83+
84+
filePath = join(tempDir, fileName);
85+
86+
const bytes = await file.arrayBuffer();
87+
const buffer = Buffer.from(bytes);
88+
await writeFile(filePath, buffer);
89+
90+
await minioClient.fPutObject(bucketName, fileName, filePath, {
91+
"Content-Type": file.type,
92+
});
93+
94+
await unlink(filePath);
95+
96+
const fileUrl = `${fileName}`;
97+
98+
return NextResponse.json({
99+
success: true,
100+
fileUrl,
101+
fileName: file.name,
102+
size: file.size,
103+
type: file.type,
104+
uploadedAt: new Date().toISOString(),
105+
message: "File uploaded successfully",
106+
});
107+
} catch {
108+
if (filePath) {
109+
try {
110+
await unlink(filePath).catch(() => {});
111+
} catch {}
112+
}
113+
114+
return NextResponse.json(
115+
{
116+
success: false,
117+
error: "File upload failed",
118+
},
119+
{ status: 500 }
120+
);
121+
}
122+
}
123+
124+
export const config = {
125+
api: {
126+
bodyParser: false,
127+
},
128+
};

0 commit comments

Comments
 (0)