├── .gitignore ├── src ├── utils │ ├── logger.ts │ └── postgresConnection.ts ├── types │ ├── sharp.d.ts │ └── long.d.ts ├── routes │ ├── index.ts │ └── certificateRoute.ts ├── index.ts └── controllers │ └── certificateController.ts ├── tsconfig.test.json ├── Dockerfile ├── Dockerfile.build ├── tsconfig.json ├── package.json └── Jenkinsfile-sun /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | dist -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | export const logger = pino({ 3 | level: 'info' 4 | }); -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "test/**/*.ts" 5 | ] 6 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | WORKDIR /usr/src/app 3 | COPY . . 4 | RUN yarn install 5 | RUN yarn build 6 | CMD ["yarn", "start"] 7 | -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | WORKDIR /usr/src/app 3 | COPY . . 4 | RUN yarn install 5 | 6 | RUN yarn build 7 | 8 | CMD ["yarn", "start"] 9 | -------------------------------------------------------------------------------- /src/types/sharp.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'sharp' { 2 | // Import types if available or declare any necessary types 3 | const sharp: any; 4 | export default sharp; 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { certificateRoute } from "./certificateRoute"; 3 | 4 | export const router = Router(); 5 | router.use("/certificate", certificateRoute); 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/routes/certificateRoute.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { getUserCertificateDetails, generateUserCertificatesFromRc } from '../controllers/certificateController'; 3 | export const certificateRoute = Router(); 4 | certificateRoute.get("/getUserCertificateDetails", getUserCertificateDetails) 5 | certificateRoute.post("/generateUserCertificatesFromRc", generateUserCertificatesFromRc) 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "./dist/src", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "typeRoots": ["./node_modules/@types", "./src/types"], 12 | "types": ["node"] 13 | }, 14 | "include": [ 15 | "src/**/*.ts", 16 | "src/types/sharp.d.ts" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | "dist" 21 | ] 22 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | import express from "express"; 3 | import { router } from "./routes/index"; 4 | import { logger } from "./utils/logger"; 5 | import bodyParser from 'body-parser'; 6 | 7 | const app = express(); 8 | app.use((req, _res, next) => { 9 | logger.info(`Requested Route: ${req.method} ${req.url}`); 10 | next(); 11 | }); 12 | app.use(bodyParser.json()); 13 | app.use(bodyParser.urlencoded({ extended: true })); 14 | app.use("/v1", router); 15 | app.listen(process.env.APPLICATION_PORT, () => { 16 | logger.info(`Sever listening on port ${process.env.APPLICATION_PORT}`); 17 | }); 18 | -------------------------------------------------------------------------------- /src/utils/postgresConnection.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "../utils/logger"; 2 | import { Client } from 'pg'; 3 | export const client = new Client({ 4 | user: process.env.POSTGRES_USER, 5 | host: process.env.POSTGRES_HOST, 6 | database: process.env.POSTGRES_DATABASE, 7 | password: process.env.POSTGRES_PASSWORD, 8 | port: 5432, 9 | }); 10 | export const connectDB = async () => { 11 | try { 12 | await client.connect(); 13 | logger.info('Database connected successfully'); 14 | } catch (error) { 15 | logger.error('Database connection error:', error); 16 | throw new Error('Database connection error'); 17 | } 18 | }; 19 | connectDB() 20 | module.exports = { client } -------------------------------------------------------------------------------- /src/types/long.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'long' { 2 | export class Long { 3 | constructor(low?: number, high?: number, unsigned?: boolean); 4 | static fromInt(value: number, unsigned?: boolean): Long; 5 | static fromValue(value: Long | number | string | { low: number; high: number; unsigned: boolean }): Long; 6 | static fromBits(lowBits: number, highBits: number, unsigned?: boolean): Long; 7 | static fromString(str: string, unsigned?: boolean | { unsigned: boolean }, radix?: number): Long; 8 | static fromNumber(value: number, unsigned?: boolean): Long; 9 | toInt(): number; 10 | toNumber(): number; 11 | toString(radix?: number): string; 12 | toBytesLE(): Uint8Array; 13 | toBytesBE(): Uint8Array; 14 | toBytes(): Uint8Array; 15 | equals(other: Long | number | string): boolean; 16 | compare(other: Long | number | string): number; 17 | isZero(): boolean; 18 | isNegative(): boolean; 19 | isPositive(): boolean; 20 | negate(): Long; 21 | add(addend: Long | number | string): Long; 22 | subtract(subtrahend: Long | number | string): Long; 23 | multiply(multiplier: Long | number | string): Long; 24 | divide(divisor: Long | number | string): Long; 25 | modulo(divisor: Long | number | string): Long; 26 | and(other: Long | number | string): Long; 27 | or(other: Long | number | string): Long; 28 | xor(other: Long | number | string): Long; 29 | shiftLeft(numBits: number): Long; 30 | shiftRight(numBits: number): Long; 31 | shiftRightUnsigned(numBits: number): Long; 32 | toSigned(): Long; 33 | toUnsigned(): Long; 34 | length(): number; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-proxy-certificate-mapper", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc -p .", 8 | "dev": "ts-node-dev src/index.ts", 9 | "start": "node dist/src/index.js", 10 | "test": "mocha --require ts-node/register --project tsconfig.test.json 'test/**/*.ts'" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "aws-sdk": "^2.1686.0", 16 | "axios": "^1.7.5", 17 | "body-parser": "^1.20.2", 18 | "canvas": "^3.1.0", 19 | "cassandra-driver": "^4.7.2", 20 | "del": "^7.1.0", 21 | "dotenv": "^16.3.1", 22 | "express": "^4.18.2", 23 | "html-to-image": "^1.11.11", 24 | "morgan": "^1.10.0", 25 | "node-html-to-image": "^4.0.0", 26 | "observation-mapping": "file:", 27 | "pdf-lib": "^1.17.1", 28 | "pdfkit": "^0.13.0", 29 | "pg": "^8.12.0", 30 | "pg-hstore": "^2.3.4", 31 | "pino": "^8.16.2", 32 | "playwright": "^1.46.1", 33 | "puppeteer": "^23.2.1", 34 | "sequelize": "^6.35.2", 35 | "sharp": "^0.29.3", 36 | "sonarqube-scanner": "^3.3.0", 37 | "svg-to-pdfkit": "^0.1.8", 38 | "svg2pdf.js": "^2.4.0", 39 | "svgo": "^3.3.2", 40 | "ts-node-dev": "^2.0.0", 41 | "uuid": "^9.0.1", 42 | "xmldom": "^0.6.0" 43 | }, 44 | "devDependencies": { 45 | "@types/body-parser": "^1.19.5", 46 | "@types/express": "^4.17.21", 47 | "@types/long": "^5.0.0", 48 | "@types/node": "^20.16.2", 49 | "@types/pg": "^8.11.8", 50 | "@types/puppeteer": "^7.0.4", 51 | "@types/sharp": "^0.32.0", 52 | "@types/uuid": "^10.0.0", 53 | "ts-node": "^10.9.2", 54 | "typescript": "^5.5.4" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Jenkinsfile-sun: -------------------------------------------------------------------------------- 1 | node('build-slave') { 2 | try { 3 | String ANSI_GREEN = "\u001B[32m" 4 | String ANSI_NORMAL = "\u001B[0m" 5 | String ANSI_BOLD = "\u001B[1m" 6 | String ANSI_RED = "\u001B[31m" 7 | String ANSI_YELLOW = "\u001B[33m" 8 | 9 | ansiColor('xterm') { 10 | stage('Checkout') { 11 | if (!env.hub_org) { 12 | println(ANSI_BOLD + ANSI_RED + "Uh Oh! Please set a Jenkins environment variable named hub_org with value as registery/sunbidrded" + ANSI_NORMAL) 13 | error 'Please resolve the errors and rerun..' 14 | } else 15 | println(ANSI_BOLD + ANSI_GREEN + "Found environment variable named hub_org with value as: " + hub_org + ANSI_NORMAL) 16 | } 17 | 18 | cleanWs() 19 | checkout scm 20 | commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() 21 | build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() 22 | echo "build_tag: " + build_tag 23 | 24 | /* stage('SonarQube analysis') { 25 | 26 | sh 'cd $docker_file_path && npm install typescript' 27 | // requires SonarQube Scanner 2.8+ 28 | def scannerHome = tool 'sonar_scanner'; 29 | withSonarQubeEnv('sonarqube') { 30 | sh ''' 31 | cd $docker_file_path && pwd && /var/lib/jenkins/tools/hudson.plugins.sonar.SonarRunnerInstallation/sonar_scanner/bin/sonar-scanner 32 | ''' 33 | } 34 | }*/ 35 | 36 | 37 | 38 | 39 | stage('Build') { 40 | env.NODE_ENV = "build" 41 | print "Environment will be : ${env.NODE_ENV}" 42 | sh('chmod 777 build.sh') 43 | sh("bash -x build.sh ${build_tag} ${env.NODE_NAME} ${hub_org}") 44 | } 45 | 46 | 47 | stage('ArchiveArtifacts') { 48 | sh ("echo ${build_tag} > build_tag.txt") 49 | archiveArtifacts "metadata.json" 50 | archiveArtifacts "build_tag.txt" 51 | currentBuild.description = "${build_tag}" 52 | } 53 | 54 | } 55 | } 56 | catch (err) { 57 | currentBuild.result = "FAILURE" 58 | throw err 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/controllers/certificateController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express" 2 | import { logger } from "../utils/logger"; 3 | import { client } from "../utils/postgresConnection"; 4 | import axios from 'axios'; 5 | import sharp from 'sharp'; 6 | import AWS from 'aws-sdk'; 7 | import { v4 as uuidv4 } from 'uuid'; 8 | import { Buffer } from 'buffer'; 9 | const PDFDocument = require("pdfkit"); 10 | const SVGtoPDF = require("svg-to-pdfkit"); 11 | import stream from 'stream'; 12 | 13 | 14 | 15 | 16 | 17 | const s3 = new AWS.S3({ 18 | accessKeyId: process.env.ACCESS_KEY_ID, 19 | secretAccessKey: process.env.SECRET_ACCESS_KEY, 20 | region: process.env.AWS_REGION, 21 | }); 22 | const bucketName = process.env.AWS_BUCKET_NAME || "sunbird-rc-proxy-certificates"; 23 | const uploadToS3 = async (fileName: string, fileBuffer: any, bucketName: string,contentType: string) => { 24 | try { 25 | const params = { 26 | Bucket: bucketName, 27 | Key: fileName, 28 | Body: fileBuffer, 29 | ContentType: contentType, 30 | }; 31 | return s3.upload(params).promise(); 32 | } catch (error) { 33 | logger.error(error) 34 | } 35 | 36 | }; 37 | 38 | export const getUserCertificateDetails = async (req: Request, res: Response) => { 39 | try { 40 | const { userId } = req.query 41 | const selectQuery = 'SELECT * FROM rc_proxy_user_mapping WHERE userId = $1'; 42 | const selectResult = await client.query(selectQuery, [userId]); 43 | const formattedResult = selectResult.rows.map((row) => { 44 | return { 45 | "userId": row.userid, 46 | "rcUserCertificateId": row.rcusercertificateid, 47 | "rcCertificateTemplateId": row.rccertificatetemplateid, 48 | "userName": row.username, 49 | "meta": row.meta, 50 | "createdAt": row.createdat, 51 | "updatedAt": row.updatedat, 52 | "certificateDownloadUrl": row.certificatedownloadurl, 53 | "certificateName": row.certificatename, 54 | "thumbnail": row.thumbnail 55 | } 56 | }) 57 | res.status(200).json({ 58 | message: "SUCCESS", 59 | data: formattedResult 60 | }) 61 | } catch (error) { 62 | logger.error(error) 63 | res.status(404).json({ 64 | "message": "User not found", 65 | "reason": "Something went wrong while retrieving user details" 66 | }) 67 | } 68 | }; 69 | const generateKeycloakAdminToken = async () => { 70 | try { 71 | const response = await axios.post( 72 | `${process.env.SUNBIRD_RC_KEYCLOAK_HOST}/auth/realms/sunbird-rc/protocol/openid-connect/token`, 73 | new URLSearchParams({ 74 | client_id: 'admin-api', 75 | grant_type: 'client_credentials', 76 | username: `${process.env.SUNBIRD_RC_KEYCLOAK_USERNAME}`, 77 | client_secret: `${process.env.SUNBIRD_RC_CLIENT_SECRET}`, 78 | password: `${process.env.SUNBIRD_RC_KEYCLOAK_PASSWORD}` 79 | }), 80 | { 81 | headers: { 82 | 'Content-Type': 'application/x-www-form-urlencoded' 83 | } 84 | } 85 | ); 86 | return response.data.access_token 87 | } catch (error) { 88 | logger.info(error) 89 | return false 90 | } 91 | }; 92 | const generateCertificateFromRC = async (templateId: String, rcCertificateGenerationBody: any, userToken: String) => { 93 | try { 94 | const generateCertificateResponseFromRc = await axios.post( 95 | `${process.env.SUNBIRD_RC_CORE_HOST}/api/v1/${templateId}`, 96 | rcCertificateGenerationBody, 97 | { 98 | headers: { 99 | 'Accept': 'application/json', 100 | 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8', 101 | 'Connection': 'keep-alive', 102 | 'Content-Type': 'application/json', 103 | 'Authorization': `Bearer ${userToken}` 104 | }, 105 | } 106 | ); 107 | return generateCertificateResponseFromRc.data.result[`${templateId}`].osid 108 | } catch (error) { 109 | logger.info(error) 110 | return false 111 | } 112 | 113 | } 114 | const getCertificateDetailsFromRC = async (certificateOsid: String, userToken: String, templateId: String) => { 115 | try { 116 | const certificateDataFromRc = await axios.get( 117 | `${process.env.SUNBIRD_RC_CORE_HOST}/api/v1/${templateId}/${certificateOsid}`, 118 | { 119 | headers: { 120 | 'Accept': 'image/svg+xml', 121 | 'template-key': 'html', 122 | 'Authorization': `Bearer ${userToken}`, 123 | }, 124 | } 125 | ); 126 | return certificateDataFromRc.data 127 | } catch (error) { 128 | logger.info(error) 129 | return false 130 | } 131 | } 132 | const uploadCertificateToS3ForMyCertificates = async (certificateDetails: any, templateId: String, userId: String, certificateCreationTime: Number, eventId: String, rcCertificateGenerationBody: any) => { 133 | try { 134 | if (typeof certificateDetails === 'string') { 135 | certificateDetails = certificateDetails.replace(/ /g, ' '); 136 | const svgStartIndex = certificateDetails.indexOf('') + 6; 138 | if (svgStartIndex !== -1 && svgEndIndex !== -1) { 139 | certificateDetails = certificateDetails.substring(svgStartIndex, svgEndIndex); 140 | } else { 141 | return false 142 | } 143 | } else { 144 | return false 145 | } 146 | const certificateBuffer = await sharp(Buffer.from(certificateDetails)) 147 | .png({ compressionLevel: 0 }) 148 | .toBuffer(); 149 | const thumbnailBuffer = await sharp(Buffer.from(certificateDetails)) 150 | .png({ quality: 100 }) 151 | .resize({ width: 200, height: 200 }) 152 | .toBuffer(); 153 | await uploadToS3(`${templateId}/${userId}/${certificateCreationTime}-certificate.png`, certificateBuffer, bucketName,"image/png"); 154 | await uploadToS3(`${templateId}/${userId}/${certificateCreationTime}-thumbnail.png`, thumbnailBuffer, bucketName,"image/png"); 155 | return true 156 | } catch (error) { 157 | logger.info(error) 158 | return false 159 | } 160 | 161 | } 162 | 163 | const uploadCertificateToS3ForMdo = async (certificateDetails: any, templateId: String, userId: String, certificateCreationTime: Number, eventId: String, rcCertificateGenerationBody: any) => { 164 | try { 165 | const cleanedSvgData=certificateDetails.replace(/<\/?head[^>]*>/g, '').replace(/<\/?style[^>]*>/g, '').replace(/<\/?body[^>]*>/g, '') 166 | const pdfDoc = new PDFDocument({ size: "A4", layout: "landscape" }); 167 | const passThroughStream = new stream.PassThrough(); 168 | pdfDoc.pipe(passThroughStream); 169 | SVGtoPDF(pdfDoc, cleanedSvgData, 0, 0); 170 | pdfDoc.end(); 171 | SVGtoPDF(pdfDoc, cleanedSvgData, 0, 0); // Pass your SVG data here 172 | await uploadToS3(`mdo-rc-certificates/${eventId}/${rcCertificateGenerationBody.name}-${rcCertificateGenerationBody.date}-certificate.pdf`, passThroughStream, bucketName,"applicatioin/pdf"); 173 | return true 174 | } catch (error) { 175 | logger.info(error) 176 | return false 177 | } 178 | 179 | } 180 | 181 | 182 | const updateUserCertificateDetails = async (userId: String, templateId: String, userName: String, certificateOsid: String, certificateCreationTime: Number, certificateName: String) => { 183 | try { 184 | const uuid: string = uuidv4(); 185 | const certificateUrl = `https://${bucketName}.s3.ap-south-1.amazonaws.com/${templateId}/${userId}/${certificateCreationTime}-certificate.png` 186 | const thumbnailUrl = `https://${bucketName}.s3.ap-south-1.amazonaws.com/${templateId}/${userId}/${certificateCreationTime}-thumbnail.png` 187 | const insertQuery = ` 188 | INSERT INTO rc_proxy_user_mapping ( 189 | uuid_id, 190 | userid, 191 | rcusercertificateid, 192 | rccertificatetemplateid, 193 | username, 194 | meta, 195 | certificatedownloadurl, 196 | certificatename, 197 | thumbnail 198 | ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`; 199 | const insertValues = [ 200 | uuid, userId, certificateOsid, templateId, userName, {}, certificateUrl, certificateName, thumbnailUrl 201 | ] 202 | await client.query(insertQuery, insertValues); 203 | return { 204 | certificateUrl, 205 | thumbnailUrl 206 | } 207 | } catch (error) { 208 | logger.info(error) 209 | return false 210 | } 211 | } 212 | export const generateUserCertificatesFromRc = async (req: Request, res: Response) => { 213 | try { 214 | const { rcCertificateGenerationBody, templateId, userId, userName, certificateName, eventId } = req.body 215 | const certificateCreationTime = Date.now() 216 | const keycloakAdminToken = await generateKeycloakAdminToken() 217 | if (!keycloakAdminToken) { 218 | return res.status(404).json({ 219 | "message": "Failed", 220 | "reason": "Something went wrong while retrieving admin token" 221 | }) 222 | } 223 | const certificateOsid = await generateCertificateFromRC(templateId, rcCertificateGenerationBody, keycloakAdminToken) 224 | if (!certificateOsid) { 225 | return res.status(404).json({ 226 | "message": "Failed", 227 | "reason": "Something went wrong while generating user certificates" 228 | }) 229 | } 230 | let certificateDetailsFromRc = await getCertificateDetailsFromRC(certificateOsid, keycloakAdminToken, templateId) 231 | if (!certificateDetailsFromRc) { 232 | return res.status(404).json({ 233 | "message": "Failed", 234 | "reason": "Something went wrong while retrieving user certificates from RC" 235 | }) 236 | } 237 | const uploadCertificateStatusforMyCertificates = await uploadCertificateToS3ForMyCertificates(certificateDetailsFromRc, templateId, userId, certificateCreationTime, eventId, rcCertificateGenerationBody) 238 | if (!uploadCertificateStatusforMyCertificates) { 239 | return res.status(404).json({ 240 | "message": "Failed", 241 | "reason": "Something went wrong while uploading user certificates to S3" 242 | }) 243 | } 244 | const uploadCertificateStatusForMdo = await uploadCertificateToS3ForMdo(certificateDetailsFromRc, templateId, userId, certificateCreationTime, eventId, rcCertificateGenerationBody) 245 | if (!uploadCertificateStatusForMdo) { 246 | return res.status(404).json({ 247 | "message": "Failed", 248 | "reason": "Something went wrong while uploading user certificates to S3 for MDO poratl" 249 | }) 250 | } 251 | const updateUserCertificateDetailStatus = await updateUserCertificateDetails(userId, templateId, userName, certificateOsid, certificateCreationTime, certificateName) 252 | if (!updateUserCertificateDetailStatus) { 253 | return res.status(404).json({ 254 | "message": "Failed", 255 | "reason": "Something went wrong while updating user certificates details" 256 | }) 257 | } 258 | res.status(200).json({ 259 | "message": "Certificate generated successfully", 260 | certificateUrl: updateUserCertificateDetailStatus.certificateUrl, 261 | thumbnailUrl: updateUserCertificateDetailStatus.thumbnailUrl 262 | }) 263 | } catch (error) { 264 | logger.info(error) 265 | return res.status(404).json({ 266 | "message": "Failed", 267 | "reason": "Something went wrong while generating user certificates" 268 | }) 269 | } 270 | } 271 | --------------------------------------------------------------------------------