diff --git a/prisma/migrations/20260609140242_add_certificate/migration.sql b/prisma/migrations/20260609140242_add_certificate/migration.sql new file mode 100644 index 0000000..7cd5e6e --- /dev/null +++ b/prisma/migrations/20260609140242_add_certificate/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e226758..f400dc7 100755 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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") +} diff --git a/src/index.js b/src/index.js index 7831774..cf953b3 100755 --- a/src/index.js +++ b/src/index.js @@ -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); diff --git a/src/modules/certificate/controllers/certificate.controller.js b/src/modules/certificate/controllers/certificate.controller.js new file mode 100644 index 0000000..77f6d16 --- /dev/null +++ b/src/modules/certificate/controllers/certificate.controller.js @@ -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(); diff --git a/src/modules/certificate/index.js b/src/modules/certificate/index.js new file mode 100644 index 0000000..f898b95 --- /dev/null +++ b/src/modules/certificate/index.js @@ -0,0 +1,5 @@ +const certificateController = require('./controllers/certificate.controller'); + +module.exports = { + certificateController, +}; diff --git a/src/modules/certificate/models/certificate.model.js b/src/modules/certificate/models/certificate.model.js new file mode 100644 index 0000000..81d67b7 --- /dev/null +++ b/src/modules/certificate/models/certificate.model.js @@ -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, +}; diff --git a/src/modules/certificate/repositories/certificate.repository.js b/src/modules/certificate/repositories/certificate.repository.js new file mode 100644 index 0000000..51e2221 --- /dev/null +++ b/src/modules/certificate/repositories/certificate.repository.js @@ -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(); diff --git a/src/modules/certificate/services/certificate.service.js b/src/modules/certificate/services/certificate.service.js new file mode 100644 index 0000000..2487e5c --- /dev/null +++ b/src/modules/certificate/services/certificate.service.js @@ -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(); diff --git a/src/modules/project/services/project.service.js b/src/modules/project/services/project.service.js index a5c64a0..f13bba8 100644 --- a/src/modules/project/services/project.service.js +++ b/src/modules/project/services/project.service.js @@ -497,19 +497,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; @@ -517,10 +514,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); } diff --git a/src/routes/certificate.routes.js b/src/routes/certificate.routes.js new file mode 100644 index 0000000..fec42f2 --- /dev/null +++ b/src/routes/certificate.routes.js @@ -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;