feat: nambahin certificate system
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `certificate` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`id_project` INTEGER NOT NULL,
|
||||
`id_user` INTEGER NOT NULL,
|
||||
`certificate_no` VARCHAR(255) NOT NULL,
|
||||
`issued_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
UNIQUE INDEX `certificate_certificate_no_key`(`certificate_no`),
|
||||
INDEX `certificate_id_project_foreign`(`id_project`),
|
||||
INDEX `certificate_id_user_foreign`(`id_user`),
|
||||
UNIQUE INDEX `certificate_id_project_id_user_key`(`id_project`, `id_user`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `certificate` ADD CONSTRAINT `certificate_id_project_foreign` FOREIGN KEY (`id_project`) REFERENCES `project`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `certificate` ADD CONSTRAINT `certificate_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
+22
-3
@@ -194,6 +194,7 @@ model User {
|
||||
lamaran_magang LamaranMagang @relation(fields: [id_lamaran_magang], references: [id], map: "user_id_lamaran_magang_foreign")
|
||||
project_members ProjectMember[]
|
||||
task_submissions TaskSubmission[]
|
||||
certificates Certificate[]
|
||||
|
||||
@@index([email])
|
||||
@@map("user")
|
||||
@@ -212,9 +213,10 @@ model Project {
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
admin Admin @relation(fields: [id_admin], references: [id], map: "project_id_admin_foreign")
|
||||
members ProjectMember[]
|
||||
tasks Task[]
|
||||
admin Admin @relation(fields: [id_admin], references: [id], map: "project_id_admin_foreign")
|
||||
members ProjectMember[]
|
||||
tasks Task[]
|
||||
certificates Certificate[]
|
||||
|
||||
@@index([id_admin], map: "project_id_admin_foreign")
|
||||
@@index([status])
|
||||
@@ -328,3 +330,20 @@ enum SubmissionType {
|
||||
file_upload
|
||||
url_link
|
||||
}
|
||||
|
||||
model Certificate {
|
||||
id Int @id @default(autoincrement())
|
||||
uuid String @unique @default(uuid()) @db.VarChar(36)
|
||||
id_project Int
|
||||
id_user Int
|
||||
certificate_no String @unique @db.VarChar(255)
|
||||
issued_at DateTime @default(now())
|
||||
|
||||
project Project @relation(fields: [id_project], references: [id], map: "certificate_id_project_foreign")
|
||||
user User @relation(fields: [id_user], references: [id], map: "certificate_id_user_foreign")
|
||||
|
||||
@@unique([id_project, id_user])
|
||||
@@index([id_project], map: "certificate_id_project_foreign")
|
||||
@@index([id_user], map: "certificate_id_user_foreign")
|
||||
@@map("certificate")
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ const faqRoutes = require('./routes/faq.routes');
|
||||
const feedbackRoutes = require('./routes/feedback.routes');
|
||||
const batchRoutes = require('./routes/batch.routes');
|
||||
const projectRoutes = require('./routes/project.routes');
|
||||
const certificateRoutes = require('./routes/certificate.routes');
|
||||
const setupSwaggerDocs = require('./docs/swagger');
|
||||
|
||||
const PORT = process.env.PORT;
|
||||
@@ -56,6 +57,7 @@ app.use("/faq-api", faqRoutes);
|
||||
app.use("/feedback-api", feedbackRoutes);
|
||||
app.use("/batch-api", batchRoutes);
|
||||
app.use("/project-api", projectRoutes);
|
||||
app.use("/certificate-api", certificateRoutes);
|
||||
// app.use("/project-member-api", projectMemberRoutes);
|
||||
// app.use("/task-api", taskRoutes);
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
const certificateService = require('../services/certificate.service');
|
||||
const { response, error } = require('../../../helpers/utils/wrapper');
|
||||
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
|
||||
const { InternalServerError } = require('../../../helpers/error');
|
||||
const { isValidPayload } = require('../../../helpers/utils/validator');
|
||||
const { claimCertificateModel } = require('../models/certificate.model');
|
||||
|
||||
const getActor = (req) => ({
|
||||
id: req.id,
|
||||
role: req.role,
|
||||
email: req.email,
|
||||
});
|
||||
|
||||
class CertificateController {
|
||||
async claimCertificate(req, res) {
|
||||
try {
|
||||
const validatePayload = isValidPayload(req.body, claimCertificateModel);
|
||||
|
||||
if (validatePayload.err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
{ err: validatePayload.err, data: null },
|
||||
'Data claim certificate tidak valid',
|
||||
ERROR.EXPECTATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
const actor = getActor(req);
|
||||
const result = await certificateService.claimCertificate(validatePayload.data, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Certificate claimed successfully', SUCCESS.CREATED);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getMyCertificates(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await certificateService.getMyCertificates(actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Certificates retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getCertificateDetail(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const actor = getActor(req);
|
||||
const result = await certificateService.getCertificateDetail(id, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Certificate details retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getProjectCertificates(req, res) {
|
||||
try {
|
||||
const { id_project } = req.params;
|
||||
const actor = getActor(req);
|
||||
const result = await certificateService.getProjectCertificates(id_project, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Project certificates retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllCertificates(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await certificateService.getAllCertificates(actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'All certificates retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyCertificate(req, res) {
|
||||
try {
|
||||
const certificate_no = req.query.certificate_no || req.params.certificate_no;
|
||||
const result = await certificateService.verifyCertificate(certificate_no);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Certificate validated successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyCertificateByUuid(req, res) {
|
||||
try {
|
||||
const { uuid } = req.params;
|
||||
const result = await certificateService.verifyCertificateByUuid(uuid);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Certificate validated successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CertificateController();
|
||||
@@ -0,0 +1,5 @@
|
||||
const certificateController = require('./controllers/certificate.controller');
|
||||
|
||||
module.exports = {
|
||||
certificateController,
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
const joi = require('joi');
|
||||
|
||||
const claimCertificateModel = joi.object().keys({
|
||||
id_project: joi.number().integer().required().messages({
|
||||
'number.base': 'Project ID harus berupa angka.',
|
||||
'number.integer': 'Project ID harus berupa bilangan bulat.',
|
||||
'any.required': 'Project ID wajib diisi.',
|
||||
}),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
claimCertificateModel,
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
const prisma = require('../../../helpers/db/db_connection');
|
||||
|
||||
class CertificateRepository {
|
||||
async create(certificateData) {
|
||||
return prisma.certificate.create({
|
||||
data: certificateData,
|
||||
include: this.certificateDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id) {
|
||||
return prisma.certificate.findUnique({
|
||||
where: { id: parseInt(id) },
|
||||
include: this.certificateDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async findByUserAndProject(id_user, id_project) {
|
||||
return prisma.certificate.findUnique({
|
||||
where: {
|
||||
id_project_id_user: {
|
||||
id_project: parseInt(id_project),
|
||||
id_user: parseInt(id_user),
|
||||
},
|
||||
},
|
||||
include: this.certificateDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async findUserCertificates(id_user) {
|
||||
return prisma.certificate.findMany({
|
||||
where: { id_user: parseInt(id_user) },
|
||||
include: this.certificateDetailInclude(),
|
||||
orderBy: { issued_at: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findProjectCertificates(id_project) {
|
||||
return prisma.certificate.findMany({
|
||||
where: { id_project: parseInt(id_project) },
|
||||
include: this.certificateDetailInclude(),
|
||||
orderBy: { issued_at: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return prisma.certificate.findMany({
|
||||
include: this.certificateDetailInclude(),
|
||||
orderBy: { issued_at: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findByCertificateNo(certificate_no) {
|
||||
return prisma.certificate.findUnique({
|
||||
where: { certificate_no },
|
||||
include: this.certificateDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async findByUuid(uuid) {
|
||||
return prisma.certificate.findUnique({
|
||||
where: { uuid },
|
||||
include: this.certificateDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async getProjectTasksAndSubmissions(id_project, id_user) {
|
||||
return prisma.project.findUnique({
|
||||
where: { id: parseInt(id_project) },
|
||||
select: {
|
||||
id: true,
|
||||
project_name: true,
|
||||
tasks: {
|
||||
select: {
|
||||
id: true,
|
||||
submissions: {
|
||||
where: { id_user: parseInt(id_user) },
|
||||
select: {
|
||||
id: true,
|
||||
submitted_at: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
members: {
|
||||
where: {
|
||||
id_user: parseInt(id_user),
|
||||
status: 'active',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
certificateDetailInclude() {
|
||||
return {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
full_name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
project_name: true,
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CertificateRepository();
|
||||
@@ -0,0 +1,225 @@
|
||||
const certificateRepository = require('../repositories/certificate.repository');
|
||||
const { data, error } = require('../../../helpers/utils/wrapper');
|
||||
const {
|
||||
BadRequestError,
|
||||
NotFoundError,
|
||||
ForbiddenError,
|
||||
ConflictError,
|
||||
} = require('../../../helpers/error');
|
||||
|
||||
class CertificateService {
|
||||
async claimCertificate(payload, actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError('User ID is required'));
|
||||
}
|
||||
|
||||
if (actor.role !== 'intern') {
|
||||
return error(
|
||||
new ForbiddenError('Access denied: only interns can claim certificates'),
|
||||
);
|
||||
}
|
||||
|
||||
const { id_project } = payload;
|
||||
|
||||
const existingCert = await certificateRepository.findByUserAndProject(actor.id, id_project);
|
||||
if (existingCert) {
|
||||
return error(new ConflictError('Certificate already claimed for this project'));
|
||||
}
|
||||
|
||||
const projectInfo = await certificateRepository.getProjectTasksAndSubmissions(id_project, actor.id);
|
||||
|
||||
if (!projectInfo) {
|
||||
return error(new NotFoundError('Project not found'));
|
||||
}
|
||||
|
||||
if (!projectInfo.members || projectInfo.members.length === 0) {
|
||||
return error(
|
||||
new ForbiddenError('Access denied: you are not an active member of this project'),
|
||||
);
|
||||
}
|
||||
|
||||
if (!projectInfo.tasks || projectInfo.tasks.length === 0) {
|
||||
return error(
|
||||
new BadRequestError('Cannot claim certificate: this project does not have any tasks assigned yet'),
|
||||
);
|
||||
}
|
||||
|
||||
const completedTasks = projectInfo.tasks.filter(
|
||||
(task) => task.submissions && task.submissions.length > 0
|
||||
);
|
||||
|
||||
if (completedTasks.length < projectInfo.tasks.length) {
|
||||
return error(
|
||||
new BadRequestError(
|
||||
`Cannot claim certificate: you have completed ${completedTasks.length} out of ${projectInfo.tasks.length} tasks`
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Generate certificate number berdsarkan format ini: CERT/YYYYMMDD/PRJ{id_project}/USR{id_user} [Rafi 09/06/2026 20:45]
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const date = String(now.getDate()).padStart(2, '0');
|
||||
const dateString = `${year}${month}${date}`;
|
||||
const certificateNo = `CERT/${dateString}/PRJ${id_project}/USR${actor.id}`;
|
||||
|
||||
const certificateData = {
|
||||
id_project: parseInt(id_project),
|
||||
id_user: parseInt(actor.id),
|
||||
certificate_no: certificateNo,
|
||||
issued_at: now,
|
||||
};
|
||||
|
||||
const certificate = await certificateRepository.create(certificateData);
|
||||
|
||||
return data(this.mapCertificate(certificate));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getMyCertificates(actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError('User ID is required'));
|
||||
}
|
||||
|
||||
if (actor.role !== 'intern') {
|
||||
return error(
|
||||
new ForbiddenError('Access denied: only interns can view their certificates'),
|
||||
);
|
||||
}
|
||||
|
||||
const certificates = await certificateRepository.findUserCertificates(actor.id);
|
||||
return data(certificates.map((cert) => this.mapCertificate(cert)));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getCertificateDetail(id, actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError('User ID is required'));
|
||||
}
|
||||
|
||||
const certificate = await certificateRepository.findById(id);
|
||||
if (!certificate) {
|
||||
return error(new NotFoundError('Certificate not found'));
|
||||
}
|
||||
|
||||
if (actor.role === 'intern' && certificate.id_user !== actor.id) {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: you cannot view other intern's certificates"),
|
||||
);
|
||||
}
|
||||
|
||||
return data(this.mapCertificate(certificate));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getProjectCertificates(id_project, actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError('Admin ID is required'));
|
||||
}
|
||||
|
||||
if (actor.role !== 'admin') {
|
||||
return error(
|
||||
new ForbiddenError('Access denied: only admins/mentors can view project certificates'),
|
||||
);
|
||||
}
|
||||
|
||||
const certificates = await certificateRepository.findProjectCertificates(id_project);
|
||||
return data(certificates.map((cert) => this.mapCertificate(cert)));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllCertificates(actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError('Admin ID is required'));
|
||||
}
|
||||
|
||||
if (actor.role !== 'admin') {
|
||||
return error(
|
||||
new ForbiddenError('Access denied: only admins/mentors can view all certificates'),
|
||||
);
|
||||
}
|
||||
|
||||
const certificates = await certificateRepository.findAll();
|
||||
return data(certificates.map((cert) => this.mapCertificate(cert)));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyCertificate(certificate_no) {
|
||||
try {
|
||||
if (!certificate_no) {
|
||||
return error(new BadRequestError('Certificate number is required'));
|
||||
}
|
||||
|
||||
const certificate = await certificateRepository.findByCertificateNo(certificate_no);
|
||||
if (!certificate) {
|
||||
return error(new NotFoundError('Certificate not found or invalid'));
|
||||
}
|
||||
|
||||
return data(this.mapCertificate(certificate));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyCertificateByUuid(uuid) {
|
||||
try {
|
||||
if (!uuid) {
|
||||
return error(new BadRequestError('Certificate UUID is required'));
|
||||
}
|
||||
|
||||
const certificate = await certificateRepository.findByUuid(uuid);
|
||||
if (!certificate) {
|
||||
return error(new NotFoundError('Certificate not found or invalid'));
|
||||
}
|
||||
|
||||
return data({
|
||||
uuid: certificate.uuid,
|
||||
intern_name: certificate.user ? certificate.user.full_name : null,
|
||||
project_name: certificate.project ? certificate.project.project_name : null,
|
||||
created_at: certificate.issued_at,
|
||||
});
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
mapCertificate(cert) {
|
||||
if (!cert) return null;
|
||||
return {
|
||||
id: cert.id,
|
||||
uuid: cert.uuid,
|
||||
id_project: cert.id_project,
|
||||
id_user: cert.id_user,
|
||||
certificate_no: cert.certificate_no,
|
||||
issued_at: cert.issued_at,
|
||||
user: cert.user ? {
|
||||
id: cert.user.id,
|
||||
full_name: cert.user.full_name,
|
||||
email: cert.user.email,
|
||||
} : null,
|
||||
project: cert.project ? {
|
||||
id: cert.project.id,
|
||||
project_name: cert.project.project_name,
|
||||
description: cert.project.description,
|
||||
} : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CertificateService();
|
||||
@@ -477,19 +477,16 @@ class ProjectService {
|
||||
return error(new BadRequestError("Project ID and User ID are required"));
|
||||
}
|
||||
|
||||
// Check if project exists
|
||||
const project = await projectRepository.findById(id_project);
|
||||
if (!project) {
|
||||
return error(new NotFoundError("Project not found"));
|
||||
}
|
||||
|
||||
// Check if user exists and is active
|
||||
const user = await projectRepository.findUserByIdAndActive(id_user);
|
||||
if (!user) {
|
||||
return error(new NotFoundError("User not found or inactive"));
|
||||
}
|
||||
|
||||
// Check if already a member of this project
|
||||
const existingMembership = await projectRepository.findMembership(id_project, id_user);
|
||||
|
||||
let membership;
|
||||
@@ -497,10 +494,8 @@ class ProjectService {
|
||||
if (existingMembership.status === 'active') {
|
||||
return error(new ConflictError("User is already an active member of this project"));
|
||||
}
|
||||
// Reactivate membership
|
||||
membership = await projectRepository.updateMembershipStatus(existingMembership.id, 'active');
|
||||
} else {
|
||||
// Create new membership
|
||||
membership = await projectRepository.assignMember(id_project, id_user);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
const express = require('express');
|
||||
const { certificateController } = require('../modules/certificate');
|
||||
const { verifyJWT } = require('../middleware/verifyJWT');
|
||||
const isAdmin = require('../middleware/isAdmin');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Certificate:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* example: 1
|
||||
* id_project:
|
||||
* type: integer
|
||||
* example: 2
|
||||
* id_user:
|
||||
* type: integer
|
||||
* example: 5
|
||||
* certificate_no:
|
||||
* type: string
|
||||
* example: "CERT/20260609/PRJ2/USR5"
|
||||
* issued_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* example: "2026-06-09T14:00:00.000Z"
|
||||
* user:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* example: 5
|
||||
* full_name:
|
||||
* type: string
|
||||
* example: "Rafi Athallah"
|
||||
* email:
|
||||
* type: string
|
||||
* example: "rafi@student.com"
|
||||
* project:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* example: 2
|
||||
* project_name:
|
||||
* type: string
|
||||
* example: "Internify Platform Dev"
|
||||
* description:
|
||||
* type: string
|
||||
* example: "Project to develop features of the Internify web app"
|
||||
* CertificateClaimRequest:
|
||||
* type: object
|
||||
* required:
|
||||
* - id_project
|
||||
* properties:
|
||||
* id_project:
|
||||
* type: integer
|
||||
* example: 2
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /certificate-api/claim:
|
||||
* post:
|
||||
* summary: Claim certificate for a completed project
|
||||
* description: Interns can claim a certificate for a project once they have submitted all assigned tasks.
|
||||
* tags: [Certificate]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/CertificateClaimRequest'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Certificate claimed successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Certificate'
|
||||
* message:
|
||||
* type: string
|
||||
* example: "Certificate claimed successfully"
|
||||
* code:
|
||||
* type: integer
|
||||
* example: 201
|
||||
* 400:
|
||||
* description: Bad request (no tasks assigned or not all tasks submitted)
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden (only interns can claim, or not a member of project)
|
||||
* 404:
|
||||
* description: Project not found
|
||||
* 409:
|
||||
* description: Conflict (certificate already claimed)
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.post('/claim', verifyJWT, certificateController.claimCertificate);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /certificate-api/my-certificates:
|
||||
* get:
|
||||
* summary: Get logged-in intern's certificates
|
||||
* description: Retrieve all certificates earned by the currently logged-in intern.
|
||||
* tags: [Certificate]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Certificates retrieved successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Certificate'
|
||||
* message:
|
||||
* type: string
|
||||
* example: "Certificates retrieved successfully"
|
||||
* code:
|
||||
* type: integer
|
||||
* example: 200
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden (only interns can access this)
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.get('/my-certificates', verifyJWT, certificateController.getMyCertificates);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /certificate-api/all:
|
||||
* get:
|
||||
* summary: Get all issued certificates (Admin only)
|
||||
* description: Admin/Mentor can retrieve all certificates issued across the system.
|
||||
* tags: [Certificate]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: All certificates retrieved successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Certificate'
|
||||
* message:
|
||||
* type: string
|
||||
* example: "All certificates retrieved successfully"
|
||||
* code:
|
||||
* type: integer
|
||||
* example: 200
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden (Admin only)
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.get('/all', verifyJWT, isAdmin, certificateController.getAllCertificates);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /certificate-api/detail/{id}:
|
||||
* get:
|
||||
* summary: Get certificate detail
|
||||
* description: Fetch detailed information for a certificate by its ID. Interns can only access their own. Admins can access any.
|
||||
* tags: [Certificate]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: Certificate ID
|
||||
* example: 1
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Certificate details retrieved successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Certificate'
|
||||
* message:
|
||||
* type: string
|
||||
* example: "Certificate details retrieved successfully"
|
||||
* code:
|
||||
* type: integer
|
||||
* example: 200
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden (Accessing another user's certificate)
|
||||
* 404:
|
||||
* description: Certificate not found
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.get('/detail/:id', verifyJWT, certificateController.getCertificateDetail);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /certificate-api/project/{id_project}:
|
||||
* get:
|
||||
* summary: Get certificates issued for a specific project (Admin only)
|
||||
* description: Admin/Mentor can retrieve all certificates issued to interns for a specific project.
|
||||
* tags: [Certificate]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id_project
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: Project ID
|
||||
* example: 2
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Project certificates retrieved successfully
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden (Admin only)
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.get('/project/:id_project', verifyJWT, isAdmin, certificateController.getProjectCertificates);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /certificate-api/verify:
|
||||
* get:
|
||||
* summary: Verify a certificate (Public)
|
||||
* description: Verify the validity of a certificate number publicly without authentication.
|
||||
* tags: [Certificate]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: certificate_no
|
||||
* schema:
|
||||
* type: string
|
||||
* required: true
|
||||
* description: The certificate number to verify
|
||||
* example: "CERT/20260609/PRJ2/USR5"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Certificate is valid
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Certificate'
|
||||
* message:
|
||||
* type: string
|
||||
* example: "Certificate validated successfully"
|
||||
* code:
|
||||
* type: integer
|
||||
* example: 200
|
||||
* 400:
|
||||
* description: Bad request (missing certificate number)
|
||||
* 404:
|
||||
* description: Certificate not found or invalid
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.get('/verify', certificateController.verifyCertificate);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /certificate-api/verify-uuid/{uuid}:
|
||||
* get:
|
||||
* summary: Verify a certificate by UUID (Public)
|
||||
* description: Verify the validity of a certificate by its unique UUID for the frontend. Returns intern's name, creation date, and UUID.
|
||||
* tags: [Certificate]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: uuid
|
||||
* schema:
|
||||
* type: string
|
||||
* format: uuid
|
||||
* required: true
|
||||
* description: The certificate UUID to verify
|
||||
* example: "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Certificate is valid
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* uuid:
|
||||
* type: string
|
||||
* example: "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
|
||||
* intern_name:
|
||||
* type: string
|
||||
* example: "Rafi Athallah"
|
||||
* project_name:
|
||||
* type: string
|
||||
* example: "Internify Platform Dev"
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* example: "2026-06-09T14:00:00.000Z"
|
||||
* message:
|
||||
* type: string
|
||||
* example: "Certificate validated successfully"
|
||||
* code:
|
||||
* type: integer
|
||||
* example: 200
|
||||
* 400:
|
||||
* description: Bad request (missing UUID)
|
||||
* 404:
|
||||
* description: Certificate not found or invalid
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.get('/verify-uuid/:uuid', certificateController.verifyCertificateByUuid);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user