Source

API/src/controllers/certificate.controllers.js

/**
 * @category Backend API
 * @subcategory Controllers
 * @module Certificate Controller
 * 
 * @description This module contains the controller methods for the certificate routes.
 * 
 * @requires module:CertificateModel~certificateSchema
 * @requires module:CourseReportModel~courseReportSchema
 * @requires module:canvas
 * 
 * The following routes are handled by this module: <br>
 * 
 * </br>
 * 
 * <b>GET</b> /api/certificate/verify/:sn <br>
 * <b>GET</b> /api/certificate/course/:id <br>
 * <b>GET</b> /api/certificate/:id <br>
 * <b>GET</b> /api/certificate/ <br>
 * 
 */

const { Certificate } = require("../models/certificate.models")
const { CourseReport } = require("../models/course.models")

const fs = require("fs")
const { createCanvas, loadImage } = require("canvas")
const { uploadToCloudinary } = require("../utils/cloudinary")
const { NotFoundError, BadRequestError, ForbiddenError } = require("../utils/errors")

/**
 * Verify Certificate
 * 
 * @description Verify certificate by serial number
 * 
 * @param {string} serial_number - Serial number of certificate to verify
 * 
 * @throws {BadRequestError} if missing param in request body
 * @throws {NotFoundError} if certificate not found
 * 
 * @returns {Object} certificate
 */
exports.verifyCertificate = async (req, res, next) => {
    const serial_number = req.params.sn

    // Check if missing params
    if (!serial_number || serial_number === ":sn") {
        return next(new BadRequestError("Missing required param in request body"))
    }

    console.log(serial_number)

    // Check if certificate exists
    const certificate = await Certificate.findOne({ serial_number: Number(serial_number) }).populate({
        path: "user course",
        select: "title description _id firstname lastname email",
    })
    if (!certificate) {
        return next(new NotFoundError("Certificate not found"))
    }

    return res.status(200).json({
        success: true,
        data: {
            message: "Certificate found",
            certificate,
        }
    })
}

/**
 * Get certificate for course
 * 
 * @description Get certificate for course if it exists, else create it
 * 
 * @param {string} course_id - Id of course to get certificate for
 * 
 * @throws {BadRequestError} if missing param in request body
 * @throws {NotFoundError} if course report not found
 * @throws {ForbiddenError} if course not completed
 * 
 * @returns {Object} certificate
 */
exports.getCertificateForCourse = async (req, res, next) => {
    const course_id = req.params.id

    // Check if missing params
    if (!course_id || course_id === ":id") {
        return next(new BadRequestError("Missing required param in request body"))
    }

    // Check if course exists
    const student_course_report = await CourseReport.findOne({ course: course_id, user: req.user.id })
    if (!student_course_report) {
        return next(new NotFoundError("User has not enrolled for course"))
    }

    // Check if user has completed course
    if (!student_course_report.isCompleted) {
        // return next(new ForbiddenError("Course not completed"))
    }

    // Check if certificate exists
    const populate_conf = {
        path: "user course",
        select: "title description _id firstname lastname email",
    }
    if (student_course_report.certificate) {
        return res.status(200).json({
            success: true,
            message: "Certificate found",
            certificate: await student_course_report.certificate.populate(populate_conf),
        })
    }

    // Create certificate   
    const certificate = await this.issueCertificate(student_course_report)

    return res.status(200).json({
        success: true,
        data: {
            message: "Certificate issued",
            certificate: await certificate.populate(populate_conf),
        }
    })
}

/**
 * Get all certificates for user
 * 
 * @description Get all users certificates 
 * 
 * @returns {Object} certificates
 * 
 */
exports.getAllUsersCertificates = async (req, res, next) => {
    const populate_conf = {
        path: "user course",
        select: "title description _id firstname lastname email",
    }
    const certificates = await Certificate.find({ user: req.user.id }).populate(populate_conf)

    return res.status(200).json({
        success: true,
        data: {
            certificates,
        }
    })
}

/**
 * Create certificate for student
 * 
 * @description Create certificate for student after completing a course
 * 
 * @param {string} student_course_report_id - Id of student course report
 * 
 * @param {MongooseDocument} student_course_report 
 * @returns {MongooseDocument} certificate
 */
