feat: ngebuat API tambahan dan upload middleware

This commit is contained in:
Rafi Athallah
2026-06-01 10:19:57 +07:00
parent 49cfd7b4b6
commit 1bd74f4b46
16 changed files with 1880 additions and 13 deletions
+211 -2
View File
@@ -16,7 +16,9 @@
"express": "^4.19.2",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.3",
"morgan": "^1.10.0"
"morgan": "^1.10.0",
"multer": "^2.1.1",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
@@ -24,6 +26,7 @@
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.10",
"@types/morgan": "^1.9.9",
"@types/multer": "^2.1.0",
"@types/node": "^20.12.12",
"prisma": "^6.19.3",
"rimraf": "^5.0.7",
@@ -770,6 +773,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/multer": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz",
"integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/node": {
"version": "20.19.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
@@ -881,6 +894,15 @@
"node": ">=0.4.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -921,6 +943,12 @@
"node": ">= 8"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@@ -1038,9 +1066,19 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -1108,6 +1146,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -1134,6 +1185,15 @@
"consola": "^3.2.3"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1161,6 +1221,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/confbox": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
@@ -1231,6 +1306,18 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -1630,6 +1717,15 @@
"node": ">= 0.6"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -2226,6 +2322,25 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/multer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"type-is": "^1.6.18"
},
"engines": {
"node": ">= 10.16.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -2548,6 +2663,20 @@
"destr": "^2.0.3"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -2818,6 +2947,18 @@
"source-map": "^0.6.0"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -2827,6 +2968,23 @@
"node": ">= 0.8"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -3228,6 +3386,12 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -3259,6 +3423,12 @@
"node": ">= 0.8"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -3300,6 +3470,24 @@
"node": ">= 8"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
@@ -3405,6 +3593,27 @@
"dev": true,
"license": "ISC"
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+4 -1
View File
@@ -24,7 +24,9 @@
"express": "^4.19.2",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.3",
"morgan": "^1.10.0"
"morgan": "^1.10.0",
"multer": "^2.1.1",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
@@ -32,6 +34,7 @@
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.10",
"@types/morgan": "^1.9.9",
"@types/multer": "^2.1.0",
"@types/node": "^20.12.12",
"prisma": "^6.19.3",
"rimraf": "^5.0.7",
+249
View File
@@ -0,0 +1,249 @@
import 'dotenv/config';
import { PrismaClient } from '../src/generated/prisma/client';
import bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
async function main() {
console.log('=== STARTING DEVELOPER DUMMY SEEDING ===');
const adminEmail = 'admin@internify.com';
const adminPassword = 'AdminInternify123!';
const passwordHash = await bcrypt.hash(adminPassword, 10);
let adminUser = await prisma.user.findUnique({ where: { email: adminEmail } });
if (!adminUser) {
adminUser = await prisma.user.create({
data: {
email: adminEmail,
passwordHash,
role: 'ADMIN',
admin: {
create: {
namaDepan: 'Super',
namaBelakang: 'Admin'
}
}
}
});
console.log(' Admin created:', adminEmail);
} else {
console.log(' Admin already exists:', adminEmail);
}
console.log(' Seeding Batches...');
const batchesData = [
{ batchNumber: 1, isActive: false },
{ batchNumber: 2, isActive: false },
{ batchNumber: 3, isActive: true } // Active Batch
];
const batches: any[] = [];
for (const b of batchesData) {
let dbBatch = await prisma.batch.findUnique({ where: { batchNumber: b.batchNumber } });
if (!dbBatch) {
dbBatch = await prisma.batch.create({ data: b });
}
batches.push(dbBatch);
}
console.log(` Seeding Batches complete! Created/Found ${batches.length} batches.`);
const activeBatch = batches.find(b => b.isActive) || batches[2];
console.log(' Seeding Student Users...');
const studentsData = [
{
email: 'alice@example.com',
namaDepan: 'Alice',
namaBelakang: 'Smith',
kontak: '08111222333',
jurusan: 'Informatika',
universitas: 'Institut Teknologi Bandung',
negara: 'Indonesia',
cvPath: '/uploads/docs/alice-cv.pdf',
portofolioPath: '/uploads/docs/alice-portfolio.pdf',
motivasi: 'Saya sangat tertarik dengan UI/UX design dan ingin mempraktekkannya.',
relevantSkills: 'Figma, Adobe XD, User Research'
},
{
email: 'bob@example.com',
namaDepan: 'Bob',
namaBelakang: 'Johnson',
kontak: '08222333444',
jurusan: 'Sistem Informasi',
universitas: 'Universitas Indonesia',
negara: 'Indonesia',
cvPath: '/uploads/docs/bob-cv.pdf',
portofolioPath: '/uploads/docs/bob-portfolio.pdf',
motivasi: 'Ingin mengembangkan keahlian React dan Tailwind CSS.',
relevantSkills: 'React, HTML, CSS, JavaScript'
},
{
email: 'charlie@example.com',
namaDepan: 'Charlie',
namaBelakang: 'Brown',
kontak: '08333444555',
jurusan: 'Teknik Komputer',
universitas: 'Telkom University',
negara: 'Indonesia',
cvPath: '/uploads/docs/charlie-cv.pdf',
portofolioPath: '/uploads/docs/charlie-portfolio.pdf',
motivasi: 'Suka membangun server scalable dan merancang arsitektur API.',
relevantSkills: 'Node.js, Express, MySQL, TypeScript'
}
];
const mahasiswaList: any[] = [];
for (const s of studentsData) {
let user = await prisma.user.findUnique({
where: { email: s.email },
include: { mahasiswa: true }
});
if (!user) {
const hash = await bcrypt.hash('Student123!', 10);
user = await prisma.user.create({
data: {
email: s.email,
passwordHash: hash,
role: 'STUDENT',
mahasiswa: {
create: {
namaDepan: s.namaDepan,
namaBelakang: s.namaBelakang,
kontak: s.kontak,
jurusan: s.jurusan,
universitas: s.universitas,
negara: s.negara,
cvPath: s.cvPath,
portofolioPath: s.portofolioPath,
motivasi: s.motivasi,
relevantSkills: s.relevantSkills
}
}
},
include: { mahasiswa: true }
});
}
mahasiswaList.push(user.mahasiswa);
}
console.log(` Student seeding complete! Created/Found ${mahasiswaList.length} students.`);
console.log(' Seeding Lowongan Listings...');
const lowongansData = [
{
id: 'lowongan-ui-ux',
posisi: 'UI/UX Designer Intern',
kelompokPeminatan: 'Design & Creative',
imagePath: '/uploads/images/ui-ux-design.png',
jobdesk: '<p>Mendesain antarmuka aplikasi mobile dan web, melakukan user testing, dan wireframing.</p>',
lokasi: 'Onsite - Bandung',
kualifikasi: 'Menguasai Figma, memiliki portofolio desain UI/UX, kreatif.',
benefit: 'Sertifikat magang resmi, uang transport harian, mentoring 1-on-1.',
paid: 'PAID' as any,
statusLowongan: 'DIBUKA' as any
},
{
id: 'lowongan-frontend',
posisi: 'Frontend Developer Intern',
kelompokPeminatan: 'Web Engineering',
imagePath: '/uploads/images/frontend-dev.png',
jobdesk: '<p>Mengembangkan antarmuka responsif menggunakan React dan Tailwind CSS.</p>',
lokasi: 'Remote',
kualifikasi: 'Memahami React JS, ES6 JavaScript, HTML, CSS.',
benefit: 'Sertifikat magang resmi, jam kerja fleksibel, free access learning course.',
paid: 'UNPAID' as any,
statusLowongan: 'DIBUKA' as any
},
{
id: 'lowongan-backend',
posisi: 'Backend Developer Intern',
kelompokPeminatan: 'Web Engineering',
imagePath: '/uploads/images/backend-dev.png',
jobdesk: '<p>Membangun REST API, mengoptimalkan query database, dan integrasi backend services.</p>',
lokasi: 'Hybrid - Jakarta',
kualifikasi: 'Memahami Node.js, Express, dan Relational Database seperti MySQL.',
benefit: 'Sertifikat magang resmi, konversi SKS, coworking space access.',
paid: 'PAID' as any,
statusLowongan: 'DIBUKA' as any
}
];
const lowonganList: any[] = [];
for (const l of lowongansData) {
let dbLowongan = await prisma.lowonganMagang.findUnique({ where: { id: l.id } });
if (!dbLowongan) {
dbLowongan = await prisma.lowonganMagang.create({
data: {
id: l.id,
idBatch: activeBatch.id,
posisi: l.posisi,
kelompokPeminatan: l.kelompokPeminatan,
imagePath: l.imagePath,
jobdesk: l.jobdesk,
lokasi: l.lokasi,
kualifikasi: l.kualifikasi,
benefit: l.benefit,
durasiAwal: new Date(),
durasiAkhir: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days from now
statusLowongan: l.statusLowongan,
paid: l.paid
}
});
}
lowonganList.push(dbLowongan);
}
console.log(` Lowongan seeding complete! Created/Found ${lowonganList.length} listings.`);
console.log(' Seeding Lamaran Records...');
const lamaranData = [
{
idMahasiswa: mahasiswaList[0].id,
idLowonganMagang: lowonganList[0].id,
status: 'DIPROSES' as any
},
{
idMahasiswa: mahasiswaList[1].id,
idLowonganMagang: lowonganList[1].id,
status: 'DIPROSES' as any
},
{
idMahasiswa: mahasiswaList[2].id,
idLowonganMagang: lowonganList[2].id,
status: 'DIPROSES' as any
}
];
let lamaranCount = 0;
for (const lam of lamaranData) {
const existing = await prisma.lamaranMagang.findFirst({
where: {
idMahasiswa: lam.idMahasiswa,
idLowonganMagang: lam.idLowonganMagang
}
});
if (!existing) {
await prisma.lamaranMagang.create({
data: {
idMahasiswa: lam.idMahasiswa,
idLowonganMagang: lam.idLowonganMagang,
status: lam.status,
batch: activeBatch.id
}
});
lamaranCount++;
}
}
console.log(` Lamaran seeding complete! Created ${lamaranCount} new applications.`);
console.log('\n=== DEVELOPER SEEDING COMPLETED SUCCESSFULLY ===');
}
main()
.catch(e => {
console.error('Seeding failed:', e);
})
.finally(async () => {
await prisma.$disconnect();
});
+9 -1
View File
@@ -1,3 +1,4 @@
import path from 'path';
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet';
@@ -8,10 +9,17 @@ import { errorHandler, CustomError } from './middleware/errorHandler';
const app = express();
app.use(helmet());
app.use(cors());
app.use(cors({
origin: true,
credentials: true,
}));
app.use(morgan('dev'));
// File upload statis biar bisa diakses lewat /uploads atau /api/uploads
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
app.use('/api/uploads', express.static(path.join(__dirname, '../uploads')));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
+46
View File
@@ -0,0 +1,46 @@
import multer from 'multer';
import path from 'path';
import fs from 'fs';
const storage = multer.diskStorage({
destination: (_req, file, cb) => {
let folder = 'uploads/';
if (file.fieldname === 'cv' || file.fieldname === 'portofolio') {
folder += 'docs/';
} else {
folder += 'images/';
}
fs.mkdirSync(folder, { recursive: true });
cb(null, folder);
},
filename: (_req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = path.extname(file.originalname);
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
}
});
const fileFilter = (_req: any, file: any, cb: any) => {
if (file.fieldname === 'cv' || file.fieldname === 'portofolio') {
if (file.mimetype === 'application/pdf') {
cb(null, true);
} else {
cb(new Error('CV dan Portofolio harus dalam format PDF!'), false);
}
} else {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('File upload harus berupa gambar!'), false);
}
}
};
export const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 10 * 1024 * 1024, // Maksimal 10MB yaa, nanti paling bisa diganti oleh admin melalui website
}
});
+5 -3
View File
@@ -17,7 +17,7 @@ const generateToken = (payload: { id: string; email: string; role: string }) =>
/**
* @route POST /api/auth/register
* @desc Mendaftarkan user baru dengan role STUDENT (Mahasiswa)
* @desc Daftarin user baru dgn role STUDENT (Mahasiswa)
* @access Public
*/
router.post('/register', async (req: Request, res: Response, next: NextFunction) => {
@@ -101,7 +101,7 @@ router.post('/register', async (req: Request, res: Response, next: NextFunction)
/**
* @route POST /api/auth/login
* @desc Autentikasi user dan dapatin token
* @desc Autentikasi user biar dapet token
* @access Public
*/
router.post('/login', async (req: Request, res: Response, next: NextFunction) => {
@@ -145,6 +145,7 @@ router.post('/login', async (req: Request, res: Response, next: NextFunction) =>
res.status(200).json({
success: true,
data: {
token,
user: {
id: user.id,
@@ -152,6 +153,7 @@ router.post('/login', async (req: Request, res: Response, next: NextFunction) =>
role: user.role,
profile,
},
},
});
} catch (error) {
next(error);
@@ -160,7 +162,7 @@ router.post('/login', async (req: Request, res: Response, next: NextFunction) =>
/**
* @route GET /api/auth/profile
* @desc Dapatin profile user yang sedang login
* @desc Dapetin profil user yg lagi login
* @access Private
*/
router.get('/profile', protect, async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
+165
View File
@@ -0,0 +1,165 @@
import { Router, Request, Response, NextFunction } from 'express';
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
import prisma from '../lib/prisma';
import { CustomError } from '../middleware/errorHandler';
const router = Router();
// Map data faq dari DB ke format frontend
function mapToFrontend(faq: any) {
return {
id: faq.id,
pertanyaan: faq.pertanyaan,
jawaban: faq.jawaban,
created_at: faq.createdAt
};
}
/**
* @route GET /api/faq-api/get
* @desc Dapetin semua list FAQ
* @access Public
*/
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
try {
const faqs = await prisma.faq.findMany({
orderBy: { createdAt: 'desc' }
});
res.status(200).json({
status: true,
data: faqs.map(mapToFrontend)
});
} catch (error) {
next(error);
}
});
/**
* @route GET /api/faq-api/get/:id
* @desc Dapetin detail satu FAQ
* @access Public
*/
router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const faq = await prisma.faq.findUnique({
where: { id: parseInt(id, 10) }
});
if (!faq) {
const error: CustomError = new Error('FAQ tidak ditemukan');
error.statusCode = 404;
return next(error);
}
res.status(200).json({
status: true,
data: mapToFrontend(faq)
});
} catch (error) {
next(error);
}
});
/**
* @route POST /api/faq-api/add
* @desc Tambah FAQ baru (khusus admin ya)
* @access Private (Admin)
*/
router.post('/add', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { pertanyaan, jawaban } = req.body;
if (!pertanyaan || !jawaban) {
const error: CustomError = new Error('Pertanyaan dan jawaban harus diisi');
error.statusCode = 400;
return next(error);
}
const faq = await prisma.faq.create({
data: {
pertanyaan,
jawaban
}
});
res.status(201).json({
status: true,
message: 'FAQ berhasil ditambahkan',
data: mapToFrontend(faq)
});
} catch (error) {
next(error);
}
});
/**
* @route PATCH /api/faq-api/update/:id
* @desc Update data FAQ (khusus admin)
* @access Private (Admin)
*/
router.patch('/update/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const { pertanyaan, jawaban } = req.body;
const existing = await prisma.faq.findUnique({
where: { id: parseInt(id, 10) }
});
if (!existing) {
const error: CustomError = new Error('FAQ tidak ditemukan');
error.statusCode = 404;
return next(error);
}
const updated = await prisma.faq.update({
where: { id: parseInt(id, 10) },
data: {
pertanyaan: pertanyaan ?? existing.pertanyaan,
jawaban: jawaban ?? existing.jawaban
}
});
res.status(200).json({
status: true,
message: 'FAQ berhasil diupdate',
data: mapToFrontend(updated)
});
} catch (error) {
next(error);
}
});
/**
* @route DELETE /api/faq-api/delete/:id
* @desc Hapus data FAQ (khusus admin)
* @access Private (Admin)
*/
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const existing = await prisma.faq.findUnique({
where: { id: parseInt(id, 10) }
});
if (!existing) {
const error: CustomError = new Error('FAQ tidak ditemukan');
error.statusCode = 404;
return next(error);
}
await prisma.faq.delete({
where: { id: parseInt(id, 10) }
});
res.status(200).json({
status: true,
message: 'FAQ berhasil dihapus'
});
} catch (error) {
next(error);
}
});
export default router;
+206
View File
@@ -0,0 +1,206 @@
import { Router, Request, Response, NextFunction } from 'express';
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
import prisma from '../lib/prisma';
import { upload } from '../middleware/upload';
import { CustomError } from '../middleware/errorHandler';
const router = Router();
// Map data feedback dari DB ke format frontend
function mapToFrontend(feedback: any) {
return {
id: feedback.id,
nama: feedback.nama,
universitas: feedback.universitas,
posisi: feedback.posisi,
batch: feedback.batch,
tahun: feedback.tahun,
pesan: feedback.pesan,
image_path: feedback.imagePath,
createdAt: feedback.createdAt
};
}
/**
* @route GET /api/feedback-api/get
* @desc Dapetin semua feedback dari mahasiswa
* @access Public
*/
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
try {
const feedbacks = await prisma.feedback.findMany({
orderBy: { createdAt: 'desc' }
});
res.status(200).json({
status: true,
data: feedbacks.map(mapToFrontend)
});
} catch (error) {
next(error);
}
});
/**
* @route GET /api/feedback-api/get/:id
* @desc Dapetin detail satu feedback
* @access Public
*/
router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const feedback = await prisma.feedback.findUnique({
where: { id: parseInt(id, 10) }
});
if (!feedback) {
const error: CustomError = new Error('Feedback tidak ditemukan');
error.statusCode = 404;
return next(error);
}
res.status(200).json({
status: true,
data: mapToFrontend(feedback)
});
} catch (error) {
next(error);
}
});
/**
* @route POST /api/feedback-api/add
* @desc Tambah feedback baru (bisa admin / student)
* @access Private (Admin/Student)
*/
router.post('/add', protect, upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { nama, universitas, posisi, batch, tahun, pesan } = req.body;
if (!nama || !universitas || !posisi || !batch || !tahun || !pesan) {
const error: CustomError = new Error('Mohon lengkapi semua field yang wajib diisi');
error.statusCode = 400;
return next(error);
}
const parsedBatch = parseInt(batch, 10);
const parsedTahun = parseInt(tahun, 10);
if (isNaN(parsedBatch) || isNaN(parsedTahun)) {
const error: CustomError = new Error('Batch dan tahun harus berupa angka');
error.statusCode = 400;
return next(error);
}
const imagePath = req.file ? `/uploads/images/${req.file.filename}` : '';
const feedback = await prisma.feedback.create({
data: {
nama,
universitas,
posisi,
batch: parsedBatch,
tahun: parsedTahun,
pesan,
imagePath
}
});
res.status(201).json({
status: true,
message: 'Feedback berhasil ditambahkan',
data: mapToFrontend(feedback)
});
} catch (error) {
next(error);
}
});
/**
* @route PATCH /api/feedback-api/update/:id
* @desc Update data feedback (bisa admin / student)
* @access Private (Admin/Student)
*/
router.patch('/update/:id', protect, upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const { nama, universitas, posisi, batch, tahun, pesan } = req.body;
const existing = await prisma.feedback.findUnique({
where: { id: parseInt(id, 10) }
});
if (!existing) {
const error: CustomError = new Error('Feedback tidak ditemukan');
error.statusCode = 404;
return next(error);
}
let imagePath = existing.imagePath;
if (req.file) {
imagePath = `/uploads/images/${req.file.filename}`;
}
const parsedBatch = batch ? parseInt(batch, 10) : undefined;
const parsedTahun = tahun ? parseInt(tahun, 10) : undefined;
if ((batch && isNaN(parsedBatch as number)) || (tahun && isNaN(parsedTahun as number))) {
const error: CustomError = new Error('Batch dan tahun harus berupa angka');
error.statusCode = 400;
return next(error);
}
const updated = await prisma.feedback.update({
where: { id: parseInt(id, 10) },
data: {
nama: nama ?? existing.nama,
universitas: universitas ?? existing.universitas,
posisi: posisi ?? existing.posisi,
batch: parsedBatch ?? existing.batch,
tahun: parsedTahun ?? existing.tahun,
pesan: pesan ?? existing.pesan,
imagePath
}
});
res.status(200).json({
status: true,
message: 'Feedback berhasil diupdate',
data: mapToFrontend(updated)
});
} catch (error) {
next(error);
}
});
/**
* @route DELETE /api/feedback-api/delete/:id
* @desc Hapus feedback (khusus admin)
* @access Private (Admin)
*/
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const existing = await prisma.feedback.findUnique({
where: { id: parseInt(id, 10) }
});
if (!existing) {
const error: CustomError = new Error('Feedback tidak ditemukan');
error.statusCode = 404;
return next(error);
}
await prisma.feedback.delete({
where: { id: parseInt(id, 10) }
});
res.status(200).json({
status: true,
message: 'Feedback berhasil dihapus'
});
} catch (error) {
next(error);
}
});
export default router;
+13
View File
@@ -1,10 +1,23 @@
import { Router, Request, Response, NextFunction } from 'express';
import prisma from '../lib/prisma';
import authRouter from './auth';
import lowonganRouter from './lowongan';
import lamaranRouter from './lamaran';
import partnershipRouter from './partnership';
import researchRouter from './research';
import feedbackRouter from './feedback';
import faqRouter from './faq';
const router = Router();
router.use('/auth', authRouter);
router.use('/auth-api', authRouter);
router.use('/lowongan-magang-api', lowonganRouter);
router.use('/lamaran-magang-api', lamaranRouter);
router.use('/partnership-api', partnershipRouter);
router.use('/hasil-research-api', researchRouter);
router.use('/feedback-api', feedbackRouter);
router.use('/faq-api', faqRouter);
/**
* @route GET /api/health
+344
View File
@@ -0,0 +1,344 @@
import { Router, Request, Response, NextFunction } from 'express';
import bcrypt from 'bcryptjs';
import * as XLSX from 'xlsx';
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
import prisma from '../lib/prisma';
import { upload } from '../middleware/upload';
import { CustomError } from '../middleware/errorHandler';
const router = Router();
// Map data lamaran dari DB ke format frontend
function mapToFrontend(lamaran: any) {
return {
id: lamaran.id,
status: (lamaran.status || 'diproses').toLowerCase(),
batch: lamaran.batch,
createdAt: lamaran.createdAt,
updatedAt: lamaran.updatedAt,
mahasiswa: lamaran.mahasiswa ? {
id: lamaran.mahasiswa.id,
nama_depan: lamaran.mahasiswa.namaDepan,
nama_belakang: lamaran.mahasiswa.namaBelakang || '',
email: lamaran.mahasiswa.user?.email || '',
kontak: lamaran.mahasiswa.kontak,
jurusan: lamaran.mahasiswa.jurusan,
universitas: lamaran.mahasiswa.universitas,
negara: lamaran.mahasiswa.negara,
cv_path: lamaran.mahasiswa.cvPath,
portofolio_path: lamaran.mahasiswa.portofolioPath,
motivasi: lamaran.mahasiswa.motivasi,
relevant_skills: lamaran.mahasiswa.relevantSkills
} : null,
lowongan_magang: lamaran.lowonganMagang ? {
id: lamaran.lowonganMagang.id,
posisi: lamaran.lowonganMagang.posisi,
kelompok_peminatan: lamaran.lowonganMagang.kelompokPeminatan
} : null
};
}
/**
* @route POST /api/lamaran-magang-api/add/:lowonganId
* @desc Kirim pendaftaran magang baru
* @access Public
*/
router.post(
'/add/:lowonganId',
upload.fields([
{ name: 'cv', maxCount: 1 },
{ name: 'portofolio', maxCount: 1 }
]),
async (req: Request, res: Response, next: NextFunction) => {
try {
const { lowonganId } = req.params;
const {
nama_depan,
nama_belakang,
email,
kontak,
universitas,
negara,
jurusan,
batch,
motivasi,
relevant_skills
} = req.body;
if (!nama_depan || !email || !kontak || !universitas || !negara || !jurusan || !motivasi) {
const error: CustomError = new Error('Mohon lengkapi semua field wajib yang diperlukan');
error.statusCode = 400;
return next(error);
}
const files = req.files as { [fieldname: string]: Express.Multer.File[] };
const cvFile = files?.['cv']?.[0];
const portofolioFile = files?.['portofolio']?.[0];
if (!cvFile || !portofolioFile) {
const error: CustomError = new Error('CV dan Portofolio wajib diunggah!');
error.statusCode = 400;
return next(error);
}
const lowongan = await prisma.lowonganMagang.findUnique({
where: { id: lowonganId }
});
if (!lowongan) {
const error: CustomError = new Error('Lowongan magang tidak ditemukan');
error.statusCode = 404;
return next(error);
}
let user = await prisma.user.findUnique({
where: { email },
include: { mahasiswa: true }
});
const cvPath = `/uploads/docs/${cvFile.filename}`;
const portofolioPath = `/uploads/docs/${portofolioFile.filename}`;
if (!user) {
// Kalo user belom terdaftar, kita buatin akun STUDENT baru
const randomPassword = Math.random().toString(36).substring(2, 15);
const salt = await bcrypt.genSalt(10);
const passwordHash = await bcrypt.hash(randomPassword, salt);
user = await prisma.user.create({
data: {
email,
passwordHash,
role: 'STUDENT',
mahasiswa: {
create: {
namaDepan: nama_depan,
namaBelakang: nama_belakang || null,
kontak,
jurusan,
universitas,
negara,
cvPath,
portofolioPath,
motivasi,
relevantSkills: relevant_skills || ''
}
}
},
include: { mahasiswa: true }
});
} else {
// Kalo user udah ada, kita update data profil mahasiswanya aja
if (user.mahasiswa) {
await prisma.mahasiswa.update({
where: { id: user.mahasiswa.id },
data: {
namaDepan: nama_depan,
namaBelakang: nama_belakang || user.mahasiswa.namaBelakang,
kontak: kontak || user.mahasiswa.kontak,
jurusan: jurusan || user.mahasiswa.jurusan,
universitas: universitas || user.mahasiswa.universitas,
negara: negara || user.mahasiswa.negara,
cvPath,
portofolioPath,
motivasi,
relevantSkills: relevant_skills || user.mahasiswa.relevantSkills
}
});
}
}
const parsedBatch = batch ? parseInt(batch, 10) : lowongan.idBatch;
const lamaran = await prisma.lamaranMagang.create({
data: {
idMahasiswa: user.mahasiswa!.id,
idLowonganMagang: lowongan.id,
status: 'DIPROSES',
batch: parsedBatch
},
include: {
mahasiswa: {
include: { user: true }
},
lowonganMagang: true
}
});
res.status(201).json({
status: true,
message: 'Pendaftaran magang berhasil dikirim',
data: mapToFrontend(lamaran)
});
} catch (error) {
next(error);
}
}
);
/**
* @route GET /api/lamaran-magang-api/get
* @desc Dapetin semua data pelamar (admin aja ya)
* @access Private (Admin)
*/
router.get('/get', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const applications = await prisma.lamaranMagang.findMany({
include: {
mahasiswa: {
include: { user: true }
},
lowonganMagang: true
},
orderBy: { createdAt: 'desc' }
});
res.status(200).json({
status: true,
data: applications.map(mapToFrontend)
});
} catch (error) {
next(error);
}
});
/**
* @route GET /api/lamaran-magang-api/get/:id
* @desc Dapetin detail satu lamaran berdasarkan id
* @access Private (Admin)
*/
router.get('/get/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const application = await prisma.lamaranMagang.findUnique({
where: { id: parseInt(id, 10) },
include: {
mahasiswa: {
include: { user: true }
},
lowonganMagang: true
}
});
if (!application) {
const error: CustomError = new Error('Lamaran tidak ditemukan');
error.statusCode = 404;
return next(error);
}
res.status(200).json({
status: true,
data: mapToFrontend(application)
});
} catch (error) {
next(error);
}
});
/**
* @route PATCH /api/lamaran-magang-api/update/:id
* @desc Ganti status lamaran (diterima, ditolak, dll)
* @access Private (Admin)
*/
router.patch('/update/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const { status } = req.body;
if (!status) {
const error: CustomError = new Error('Mohon masukkan status baru');
error.statusCode = 400;
return next(error);
}
const formattedStatus = status.toUpperCase();
if (formattedStatus !== 'DITERIMA' && formattedStatus !== 'DITOLAK' && formattedStatus !== 'DIPROSES') {
const error: CustomError = new Error('Status tidak valid');
error.statusCode = 400;
return next(error);
}
const updated = await prisma.lamaranMagang.update({
where: { id: parseInt(id, 10) },
data: {
status: formattedStatus as any
},
include: {
mahasiswa: {
include: { user: true }
},
lowonganMagang: true
}
});
res.status(200).json({
status: true,
message: 'Status lamaran berhasil diupdate',
data: mapToFrontend(updated)
});
} catch (error) {
next(error);
}
});
/**
* @route DELETE /api/lamaran-magang-api/delete
* @desc Hapus semua data pelamar
* @access Private (Admin)
*/
router.delete('/delete', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
await prisma.lamaranMagang.deleteMany({});
res.status(200).json({
status: true,
message: 'Seluruh data pelamar berhasil dihapus'
});
} catch (error) {
next(error);
}
});
/**
* @route GET /api/lamaran-magang-api/export
* @desc Ekspor data pelamar ke file Excel (.xlsx)
* @access Private (Admin)
*/
router.get('/export', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const applications = await prisma.lamaranMagang.findMany({
include: {
mahasiswa: {
include: { user: true }
},
lowonganMagang: true
}
});
const rows = applications.map((app) => ({
ID: app.id,
Nama: `${app.mahasiswa?.namaDepan} ${app.mahasiswa?.namaBelakang || ''}`.trim(),
Email: app.mahasiswa?.user?.email || '',
Kontak: app.mahasiswa?.kontak || '',
Universitas: app.mahasiswa?.universitas || '',
Jurusan: app.mahasiswa?.jurusan || '',
Negara: app.mahasiswa?.negara || '',
Posisi: app.lowonganMagang?.posisi || '',
Status: app.status,
Motivasi: app.mahasiswa?.motivasi || '',
Skills: app.mahasiswa?.relevantSkills || ''
}));
const worksheet = XLSX.utils.json_to_sheet(rows);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Applications');
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename="data.xlsx"');
res.send(buffer);
} catch (error) {
next(error);
}
});
export default router;
+262
View File
@@ -0,0 +1,262 @@
import { Router, Request, Response, NextFunction } from 'express';
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
import prisma from '../lib/prisma';
import { upload } from '../middleware/upload';
import { CustomError } from '../middleware/errorHandler';
const router = Router();
// Nyari batch yg aktif, kalo ga ada ya kita bikinin yg baru
async function getOrCreateActiveBatch() {
let batch = await prisma.batch.findFirst({
where: { isActive: true }
});
if (!batch) {
batch = await prisma.batch.create({
data: {
batchNumber: 1,
isActive: true
}
});
}
return batch;
}
function mapToFrontend(lowongan: any) {
return {
id: lowongan.id,
id_batch: lowongan.idBatch,
posisi: lowongan.posisi,
kelompok_peminatan: lowongan.kelompokPeminatan,
image_path: lowongan.imagePath,
jobdesk: lowongan.jobdesk,
lokasi: lowongan.lokasi,
kualifikasi: lowongan.kualifikasi,
benefit: lowongan.benefit,
durasi_awal: lowongan.durasiAwal,
durasi_akhir: lowongan.durasiAkhir,
status_lowongan: lowongan.statusLowongan,
paid: lowongan.paid,
createdAt: lowongan.createdAt
};
}
/**
* @route GET /api/lowongan-magang-api/get
* @desc Ambil semua daftar lowongan magang nih
* @access Public
*/
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
try {
const lowongans = await prisma.lowonganMagang.findMany({
orderBy: { createdAt: 'desc' }
});
res.status(200).json({
status: true,
data: lowongans.map(mapToFrontend)
});
} catch (error) {
next(error);
}
});
/**
* @route GET /api/lowongan-magang-api/get/kelompok-all
* @desc Ambil semua kelompok peminatan yg unik
* @access Public
*/
router.get('/get/kelompok-all', async (_req: Request, res: Response, next: NextFunction) => {
try {
const result = await prisma.lowonganMagang.findMany({
select: { kelompokPeminatan: true },
distinct: ['kelompokPeminatan']
});
const categories = result.map((r) => r.kelompokPeminatan);
res.status(200).json({
status: true,
data: categories
});
} catch (error) {
next(error);
}
});
/**
* @route GET /api/lowongan-magang-api/get/id/:id
* @desc Ambil detail lowongan magang berdasarkan id
* @access Public
*/
router.get('/get/id/:id', async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const lowongan = await prisma.lowonganMagang.findUnique({
where: { id }
});
if (!lowongan) {
const error: CustomError = new Error('Lowongan tidak ditemukan');
error.statusCode = 404;
return next(error);
}
res.status(200).json({
status: true,
data: mapToFrontend(lowongan)
});
} catch (error) {
next(error);
}
});
/**
* @route POST /api/lowongan-magang-api/add
* @desc Tambah lowongan magang baru (khusus admin ya)
* @access Private (Admin)
*/
router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const {
posisi,
kelompok_peminatan,
lokasi,
jobdesk,
kualifikasi,
benefit,
durasi_awal,
durasi_akhir,
paid
} = req.body;
if (!posisi || !kelompok_peminatan || !lokasi || !jobdesk || !durasi_awal || !durasi_akhir || !paid) {
const error: CustomError = new Error('Mohon lengkapi semua field yang wajib diisi');
error.statusCode = 400;
return next(error);
}
const batch = await getOrCreateActiveBatch();
const imagePath = req.file ? `/uploads/images/${req.file.filename}` : '';
const id = `lowongan-${Date.now()}`;
const lowongan = await prisma.lowonganMagang.create({
data: {
id,
idBatch: batch.id,
posisi,
kelompokPeminatan: kelompok_peminatan,
imagePath,
jobdesk,
lokasi,
kualifikasi: kualifikasi || '',
benefit: benefit || '',
durasiAwal: new Date(durasi_awal),
durasiAkhir: new Date(durasi_akhir),
statusLowongan: 'DIBUKA',
paid: paid.toUpperCase() === 'PAID' ? 'PAID' : 'UNPAID'
}
});
res.status(201).json({
status: true,
message: 'Internship lowongan berhasil ditambahkan',
data: mapToFrontend(lowongan)
});
} catch (error) {
next(error);
}
});
/**
* @route PATCH /api/lowongan-magang-api/update/:id
* @desc Update data lowongan magang (khusus admin)
* @access Private (Admin)
*/
router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const {
posisi,
kelompok_peminatan,
lokasi,
jobdesk,
kualifikasi,
benefit,
durasi_awal,
durasi_akhir,
paid,
status_lowongan
} = req.body;
const existing = await prisma.lowonganMagang.findUnique({
where: { id }
});
if (!existing) {
const error: CustomError = new Error('Lowongan tidak ditemukan');
error.statusCode = 404;
return next(error);
}
let imagePath = existing.imagePath;
if (req.file) {
imagePath = `/uploads/images/${req.file.filename}`;
}
const updated = await prisma.lowonganMagang.update({
where: { id },
data: {
posisi: posisi ?? existing.posisi,
kelompokPeminatan: kelompok_peminatan ?? existing.kelompokPeminatan,
lokasi: lokasi ?? existing.lokasi,
jobdesk: jobdesk ?? existing.jobdesk,
kualifikasi: kualifikasi ?? existing.kualifikasi,
benefit: benefit ?? existing.benefit,
durasiAwal: durasi_awal ? new Date(durasi_awal) : existing.durasiAwal,
durasiAkhir: durasi_akhir ? new Date(durasi_akhir) : existing.durasiAkhir,
statusLowongan: (status_lowongan ?? existing.statusLowongan) === 'DIBUKA' ? 'DIBUKA' : 'DITUTUP',
paid: paid ? (paid.toUpperCase() === 'PAID' ? 'PAID' : 'UNPAID') : existing.paid,
imagePath
}
});
res.status(200).json({
status: true,
message: 'Lowongan berhasil diupdate',
data: mapToFrontend(updated)
});
} catch (error) {
next(error);
}
});
/**
* @route DELETE /api/lowongan-magang-api/delete/:id
* @desc Hapus lowongan magang (khusus admin)
* @access Private (Admin)
*/
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const existing = await prisma.lowonganMagang.findUnique({
where: { id }
});
if (!existing) {
const error: CustomError = new Error('Lowongan tidak ditemukan');
error.statusCode = 404;
return next(error);
}
await prisma.lowonganMagang.delete({
where: { id }
});
res.status(200).json({
status: true,
message: 'Lowongan berhasil dihapus'
});
} catch (error) {
next(error);
}
});
export default router;
+178
View File
@@ -0,0 +1,178 @@
import { Router, Request, Response, NextFunction } from 'express';
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
import prisma from '../lib/prisma';
import { upload } from '../middleware/upload';
import { CustomError } from '../middleware/errorHandler';
const router = Router();
// Map data partnership dari DB ke format frontend
function mapToFrontend(partner: any) {
return {
id: partner.id,
nama_partner: partner.namaPartner,
image_path: partner.imagePath,
createdAt: partner.createdAt
};
}
/**
* @route GET /api/partnership-api/get
* @desc Dapetin semua data partnerships
* @access Public
*/
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
try {
const partners = await prisma.partnership.findMany({
orderBy: { createdAt: 'desc' }
});
res.status(200).json({
status: true,
data: partners.map(mapToFrontend)
});
} catch (error) {
next(error);
}
});
/**
* @route GET /api/partnership-api/get/:id
* @desc Dapetin detail satu partnership
* @access Public
*/
router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const partner = await prisma.partnership.findFirst({
where: { id: parseInt(id, 10) }
});
if (!partner) {
const error: CustomError = new Error('Partnership tidak ditemukan');
error.statusCode = 404;
return next(error);
}
res.status(200).json({
status: true,
data: [mapToFrontend(partner)] // Kadang list/detail di frontend minta dibungkus array
});
} catch (error) {
next(error);
}
});
/**
* @route POST /api/partnership-api/add
* @desc Tambah partnership baru (khusus admin ya)
* @access Private (Admin)
*/
router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { nama_partner } = req.body;
if (!nama_partner) {
const error: CustomError = new Error('Nama partnership harus diisi');
error.statusCode = 400;
return next(error);
}
if (!req.file) {
const error: CustomError = new Error('Thumbnail/image harus diunggah');
error.statusCode = 400;
return next(error);
}
const imagePath = `/uploads/images/${req.file.filename}`;
const partner = await prisma.partnership.create({
data: {
namaPartner: nama_partner,
imagePath
}
});
res.status(201).json({
status: true,
message: 'Partnership berhasil ditambahkan',
data: mapToFrontend(partner)
});
} catch (error) {
next(error);
}
});
/**
* @route PATCH /api/partnership-api/update/:id
* @desc Update data partnership (khusus admin)
* @access Private (Admin)
*/
router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const { nama_partner } = req.body;
const existing = await prisma.partnership.findUnique({
where: { id: parseInt(id, 10) }
});
if (!existing) {
const error: CustomError = new Error('Partnership tidak ditemukan');
error.statusCode = 404;
return next(error);
}
let imagePath = existing.imagePath;
if (req.file) {
imagePath = `/uploads/images/${req.file.filename}`;
}
const updated = await prisma.partnership.update({
where: { id: parseInt(id, 10) },
data: {
namaPartner: nama_partner ?? existing.namaPartner,
imagePath
}
});
res.status(200).json({
status: true,
message: 'Partnership berhasil diupdate',
data: mapToFrontend(updated)
});
} catch (error) {
next(error);
}
});
/**
* @route DELETE /api/partnership-api/delete/:id
* @desc Hapus data partnership (khusus admin)
* @access Private (Admin)
*/
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const existing = await prisma.partnership.findUnique({
where: { id: parseInt(id, 10) }
});
if (!existing) {
const error: CustomError = new Error('Partnership tidak ditemukan');
error.statusCode = 404;
return next(error);
}
await prisma.partnership.delete({
where: { id: parseInt(id, 10) }
});
res.status(200).json({
status: true,
message: 'Partnership berhasil dihapus'
});
} catch (error) {
next(error);
}
});
export default router;
+179
View File
@@ -0,0 +1,179 @@
import { Router, Request, Response, NextFunction } from 'express';
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
import prisma from '../lib/prisma';
import { upload } from '../middleware/upload';
import { CustomError } from '../middleware/errorHandler';
const router = Router();
// Map data research product dari DB ke format frontend
function mapToFrontend(product: any) {
return {
id: product.id,
nama_project: product.namaProject,
deskripsi: product.deskripsi,
link_project: product.linkProject,
image_path: product.imagePath,
createdAt: product.createdAt
};
}
/**
* @route GET /api/hasil-research-api/get
* @desc Dapetin semua hasil riset/products
* @access Public
*/
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
try {
const products = await prisma.hasilResearch.findMany({
orderBy: { createdAt: 'desc' }
});
res.status(200).json({
status: true,
data: products.map(mapToFrontend)
});
} catch (error) {
next(error);
}
});
/**
* @route GET /api/hasil-research-api/get/:id
* @desc Dapetin detail satu hasil riset/product
* @access Public
*/
router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const product = await prisma.hasilResearch.findUnique({
where: { id: parseInt(id, 10) }
});
if (!product) {
const error: CustomError = new Error('Project tidak ditemukan');
error.statusCode = 404;
return next(error);
}
res.status(200).json({
status: true,
data: mapToFrontend(product)
});
} catch (error) {
next(error);
}
});
/**
* @route POST /api/hasil-research-api/add
* @desc Tambah hasil riset baru (khusus admin ya)
* @access Private (Admin)
*/
router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { nama_project, deskripsi, link_project } = req.body;
if (!nama_project || !deskripsi || !link_project) {
const error: CustomError = new Error('Mohon lengkapi semua field yang wajib diisi');
error.statusCode = 400;
return next(error);
}
const imagePath = req.file ? `/uploads/images/${req.file.filename}` : '';
const product = await prisma.hasilResearch.create({
data: {
namaProject: nama_project,
deskripsi,
linkProject: link_project,
imagePath
}
});
res.status(201).json({
status: true,
message: 'Research project berhasil ditambahkan',
data: mapToFrontend(product)
});
} catch (error) {
next(error);
}
});
/**
* @route PATCH /api/hasil-research-api/update/:id
* @desc Update data hasil riset (khusus admin)
* @access Private (Admin)
*/
router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const { nama_project, deskripsi, link_project } = req.body;
const existing = await prisma.hasilResearch.findUnique({
where: { id: parseInt(id, 10) }
});
if (!existing) {
const error: CustomError = new Error('Project tidak ditemukan');
error.statusCode = 404;
return next(error);
}
let imagePath = existing.imagePath;
if (req.file) {
imagePath = `/uploads/images/${req.file.filename}`;
}
const updated = await prisma.hasilResearch.update({
where: { id: parseInt(id, 10) },
data: {
namaProject: nama_project ?? existing.namaProject,
deskripsi: deskripsi ?? existing.deskripsi,
linkProject: link_project ?? existing.linkProject,
imagePath
}
});
res.status(200).json({
status: true,
message: 'Research project berhasil diupdate',
data: mapToFrontend(updated)
});
} catch (error) {
next(error);
}
});
/**
* @route DELETE /api/hasil-research-api/delete/:id
* @desc Hapus data hasil riset (khusus admin)
* @access Private (Admin)
*/
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const existing = await prisma.hasilResearch.findUnique({
where: { id: parseInt(id, 10) }
});
if (!existing) {
const error: CustomError = new Error('Project tidak ditemukan');
error.statusCode = 404;
return next(error);
}
await prisma.hasilResearch.delete({
where: { id: parseInt(id, 10) }
});
res.status(200).json({
status: true,
message: 'Research project berhasil dihapus'
});
} catch (error) {
next(error);
}
});
export default router;
@@ -0,0 +1 @@
%PDF-1.4 mock cv
@@ -0,0 +1 @@
%PDF-1.4 mock portfolio
@@ -0,0 +1 @@
fake-image-content