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
+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
}
});
+11 -9
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,12 +145,14 @@ router.post('/login', async (req: Request, res: Response, next: NextFunction) =>
res.status(200).json({
success: true,
token,
user: {
id: user.id,
email: user.email,
role: user.role,
profile,
data: {
token,
user: {
id: user.id,
email: user.email,
role: user.role,
profile,
},
},
});
} catch (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;