async function createCertificate(student_course_report) {
    const certificate = new Certificate({
        user: student_course_report.user,
        course: student_course_report.course,
        course_report: student_course_report._id,
        serial_number: Date.now(),
    });

    // Create certificate image
    const sample_certificate_path = "src/assets/sample_certificate.png"
    const image = await loadImage(sample_certificate_path)

    // Create canvas
    const canvas = createCanvas(image.width, image.height)
    const context = canvas.getContext("2d")

    // Draw image on canvas
    context.drawImage(image, 0, 0, image.width, image.height)

    // Draw text on canvas
    context.font = "italic 80px Arial"
    context.fillStyle = "black"
    context.textAlign = "center"

    // Add user's fullname
    let users_firstname = student_course_report.user.firstname,
        users_lastname = student_course_report.user.lastname

    // Capitalize first letter of firstname and lastname
    users_firstname = users_firstname.charAt(0).toUpperCase() + users_firstname.slice(1)
    users_lastname = users_lastname.charAt(0).toUpperCase() + users_lastname.slice(1)

    const users_fullmame = `${users_firstname} ${users_lastname}`
    context.fillText(users_fullmame, 1000, 900)

    // Add course title
    const course_title = student_course_report.course.title
    context.fillText(course_title, 1000, 100)

    // Add certificate id
    const certificate_id = certificate.serial_number.toString()
    context.font = "italic 30px Arial"
    context.fillText(certificate_id, 1000, image.height - 60)

    // Save image to file
    const modified_image = canvas.toBuffer("image/png")
    const image_path = "src/assets/certificate" + `_${certificate.serial_number.toString()}.png`
    fs.writeFileSync(image_path, modified_image)

    return certificate
}

/**
 * Issue certificate to student
 * 
 * @param {ObjectId} report_id 
 * @returns certificate 
 * 
 * @throws {Error} if missing Course report not found
 * @throws {Error} if student has not completed course
 */
exports.issueCertificate = async (report_id) => {
    const student_course_report = await CourseReport.findById(report_id).populate({
        path: "course",
        select: "title description _id",
        populate: {
            path: "author",
            select: "name email _id",
        },
    }).populate('user certificate');

    // Check if course report exists
    if (!student_course_report) {
        throw new BadRequestError("User has not enrolled for course");
    }

    // Check if student has already been issued a certificate
    if (student_course_report.certificate) {
        return student_course_report.certificate
    }

    // Check if student completed course
    if (!student_course_report.isCompleted) {
        throw new BadRequestError("Course not completed");
    }

    let certificate = await createCertificate(student_course_report)

    // Upload file to cloudinary
    const file_url = await uploadToCloudinary({
        path: "src/assets/certificate" + `_${certificate.serial_number.toString()}.png`,
        file_name: `certificate_${certificate._id}`,
        destination_path: `course_${certificate.course._id}/user_${certificate.user._id}`,
    });

    // Delete file from local storage
    fs.unlinkSync("src/assets/certificate" + `_${certificate.serial_number.toString()}.png`)
    
    // Save file url to database
    certificate.certificate_url = file_url
    certificate = await certificate.save()

    return certificate
}

/**
 * Get certificate data
 * 
 * @description Get certificate data
 * 
 * @param {string} certificate_id - Id of certificate
 * 
 * @returns {Object} certificate
 * 
 * @throws {BadRequestError} if missing certificate_id
 * @throws {NotfoundError} if certificate not found
 */
exports.getCertificateData = async (req, res, next) => {
    const certificate_id = req.params.id

    // Check if missing params
    if (!certificate_id || certificate_id === ":id") {
        return next(new BadRequestError("Missing required param in request body"))
    }

    // Check if certificate exists
    const certificate = await Certificate.findById(certificate_id).populate({
        path: "course",
        select: "title description _id",
    }).populate('user');

    if (!certificate) {
        return next(new NotFoundError('Certificate not found'))
    }

    return res.status(200).json({
        success: true,
        data: {
            certificate,
        }
    })
}