├── .gitignore ├── certificate ├── .prettierignore ├── .dockerignore ├── .prettierrc ├── .gitignore ├── Dockerfile ├── package.json ├── jwtRS256_digitalsaathi.key.pub ├── certificate-template-final.html └── server.js ├── api ├── .prettierignore ├── src │ ├── datasources │ │ ├── index.ts │ │ ├── README.md │ │ └── mongo.datasource.ts │ ├── __tests__ │ │ ├── README.md │ │ └── acceptance │ │ │ ├── ping.controller.acceptance.ts │ │ │ ├── test-helper.ts │ │ │ └── home-page.acceptance.ts │ ├── models │ │ ├── README.md │ │ ├── index.ts │ │ ├── donate-device.model.ts │ │ └── request-device.model.ts │ ├── repositories │ │ ├── README.md │ │ ├── index.ts │ │ ├── donate-device.repository.ts │ │ └── request-device.repository.ts │ ├── sequence.ts │ ├── controllers │ │ ├── index.ts │ │ ├── graphQL-query.ts │ │ ├── README.md │ │ ├── ping.controller.ts │ │ ├── request-device-graphQL-model.ts │ │ ├── graphQL-helper.ts │ │ ├── donate-device-graphQL-model.ts │ │ ├── donate-device.controller.ts │ │ └── request-device.controller.ts │ ├── utils │ │ ├── adminLogger.ts │ │ └── sendSMS.ts │ ├── migrate.ts │ ├── openapi-spec.ts │ ├── index.ts │ └── application.ts ├── .eslintignore ├── .eslintrc.js ├── .mocharc.json ├── .yo-rc.json ├── .dockerignore ├── .prettierrc ├── tsconfig.json ├── Dockerfile ├── .vscode │ ├── tasks.json │ ├── settings.json │ └── launch.json ├── .gitignore ├── DEVELOPING.md ├── README.md ├── public │ └── index.html └── package.json ├── portal ├── components │ ├── react-admin │ │ ├── base │ │ │ ├── resources │ │ │ │ ├── vacancyData.js │ │ │ │ ├── candidateProfile │ │ │ │ │ ├── index.js │ │ │ │ │ ├── candidate-show.js │ │ │ │ │ └── candidate-profile.js │ │ │ │ ├── request-device.js │ │ │ │ └── recruiterData │ │ │ │ │ └── Recruiter.js │ │ │ └── components │ │ │ │ ├── EditNoDeleteToolbar.js │ │ │ │ └── BackButton.js │ │ ├── customHasura │ │ │ ├── getFinalType.js │ │ │ ├── fetchActions.js │ │ │ └── customFields.js │ │ ├── layout │ │ │ ├── logoutButton.js │ │ │ ├── customBreadcrumbs.js │ │ │ ├── sidebarHeader.js │ │ │ ├── config.js │ │ │ ├── verticalItem.js │ │ │ ├── customAppBar.js │ │ │ ├── customUserMenu.js │ │ │ ├── index.js │ │ │ ├── customSidebar.js │ │ │ └── verticalCollapse.js │ │ ├── theme.js │ │ └── app.js │ ├── track │ │ ├── track.config.js │ │ └── track.js │ ├── login │ │ ├── form.config.js │ │ └── login.js │ ├── layout.js │ └── config.js ├── .eslintrc ├── next.config.js ├── public │ ├── cm.png │ ├── em.png │ ├── em2.png │ ├── logo.png │ ├── DPlogo.png │ ├── DPlogo1.png │ ├── default.png │ ├── device.png │ ├── favicon.ico │ ├── SSA_logo.png │ ├── cm-circle.png │ ├── em-circle.png │ ├── Bahnschrift.otf │ ├── bahnschrift.ttf │ ├── campaign_logo.png │ ├── em-final-square.png │ ├── govt_of_hp_logo.png │ └── vercel.svg ├── styles │ ├── Request.module.css │ ├── Home.module.css │ ├── Login.module.css │ ├── Track.module.css │ ├── globals.css │ └── layout.module.css ├── utils │ ├── buildGupshup.js │ ├── deepEqual.js │ ├── sendLog.js │ └── sendSMS.js ├── jsconfig.json ├── pages │ ├── track.js │ ├── _app.js │ ├── api │ │ ├── certificate.js │ │ ├── graphql.js │ │ ├── captcha.js │ │ ├── log.js │ │ ├── sms.js │ │ ├── auth │ │ │ └── [...nextauth].js │ │ └── track.js │ ├── admin.js │ ├── index.js │ ├── school.js │ ├── _document.js │ └── login.js ├── .gitignore ├── package.json ├── .env.development ├── Dockerfile └── README.md ├── captcha-service ├── .gitignore ├── Dockerfile ├── README.md ├── package.json ├── index.sample.html ├── app.js └── captchapng.js ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── deploy.yml ├── README.md ├── LICENSE └── docker-compose.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /certificate/.prettierignore: -------------------------------------------------------------------------------- 1 | *.html -------------------------------------------------------------------------------- /certificate/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /api/.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.json 3 | -------------------------------------------------------------------------------- /portal/components/react-admin/base/resources/vacancyData.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/src/datasources/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mongo.datasource'; 2 | -------------------------------------------------------------------------------- /api/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | .eslintrc.js 5 | -------------------------------------------------------------------------------- /certificate/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } -------------------------------------------------------------------------------- /portal/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /portal/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | }; 4 | -------------------------------------------------------------------------------- /api/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@loopback/eslint-config', 3 | }; 4 | -------------------------------------------------------------------------------- /api/src/__tests__/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | Please place your tests in this folder. 4 | -------------------------------------------------------------------------------- /captcha-service/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | nodemon.json 3 | db.json 4 | node_modules -------------------------------------------------------------------------------- /certificate/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | nodemon.json 3 | jwtRS256_digitalsaathi.key 4 | assets/* -------------------------------------------------------------------------------- /portal/public/cm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/cm.png -------------------------------------------------------------------------------- /portal/public/em.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/em.png -------------------------------------------------------------------------------- /api/src/models/README.md: -------------------------------------------------------------------------------- 1 | # Models 2 | 3 | This directory contains code for models provided by this app. 4 | -------------------------------------------------------------------------------- /portal/public/em2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/em2.png -------------------------------------------------------------------------------- /portal/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/logo.png -------------------------------------------------------------------------------- /api/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './donate-device.model'; 2 | export * from './request-device.model'; 3 | -------------------------------------------------------------------------------- /portal/public/DPlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/DPlogo.png -------------------------------------------------------------------------------- /portal/public/DPlogo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/DPlogo1.png -------------------------------------------------------------------------------- /portal/public/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/default.png -------------------------------------------------------------------------------- /portal/public/device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/device.png -------------------------------------------------------------------------------- /portal/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/favicon.ico -------------------------------------------------------------------------------- /portal/public/SSA_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/SSA_logo.png -------------------------------------------------------------------------------- /portal/public/cm-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/cm-circle.png -------------------------------------------------------------------------------- /portal/public/em-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/em-circle.png -------------------------------------------------------------------------------- /api/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "exit": true, 3 | "recursive": true, 4 | "require": "source-map-support/register" 5 | } 6 | -------------------------------------------------------------------------------- /api/.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "@loopback/cli": { 3 | "packageManager": "yarn", 4 | "version": "2.21.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /api/src/datasources/README.md: -------------------------------------------------------------------------------- 1 | # Datasources 2 | 3 | This directory contains config for datasources used by this app. 4 | -------------------------------------------------------------------------------- /portal/public/Bahnschrift.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/Bahnschrift.otf -------------------------------------------------------------------------------- /portal/public/bahnschrift.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/bahnschrift.ttf -------------------------------------------------------------------------------- /api/src/repositories/README.md: -------------------------------------------------------------------------------- 1 | # Repositories 2 | 3 | This directory contains code for repositories provided by this app. 4 | -------------------------------------------------------------------------------- /api/src/repositories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './donate-device.repository'; 2 | export * from './request-device.repository'; 3 | -------------------------------------------------------------------------------- /portal/public/campaign_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/campaign_logo.png -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | /dist 4 | # Cache used by TypeScript's incremental build 5 | *.tsbuildinfo 6 | -------------------------------------------------------------------------------- /portal/public/em-final-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/em-final-square.png -------------------------------------------------------------------------------- /portal/public/govt_of_hp_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samagra-Development/x-admin/HEAD/portal/public/govt_of_hp_logo.png -------------------------------------------------------------------------------- /api/src/sequence.ts: -------------------------------------------------------------------------------- 1 | import {MiddlewareSequence} from '@loopback/rest'; 2 | 3 | export class MySequence extends MiddlewareSequence {} 4 | -------------------------------------------------------------------------------- /portal/styles/Request.module.css: -------------------------------------------------------------------------------- 1 | .grid_one { 2 | display: grid; 3 | grid-template-columns: 1fr; 4 | margin-top: 3rem; 5 | margin-bottom: 3rem; 6 | } 7 | -------------------------------------------------------------------------------- /api/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "trailingComma": "all", 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /api/src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './donate-device.controller'; 2 | export * from './ping.controller'; 3 | 4 | export * from './request-device.controller'; 5 | -------------------------------------------------------------------------------- /api/src/controllers/graphQL-query.ts: -------------------------------------------------------------------------------- 1 | export interface graphQLQuery { 2 | operationsDoc: string; 3 | operationName: string; 4 | variableName: string; 5 | databaseOperationName: string; 6 | } 7 | -------------------------------------------------------------------------------- /portal/utils/buildGupshup.js: -------------------------------------------------------------------------------- 1 | const buildGupshup = (template, variables) => { 2 | let index = 0; 3 | return template.replace(/{#var#}/gi, () => { 4 | return String(variables[index++]); 5 | }); 6 | }; 7 | 8 | export default buildGupshup; 9 | -------------------------------------------------------------------------------- /portal/components/track/track.config.js: -------------------------------------------------------------------------------- 1 | const controls = [ 2 | { 3 | name: "trackingKey", 4 | type: "text", 5 | placeholder: "Tracking ID", 6 | pattern: "[A-z0-9]{8}", 7 | required: true, 8 | }, 9 | ]; 10 | 11 | export default controls; 12 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "extends": "@loopback/build/config/tsconfig.common.json", 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "typeRoots": ["./node_modules/@types"] 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /portal/jsconfig.json: -------------------------------------------------------------------------------- 1 | // tsconfig.json or jsconfig.json 2 | { 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/components/*": ["components/*"], 7 | "@/utils/*": ["utils/*"], 8 | "@/resources/*": ["components/react-admin/base/resources/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /certificate/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM zenika/alpine-chrome:77-with-node 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json ./ 6 | 7 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true 8 | 9 | USER root 10 | 11 | RUN npm install 12 | 13 | COPY . . 14 | 15 | ENV HOST=0.0.0.0 PORT=3000 16 | 17 | EXPOSE ${PORT} 18 | 19 | CMD ["npm", "run", "start"] 20 | 21 | -------------------------------------------------------------------------------- /portal/components/react-admin/base/components/EditNoDeleteToolbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Toolbar, SaveButton } from "react-admin"; 3 | 4 | const EditNoDeleteToolbar = (props) => ( 5 | 6 | 7 | 8 | ); 9 | 10 | export default EditNoDeleteToolbar; 11 | -------------------------------------------------------------------------------- /portal/components/react-admin/base/resources/candidateProfile/index.js: -------------------------------------------------------------------------------- 1 | import { concatAST } from "graphql"; 2 | import { CandidateList } from "./candidate-profile"; 3 | import { CandidateShow } from "./candidate-show"; 4 | 5 | export { CandidateList, CandidateShow }; 6 | // export const CandidateS_how = CandidateShow; 7 | // export const Candidate_List = CandidateList; 8 | -------------------------------------------------------------------------------- /portal/pages/track.js: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { useState, useEffect } from "react"; 3 | import Layout from "../components/layout"; 4 | import Track from "../components/track/track"; 5 | 6 | const TrackWrapper = (props) => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default TrackWrapper; 15 | -------------------------------------------------------------------------------- /api/src/controllers/README.md: -------------------------------------------------------------------------------- 1 | # Controllers 2 | 3 | This directory contains source files for the controllers exported by this app. 4 | 5 | To add a new empty controller, type in `lb4 controller []` from the 6 | command-line of your application's root directory. 7 | 8 | For more information, please visit 9 | [Controller generator](http://loopback.io/doc/en/lb4/Controller-generator.html). 10 | -------------------------------------------------------------------------------- /portal/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | display: grid; 3 | grid-template-columns: 1fr; 4 | margin-top: 0; 5 | } 6 | 7 | .primary { 8 | color: #303765; 9 | } 10 | 11 | .secondary { 12 | color: #055e4e; 13 | } 14 | 15 | @media screen and (min-width: 768px) { 16 | .grid { 17 | width: 70vw; 18 | margin: 0 auto; 19 | padding: 2rem; 20 | grid-template-columns: 1fr 1fr; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /portal/components/login/form.config.js: -------------------------------------------------------------------------------- 1 | const controls = [ 2 | { 3 | name: "username", 4 | type: "text", 5 | placeholder: "Username", 6 | autocomplete: "username", 7 | required: true, 8 | }, 9 | { 10 | name: "password", 11 | type: "password", 12 | placeholder: "Password", 13 | autocomplete: "current-password", 14 | required: true, 15 | }, 16 | ]; 17 | 18 | export default controls; 19 | -------------------------------------------------------------------------------- /portal/components/react-admin/customHasura/getFinalType.js: -------------------------------------------------------------------------------- 1 | import { TypeKind } from "graphql"; 2 | 3 | /** 4 | * Ensure we get the real type even if the root type is NON_NULL or LIST 5 | * @param {GraphQLType} type 6 | */ 7 | const getFinalType = (type) => { 8 | if (type.kind === TypeKind.NON_NULL || type.kind === TypeKind.LIST) { 9 | return getFinalType(type.ofType); 10 | } 11 | 12 | return type; 13 | }; 14 | 15 | export default getFinalType; 16 | -------------------------------------------------------------------------------- /captcha-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 8 | # where available (npm@5+) 9 | COPY package*.json ./ 10 | 11 | RUN npm install 12 | # If you are building your code for production 13 | # RUN npm ci --only=production 14 | 15 | # Bundle app source 16 | COPY . . 17 | 18 | EXPOSE 9000 19 | CMD [ "node", "app.js" ] -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please describe the changes in your pull request in few words here. 4 | 5 | ### Changes 6 | 7 | List the changes done to fix a bug or introducing a new feature. 8 | 9 | ## How to test 10 | 11 | Describe the steps required to test the changes proposed in the pull request. 12 | 13 | Please consider using the closing keyword if the pull request is proposed to 14 | fix an issue already created in the repository 15 | (https://help.github.com/articles/closing-issues-using-keywords/) 16 | -------------------------------------------------------------------------------- /portal/pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import { Provider } from "next-auth/client"; 3 | import { ToastProvider } from "react-toast-notifications"; 4 | 5 | function MyApp({ Component, pageProps }) { 6 | return ( 7 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default MyApp; 20 | -------------------------------------------------------------------------------- /certificate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "certificate", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "dependencies": { 7 | "fastify": "^3.18.0", 8 | "jsonwebtoken": "^8.5.1", 9 | "puppeteer": "^10.0.0", 10 | "puppeteer-core": "^10.0.0", 11 | "qrcode": "1.4.4" 12 | }, 13 | "devDependencies": { 14 | "nodemon": "^2.0.7" 15 | }, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1", 18 | "start": "nodemon server.js" 19 | }, 20 | "author": "", 21 | "license": "ISC" 22 | } -------------------------------------------------------------------------------- /api/src/repositories/donate-device.repository.ts: -------------------------------------------------------------------------------- 1 | import {inject} from '@loopback/core'; 2 | import {DefaultCrudRepository} from '@loopback/repository'; 3 | import {MongoDataSource} from '../datasources'; 4 | import {DonateDevice, DonateDeviceRelations} from '../models'; 5 | 6 | export class DonateDeviceRepository extends DefaultCrudRepository< 7 | DonateDevice, 8 | typeof DonateDevice.prototype.id, 9 | DonateDeviceRelations 10 | > { 11 | constructor(@inject('datasources.mongo') dataSource: MongoDataSource) { 12 | super(DonateDevice, dataSource); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /portal/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /api/src/repositories/request-device.repository.ts: -------------------------------------------------------------------------------- 1 | import {inject} from '@loopback/core'; 2 | import {DefaultCrudRepository} from '@loopback/repository'; 3 | import {MongoDataSource} from '../datasources'; 4 | import {RequestDevice, RequestDeviceRelations} from '../models'; 5 | 6 | export class RequestDeviceRepository extends DefaultCrudRepository< 7 | RequestDevice, 8 | typeof RequestDevice.prototype.id, 9 | RequestDeviceRelations 10 | > { 11 | constructor(@inject('datasources.mongo') dataSource: MongoDataSource) { 12 | super(RequestDevice, dataSource); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /portal/utils/deepEqual.js: -------------------------------------------------------------------------------- 1 | export const deepEqual = function (x, y) { 2 | if (x === y) { 3 | return true; 4 | } else if ( 5 | typeof x == "object" && 6 | x != null && 7 | typeof y == "object" && 8 | y != null 9 | ) { 10 | if (Object.keys(x).length != Object.keys(y).length) return false; 11 | 12 | for (var prop in x) { 13 | if (y.hasOwnProperty(prop)) { 14 | if (!deepEqual(x[prop], y[prop])) return false; 15 | } else return false; 16 | } 17 | 18 | return true; 19 | } else return false; 20 | }; 21 | 22 | export default deepEqual; 23 | -------------------------------------------------------------------------------- /portal/pages/api/certificate.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getSession, session } from "next-auth/client"; 3 | 4 | const handler = async (req, res) => { 5 | if (req.method === "POST") { 6 | const response = await axios({ 7 | method: "POST", 8 | url: `${process.env.CERTIFICATE_URL}/`, 9 | headers: req.headers, 10 | data: req.body, 11 | }); 12 | const responseObject = response.data; 13 | if (responseObject) { 14 | res.status(200).json({ errors: null, success: responseObject }); 15 | } 16 | } 17 | }; 18 | 19 | export default handler; 20 | -------------------------------------------------------------------------------- /portal/utils/sendLog.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const sendLog = async (msg) => { 4 | try { 5 | const response = await axios({ 6 | method: "POST", 7 | url: `${process.env.NEXT_PUBLIC_API_URL}/log`, 8 | data: { 9 | msg: msg, 10 | }, 11 | }); 12 | if (response.data?.error) { 13 | return { error: response.data?.error, success: null }; 14 | } else if (response.data?.success) { 15 | return { success: response.data?.success, error: null }; 16 | } 17 | } catch (err) { 18 | return err; 19 | } 20 | }; 21 | 22 | export default sendLog; 23 | -------------------------------------------------------------------------------- /api/src/utils/adminLogger.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const sendLog = async (msg: string, channelId: string | undefined) => { 4 | const response = await axios({ 5 | method: 'POST', 6 | url: 'https://slack.com/api/chat.postMessage', 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | Authorization: `Bearer ${process.env.SLACK_ADMIN_LOGGER_AUTH_TOKEN}`, 10 | }, 11 | data: { 12 | channel: channelId, 13 | text: msg, 14 | }, 15 | }); 16 | 17 | const responseObject = response.data; 18 | return responseObject; 19 | }; 20 | 21 | export default sendLog; 22 | -------------------------------------------------------------------------------- /captcha-service/README.md: -------------------------------------------------------------------------------- 1 | ## Captcha Service 2 | A numeric captcha generator for Node.js. Has a built in fast server that will allow you to serve and validate captchas realtime. The use case that we built this for was to generate offline captchas (an internal requirement) where we can never use google captchas. 3 | 4 | ## Features (From the original GeorgeChan Library) 5 | * Only generate numeric captcha PNG image 6 | * Build-in fonts 7 | * Characters up and down, left and right limits, random displacement 8 | * Full JavaScript 9 | 10 | ## TODO List 11 | - [ ] Async management of database. 12 | - [ ] Server features to be added to docker. 13 | -------------------------------------------------------------------------------- /api/src/__tests__/acceptance/ping.controller.acceptance.ts: -------------------------------------------------------------------------------- 1 | import {Client, expect} from '@loopback/testlab'; 2 | import {ApiApplication} from '../..'; 3 | import {setupApplication} from './test-helper'; 4 | 5 | describe('PingController', () => { 6 | let app: ApiApplication; 7 | let client: Client; 8 | 9 | before('setupApplication', async () => { 10 | ({app, client} = await setupApplication()); 11 | }); 12 | 13 | after(async () => { 14 | await app.stop(); 15 | }); 16 | 17 | it('invokes GET /ping', async () => { 18 | const res = await client.get('/ping?msg=world').expect(200); 19 | expect(res.body).to.containEql({greeting: 'Hello from LoopBack'}); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request a new feature in x-admin 4 | labels: enhancement 5 | --- 6 | 7 | 9 | 10 | ### Description 11 | 12 | Please describe your situation in few words here. 13 | 14 | #### Steps followed and expected result 15 | 16 | Describe the steps followed by you and your expected results after following the steps. 17 | 18 | #### Screenshots 19 | 20 | If applicable, add screenshots to help explain your problem. 21 | -------------------------------------------------------------------------------- /portal/pages/admin.js: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import { useSession } from "next-auth/client"; 3 | import Login from "./login"; 4 | 5 | const ReactAdmin = dynamic(() => import("../components/react-admin/app"), { 6 | ssr: false, 7 | }); 8 | 9 | const Admin = () => { 10 | const [session, loading] = useSession(); 11 | 12 | if (loading) return null; 13 | 14 | if (!loading && !session) { 15 | // signIn("fusionauth") 16 | return ; 17 | } 18 | if (session) { 19 | // if(loading) return null; 20 | // if(!loading && !session) { 21 | // signIn("fusionauth"); 22 | // return null; 23 | // } 24 | return ; 25 | } 26 | }; 27 | 28 | export default Admin; 29 | -------------------------------------------------------------------------------- /api/src/migrate.ts: -------------------------------------------------------------------------------- 1 | import {ApiApplication} from './application'; 2 | 3 | export async function migrate(args: string[]) { 4 | const existingSchema = args.includes('--rebuild') ? 'drop' : 'alter'; 5 | console.log('Migrating schemas (%s existing schema)', existingSchema); 6 | 7 | const app = new ApiApplication(); 8 | await app.boot(); 9 | await app.migrateSchema({existingSchema}); 10 | 11 | // Connectors usually keep a pool of opened connections, 12 | // this keeps the process running even after all work is done. 13 | // We need to exit explicitly. 14 | process.exit(0); 15 | } 16 | 17 | migrate(process.argv).catch(err => { 18 | console.error('Cannot migrate database schema', err); 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /api/src/openapi-spec.ts: -------------------------------------------------------------------------------- 1 | import {ApplicationConfig} from '@loopback/core'; 2 | import {ApiApplication} from './application'; 3 | 4 | /** 5 | * Export the OpenAPI spec from the application 6 | */ 7 | async function exportOpenApiSpec(): Promise { 8 | const config: ApplicationConfig = { 9 | rest: { 10 | port: +(process.env.PORT ?? 3000), 11 | host: process.env.HOST ?? 'localhost', 12 | }, 13 | }; 14 | const outFile = process.argv[2] ?? ''; 15 | const app = new ApiApplication(config); 16 | await app.boot(); 17 | await app.exportOpenApiSpec(outFile); 18 | } 19 | 20 | exportOpenApiSpec().catch(err => { 21 | console.error('Fail to export OpenAPI spec from the application.', err); 22 | process.exit(1); 23 | }); 24 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | # Check out https://hub.docker.com/_/node to select a new base image 2 | FROM node:10-slim 3 | 4 | # Set to a non-root built-in user `node` 5 | USER node 6 | 7 | # Create app directory (with user `node`) 8 | RUN mkdir -p /home/node/app 9 | 10 | WORKDIR /home/node/app 11 | 12 | # Install app dependencies 13 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 14 | # where available (npm@5+) 15 | COPY --chown=node package*.json ./ 16 | 17 | RUN npm install 18 | 19 | # Bundle app source code 20 | COPY --chown=node . . 21 | 22 | RUN npm run build 23 | 24 | # Bind to all network interfaces so that it can be mapped to the host OS 25 | ENV HOST=0.0.0.0 PORT=3000 26 | 27 | EXPOSE ${PORT} 28 | CMD [ "node", "." ] 29 | -------------------------------------------------------------------------------- /portal/pages/api/graphql.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getSession, session } from "next-auth/client"; 3 | 4 | const handler = async (req, res) => { 5 | const session = await getSession({ req }); 6 | if (session) { 7 | if (req.method === "POST") { 8 | const response = await axios({ 9 | method: "POST", 10 | url: process.env.HASURA_URL, 11 | headers: req.headers, 12 | data: req.body, 13 | }); 14 | 15 | const responseObject = response.data; 16 | if (responseObject) { 17 | res.status(200).json(responseObject); 18 | } 19 | } 20 | } else { 21 | // console.log("Error in graphQl") 22 | res.status(401).json({ error: "Unauthorised access.", success: null }); 23 | } 24 | }; 25 | 26 | export default handler; 27 | -------------------------------------------------------------------------------- /portal/components/react-admin/base/components/BackButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Button from "@material-ui/core/Button"; 3 | import ArrowBackIosIcon from "@material-ui/icons/ArrowBackIos"; 4 | import { makeStyles } from "@material-ui/core"; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | button: { 8 | color: theme.palette.primary.main, 9 | padding: "0 0 1rem 0", 10 | }, 11 | })); 12 | 13 | const BackButton = ({ history }) => { 14 | const classes = useStyles(); 15 | return ( 16 | 26 | ); 27 | }; 28 | 29 | export default BackButton; 30 | -------------------------------------------------------------------------------- /api/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Watch and Compile Project", 8 | "type": "shell", 9 | "command": "npm", 10 | "args": ["--silent", "run", "build:watch"], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | }, 15 | "problemMatcher": "$tsc-watch" 16 | }, 17 | { 18 | "label": "Build, Test and Lint", 19 | "type": "shell", 20 | "command": "npm", 21 | "args": ["--silent", "run", "test:dev"], 22 | "group": { 23 | "kind": "test", 24 | "isDefault": true 25 | }, 26 | "problemMatcher": ["$tsc", "$eslint-compact", "$eslint-stylish"] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: bug 5 | --- 6 | 7 | 9 | 10 | ### Description 11 | 12 | Please describe your issue in few words here. 13 | 14 | #### How to reproduce 15 | 16 | Describe the bug and list the steps you used when the issue occurred. 17 | 18 | #### Screenshots 19 | 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | ### Versions 23 | 24 | * Last commit id on master: 25 | * Operating System (lsb_release -a): 26 | * Browser [e.g. chrome, safari]: 27 | 28 | ### Logs 29 | 30 | Any logs (if any) generated in 31 | -------------------------------------------------------------------------------- /api/src/__tests__/acceptance/test-helper.ts: -------------------------------------------------------------------------------- 1 | import {ApiApplication} from '../..'; 2 | import { 3 | createRestAppClient, 4 | givenHttpServerConfig, 5 | Client, 6 | } from '@loopback/testlab'; 7 | 8 | export async function setupApplication(): Promise { 9 | const restConfig = givenHttpServerConfig({ 10 | // Customize the server configuration here. 11 | // Empty values (undefined, '') will be ignored by the helper. 12 | // 13 | // host: process.env.HOST, 14 | // port: +process.env.PORT, 15 | }); 16 | 17 | const app = new ApiApplication({ 18 | rest: restConfig, 19 | }); 20 | 21 | await app.boot(); 22 | await app.start(); 23 | 24 | const client = createRestAppClient(app); 25 | 26 | return {app, client}; 27 | } 28 | 29 | export interface AppWithClient { 30 | app: ApiApplication; 31 | client: Client; 32 | } 33 | -------------------------------------------------------------------------------- /certificate/jwtRS256_digitalsaathi.key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2tMoTNVXdU9JwiHDWn4Q 3 | 6mehTcbS24t96NwowLiBrODmSWSI2YB7CuN73lRB8D0MEYxZUFLthgbUeKVc0es9 4 | sSPrJgIqqYJEtIIPqG14uuptCz8efaqqw9YIjk7DJtO99kWrCqakD2GrI59PmPhk 5 | oraRkdk1GQv4+oCBeYbaA6a6gd1YeCYsf2zmlC0qGybFAPNEYlxcoXxcRr2CvuH0 6 | OC4S4gYNVG/JSeuKpCekj1IoQOpXk7wVDQam8LGbrTLBkXZWL1pGzXGJJ7tgGXZD 7 | KKGqHMmaEbzHUEupn5miR6HXg0RqB443fi0Y6H/yCRRKWtLNT3VAVUEnmVklECam 8 | LkRotmTmKbWRFD8JBuS1QT5ALFvWUJzUvUAzkUvq6ZfXCXWGyxS3NsVJcv2bs68z 9 | mUzGsNfTJJBLXB0kbjO8UtdOSAaxD24LJaAa7jCS4hXMN0PMHY0qV+n0mWpy+sMY 10 | 6YAEXCAqXg068sv0LxvyVDN4SwLcsTT3/h8ArytGMWKRWgyz8SQpcmqZa8Ws4b5u 11 | UcG87n1X9aY4FEEqkOCtqzHpDsvVp1ad7hJZAU5rId+OW+YcjJBDWzd6DxLzmzqD 12 | sT40UCqd4wfTD/1lZ0pTciTziM/e/iKsbGjdRcQSGbvqG2agWTNqQKEFtsHzd54k 13 | 89+K7WfZz06QchuL3XFcM30CAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /api/src/__tests__/acceptance/home-page.acceptance.ts: -------------------------------------------------------------------------------- 1 | import {Client} from '@loopback/testlab'; 2 | import {ApiApplication} from '../..'; 3 | import {setupApplication} from './test-helper'; 4 | 5 | describe('HomePage', () => { 6 | let app: ApiApplication; 7 | let client: Client; 8 | 9 | before('setupApplication', async () => { 10 | ({app, client} = await setupApplication()); 11 | }); 12 | 13 | after(async () => { 14 | await app.stop(); 15 | }); 16 | 17 | it('exposes a default home page', async () => { 18 | await client 19 | .get('/') 20 | .expect(200) 21 | .expect('Content-Type', /text\/html/); 22 | }); 23 | 24 | it('exposes self-hosted explorer', async () => { 25 | await client 26 | .get('/explorer/') 27 | .expect(200) 28 | .expect('Content-Type', /text\/html/) 29 | .expect(/LoopBack API Explorer/); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /api/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [80], 3 | "editor.tabCompletion": "on", 4 | "editor.tabSize": 2, 5 | "editor.trimAutoWhitespace": true, 6 | "editor.formatOnSave": true, 7 | "editor.codeActionsOnSave": { 8 | "source.organizeImports": true, 9 | "source.fixAll.eslint": true 10 | }, 11 | 12 | "files.exclude": { 13 | "**/.DS_Store": true, 14 | "**/.git": true, 15 | "**/.hg": true, 16 | "**/.svn": true, 17 | "**/CVS": true, 18 | "dist": true 19 | }, 20 | "files.insertFinalNewline": true, 21 | "files.trimTrailingWhitespace": true, 22 | 23 | "typescript.tsdk": "./node_modules/typescript/lib", 24 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, 25 | "typescript.preferences.quoteStyle": "single", 26 | "eslint.run": "onSave", 27 | "eslint.nodePath": "./node_modules", 28 | "eslint.validate": ["javascript", "typescript"] 29 | } 30 | -------------------------------------------------------------------------------- /portal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portal-2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "format": "prettier --write .", 11 | "format:check": "prettier --check ." 12 | }, 13 | "dependencies": { 14 | "@apollo/client": "^3.3.20", 15 | "@hcaptcha/react-hcaptcha": "^0.3.6", 16 | "axios": "^0.21.1", 17 | "clsx": "^1.1.1", 18 | "graphql": "^15.5.0", 19 | "graphql-ast-types-browser": "^1.0.2", 20 | "next": "11.0.0", 21 | "next-auth": "^3.27.0", 22 | "prettier": "^2.6.2", 23 | "prop-types": "^15.7.2", 24 | "ra-data-hasura": "^0.1.0", 25 | "react": "17.0.2", 26 | "react-admin": "^3.16.2", 27 | "react-dom": "17.0.2", 28 | "react-toast-notifications": "^2.4.4" 29 | }, 30 | "devDependencies": { 31 | "eslint": "7.29.0", 32 | "eslint-config-next": "11.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /api/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "node", 5 | "request": "launch", 6 | "name": "Launch Program", 7 | "skipFiles": ["<node_internals>/**"], 8 | "program": "${workspaceFolder}/dist/index.js" 9 | }, 10 | { 11 | "type": "node", 12 | "request": "launch", 13 | "name": "Run Mocha tests", 14 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 15 | "runtimeArgs": [ 16 | "-r", 17 | "${workspaceRoot}/node_modules/source-map-support/register" 18 | ], 19 | "cwd": "${workspaceRoot}", 20 | "autoAttachChildProcesses": true, 21 | "args": [ 22 | "--config", 23 | "${workspaceRoot}/.mocharc.json", 24 | "${workspaceRoot}/dist/__tests__/**/*.js", 25 | "-t", 26 | "0" 27 | ] 28 | }, 29 | { 30 | "type": "node", 31 | "request": "attach", 32 | "name": "Attach to Process", 33 | "port": 5858 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /portal/utils/sendSMS.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import sendLog from "./sendLog"; 3 | 4 | const sendSMS = async (msg, templateId, contactNumber) => { 5 | try { 6 | const response = await axios({ 7 | method: "POST", 8 | url: `${process.env.NEXT_PUBLIC_API_URL}/sms`, 9 | data: { 10 | contactNumber: contactNumber, 11 | msg: msg, 12 | templateId: templateId, 13 | }, 14 | }); 15 | if (response.data?.error) { 16 | sendLog( 17 | `*⚠️samarth-device*: SMS status update notification sending to *${contactNumber}* failed` 18 | ); 19 | return { error: response.data?.error, success: null }; 20 | } else if (response.data?.success) { 21 | sendLog( 22 | `*✅samarth-device*: SMS status update notification successfully sent to *${contactNumber}*` 23 | ); 24 | return { success: response.data?.success, error: null }; 25 | } 26 | } catch (err) { 27 | return err; 28 | } 29 | }; 30 | 31 | export default sendSMS; 32 | -------------------------------------------------------------------------------- /portal/components/react-admin/layout/logoutButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PowerSettingsNewIcon from "@material-ui/icons/PowerSettingsNew"; 3 | import Button from "@material-ui/core/Button"; 4 | import { makeStyles } from "@material-ui/core"; 5 | import { signOut } from "next-auth/client"; 6 | 7 | const useStyles = makeStyles((theme) => ({ 8 | logOutButton: { 9 | color: theme.palette.grey[500], 10 | padding: "1rem", 11 | fontSize: "0.8rem", 12 | textTransform: "capitalize", 13 | }, 14 | logOutIcon: { 15 | fontSize: "1rem", 16 | marginRight: "0.5rem", 17 | transform: "translateY(-10%)", 18 | }, 19 | })); 20 | 21 | const Logout = () => { 22 | const classes = useStyles(); 23 | return ( 24 | <Button 25 | className={classes.logOutButton} 26 | onClick={() => { 27 | signOut(); 28 | }} 29 | > 30 | <PowerSettingsNewIcon className={classes.logOutIcon} /> 31 | <span>Log out</span> 32 | </Button> 33 | ); 34 | }; 35 | 36 | export default Logout; 37 | -------------------------------------------------------------------------------- /portal/components/react-admin/layout/customBreadcrumbs.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import config from "./config"; 3 | 4 | const useStyles = makeStyles((theme) => ({ 5 | logOutButton: { 6 | color: theme.palette.grey[500], 7 | padding: "1rem", 8 | fontSize: "0.8rem", 9 | textTransform: "capitalize", 10 | }, 11 | logOutIcon: { 12 | fontSize: "1rem", 13 | marginRight: "0.5rem", 14 | transform: "translateY(-10%)", 15 | }, 16 | })); 17 | 18 | const Breadcrumbs = ({ location, source, record }) => { 19 | const { resource = null, id = null } = location ?? {}; 20 | const labels = []; 21 | labels.push(getResourceName(resource)); 22 | if (!id) labels.push("List"); 23 | else labels.push(record[source]); 24 | 25 | return ( 26 | <span> 27 | {labels.map((label, index) => { 28 | return ( 29 | <> 30 | <span>{label}</span> 31 | {index === labels.length ? <span>/</span> : null} 32 | </> 33 | ); 34 | })} 35 | </span> 36 | ); 37 | }; 38 | 39 | export default Logout; 40 | -------------------------------------------------------------------------------- /portal/public/vercel.svg: -------------------------------------------------------------------------------- 1 | <svg width="283" height="64" viewBox="0 0 283 64" fill="none" 2 | xmlns="http://www.w3.org/2000/svg"> 3 | <path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/> 4 | </svg> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## X Admin 2 | 3 | This project is an open-source, low-code framework to create internal admin tools for governance use cases. 4 | 5 | --- 6 | 7 | ### Features 8 | 9 | The following features are present out of the box: 10 | - Authentication and Authorization using Next-Auth and FusionAuth 11 | - Data collection through ODK *(WIP)* 12 | - REST API on collected data through Loopback 13 | - GraphQL API on collected data through Hasura 14 | - Admin console to view (sort/filter), update and delete data through Next.js + React-Admin 15 | - Customize admin theme through tenants *(WIP)* 16 | - Role-based access control on the frontend and API layer via Hasura and React-Admin 17 | - Metabase to visualise collected data inside Postgres *(WIP)* 18 | 19 | All of the parts are unbundled and can be extended to meet any standard (or non-standard) requirements. 20 | 21 | ### Developer Installation 22 | 23 | - In the root directory: 24 | ```bash 25 | docker-compose up -d --build \ 26 | cd portal \ 27 | npm run dev 28 | ``` 29 | - Navigate to **`localhost:3000`** to view the admin console running 30 | -------------------------------------------------------------------------------- /portal/pages/api/captcha.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getSession, session } from "next-auth/client"; 3 | 4 | const handler = async (req, res) => { 5 | if (req.method === "POST") { 6 | const response = await axios.get(`${process.env.CAPTCHA_URL}/verify`, { 7 | params: req.body, 8 | }); 9 | const responseObject = response.data; 10 | if (responseObject) { 11 | res.status(200).json({ errors: null, success: responseObject }); 12 | } 13 | } 14 | if (req.method === "GET") { 15 | try { 16 | const response = await axios.get(`${process.env.CAPTCHA_URL}/`); 17 | if (response.data) { 18 | res.setHeader("token", response.headers.token); 19 | res.status(200).json(response.data); 20 | } else { 21 | res 22 | .status(200) 23 | .json({ errors: "Failed to get captcha", success: null }); 24 | } 25 | return true; 26 | } catch (err) { 27 | res 28 | .status(500) 29 | .json({ errors: "Captcha service unavailable", success: null }); 30 | return true; 31 | } 32 | } 33 | }; 34 | 35 | export default handler; 36 | -------------------------------------------------------------------------------- /api/src/datasources/mongo.datasource.ts: -------------------------------------------------------------------------------- 1 | import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core'; 2 | import {juggler} from '@loopback/repository'; 3 | 4 | const config = { 5 | name: 'mongo', 6 | connector: 'mongodb', 7 | url: '', 8 | host: 'mongo', 9 | port: 27017, 10 | user: process.env.MONGO_DATASOURCE_USER, 11 | password: process.env.MONGO_DATASOURCE_PASSWORD, 12 | database: 'admin', 13 | useNewUrlParser: true, 14 | }; 15 | 16 | // Observe application's life cycle to disconnect the datasource when 17 | // application is stopped. This allows the application to be shut down 18 | // gracefully. The `stop()` method is inherited from `juggler.DataSource`. 19 | // Learn more at https://loopback.io/doc/en/lb4/Life-cycle.html 20 | @lifeCycleObserver('datasource') 21 | export class MongoDataSource 22 | extends juggler.DataSource 23 | implements LifeCycleObserver 24 | { 25 | static dataSourceName = 'mongo'; 26 | static readonly defaultConfig = config; 27 | 28 | constructor( 29 | @inject('datasources.config.mongo', {optional: true}) 30 | dsConfig: object = config, 31 | ) { 32 | super(dsConfig); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy-to-production 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | deploy-to-production: 7 | environment: production 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | 13 | - name: Install SSH key 14 | uses: shimataro/ssh-key-action@v2 15 | with: 16 | key: ${{ secrets.SSH_PRIVATE_KEY }} 17 | known_hosts: "placeholder-text" 18 | 19 | - name: Adding known hosts 20 | run: ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 21 | 22 | - name: Deploy files with rsync 23 | run: rsync -avz --exclude '.git' --exclude '.github' . ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/home/device-donation/ 24 | 25 | - name: Rebuild containers with docker 26 | uses: appleboy/ssh-action@master 27 | with: 28 | host: ${{ secrets.SSH_HOST }} 29 | username: ${{ secrets.SSH_USER }} 30 | key: ${{ secrets.SSH_PRIVATE_KEY }} 31 | script: | 32 | cd /home/device-donation/ 33 | docker-compose up -d --build 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Samagra | Transforming Governance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /portal/pages/index.js: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import Layout from "../components/layout"; 4 | import styles from "../styles/Home.module.css"; 5 | import config from "@/components/config"; 6 | import Login from "./login"; 7 | const Home = () => { 8 | return ( 9 | // <Layout> 10 | <div className={styles.grid}> 11 | <Login></Login> 12 | {/* {config.homepageCards.map((card, index) => { 13 | return ( 14 | <Link key={index} href={card.target} passHref> 15 | <div className="card logo-card card-center"> 16 | <span 17 | className={`material-icons ${styles.icon} ${ 18 | styles[card.colour] 19 | }`} 20 | > 21 | {card.icon} 22 | </span> 23 | <h2> 24 | {" "} 25 | {card.title.en} / 26 | <br /> {card.title.hi} ⟶ 27 | </h2> 28 | </div> 29 | </Link> 30 | ); 31 | })} */} 32 | </div> 33 | // </Layout> 34 | ); 35 | }; 36 | 37 | export default Home; 38 | -------------------------------------------------------------------------------- /api/src/models/donate-device.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, model, property} from '@loopback/repository'; 2 | 3 | @model({settings: {strict: false}}) 4 | export class DonateDevice extends Entity { 5 | @property({ 6 | type: 'string', 7 | id: true, 8 | generated: true, 9 | }) 10 | id?: string; 11 | 12 | @property({ 13 | type: 'string', 14 | }) 15 | token?: string; 16 | 17 | @property({ 18 | type: 'string', 19 | }) 20 | content?: string; 21 | 22 | @property({ 23 | type: 'string', 24 | }) 25 | formId?: string; 26 | 27 | @property({ 28 | type: 'string', 29 | }) 30 | formVersion?: string; 31 | 32 | @property({ 33 | type: 'array', 34 | itemType: 'object', 35 | }) 36 | data?: object[]; 37 | 38 | // Define well-known properties here 39 | 40 | // Indexer property to allow additional data 41 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 42 | [prop: string]: any; 43 | 44 | constructor(data?: Partial<DonateDevice>) { 45 | super(data); 46 | } 47 | } 48 | 49 | export interface DonateDeviceRelations { 50 | // describe navigational properties here 51 | } 52 | 53 | export type DonateDeviceWithRelations = DonateDevice & DonateDeviceRelations; 54 | -------------------------------------------------------------------------------- /api/src/models/request-device.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, model, property} from '@loopback/repository'; 2 | 3 | @model({settings: {strict: false}}) 4 | export class RequestDevice extends Entity { 5 | @property({ 6 | type: 'string', 7 | id: true, 8 | generated: true, 9 | }) 10 | id?: string; 11 | 12 | @property({ 13 | type: 'string', 14 | }) 15 | token?: string; 16 | 17 | @property({ 18 | type: 'string', 19 | }) 20 | content?: string; 21 | 22 | @property({ 23 | type: 'string', 24 | }) 25 | formId?: string; 26 | 27 | @property({ 28 | type: 'string', 29 | }) 30 | formVersion?: string; 31 | 32 | @property({ 33 | type: 'array', 34 | itemType: 'object', 35 | }) 36 | data?: object[]; 37 | 38 | // Define well-known properties here 39 | 40 | // Indexer property to allow additional data 41 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 42 | [prop: string]: any; 43 | 44 | constructor(data?: Partial<RequestDevice>) { 45 | super(data); 46 | } 47 | } 48 | 49 | export interface RequestDeviceRelations { 50 | // describe navigational properties here 51 | } 52 | 53 | export type RequestDeviceWithRelations = RequestDevice & RequestDeviceRelations; 54 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Transpiled JavaScript files from Typescript 61 | /dist 62 | 63 | # Cache used by TypeScript's incremental build 64 | *.tsbuildinfo 65 | -------------------------------------------------------------------------------- /portal/pages/api/log.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getSession, session } from "next-auth/client"; 3 | 4 | const handler = async (req, res) => { 5 | const session = await getSession({ req }); 6 | if (session) { 7 | if (req.method === "POST") { 8 | const { msg } = req.body; 9 | const response = await axios({ 10 | method: "POST", 11 | url: "https://slack.com/api/chat.postMessage", 12 | headers: { 13 | "Content-Type": "application/json", 14 | Authorization: `Bearer ${process.env.SLACK_ADMIN_LOGGER_AUTH_TOKEN}`, 15 | }, 16 | data: { 17 | channel: process.env.NEXT_PUBLIC_SLACK_ADMIN_LOGS_CHANNEL_ID, 18 | text: msg, 19 | }, 20 | }); 21 | 22 | const responseObject = response.data; 23 | if (responseObject?.ok === true) { 24 | res.status(200).json({ 25 | error: null, 26 | success: "Successfully logged.", 27 | }); 28 | } else { 29 | res.status(200).json({ 30 | error: `Error sending log - Provider error code ${responseObject.error}`, 31 | success: null, 32 | }); 33 | } 34 | } 35 | } else res.status(401).json({ error: "Unauthorised access.", success: null }); 36 | }; 37 | 38 | export default handler; 39 | -------------------------------------------------------------------------------- /portal/pages/api/sms.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getSession, session } from "next-auth/client"; 3 | 4 | const handler = async (req, res) => { 5 | const session = await getSession({ req }); 6 | if (session) { 7 | if (req.method === "POST") { 8 | const { msg, contactNumber, templateId } = req.body; 9 | const response = await axios({ 10 | method: "get", 11 | url: `http://enterprise.smsgupshup.com/GatewayAPI/rest?msg=${msg}&v=1.1&userid=${process.env.GUPSHUP_USERNAME}&password=${process.env.GUPSHUP_PASSWORD}&send_to=${contactNumber}&msg_type=text&method=sendMessage&format=JSON&principalEntityId=${process.env.GUPSHUP_PRINCIPAL_ENTITY_ID}&dltTemplateId=${templateId}`, 12 | }); 13 | 14 | const responseObject = response.data?.response; 15 | if (responseObject?.status === "success") { 16 | res.status(200).json({ 17 | error: null, 18 | success: "Successfully sent SMS notification!", 19 | }); 20 | } else { 21 | res.status(200).json({ 22 | error: `Error sending SMS - Provider error code ${responseObject.id}`, 23 | success: null, 24 | }); 25 | } 26 | } 27 | } else res.status(401).json({ error: "Unauthorised access.", success: null }); 28 | }; 29 | 30 | export default handler; 31 | -------------------------------------------------------------------------------- /portal/.env.development: -------------------------------------------------------------------------------- 1 | #URL 2 | NEXT_PUBLIC_URL="http://localhost:3000" 3 | 4 | #API 5 | NEXT_PUBLIC_API_URL="http://localhost:3000/api" 6 | NEXT_PUBLIC_HASURA_URL="http://localhost:3000/api/graphql" 7 | 8 | #RESOURCES 9 | NEXT_PUBLIC_DONATE_DEVICE_INDIV_FORM_URL="https://enketo.samagra.io/::Zg7hHxg5" 10 | NEXT_PUBLIC_DONATE_DEVICE_CORP_FORM_URL="https://enketo.samagra.io/::yfqSuTey" 11 | NEXT_PUBLIC_REQUEST_DEVICE_FORM_URL="https://enketo.samagra.io/::M34oPF2I" 12 | NEXT_PUBLIC_FAQ_DOCUMENT_URL="https://drive.google.com/file/d/1uKVDD2_c94I3qU85VEJrlKx4I8VDd0lp/view" 13 | NEXT_PUBLIC_GA_TAG_ID="G-9QL603X5R9" 14 | 15 | # FUSION AUTH 16 | FUSIONAUTH_DOMAIN="http://auth.samagra.io:9011" 17 | FUSIONAUTH_API_KEY="YFpyHxhW0-NoKRwQrXgCU5QIAQq8nBNhE--i5_n3pTU" 18 | NEXT_PUBLIC_FUSIONAUTH_STATE_APP_ID="2875657e-71aa-4ec0-93fb-b21611998b21" 19 | #NEXT_PUBLIC_FUSIONAUTH_SCHOOL_APP_ID="f0ddb3f6-091b-45e4-8c0f-889f89d4f5da" 20 | #NEXT_PUBLIC_FUSIONAUTH_OPERATOR_APP_ID="f0ddb3f6-091b-45e4-8c0f-889f89d4f5da" 21 | 22 | # HASURA 23 | HASURA_URL="http://143.110.186.108:5001/v1/graphql" 24 | HASURA_ADMIN_SECRET="2OWslm5aAjlTARU" 25 | 26 | 27 | #SLACK 28 | NEXT_PUBLIC_SLACK_ADMIN_LOGS_CHANNEL_ID="C028K9S4Q1J" 29 | 30 | #NEXT 31 | NEXTAUTH_URL="http://localhost:3000" 32 | 33 | #CAPTCHA 34 | NEXT_PUBLIC_CAPTCHA_URL="http://localhost:3000/api/captcha" -------------------------------------------------------------------------------- /portal/components/react-admin/layout/sidebarHeader.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Image from "next/image"; 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | 5 | const useStyles = makeStyles((theme) => ({ 6 | sidebarHeader: { 7 | gridColumn: "1 / 2", 8 | gridRow: "1 / 2", 9 | height: "9vh", 10 | textAlign: "center", 11 | boxSizing: "content-box", 12 | backgroundColor: theme.palette.grey.darker, 13 | boxShadow: 14 | "0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%)", 15 | opacity: 0.7, 16 | }, 17 | sidebarHeaderLogo: { 18 | filter: "grayscale(1)", 19 | [theme.breakpoints.down("sm")]: { 20 | width: "25%", 21 | height: "7vh", 22 | }, 23 | [theme.breakpoints.up("sm")]: { 24 | width: "20%", 25 | }, 26 | }, 27 | })); 28 | 29 | const UserNavbarHeader = () => { 30 | const classes = useStyles(); 31 | return ( 32 | <div className={classes.sidebarHeader}> 33 | <Image 34 | className={classes.sidebarHeaderLogo} 35 | src="/DPlogo1.png" 36 | alt="logo" 37 | width={75} 38 | height={50} 39 | layout={"intrinsic"} 40 | /> 41 | </div> 42 | ); 43 | }; 44 | 45 | export default UserNavbarHeader; 46 | -------------------------------------------------------------------------------- /portal/components/react-admin/layout/config.js: -------------------------------------------------------------------------------- 1 | import { 2 | DonateDeviceRequestEdit, 3 | DonateDeviceRequestList, 4 | } from "@/resources/donate-device"; 5 | import { 6 | RequestDeviceEdit, 7 | RequestDeviceList, 8 | } from "@/resources/request-device"; 9 | import { 10 | CandidateShow, 11 | CandidateList, 12 | } from "@/components/react-admin/base/resources/candidateProfile/index"; 13 | export const resourceConfig = [ 14 | { 15 | name: "candidate_profile", 16 | list: CandidateList, 17 | // edit: CandidateEdit, 18 | show: CandidateShow, 19 | create: null, 20 | label: "Candidate Data", 21 | icon: "person", 22 | }, 23 | // { 24 | // name: "candidate_vacancy_interest", 25 | // list: InterestedCandidateist, 26 | // edit: InterestedCandidateEdit, 27 | // create: null, 28 | // label: "Interested Candidates", 29 | // icon: "person", 30 | // }, 31 | // { 32 | // name: "employer_details", 33 | // list: RecruiterList, 34 | // edit: RecruiterEdit, 35 | // create: null, 36 | // label: "Recruiter Data", 37 | // icon: "person", 38 | // }, 39 | // { 40 | // name: "vacancy_details", 41 | // list: VacancyList, 42 | // edit: VacancyEdit, 43 | // create: null, 44 | // label: "Vacancy Data", 45 | // icon: "person", 46 | // }, 47 | ]; 48 | -------------------------------------------------------------------------------- /api/src/index.ts: -------------------------------------------------------------------------------- 1 | import {ApiApplication, ApplicationConfig} from './application'; 2 | 3 | export * from './application'; 4 | 5 | export async function main(options: ApplicationConfig = {}) { 6 | const app = new ApiApplication(options); 7 | await app.boot(); 8 | await app.start(); 9 | 10 | const url = app.restServer.url; 11 | console.log(`Server is running at ${url}`); 12 | console.log(`Try ${url}/ping`); 13 | 14 | return app; 15 | } 16 | 17 | if (require.main === module) { 18 | // Run the application 19 | const config = { 20 | rest: { 21 | port: +(process.env.PORT ?? 3001), 22 | host: process.env.HOST, 23 | // The `gracePeriodForClose` provides a graceful close for http/https 24 | // servers with keep-alive clients. The default value is `Infinity` 25 | // (don't force-close). If you want to immediately destroy all sockets 26 | // upon stop, set its value to `0`. 27 | // See https://www.npmjs.com/package/stoppable 28 | gracePeriodForClose: 5000, // 5 seconds 29 | openApiSpec: { 30 | // useful when used with OpenAPI-to-GraphQL to locate your application 31 | setServersFromRequest: true, 32 | }, 33 | }, 34 | }; 35 | main(config).catch(err => { 36 | console.error('Cannot start the application.', err); 37 | process.exit(1); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /portal/pages/school.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useSession } from "next-auth/client"; 3 | import Layout from "../components/layout"; 4 | import Login from "./login"; 5 | import styles from "../styles/Home.module.css"; 6 | import config from "@/components/config"; 7 | 8 | const School = () => { 9 | const [session, loading] = useSession(); 10 | 11 | if (loading) return null; 12 | 13 | if (!loading && !session) { 14 | // signIn("fusionauth") 15 | return <Login />; 16 | } 17 | return ( 18 | <Layout> 19 | <div className={styles.grid}> 20 | {config.schoolPageCards.map((card, index) => { 21 | return ( 22 | <Link key={index} href={card.target} passHref> 23 | <div className="card logo-card"> 24 | <span 25 | className={`material-icons ${styles.icon} ${ 26 | styles[card.colour] 27 | }`} 28 | > 29 | {card.icon} 30 | </span> 31 | <h2> 32 | {" "} 33 | {card.title.en} / 34 | <br /> {card.title.hi} ⟶ 35 | </h2> 36 | </div> 37 | </Link> 38 | ); 39 | })} 40 | </div> 41 | </Layout> 42 | ); 43 | }; 44 | 45 | export default School; 46 | -------------------------------------------------------------------------------- /api/src/application.ts: -------------------------------------------------------------------------------- 1 | import {BootMixin} from '@loopback/boot'; 2 | import {ApplicationConfig} from '@loopback/core'; 3 | import { 4 | RestExplorerBindings, 5 | RestExplorerComponent, 6 | } from '@loopback/rest-explorer'; 7 | import {RepositoryMixin} from '@loopback/repository'; 8 | import {RestApplication} from '@loopback/rest'; 9 | import {ServiceMixin} from '@loopback/service-proxy'; 10 | import path from 'path'; 11 | import {MySequence} from './sequence'; 12 | 13 | export {ApplicationConfig}; 14 | 15 | export class ApiApplication extends BootMixin( 16 | ServiceMixin(RepositoryMixin(RestApplication)), 17 | ) { 18 | constructor(options: ApplicationConfig = {}) { 19 | super(options); 20 | 21 | // Set up the custom sequence 22 | this.sequence(MySequence); 23 | 24 | // Set up default home page 25 | this.static('/', path.join(__dirname, '../public')); 26 | 27 | // Customize @loopback/rest-explorer configuration here 28 | this.configure(RestExplorerBindings.COMPONENT).to({ 29 | path: '/explorer', 30 | }); 31 | this.component(RestExplorerComponent); 32 | 33 | this.projectRoot = __dirname; 34 | // Customize @loopback/boot Booter Conventions here 35 | this.bootOptions = { 36 | controllers: { 37 | // Customize ControllerBooter Conventions here 38 | dirs: ['controllers'], 39 | extensions: ['.controller.js'], 40 | nested: true, 41 | }, 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /portal/styles/Login.module.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | display: grid; 3 | margin-top: 1rem; 4 | margin-bottom: 1rem; 5 | text-align: center; 6 | } 7 | 8 | .grid-one { 9 | grid-template-columns: 1fr; 10 | width: 30vw; 11 | margin: 0 auto; 12 | } 13 | 14 | .grid-two { 15 | grid-template-columns: 1fr 1fr; 16 | width: 70vw; 17 | margin: 0 auto; 18 | } 19 | 20 | .description { 21 | font-size: 1.5rem; 22 | padding: 1rem 0; 23 | } 24 | 25 | .helper { 26 | font-size: 1.1rem; 27 | padding: 1rem 0; 28 | } 29 | 30 | .form { 31 | display: flex; 32 | flex-direction: column; 33 | } 34 | 35 | .form input[type="text"], 36 | .form input[type="password"] { 37 | border: 0; 38 | border-bottom: 1px solid gray; 39 | margin-bottom: 1rem; 40 | font-family: "Bahnschrift", sans-serif; 41 | } 42 | 43 | .form button { 44 | font-family: "Bahnschrift"; 45 | border: 0; 46 | padding: 0.5rem 0; 47 | background-color: #303765; 48 | color: #eee; 49 | border-radius: 5px; 50 | font-weight: 600; 51 | transition: 100ms ease-in all; 52 | } 53 | 54 | .form button:disabled { 55 | color: #888; 56 | background-color: #efefef; 57 | } 58 | 59 | .form input:focus, 60 | .form input:active { 61 | outline: 0; 62 | } 63 | 64 | @media screen and (max-width: 600px) { 65 | .grid { 66 | margin-top: 3rem; 67 | margin-bottom: 3rem; 68 | } 69 | 70 | .grid-two { 71 | width: 100vw; 72 | } 73 | 74 | .grid-one { 75 | width: 70vw; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /api/src/utils/sendSMS.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import sendLog from './adminLogger'; 3 | 4 | const sendSMS = async ( 5 | msg: string, 6 | trackingKey: string, 7 | contactNumber: string, 8 | ) => { 9 | const response = await axios({ 10 | method: 'get', 11 | url: `http://enterprise.smsgupshup.com/GatewayAPI/rest?msg=${msg}&v=1.1&userid=${process.env.GUPSHUP_USERNAME}&password=${process.env.GUPSHUP_PASSWORD}&send_to=${contactNumber}&msg_type=text&method=sendMessage&format=JSON&principalEntityId=${process.env.GUPSHUP_PRINCIPAL_ENTITY_ID}&dltTemplateId=1007434778563689331`, 12 | }); 13 | const responseObject = response.data?.response; 14 | if (responseObject?.status === 'success') { 15 | sendLog( 16 | `*✅ samarth-device*: SMS notiification successfully sent to _${contactNumber}_ with Tracking ID: *${trackingKey}*`, 17 | process.env.SLACK_ADMIN_LOGS_CHANNEL_ID, 18 | ); 19 | return { 20 | statusCode: 200, 21 | error: null, 22 | success: 'Successfully sent!', 23 | }; 24 | } else { 25 | sendLog( 26 | `*⚠️ samarth-device*: SMS notiification sending failed to _${contactNumber}_. 🦺 Skipping graphQL insertion for this record.`, 27 | process.env.SLACK_ADMIN_LOGS_CHANNEL_ID, 28 | ); 29 | return { 30 | statusCode: 500, 31 | error: `Error sending SMS - Provider error code ${responseObject.id}`, 32 | success: null, 33 | }; 34 | } 35 | }; 36 | 37 | export default sendSMS; 38 | -------------------------------------------------------------------------------- /api/src/controllers/ping.controller.ts: -------------------------------------------------------------------------------- 1 | import {inject} from '@loopback/core'; 2 | import { 3 | Request, 4 | RestBindings, 5 | get, 6 | response, 7 | ResponseObject, 8 | } from '@loopback/rest'; 9 | 10 | /** 11 | * OpenAPI response for ping() 12 | */ 13 | const PING_RESPONSE: ResponseObject = { 14 | description: 'Ping Response', 15 | content: { 16 | 'application/json': { 17 | schema: { 18 | type: 'object', 19 | title: 'PingResponse', 20 | properties: { 21 | greeting: {type: 'string'}, 22 | date: {type: 'string'}, 23 | url: {type: 'string'}, 24 | headers: { 25 | type: 'object', 26 | properties: { 27 | 'Content-Type': {type: 'string'}, 28 | }, 29 | additionalProperties: true, 30 | }, 31 | }, 32 | }, 33 | }, 34 | }, 35 | }; 36 | 37 | /** 38 | * A simple controller to bounce back http requests 39 | */ 40 | export class PingController { 41 | constructor(@inject(RestBindings.Http.REQUEST) private req: Request) {} 42 | 43 | // Map to `GET /ping` 44 | @get('/ping') 45 | @response(200, PING_RESPONSE) 46 | ping(): object { 47 | // Reply with a greeting, the current time, the url, and request headers 48 | return { 49 | greeting: 'Hello from LoopBack', 50 | date: new Date(), 51 | url: this.req.url, 52 | headers: Object.assign({}, this.req.headers), 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /portal/Dockerfile: -------------------------------------------------------------------------------- 1 | # Install dependencies only when needed 2 | FROM node:alpine AS deps 3 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 4 | RUN apk add --no-cache libc6-compat 5 | WORKDIR /app 6 | COPY package.json package-lock.json ./ 7 | RUN npm ci --ignore-scripts --prefer-offline 8 | 9 | # Rebuild the source code only when needed 10 | FROM node:alpine AS builder 11 | WORKDIR /app 12 | COPY . . 13 | COPY --from=deps /app/node_modules ./node_modules 14 | RUN npm run build 15 | 16 | # Production image, copy all the files and run next 17 | FROM node:alpine AS runner 18 | WORKDIR /app 19 | 20 | ENV NODE_ENV production 21 | 22 | 23 | 24 | RUN addgroup -g 1001 -S nodejs 25 | RUN adduser -S nextjs -u 1001 26 | 27 | # You only need to copy next.config.js if you are NOT using the default configuration 28 | # COPY --from=builder /app/next.config.js ./ 29 | COPY --from=builder /app/public ./public 30 | COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next 31 | COPY --from=builder /app/node_modules ./node_modules 32 | COPY --from=builder /app/package.json ./package.json 33 | 34 | USER nextjs 35 | 36 | EXPOSE 3000 37 | 38 | # Next.js collects completely anonymous telemetry data about general usage. 39 | # Learn more here: https://nextjs.org/telemetry 40 | # Uncomment the following line in case you want to disable telemetry. 41 | ENV NEXT_TELEMETRY_DISABLED 1 42 | 43 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /portal/components/react-admin/customHasura/fetchActions.js: -------------------------------------------------------------------------------- 1 | export const GET_LIST = "GET_LIST"; 2 | export const GET_ONE = "GET_ONE"; 3 | export const GET_MANY = "GET_MANY"; 4 | export const GET_MANY_REFERENCE = "GET_MANY_REFERENCE"; 5 | export const CREATE = "CREATE"; 6 | export const UPDATE = "UPDATE"; 7 | export const UPDATE_MANY = "UPDATE_MANY"; 8 | export const DELETE = "DELETE"; 9 | export const DELETE_MANY = "DELETE_MANY"; 10 | 11 | export const fetchActionsWithRecordResponse = [GET_ONE, CREATE, UPDATE]; 12 | export const fetchActionsWithArrayOfIdentifiedRecordsResponse = [ 13 | GET_LIST, 14 | GET_MANY, 15 | GET_MANY_REFERENCE, 16 | ]; 17 | export const fetchActionsWithArrayOfRecordsResponse = [ 18 | ...fetchActionsWithArrayOfIdentifiedRecordsResponse, 19 | UPDATE_MANY, 20 | DELETE_MANY, 21 | ]; 22 | export const fetchActionsWithTotalResponse = [GET_LIST, GET_MANY_REFERENCE]; 23 | 24 | export const sanitizeFetchType = (fetchType) => { 25 | switch (fetchType) { 26 | case GET_LIST: 27 | return "getList"; 28 | case GET_ONE: 29 | return "getOne"; 30 | case GET_MANY: 31 | return "getMany"; 32 | case GET_MANY_REFERENCE: 33 | return "getManyReference"; 34 | case CREATE: 35 | return "create"; 36 | case UPDATE: 37 | return "update"; 38 | case UPDATE_MANY: 39 | return "updateMany"; 40 | case DELETE: 41 | return "delete"; 42 | case DELETE_MANY: 43 | return "deleteMany"; 44 | default: 45 | return fetchType; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /portal/pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | class MyDocument extends Document { 4 | static async getInitialProps(ctx) { 5 | const initialProps = await Document.getInitialProps(ctx); 6 | return { ...initialProps }; 7 | } 8 | 9 | render() { 10 | return ( 11 | <Html lang="en" className="notranslate" translate="no"> 12 | <Head> 13 | <link rel="preconnect" href="https://fonts.gstatic.com" /> 14 | <link 15 | href="https://fonts.googleapis.com/icon?family=Material+Icons" 16 | rel="stylesheet" 17 | ></link> 18 | <meta name="google" content="notranslate" /> 19 | {/* Global Site Tag (gtag.js) - Google Analytics */} 20 | <script 21 | async 22 | src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_TAG_ID}`} 23 | /> 24 | <script 25 | dangerouslySetInnerHTML={{ 26 | __html: ` 27 | window.dataLayer = window.dataLayer || []; 28 | function gtag(){dataLayer.push(arguments);} 29 | gtag('js', new Date()); 30 | gtag('config', '${process.env.NEXT_PUBLIC_GA_TAG_ID}', { 31 | page_path: window.location.pathname, 32 | });`, 33 | }} 34 | /> 35 | </Head> 36 | <body> 37 | <Main /> 38 | <NextScript /> 39 | </body> 40 | </Html> 41 | ); 42 | } 43 | } 44 | 45 | export default MyDocument; 46 | -------------------------------------------------------------------------------- /api/DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developer's Guide 2 | 3 | We use Visual Studio Code for developing LoopBack and recommend the same to our 4 | users. 5 | 6 | ## VSCode setup 7 | 8 | Install the following extensions: 9 | 10 | - [eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 11 | - [prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 12 | 13 | ## Development workflow 14 | 15 | ### Visual Studio Code 16 | 17 | 1. Start the build task (Cmd+Shift+B) to run TypeScript compiler in the 18 | background, watching and recompiling files as you change them. Compilation 19 | errors will be shown in the VSCode's "PROBLEMS" window. 20 | 21 | 2. Execute "Run Rest Task" from the Command Palette (Cmd+Shift+P) to re-run the 22 | test suite and lint the code for both programming and style errors. Linting 23 | errors will be shown in VSCode's "PROBLEMS" window. Failed tests are printed 24 | to terminal output only. 25 | 26 | ### Other editors/IDEs 27 | 28 | 1. Open a new terminal window/tab and start the continuous build process via 29 | `npm run build:watch`. It will run TypeScript compiler in watch mode, 30 | recompiling files as you change them. Any compilation errors will be printed 31 | to the terminal. 32 | 33 | 2. In your main terminal window/tab, run `npm run test:dev` to re-run the test 34 | suite and lint the code for both programming and style errors. You should run 35 | this command manually whenever you have new changes to test. Test failures 36 | and linter errors will be printed to the terminal. 37 | -------------------------------------------------------------------------------- /portal/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /portal/pages/login.js: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { useState } from "react"; 3 | import Layout from "../components/layout"; 4 | import Login from "../components/login/login"; 5 | import styles from "../styles/Login.module.css"; 6 | import config from "@/components/config"; 7 | 8 | const LoginWrapper = () => { 9 | const [selectedPersona, setSelectedPersona] = useState({ 10 | consonant: false, 11 | en: "official", 12 | hi: "अधिकारी", 13 | credentials: "Shiksha Saathi", 14 | applicationId: "2875657e-71aa-4ec0-93fb-b21611998b21", 15 | redirectUrl: `admin#/candidate_profile`, 16 | }); 17 | if (selectedPersona) { 18 | return ( 19 | <Layout> 20 | <Login persona={selectedPersona}></Login> 21 | </Layout> 22 | ); 23 | } 24 | 25 | return ( 26 | <Layout> 27 | <> 28 | <h2 className="text-center">Login / लॉग इन</h2> 29 | <div className={`${styles.grid} ${styles["grid-two"]}`}> 30 | {config.personas.map((persona, index) => ( 31 | <div 32 | onClick={() => { 33 | 34 | setSelectedPersona(persona); 35 | }} 36 | key={index} 37 | className={`card`} 38 | > 39 | <h2 className={"capitalize"}> 40 | {persona.en} / <br /> 41 | {persona.hi}→ 42 | </h2> 43 | <p> 44 | I am a{persona.consonant ? "" : "n"} {persona.en} 45 | <br /> मैं राज्य में {persona.hi} हूँ{" "} 46 | </p> 47 | </div> 48 | ))} 49 | </div> 50 | </> 51 | </Layout> 52 | ); 53 | }; 54 | 55 | export default LoginWrapper; 56 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # api 2 | 3 | This application is generated using [LoopBack 4 CLI](https://loopback.io/doc/en/lb4/Command-line-interface.html) with the 4 | [initial project layout](https://loopback.io/doc/en/lb4/Loopback-application-layout.html). 5 | 6 | ## Install dependencies 7 | 8 | By default, dependencies were installed when this application was generated. 9 | Whenever dependencies in `package.json` are changed, run the following command: 10 | 11 | ```sh 12 | yarn install 13 | ``` 14 | 15 | ## Run the application 16 | 17 | ```sh 18 | yarn start 19 | ``` 20 | 21 | You can also run `node .` to skip the build step. 22 | 23 | Open http://127.0.0.1:3000 in your browser. 24 | 25 | ## Rebuild the project 26 | 27 | To incrementally build the project: 28 | 29 | ```sh 30 | yarn run build 31 | ``` 32 | 33 | To force a full build by cleaning up cached artifacts: 34 | 35 | ```sh 36 | yarn run rebuild 37 | ``` 38 | 39 | ## Fix code style and formatting issues 40 | 41 | ```sh 42 | yarn run lint 43 | ``` 44 | 45 | To automatically fix such issues: 46 | 47 | ```sh 48 | yarn run lint:fix 49 | ``` 50 | 51 | ## Other useful commands 52 | 53 | - `yarn run migrate`: Migrate database schemas for models 54 | - `yarn run openapi-spec`: Generate OpenAPI spec into a file 55 | - `yarn run docker:build`: Build a Docker image for this application 56 | - `yarn run docker:run`: Run this application inside a Docker container 57 | 58 | ## Tests 59 | 60 | ```sh 61 | yarn test 62 | ``` 63 | 64 | ## What's next 65 | 66 | Please check out [LoopBack 4 documentation](https://loopback.io/doc/en/lb4/) to 67 | understand how you can continue to add features to this application. 68 | 69 | [![LoopBack](<https://github.com/strongloop/loopback-next/raw/master/docs/site/imgs/branding/Powered-by-LoopBack-Badge-(blue)-@2x.png>)](http://loopback.io/) 70 | -------------------------------------------------------------------------------- /captcha-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "captchapng", 3 | "version": "0.0.1", 4 | "description": "A numeric captcha generator for Node.js", 5 | "author": { 6 | "name": "George Chan" 7 | }, 8 | "main": "./lib/captchapng.js", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/GeorgeChan/captchapng" 12 | }, 13 | "keywords": [ 14 | "captchapng" 15 | ], 16 | "dependencies": { 17 | "cors": "^2.8.5", 18 | "express": "^4.17.1", 19 | "lowdb": "^1.0.0", 20 | "pnglib": "0.0.1", 21 | "uuid": "^8.3.2" 22 | }, 23 | "license": "BSD", 24 | "readme": "#Captcha PNG generator\nA numeric captcha generator for Node.js\n\n##Features\n* Only generate numeric captcha PNG image\n* Build-in fonts\n* Characters up and down, left and right limits, random displacement\n* Full JavaScript\n\n##Examples\n```javascript\n/**\n * Captcha PNG img generator\n * @Author: George Chan\n * @Email: gchan@21cn.com\n * @Version: 1.0\n * @Date: 2013-08-18\n * @license http://www.opensource.org/licenses/bsd-license.php BSD License\n */\n\nvar http = require('http');\nvar captchapng = require('captchapng');\n\nhttp.createServer(function (request, response) {\n if(request.url == '/captcha.png') {\n var p = new captchapng(80,30,parseInt(Math.random()*9000+1000)); // width,height,numeric captcha\n p.color(0, 0, 0, 0); // First color: background (red, green, blue, alpha)\n p.color(80, 80, 80, 255); // Second color: paint (red, green, blue, alpha)\n\n var img = p.getBase64();\n var imgbase64 = new Buffer(img,'base64');\n response.writeHead(200, {\n 'Content-Type': 'image/png'\n });\n response.end(imgbase64);\n } else response.end('');\n}).listen(8181);\n\nconsole.log('Web server started.\\n http:\\\\127.0.0.1:8181\\captcha.png');\n```", 25 | "readmeFilename": "README.md" 26 | } 27 | -------------------------------------------------------------------------------- /api/src/controllers/request-device-graphQL-model.ts: -------------------------------------------------------------------------------- 1 | export class RequestDevice { 2 | name?: string | null; 3 | phone_number?: string | null; 4 | created_at?: string | null; 5 | district?: string | null; 6 | block?: string | null; 7 | pincode?: string | null; 8 | school_name?: string | null; 9 | udise?: number | null; 10 | total_students?: number | null; 11 | student_count_no_smartphone?: string | null; 12 | student_details?: string | null; 13 | declaration?: Boolean | null; 14 | 15 | operationsDoc = ` 16 | mutation insertDemand($demand: device_demand_response_insert_input!) { 17 | insert_device_demand_response_one(object: $demand) { 18 | id 19 | } 20 | } 21 | `; 22 | variableName = `demand`; 23 | operationName = `insertDemand`; 24 | databaseOperationName = `insert_device_demand_response_one`; 25 | 26 | constructor(data: any) { 27 | this.name = data?.name ?? null; 28 | this.phone_number = data?.contact ?? null; 29 | this.created_at = data?.['*meta-submission-date*'] ?? null; 30 | this.district = data?.district ?? null; 31 | this.block = data?.block ?? null; 32 | this.school_name = data?.school ?? null; 33 | this.pincode = this.convertToString(data?.pincode) ?? null; 34 | this.udise = this.extractUDISE(data?.school) ?? null; 35 | this.total_students = data?.totalstudents ?? null; 36 | this.student_count_no_smartphone = data?.donation ?? null; 37 | this.student_details = data?.details ?? null; 38 | this.declaration = this.convertToBoolean(data?.declaration) ?? null; 39 | } 40 | 41 | convertToBoolean(response: string): boolean { 42 | if (response === 'yes') return true; 43 | else return false; 44 | } 45 | 46 | extractUDISE(response: string): number | null { 47 | if (response.split('-').length > 1) return parseInt(response.split('-')[1]); 48 | return null; 49 | } 50 | 51 | convertToString(response: number): string | null { 52 | if (response) { 53 | return String(response); 54 | } 55 | return null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /portal/components/react-admin/layout/verticalItem.js: -------------------------------------------------------------------------------- 1 | import React, { createElement } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import { Link } from "react-router-dom"; 4 | import SmartphoneIcon from "@material-ui/icons/Smartphone"; 5 | import SchoolIcon from "@material-ui/icons/School"; 6 | 7 | const useStyles = makeStyles((theme) => ({ 8 | sidebarItem: { 9 | display: "flex", 10 | flexDirection: "row", 11 | justifyContent: "flex-start", 12 | padding: "16px 0px 16px 16px", 13 | }, 14 | "&:focus, &:active": { 15 | backgroundColor: "inherit", 16 | }, 17 | selected: { 18 | backgroundColor: theme.palette.grey.hover, 19 | }, 20 | listItem: { 21 | fontSize: "1.2rem", 22 | lineHeight: "1em", 23 | textAlign: "center", 24 | fontVariant: "all-small-caps", 25 | marginLeft: "1rem", 26 | fontWeight: "700", 27 | color: theme.palette.grey.light, 28 | }, 29 | sidebarIcon: { 30 | color: "#626D74", 31 | fontSize: "1.2rem", 32 | }, 33 | })); 34 | 35 | const Icon = (props) => { 36 | if (props.type === "smartphone") 37 | return <SmartphoneIcon className={props.className} />; 38 | if (props.type === "school") 39 | return <SchoolIcon className={props.className} />; 40 | return <></>; 41 | }; 42 | 43 | const VerticalItem = (props) => { 44 | const { item, nestedLevel, activePath } = props; 45 | const classes = useStyles({ 46 | itemPadding: nestedLevel > 0 ? 30 + nestedLevel * 16 : 24, 47 | }); 48 | const { onMenuClick } = props; 49 | 50 | let sidebarItemName = item.label; 51 | if (item.options !== undefined && item.options.label !== undefined) { 52 | sidebarItemName = item.options.label; 53 | } 54 | return ( 55 | <Link 56 | to={`/${item.name}`} 57 | className={`${classes.sidebarItem} ${ 58 | activePath?.split("/")?.includes(item.name) ? classes.selected : "" 59 | }`} 60 | > 61 | <Icon className={classes.sidebarIcon} type={item.icon} /> 62 | <span className={classes.listItem}>{sidebarItemName}</span> 63 | </Link> 64 | ); 65 | }; 66 | 67 | export default VerticalItem; 68 | -------------------------------------------------------------------------------- /portal/pages/api/auth/[...nextauth].js: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import Providers from "next-auth/providers"; 3 | import axios from "axios"; 4 | 5 | const fusionAuthLogin = async (path, credentials) => { 6 | const options = { 7 | headers: { Authorization: process.env.FUSIONAUTH_API_KEY }, 8 | }; 9 | const response = await axios.post( 10 | `${path}/api/login`, 11 | { 12 | loginId: credentials.loginId, 13 | password: credentials.password, 14 | applicationId: credentials.applicationId, 15 | }, 16 | options 17 | ); 18 | return response; 19 | }; 20 | 21 | export default NextAuth({ 22 | // Configure one or more authentication providers 23 | providers: [ 24 | Providers.Credentials({ 25 | id: "fusionauth", 26 | name: "FusionAuth Credentials Login", 27 | async authorize(credentials, req) { 28 | let response = null; 29 | try { 30 | response = await fusionAuthLogin( 31 | process.env.FUSIONAUTH_DOMAIN, 32 | credentials 33 | ); 34 | if (response) { 35 | return response.data; 36 | } 37 | } catch (err) { 38 | throw err; 39 | } 40 | }, 41 | }), 42 | ], 43 | session: { 44 | jwt: true, 45 | }, 46 | callbacks: { 47 | redirect(url, baseUrl) { 48 | return url; 49 | }, 50 | async jwt(token, user, account, profile, isNewUser) { 51 | // Add access_token to the token right after signin 52 | if (account) { 53 | token.username = profile.user?.username; 54 | token.fullName = profile.user?.fullName; 55 | token.role = profile.user?.registrations?.[0].roles?.[0]; 56 | token.applicationId = profile.user?.registrations?.[0].applicationId; 57 | token.jwt = profile.token; 58 | } 59 | return token; 60 | }, 61 | async session(session, token) { 62 | session.jwt = token.jwt; 63 | session.role = token.role; 64 | session.fullName = token.fullName; 65 | session.username = token.username; 66 | session.applicationId = token.applicationId; 67 | return session; 68 | }, 69 | }, 70 | }); 71 | -------------------------------------------------------------------------------- /portal/components/react-admin/customHasura/customFields.js: -------------------------------------------------------------------------------- 1 | import { buildFields } from "ra-data-hasura"; 2 | import gql from "graphql-tag"; 3 | /** 4 | * Extracts just the fields from a GraphQL AST. 5 | * @param {GraphQL AST} queryAst 6 | */ 7 | const extractFieldsFromQuery = (queryAst) => { 8 | return queryAst.definitions[0].selectionSet.selections; 9 | }; 10 | 11 | // Define the additional fields that we want. 12 | const EXTENDED_CANDIDATE_RECORD = gql` 13 | { 14 | qualification_detail { 15 | qualification_name 16 | } 17 | gender { 18 | gender_name 19 | } 20 | district_name { 21 | name 22 | } 23 | highest_level_qualification { 24 | highest_level_qualification_name 25 | } 26 | expected_salary { 27 | salary_range 28 | } 29 | current_employment { 30 | id 31 | } 32 | monthly_salary_details { 33 | salary_range 34 | } 35 | driver_license { 36 | driver_license_choice 37 | } 38 | district_travel { 39 | district_travel_choice 40 | } 41 | pan_card { 42 | pan_card_choice 43 | } 44 | 45 | computer_operator { 46 | computer_operator_choice 47 | } 48 | current_employment { 49 | current_employment_status 50 | } 51 | ever_employment { 52 | employment_status 53 | } 54 | work_experience_details { 55 | work_experience_choices 56 | } 57 | sector_preference_3 { 58 | sector_preference_name 59 | } 60 | sector_preference_1 { 61 | sector_preference_name 62 | } 63 | sector_preference_2 { 64 | sector_preference_name 65 | } 66 | english_knowledge_choice { 67 | english_choice 68 | } 69 | } 70 | `; 71 | 72 | const customBuildFields = (type, fetchType) => { 73 | const resourceName = type.name; 74 | const defaultFields = buildFields(type, fetchType); 75 | if (resourceName === "candidate_profile") { 76 | if (fetchType === "GET_LIST" || fetchType === "GET_ONE") { 77 | const relatedEntities = extractFieldsFromQuery(EXTENDED_CANDIDATE_RECORD); 78 | defaultFields.push(...relatedEntities); 79 | } 80 | } 81 | 82 | return defaultFields; 83 | }; 84 | 85 | export default customBuildFields; 86 | -------------------------------------------------------------------------------- /api/src/controllers/graphQL-helper.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import sendLog from '../utils/adminLogger'; 3 | import {graphQLQuery} from './graphQL-query'; 4 | 5 | export class graphQLHelper { 6 | async fetchGraphQL( 7 | operationsDoc: string, 8 | operationName: string, 9 | variables: any, 10 | ): Promise<any> { 11 | try { 12 | const result = await axios({ 13 | url: process.env.HASURA_URL, 14 | headers: { 15 | 'content-type': 'application/json', 16 | 'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET, 17 | }, 18 | method: 'POST', 19 | data: { 20 | query: operationsDoc, 21 | variables: variables, 22 | operationName: operationName, 23 | }, 24 | }); 25 | return result.data; 26 | } catch (error) { 27 | console.error(error); 28 | return error; 29 | } 30 | } 31 | 32 | async executeInsert(obj: graphQLQuery) { 33 | const sanitisedObject: Partial<graphQLQuery> = {...obj}; 34 | delete sanitisedObject.operationName; 35 | delete sanitisedObject.operationsDoc; 36 | delete sanitisedObject.variableName; 37 | delete sanitisedObject.databaseOperationName; 38 | 39 | return this.fetchGraphQL(obj.operationsDoc, obj.operationName, { 40 | [obj.variableName]: sanitisedObject, 41 | }); 42 | } 43 | 44 | async startExecuteInsert(obj: graphQLQuery) { 45 | const {errors, data} = await this.executeInsert(obj); 46 | console.log(obj); 47 | if (errors) { 48 | console.error(errors); 49 | sendLog( 50 | `*⚠️ samarth-device*: *${obj.operationName}* failed with the following error: \`\`\`${errors?.[0]?.message}\`\`\``, 51 | process.env.SLACK_ADMIN_LOGS_CHANNEL_ID, 52 | ); 53 | return {errors: errors, data: null}; 54 | } else { 55 | console.log(data); 56 | sendLog( 57 | `*✅ samarth-device*: *${ 58 | obj.operationName 59 | }* successfully done for: \`\`\`${JSON.stringify( 60 | data[obj.databaseOperationName], 61 | )}\`\`\``, 62 | process.env.SLACK_ADMIN_LOGS_CHANNEL_ID, 63 | ); 64 | return {errors: null, data: data}; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /api/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <title>samarth device 5 | 6 | 7 | 8 | 9 | 14 | 15 | 79 | 80 | 81 | 82 |
83 |

api

84 |

Version 1.0.0

85 | 86 |

OpenAPI spec: /openapi.json

87 |

API Explorer: /explorer

88 |
89 | 90 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /portal/styles/Track.module.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | display: grid; 3 | margin: 1rem auto; 4 | text-align: center; 5 | grid-template-columns: 1fr 1fr; 6 | width: 70vw; 7 | } 8 | 9 | .description { 10 | font-size: 1.5rem; 11 | padding: 1rem 0; 12 | } 13 | 14 | .form { 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | 19 | .form input[type="text"] { 20 | border: 0; 21 | border-bottom: 1px solid gray; 22 | margin-bottom: 1rem; 23 | font-family: "Bahnschrift", "sans-serif"; 24 | text-transform: uppercase; 25 | } 26 | 27 | .button { 28 | font-family: "Bahnschrift", "sans-serif"; 29 | border: 0; 30 | padding: 0.5rem 0; 31 | background-color: #303765; 32 | color: #eee; 33 | border-radius: 5px; 34 | font-weight: 600; 35 | transition: 100ms ease-in all; 36 | } 37 | 38 | .button:disabled { 39 | color: #888; 40 | background-color: #efefef; 41 | } 42 | 43 | .form input:focus, 44 | .form input:active { 45 | outline: 0; 46 | } 47 | 48 | .table { 49 | width: 100%; 50 | } 51 | 52 | .tableRow { 53 | display: table-row; 54 | } 55 | 56 | .tableHeader { 57 | font-variant: all-small-caps; 58 | color: #22222299; 59 | } 60 | 61 | .tableCell { 62 | display: table-cell; 63 | text-align: center; 64 | } 65 | 66 | .icon { 67 | transform: translateY(0.1rem); 68 | font-size: 1.2rem; 69 | } 70 | 71 | td.certificate { 72 | padding: 0.5rem 0; 73 | } 74 | 75 | .certificate button { 76 | width: 100%; 77 | } 78 | 79 | .certificateIcon { 80 | font-size: 1.1rem; 81 | transform: translateY(3px); 82 | } 83 | 84 | .success { 85 | color: green; 86 | } 87 | 88 | .pending { 89 | color: darkgoldenrod; 90 | } 91 | 92 | .error { 93 | color: red; 94 | } 95 | 96 | .blurrable-card { 97 | transition: filter 0.5s cubic-bezier(0.165, 0.84, 0.44, 1), color 0.15s ease, 98 | border-color 0.15s ease; 99 | } 100 | 101 | .blur { 102 | filter: blur(2px); 103 | } 104 | 105 | @media screen and (max-width: 600px) { 106 | .grid { 107 | margin-top: 0; 108 | margin-bottom: 1rem; 109 | grid-template-columns: 1fr; 110 | width: 100vw; 111 | } 112 | 113 | .tableHeader { 114 | font-variant: normal; 115 | } 116 | 117 | .certificateIcon { 118 | font-size: 1.5rem; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /portal/components/react-admin/layout/customAppBar.js: -------------------------------------------------------------------------------- 1 | // in src/MyAppBar.js 2 | import React from "react"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { setSidebarVisibility } from "react-admin"; 5 | import AppBar from "@material-ui/core/AppBar"; 6 | import Toolbar from "@material-ui/core/Toolbar"; 7 | import Typography from "@material-ui/core/Typography"; 8 | import UserMenu from "./customUserMenu"; 9 | import IconButton from "@material-ui/core/IconButton"; 10 | import MenuIcon from "@material-ui/icons/Menu"; 11 | import { makeStyles } from "@material-ui/core/styles"; 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | title: { 15 | flex: 1, 16 | textOverflow: "ellipsis", 17 | whiteSpace: "nowrap", 18 | overflow: "hidden", 19 | fontWeight: 500, 20 | }, 21 | appBarStyle: { 22 | backgroundColor: theme.palette.primary.main, 23 | color: theme.palette.heading.main, 24 | }, 25 | menuButton: { 26 | [theme.breakpoints.down("sm")]: { 27 | width: "1.5rem", 28 | marginRight: "1rem", 29 | }, 30 | [theme.breakpoints.up("sm")]: { 31 | display: "none", 32 | }, 33 | color: theme.palette.heading.main, 34 | }, 35 | userMenuButton: { 36 | position: "fixed", 37 | right: "1rem", 38 | }, 39 | })); 40 | 41 | const AppBarCustom = (props) => { 42 | const classes = useStyles(); 43 | const dispatch = useDispatch(); 44 | const open = useSelector((state) => state.admin.ui.sidebarOpen); 45 | const toggleSidebarOpen = () => { 46 | if (open) dispatch(setSidebarVisibility(false)); 47 | else dispatch(setSidebarVisibility(true)); 48 | }; 49 | return ( 50 | <> 51 | 52 | 53 | { 58 | toggleSidebarOpen(); 59 | }} 60 | > 61 | 62 | 63 | 68 | 69 | 70 | 71 | 72 | ); 73 | }; 74 | 75 | export default React.memo(AppBarCustom); 76 | -------------------------------------------------------------------------------- /captcha-service/index.sample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | captcha 13 | 19 |
20 | 21 | 27 |
28 | 29 | 46 | 47 | 82 | 83 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.1", 4 | "description": "samarth device", 5 | "keywords": [ 6 | "loopback-application", 7 | "loopback" 8 | ], 9 | "main": "dist/index.js", 10 | "types": "dist/index.d.ts", 11 | "engines": { 12 | "node": ">=10.16" 13 | }, 14 | "scripts": { 15 | "build": "lb-tsc", 16 | "build:watch": "lb-tsc --watch", 17 | "lint": "yarn run eslint && yarn run prettier:check", 18 | "lint:fix": "yarn run eslint:fix && yarn run prettier:fix", 19 | "prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"", 20 | "prettier:check": "yarn run prettier:cli -l", 21 | "prettier:fix": "yarn run prettier:cli --write", 22 | "eslint": "lb-eslint --report-unused-disable-directives .", 23 | "eslint:fix": "yarn run eslint --fix", 24 | "pretest": "yarn run rebuild", 25 | "test": "lb-mocha --allow-console-logs \"dist/__tests__\"", 26 | "posttest": "yarn run lint", 27 | "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && yarn run posttest", 28 | "docker:build": "docker build -t api .", 29 | "docker:run": "docker run -p 3000:3000 -d api", 30 | "premigrate": "yarn run build", 31 | "migrate": "node ./dist/migrate", 32 | "preopenapi-spec": "yarn run build", 33 | "openapi-spec": "node ./dist/openapi-spec", 34 | "prestart": "yarn run rebuild", 35 | "start": "export HOST=localhost && node -r source-map-support/register .", 36 | "clean": "lb-clean dist *.tsbuildinfo .eslintcache", 37 | "rebuild": "yarn run clean && yarn run build" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "" 42 | }, 43 | "author": "Bharat Kashyap ", 44 | "license": "ISC", 45 | "files": [ 46 | "README.md", 47 | "dist", 48 | "src", 49 | "!*/__tests__" 50 | ], 51 | "dependencies": { 52 | "@loopback/boot": "^3.4.1", 53 | "@loopback/core": "^2.16.1", 54 | "@loopback/repository": "^3.7.0", 55 | "@loopback/rest": "^9.3.1", 56 | "@loopback/rest-explorer": "^3.3.1", 57 | "@loopback/service-proxy": "^3.2.1", 58 | "axios": "^0.21.1", 59 | "loopback-connector-mongodb": "^5.2.3", 60 | "tslib": "^2.0.0" 61 | }, 62 | "devDependencies": { 63 | "@loopback/build": "^6.4.1", 64 | "@loopback/eslint-config": "^10.2.1", 65 | "@loopback/testlab": "^3.4.1", 66 | "@types/node": "^10.17.60", 67 | "eslint": "^7.28.0", 68 | "source-map-support": "^0.5.19", 69 | "typescript": "~4.3.2" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /portal/components/react-admin/layout/customUserMenu.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Typography, useMediaQuery, makeStyles } from "@material-ui/core"; 3 | import Avatar from "@material-ui/core/Avatar"; 4 | import Button from "@material-ui/core/Button"; 5 | import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown"; 6 | import Popover from "@material-ui/core/Popover"; 7 | import { useSession } from "next-auth/client"; 8 | import CustomLogoutButton from "./logoutButton"; 9 | 10 | const UserMenu = ({ logout }) => { 11 | const [session, loading] = useSession(); 12 | if (session) { 13 | return ; 14 | } 15 | return <>; 16 | }; 17 | 18 | const useStyles = makeStyles((theme) => ({ 19 | userFullName: { 20 | fontSize: "0.8rem", 21 | color: theme.palette.heading.light, 22 | marginLeft: "1rem", 23 | }, 24 | downArrow: { 25 | marginLeft: "0.5rem", 26 | fontSize: "1.2rem", 27 | transform: "translateY(-10%)", 28 | fontWeight: "400", 29 | }, 30 | })); 31 | 32 | const UserMenuComponent = ({ user, logout }) => { 33 | const [userMenu, setUserMenu] = React.useState(null); 34 | const isSmall = useMediaQuery((theme) => theme.breakpoints.down("sm")); 35 | const classes = useStyles(); 36 | 37 | const userMenuClick = (event) => { 38 | setUserMenu(event.currentTarget); 39 | }; 40 | 41 | const userMenuClose = () => { 42 | setUserMenu(null); 43 | }; 44 | return ( 45 |
46 | 62 | 63 | 79 | 80 | 81 |
82 | ); 83 | }; 84 | 85 | export default UserMenu; 86 | -------------------------------------------------------------------------------- /portal/components/react-admin/layout/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect, useDispatch } from "react-redux"; 3 | import { 4 | getResources, 5 | Notification, 6 | setSidebarVisibility, 7 | Sidebar, 8 | } from "react-admin"; 9 | import { makeStyles, useMediaQuery } from "@material-ui/core"; 10 | import CustomSidebar from "./customSidebar"; 11 | import AppBar from "./customAppBar"; 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | wrapper: { 15 | height: "100vh", 16 | display: "grid", 17 | gridTemplateRows: "8vh auto", 18 | [theme.breakpoints.down("sm")]: { 19 | gridTemplateColumns: "1fr", 20 | }, 21 | [theme.breakpoints.up("sm")]: { 22 | gridTemplateColumns: "18vw auto", 23 | }, 24 | }, 25 | sidebar: { 26 | gridColumn: "1 / 2", //Start End 27 | gridRow: "1 / 3", 28 | inset: "unset!important", 29 | "& > div": { 30 | display: "grid", 31 | gridTemplateColumns: "1fr", 32 | gridTemplateRows: "8vh auto", 33 | [theme.breakpoints.down("sm")]: { 34 | width: "70vw", 35 | }, 36 | [theme.breakpoints.up("sm")]: { 37 | width: "18vw", 38 | }, 39 | backgroundColor: theme.palette.grey.main, 40 | }, 41 | }, 42 | container: { 43 | display: "grid", 44 | gridTemplateRows: "9vh auto", 45 | gridTemplateColumns: "auto", 46 | "& > header": { 47 | position: "inherit", 48 | }, 49 | [theme.breakpoints.down("sm")]: { 50 | gridColumn: "1 / 3", 51 | }, 52 | [theme.breakpoints.up("sm")]: { 53 | gridColumn: "2 / 3", 54 | }, 55 | gridRow: "1 / 3", 56 | }, 57 | })); 58 | 59 | const CustomLayout = (props) => { 60 | const dispatch = useDispatch(); 61 | const classes = useStyles(); 62 | const { children, logout, open, title } = props; 63 | return ( 64 |
65 | 66 | 67 | 68 |
{ 70 | if (props.sidebarOpen) { 71 | dispatch(setSidebarVisibility(false)); 72 | } 73 | }} 74 | className={classes.container} 75 | > 76 | 77 |
{children}
78 |
79 | 80 |
81 | ); 82 | }; 83 | 84 | const mapStateToProps = (state) => ({ 85 | isLoading: state.admin.loading > 0, 86 | resources: getResources(state), 87 | sidebarOpen: state.admin.ui.sidebarOpen, 88 | }); 89 | export default connect(mapStateToProps, { setSidebarVisibility })(CustomLayout); 90 | -------------------------------------------------------------------------------- /portal/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: "Martel Sans", -apple-system, BlinkMacSystemFont, Segoe UI, 6 | Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, 7 | sans-serif; 8 | text-rendering: optimizeLegibility; 9 | } 10 | 11 | a { 12 | color: inherit; 13 | text-decoration: none; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | 20 | /* 21 | * FONT FACE 22 | */ 23 | @font-face { 24 | font-family: "Bahnschrift"; 25 | src: url("/Bahnschrift.otf"); 26 | font-style: normal; 27 | font-weight: 400; 28 | font-display: swap; 29 | } 30 | 31 | /** 32 | * ============= 33 | * CARD STYLES 34 | */ 35 | 36 | .card { 37 | margin: 1rem; 38 | padding: 1.5rem; 39 | text-align: left; 40 | color: inherit; 41 | text-decoration: none; 42 | border: 1px solid #eaeaea; 43 | border-radius: 10px; 44 | transition: color 0.15s ease, border-color 0.15s ease; 45 | } 46 | 47 | .logo-card { 48 | display: grid; 49 | grid-template-columns: 10% 90%; 50 | } 51 | 52 | @media screen and (min-width: 768px) { 53 | .card-center:last-child { 54 | transform: translateX(50%); 55 | } 56 | } 57 | 58 | .card:hover, 59 | .card:focus, 60 | .card:active { 61 | color: #0070f3; 62 | border-color: #0070f3; 63 | } 64 | 65 | .card h2 { 66 | margin: 0 0 1rem 0; 67 | font-size: 1rem; 68 | } 69 | 70 | .card p { 71 | margin: 0; 72 | font-size: 1.05rem; 73 | line-height: 1.5; 74 | } 75 | 76 | /** 77 | * UTIL CLASSES 78 | */ 79 | 80 | .center { 81 | transform: translate(50%); 82 | } 83 | 84 | .text-center { 85 | text-align: center; 86 | } 87 | 88 | .text-bold { 89 | font-weight: 700; 90 | } 91 | 92 | .capitalize { 93 | text-transform: capitalize; 94 | } 95 | 96 | .pointer:hover { 97 | cursor: pointer; 98 | } 99 | 100 | /** 101 | * ============= 102 | * REACT-ADMIN OVERRIDES 103 | */ 104 | 105 | .edit-page { 106 | margin: 1rem 2rem; 107 | } 108 | 109 | .edit-page > div { 110 | margin-top: 0; 111 | } 112 | 113 | .simple-form > div:last-child { 114 | height: 0rem; 115 | } 116 | 117 | /** 118 | * ============= 119 | * MEDIA QUERIES 120 | */ 121 | 122 | @media (max-width: 600px) { 123 | html { 124 | font-size: 75%; 125 | } 126 | 127 | .card > p { 128 | font-size: 1rem; 129 | } 130 | 131 | .card h2 { 132 | font-size: 1.4rem; 133 | } 134 | 135 | /* React Admin Override */ 136 | .edit-page { 137 | height: 100vh; 138 | overflow: scroll; 139 | } 140 | 141 | /* .grid { 142 | width: 100%; 143 | flex-direction: column; 144 | } */ 145 | } 146 | -------------------------------------------------------------------------------- /portal/components/react-admin/theme.js: -------------------------------------------------------------------------------- 1 | import { defaultTheme } from "react-admin"; 2 | import merge from "lodash/merge"; 3 | import red from "@material-ui/core/colors/red"; 4 | 5 | const primaryColour = { 6 | main: "#303765", 7 | }; 8 | 9 | const lavender = { 10 | main: "#E6E6FA", 11 | }; 12 | 13 | const darkGrey = { 14 | main: "#343A40", 15 | light: "#AEAEAE", 16 | hover: "#4C4C4C", 17 | darker: "#2D2D2D", 18 | }; 19 | 20 | const heading = { 21 | main: "#FAFAFAEE", 22 | light: "#FAFAFAAA", 23 | }; 24 | 25 | const customTheme = merge({}, defaultTheme, { 26 | palette: { 27 | primary: primaryColour, 28 | heading: heading, 29 | secondary: lavender, 30 | error: red, 31 | grey: darkGrey, 32 | contrastThreshold: 3, 33 | tonalOffset: 0.2, 34 | }, 35 | typography: { 36 | // Use the system font instead of the default Roboto font. 37 | fontFamily: [ 38 | "Bahnschrift", 39 | "-apple-system", 40 | "BlinkMacSystemFont", 41 | '"Segoe UI"', 42 | "Arial", 43 | "sans-serif", 44 | ].join(","), 45 | body1: { 46 | fontSize: "1.4rem", 47 | }, 48 | h6: { 49 | fontSize: "1.45rem", 50 | }, 51 | h5: { 52 | fontSize: "1.65rem", 53 | }, 54 | }, 55 | overrides: { 56 | MuiButton: { 57 | // override the styles of all instances of this component 58 | root: { 59 | // Name of the rule 60 | color: "white", // Some CSS 61 | }, 62 | }, 63 | MuiTableCell: { 64 | // Replace the font of the list items 65 | root: { 66 | fontFamily: "Bahnschrift", 67 | }, 68 | head: { 69 | // Add styling for the heading row in lists 70 | color: "#00000058", 71 | fontSize: "0.8rem", 72 | fontWeight: "bold", 73 | borderBottom: "1px solid lavender", 74 | textTransform: "uppercase", 75 | backgroundColor: "#F8FAFC", 76 | }, 77 | }, 78 | MuiToolbar: { 79 | regular: { 80 | minHeight: "10vh", 81 | }, 82 | }, 83 | MuiFilledInput: { 84 | root: { 85 | backgroundColor: "#F8FAFC", 86 | }, 87 | input: { 88 | fontSize: "0.9rem", 89 | }, 90 | }, 91 | MuiMenuItem: { 92 | root: { 93 | fontSize: "0.9rem", 94 | }, 95 | }, 96 | MuiTableBody: { 97 | root: { 98 | "& > tr:nth-child(odd)": { 99 | backgroundColor: lavender.main, 100 | }, 101 | }, 102 | }, 103 | MuiList: { 104 | root: { 105 | "& > a:nth-child(odd) > div": { 106 | backgroundColor: lavender.main, 107 | }, 108 | }, 109 | }, 110 | }, 111 | }); 112 | 113 | export default customTheme; 114 | -------------------------------------------------------------------------------- /portal/components/layout.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Image from "next/image"; 3 | import { useState, useEffect } from "react"; 4 | import styles from "../styles/layout.module.css"; 5 | 6 | const Layout = ({ children, home }) => { 7 | const transitionStages = { 8 | FADE_OUT: "fadeOut", 9 | FADE_IN: "fadeIn", 10 | }; 11 | 12 | const [activeChildren, setActiveChildren] = useState(children); 13 | const [transitionStage, setTransitionStage] = useState( 14 | transitionStages.FADE_OUT 15 | ); 16 | 17 | const compareElem = (a, b) => { 18 | return a.type.name === b.type.name; 19 | }; 20 | 21 | const transitionEnd = () => { 22 | if (transitionStage === transitionStages.FADE_OUT) { 23 | setActiveChildren(children); 24 | setTransitionStage(transitionStages.FADE_IN); 25 | } 26 | }; 27 | 28 | useEffect(() => { 29 | setTransitionStage(transitionStages.FADE_IN); 30 | }, [transitionStages.FADE_IN]); 31 | 32 | useEffect(() => { 33 | if (!compareElem(children, activeChildren)) 34 | setTransitionStage(transitionStages.FADE_OUT); 35 | }, [transitionStages.FADE_OUT, children, activeChildren]); 36 | 37 | return ( 38 | <> 39 | 40 | समर्थ हिमाचल 41 | 42 | 43 |
44 |
45 |

Rozgar Saathi

46 | {/*

Bacchon ka sahara, phone humara

47 |

48 | An initiative of the Government of Himachal Pradesh, India 49 |

*/} 50 |
51 |
{ 53 | transitionEnd(); 54 | }} 55 | className={`${styles.main} ${styles[transitionStage]}`} 56 | > 57 | {activeChildren} 58 |
59 | 60 | For more details, contact 1800-180-8190{" "} 61 | 62 |
63 |
64 | HP Govt Logo 70 | 71 | State Project Office (Samagra Shiksha), Directorate Education, 72 | DPEP Bhawan, Below Old ISBT, Lalpani, Shimla - 171001 73 | 74 | SSA Logo 81 |
82 |
83 |
84 | 85 | ); 86 | }; 87 | 88 | export default Layout; 89 | -------------------------------------------------------------------------------- /portal/styles/layout.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | align-items: center; 5 | height: 100vh; 6 | display: grid; 7 | grid-template-rows: auto; 8 | } 9 | 10 | .header { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | margin-top: 1rem; 16 | } 17 | 18 | .headerLogos { 19 | display: flex; 20 | flex-direction: row; 21 | width: 70vw; 22 | justify-content: space-around; 23 | } 24 | 25 | .main { 26 | opacity: 0; 27 | transition-property: opacity; 28 | transition-timing-function: ease-in; 29 | transition-delay: 0ms; 30 | transition-duration: 500ms; 31 | } 32 | 33 | .footer { 34 | width: 100%; 35 | border-top: 1px solid #eaeaea; 36 | display: flex; 37 | flex-direction: column; 38 | justify-content: center; 39 | align-items: center; 40 | } 41 | 42 | .footer a { 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | flex-grow: 1; 47 | } 48 | 49 | .title a, 50 | .title span { 51 | color: #0070f3; 52 | text-decoration: none; 53 | } 54 | 55 | .title a:hover, 56 | .title a:focus, 57 | .title a:active { 58 | text-decoration: underline; 59 | } 60 | 61 | .title { 62 | margin: 0; 63 | line-height: 1.15; 64 | font-size: 2.75rem; 65 | color: #303765; 66 | margin-bottom: 0.2rem; 67 | } 68 | .subtitle { 69 | margin: 0; 70 | line-height: 1.15; 71 | font-size: 1.6rem; 72 | color: #000; 73 | font-weight: 400; 74 | margin-bottom: 0.4rem; 75 | } 76 | .subsubtitle { 77 | margin: 0rem; 78 | line-height: 1.15; 79 | font-size: 1rem; 80 | color: #000; 81 | font-weight: 400; 82 | font-style: italic; 83 | } 84 | 85 | .title, 86 | .description { 87 | text-align: center; 88 | } 89 | 90 | .logo { 91 | display: flex; 92 | flex-direction: row; 93 | justify-content: space-between; 94 | } 95 | 96 | .credit { 97 | font-weight: 700; 98 | text-align: center; 99 | font-size: 1rem; 100 | } 101 | 102 | .address { 103 | text-align: center; 104 | font-size: 0.8rem; 105 | opacity: 0.65; 106 | font-weight: 500; 107 | } 108 | 109 | /** 110 | * ============= 111 | * TRANSITION STYLES 112 | */ 113 | 114 | .fadeIn { 115 | opacity: 1; 116 | } 117 | 118 | .fadeOut { 119 | } 120 | /** 121 | * ============= 122 | * MEDIA QUERIES 123 | */ 124 | 125 | @media (max-width: 1600px) { 126 | .logo { 127 | width: 60vw; 128 | margin-top: 1rem; 129 | } 130 | .address { 131 | width: 30vw; 132 | } 133 | } 134 | 135 | @media (max-width: 768px) { 136 | .headerLogos { 137 | width: 100%; 138 | justify-content: space-between; 139 | } 140 | 141 | .logo { 142 | width: 100vw; 143 | } 144 | 145 | .face { 146 | transform: scale(0.8); 147 | } 148 | 149 | .logo img { 150 | transform: scale(0.625); 151 | } 152 | .address { 153 | width: 80vw; 154 | font-size: 0.7rem; 155 | transform: translateX(10px); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /portal/pages/api/track.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const handler = async (req, res) => { 4 | if (req.method === "POST") { 5 | try { 6 | const { captcha, captchaToken } = req.body; 7 | const responseObjectCaptcha = await captchaVerify(captcha, captchaToken); 8 | const { id } = req.body; 9 | const responseObject = await startFetchTrackDevice(id); 10 | if (responseObject?.errors) { 11 | res 12 | .status(500) 13 | .json({ error: responseObject?.errors?.[0]?.message, success: null }); 14 | } else if (responseObject?.data) { 15 | if (responseObject?.data?.["device_donation_donor"]?.length) { 16 | res.status(200).json({ 17 | error: null, 18 | success: { 19 | data: maskPhoneNumber( 20 | responseObject.data["device_donation_donor"] 21 | ), 22 | }, 23 | }); 24 | } else 25 | res.status(200).json({ 26 | error: 27 | "No device found with this ID/ इस आईडी से कोई फ़ोन नहीं मिला!", 28 | success: null, 29 | }); 30 | } 31 | } catch (e) { 32 | res 33 | .status(200) 34 | .json({ error: "Incorect Captcha/ Captcha कोड गलत है!", success: e }); 35 | } 36 | } 37 | }; 38 | 39 | function maskPhoneNumber(array) { 40 | const obj = array[0]; 41 | let { phone_number } = obj; 42 | phone_number = `******${phone_number.slice(6)}`; 43 | obj.phone_number = phone_number; 44 | return obj; 45 | } 46 | 47 | async function captchaVerify(captcha, captchaToken) { 48 | const result = await axios({ 49 | method: "POST", 50 | url: `${process.env.NEXT_PUBLIC_CAPTCHA_URL}`, 51 | data: { 52 | captcha: captcha, 53 | token: captchaToken, 54 | }, 55 | }); 56 | 57 | return result; 58 | } 59 | 60 | async function fetchGraphQL(operationsDoc, operationName, variables) { 61 | const result = await axios({ 62 | method: "POST", 63 | headers: { 64 | "x-hasura-admin-secret": "2OWslm5aAjlTARU", 65 | }, 66 | url: "http://143.110.186.108:5001/v1/graphql", 67 | data: { 68 | query: operationsDoc, 69 | variables: variables, 70 | operationName: operationName, 71 | }, 72 | }); 73 | 74 | return await result; 75 | } 76 | 77 | const operationsDoc = ` 78 | query trackDevice($trackingKey: String) { 79 | device_donation_donor(where: {device_tracking_key: {_eq: $trackingKey}}) { 80 | delivery_status 81 | phone_number 82 | name 83 | recipient_school { 84 | udise 85 | } 86 | } 87 | } 88 | `; 89 | 90 | function fetchTrackDevice(trackingKey) { 91 | return fetchGraphQL(operationsDoc, "trackDevice", { 92 | trackingKey: trackingKey, 93 | }); 94 | } 95 | 96 | async function startFetchTrackDevice(trackingKey) { 97 | const response = await fetchTrackDevice(trackingKey); 98 | 99 | return response.data; 100 | } 101 | 102 | export default handler; 103 | -------------------------------------------------------------------------------- /portal/components/react-admin/app.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { AdminContext, AdminUI, Resource, useDataProvider } from "react-admin"; 3 | import buildHasuraProvider, { buildFields } from "ra-data-hasura"; 4 | import { ApolloClient, InMemoryCache } from "@apollo/client"; 5 | import { useSession } from "next-auth/client"; 6 | import { MuiThemeProvider, createMuiTheme } from "@material-ui/core"; 7 | import customTheme from "./theme"; 8 | import customLayout from "./layout/"; 9 | import customFields from "./customHasura/customFields"; 10 | import customVariables from "./customHasura/customVariables"; 11 | import { resourceConfig } from "./layout/config"; 12 | 13 | const App = () => { 14 | const [dataProvider, setDataProvider] = useState(null); 15 | const [apolloClient, setApolloClient] = useState(null); 16 | const [session] = useSession(); 17 | 18 | useEffect(() => { 19 | // console.log("ENtered app.js",process.env.NEXT_PUBLIC_HASURA_URL) 20 | const hasuraHeaders = {}; 21 | hasuraHeaders.Authorization = `Bearer ${session.jwt}`; 22 | if (session.role) hasuraHeaders["x-hasura-role"] = session.role; 23 | 24 | let tempClient = new ApolloClient({ 25 | uri: process.env.NEXT_PUBLIC_HASURA_URL, 26 | cache: new InMemoryCache(), 27 | headers: hasuraHeaders, 28 | }); 29 | // console.log("temp Client:", tempClient) 30 | async function buildDataProvider() { 31 | const hasuraProvider = await buildHasuraProvider( 32 | { client: tempClient }, 33 | { 34 | buildFields: customFields, 35 | }, 36 | customVariables 37 | ); 38 | setDataProvider(() => hasuraProvider); 39 | setApolloClient(tempClient); 40 | } 41 | buildDataProvider(); 42 | }, [session]); 43 | 44 | if (!dataProvider || !apolloClient) return null; 45 | return ( 46 | 47 | 48 | 49 | ); 50 | }; 51 | function AsyncResources({ client }) { 52 | let introspectionResultObjects = 53 | client.cache?.data?.data?.ROOT_QUERY?.__schema.types 54 | ?.filter((obj) => obj.kind === "OBJECT") 55 | ?.map((elem) => elem.name); 56 | const resources = resourceConfig; 57 | let filteredResources = resources; 58 | if (introspectionResultObjects) { 59 | filteredResources = resources.filter((elem) => 60 | introspectionResultObjects.includes(elem.name) 61 | ); 62 | console.log("introspectionResultObjects", filteredResources); 63 | } 64 | if (!resources) return null; 65 | return ( 66 | 67 | 68 | {filteredResources.map((resource) => ( 69 | 77 | ))} 78 | 79 | 80 | ); 81 | } 82 | 83 | export default App; 84 | -------------------------------------------------------------------------------- /portal/components/login/login.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { useToasts } from "react-toast-notifications"; 3 | import { signIn } from "next-auth/client"; 4 | import { useRouter } from "next/router"; 5 | import controls from "./form.config"; 6 | import styles from "../../styles/Login.module.css"; 7 | 8 | export default function Login(props) { 9 | const { persona } = props; 10 | const [input, setInput] = useState({}); 11 | const router = useRouter(); 12 | const [inputValidity, setInputValidity] = useState( 13 | controls.map((control) => { 14 | return { 15 | [control.name]: false, 16 | }; 17 | }) 18 | ); 19 | const [formValidity, setFormValidity] = useState(false); 20 | 21 | const handleInput = (e) => { 22 | setInput({ ...input, [e.target.name]: e.target.value }); 23 | setInputValidity({ 24 | ...inputValidity, 25 | [e.target.name]: e.target.validity.valid, 26 | }); 27 | }; 28 | 29 | useEffect(() => { 30 | let validity = controls.reduce( 31 | (acc, control) => (acc = acc && inputValidity[control.name]), 32 | true 33 | ); 34 | setFormValidity(validity); 35 | }, [inputValidity]); 36 | 37 | const { addToast } = useToasts(); 38 | 39 | const signUserIn = async (e) => { 40 | e.preventDefault(); 41 | console.log("person:", persona); 42 | console.log( 43 | "Env Variables", 44 | `${process.env.NEXT_PUBLIC_URL}/${persona.redirectUrl}` 45 | ); 46 | const { error, url } = await signIn("fusionauth", { 47 | loginId: input.username, 48 | password: input.password, 49 | applicationId: persona.applicationId, 50 | redirect: false, 51 | callbackUrl: `${ 52 | persona.redirectUrl.search("http") < 0 53 | ? `${process.env.NEXT_PUBLIC_URL}/${persona.redirectUrl}` 54 | : persona.redirectUrl 55 | }`, 56 | }); 57 | if (url) { 58 | router.push(url); 59 | } 60 | if (error) { 61 | addToast(error, { appearance: "error" }); 62 | } 63 | }; 64 | 65 | return ( 66 |
67 | 68 | Log in as {persona.en}/
69 | {persona.hi} लॉग इन 70 |
71 | 72 | {persona.credentials} के यूज़र 73 | नाम/पासवर्ड से लॉग इन कीजिए 74 | 75 |
76 | {controls.map((control) => ( 77 | 87 | ))} 88 | 95 |
96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /portal/components/react-admin/layout/customSidebar.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import { ListSubheader } from "@material-ui/core"; 5 | import clsx from "clsx"; 6 | import UserSidebarHeader from "./sidebarHeader"; 7 | import VerticalCollapse from "./verticalCollapse"; 8 | import VerticalItem from "./verticalItem"; 9 | import { resourceConfig } from "./config"; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | listTitle: { 13 | fontSize: "0.9rem", 14 | textTransform: "uppercase", 15 | textAlign: "center", 16 | fontWeight: "700", 17 | color: theme.palette.grey[500], 18 | }, 19 | sidebarHeader: { 20 | backgroundColor: theme.palette.grey[700], 21 | "& > div": { 22 | marginTop: "1ch;", 23 | }, 24 | }, 25 | sidebarList: { 26 | display: "flex", 27 | flexDirection: "column", 28 | marginTop: "1rem", 29 | }, 30 | })); 31 | 32 | const CustomSidebar = (props) => { 33 | const [activePath, setActivePath] = useState(null); 34 | const { location, resources } = props; 35 | 36 | let filteredResources = resourceConfig; 37 | if (props.resources) { 38 | filteredResources = resourceConfig?.filter( 39 | (configResource) => 40 | (resources?.some( 41 | (resource) => resource?.name === configResource?.name 42 | ) && 43 | configResource.label) || 44 | configResource.title 45 | ); 46 | } 47 | useEffect(() => { 48 | const pathname = location.pathname.replace(/\//, ""); 49 | if (activePath !== pathname) { 50 | setActivePath(pathname); 51 | } 52 | }, [location, activePath]); 53 | 54 | return ( 55 | 59 | ); 60 | }; 61 | 62 | const SidebarWrapper = React.memo(function SidebarWrapper({ 63 | activePath, 64 | filteredResources, 65 | }) { 66 | const classes = useStyles(); 67 | return ( 68 | <> 69 | 70 |
71 | {filteredResources.map((item, index) => { 72 | if (item.title) 73 | return ( 74 | 75 | {item.label} 76 | 77 | ); 78 | if (item.children) { 79 | return ( 80 | 86 | ); 87 | } 88 | return ( 89 | 95 | ); 96 | })} 97 |
98 | 99 | ); 100 | }); 101 | 102 | export default withRouter((props) => ); 103 | -------------------------------------------------------------------------------- /captcha-service/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Captcha PNG img generator 3 | * @Author: Chakshu Gautam 4 | * @Email: chaks.gautam@gmail.com 5 | * @Version: 2.0 6 | * @Date: 2020-08-18 7 | * @license http://www.opensource.org/licenses/bsd-license.php BSD License 8 | */ 9 | const { v1: uuid } = require("uuid"); 10 | 11 | const captchapng = require("./captchapng"); 12 | const cors = require("cors"); 13 | 14 | const express = require("express"); 15 | const app = express(); 16 | const port = process.env.PORT || 9000; 17 | const host = process.env.PORT || "0.0.0.0"; 18 | 19 | const low = require("lowdb"); 20 | const FileSync = require("lowdb/adapters/FileSync"); 21 | const adapter = new FileSync("db.json"); 22 | const db = low(adapter); 23 | app.use( 24 | cors({ 25 | origin: "*", 26 | optionsSuccessStatus: 200, 27 | }) 28 | ); 29 | 30 | db.defaults({ captchas: [] }).write(); 31 | 32 | app.listen(port, host, () => console.log(`listening on port: ${port}`)); 33 | 34 | app.get("/", (request, response) => { 35 | const captchaParsed = parseInt(Math.random() * 900000 + 100000); 36 | var p = new captchapng(120, 30, captchaParsed); // width,height,numeric captcha 37 | p.color(0, 0, 0, 0); // First color: background (red, green, blue, alpha) 38 | p.color(80, 80, 80, 255); // Second color: paint (red, green, blue, alpha) 39 | 40 | var img = p.getBase64(); 41 | // var imgbase64 = new Buffer(img,'base64'); 42 | 43 | const token = uuid(); 44 | db.get("captchas") 45 | .push({ token, captchaParsed, timestamp: Math.floor(Date.now() / 1000) }) 46 | .write(); 47 | console.log({ token }); 48 | 49 | response.header({ 50 | "Content-Type": "application/json", 51 | token, 52 | "access-control-expose-headers": "token", 53 | }); 54 | 55 | // Request token 56 | // response.send(imgbase64); 57 | response.json({ blob: img }); 58 | }); 59 | 60 | const removeOldCaptchas = () => { 61 | // Delete all captchas that are more than 600 seconds old. 62 | const now = Math.floor(Date.now() / 1000); 63 | const allData = db.get("captchas").value(); 64 | 65 | allData 66 | .filter((captcha) => now - captcha.timestamp > 600) 67 | .forEach((filteredCaptcha) => { 68 | db.get("captchas").remove({ token: filteredCaptcha.token }).write(); 69 | }); 70 | }; 71 | 72 | app.get("/verify", (request, response) => { 73 | removeOldCaptchas(); 74 | 75 | try { 76 | const userResponse = request.query.captcha; 77 | const token = request.query.token; 78 | const captcha = db.get("captchas").find({ token: token }).value(); 79 | 80 | if (!captcha) { 81 | response.status(400).send({ status: "Token Not Found" }); 82 | return; 83 | } 84 | 85 | deleteUsedCaptcha(token); 86 | if (parseInt(userResponse) === (captcha && captcha.captchaParsed)) { 87 | response.status(200).send({ status: "Success" }); 88 | } else { 89 | response.status(400).send({ status: "Code Incorrect" }); 90 | } 91 | } catch (err) { 92 | console.log({ err }); 93 | response.status(500).send({ status: "Internal Server Error" }); 94 | } 95 | }); 96 | function deleteUsedCaptcha(token) { 97 | db.get("captchas").remove({ token: token }).write(); 98 | } 99 | -------------------------------------------------------------------------------- /certificate/certificate-template-final.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 115 | 116 | 119 | 120 |
121 | 122 |
123 | 124 |
is presented to
125 |
126 |
127 | for their contribution to a child’s online learning through the donation 128 | of a smartphone under the ‘Digital Saathi’ program 129 |
130 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /api/src/controllers/donate-device-graphQL-model.ts: -------------------------------------------------------------------------------- 1 | import pincodeToBlock from '../datasources/pincodeToBlock'; 2 | export class DonateDevice { 3 | name?: string | null; 4 | phone_number?: string | null; 5 | created_at?: string | null; 6 | state_ut?: string | null; 7 | district?: string | null; 8 | block?: string | null; 9 | other_district?: null; 10 | address?: string | null; 11 | landmark?: string | null; 12 | pincode?: string | null; 13 | delivery_mode?: string | null; 14 | delivery_mode_outside_HP?: string | null; 15 | declaration_acknowledgement?: string | null; 16 | device_company?: string | null; 17 | device_other_model?: string | null; 18 | device_model?: string | null; 19 | device_size?: string | null; 20 | device_age?: number | null; 21 | device_condition?: string | null; 22 | call_function?: boolean | null; 23 | wa_function?: boolean | null; 24 | yt_function?: boolean | null; 25 | charger_available?: boolean | null; 26 | final_declaration?: string | null; 27 | device_tracking_key?: string | null; 28 | delivery_status?: string | null; 29 | 30 | operationsDoc = ` 31 | mutation insertDonor($donor: device_donation_donor_insert_input!) { 32 | insert_device_donation_donor_one(object: $donor) { 33 | id 34 | phone_number 35 | device_tracking_key 36 | } 37 | } 38 | `; 39 | variableName = `donor`; 40 | operationName = `insertDonor`; 41 | databaseOperationName = `insert_device_donation_donor_one`; 42 | 43 | constructor(data: any) { 44 | this.name = data?.name ?? null; 45 | this.phone_number = data.contact ?? null; 46 | this.created_at = data['*meta-submission-date*'] ?? null; 47 | this.state_ut = data.state ?? null; 48 | this.district = data.district ?? null; 49 | this.block = this.fetchBlock(this.convertToString(data.pincode)) ?? null; 50 | this.other_district = data.otherdistrict ?? null; 51 | this.address = data.address ?? null; 52 | this.landmark = data.landmark ?? null; 53 | this.pincode = this.convertToString(data.pincode) ?? null; 54 | this.delivery_mode = data.delivery ?? null; 55 | this.declaration_acknowledgement = data.declaration ?? null; 56 | this.delivery_mode_outside_HP = data.deliverynonhp ?? null; 57 | this.device_company = data.company ?? null; 58 | this.device_other_model = data.companyother ?? null; 59 | this.device_model = data.modelname ?? null; 60 | this.device_size = data.screen ?? null; 61 | this.device_age = data.years ?? null; 62 | this.device_condition = data.condition ?? null; 63 | this.call_function = this.convertToBoolean(data.calls) ?? null; 64 | this.wa_function = this.convertToBoolean(data.wa) ?? null; 65 | this.yt_function = this.convertToBoolean(data.youtube) ?? null; 66 | this.charger_available = this.convertToBoolean(data.charger) ?? null; 67 | this.final_declaration = data.finalDecalaration ?? null; 68 | this.device_tracking_key = data.trackingKey ?? null; 69 | this.delivery_status = 'no-action-taken'; 70 | } 71 | 72 | fetchBlock(pincode: string | null): string | null { 73 | if (!pincode) return null; 74 | const pincodeBlockMapping = new pincodeToBlock(); 75 | if (!pincodeBlockMapping.mapping[pincode]) return 'OTHER'; 76 | return pincodeBlockMapping.mapping[pincode]; 77 | } 78 | 79 | convertToBoolean(response: string): boolean { 80 | if (response?.charAt(response.length - 1) === 'y') return true; 81 | else return false; 82 | } 83 | 84 | convertToString(response: number): string | null { 85 | if (response) { 86 | return String(response); 87 | } 88 | return null; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /certificate/server.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const fs = require("fs").promises; 3 | const puppeteer = require("puppeteer"); 4 | const QRCode = require("qrcode"); 5 | const crypto = require("crypto"); 6 | 7 | const fastify = require("fastify")({ 8 | logger: true, 9 | }); 10 | 11 | fastify.listen(process.env.PORT, process.env.HOST, (err, address) => { 12 | if (err) throw err; 13 | // Server is now listening on ${address} 14 | }); 15 | 16 | fastify.post("/", async (request, reply) => { 17 | const { body = {} } = request; 18 | const { name = "", trackingKey = "", udise = "" } = body; 19 | const pdf = await printPdf(name, trackingKey, udise); 20 | reply.code(200).send({ base64String: pdf }); 21 | }); 22 | 23 | async function printPdf(name, trackingKey, udise) { 24 | const template = await fs.readFile( 25 | "./certificate-template-final.html", 26 | "utf-8" 27 | ); 28 | const base64logo = await fs.readFile("./assets/cert-logo.png", "base64"); 29 | const base64footer = await fs.readFile("./assets/cert-footer.png", "base64"); 30 | const base64bahnschrift = await fs.readFile( 31 | "./assets/bahnschrift.ttf", 32 | "base64" 33 | ); 34 | const privateKey = await fs.readFile("./jwtRS256_digitalsaathi.key", "utf-8"); 35 | const payload = { 36 | sub: `did:tracking-key:${trackingKey}`, 37 | jti: crypto.randomBytes(16).toString("hex"), 38 | iss: "https://hpdigitalsaathi.in", 39 | vc: { 40 | "@context": [ 41 | "https://www.w3.org/2018/credentials/v1", 42 | "https://www.w3.org/2018/credentials/examples/v1", 43 | ], 44 | type: ["VerifiableCredential", "DeviceDonorCredential"], 45 | credentialSubject: { 46 | donor: { 47 | trackingKey: trackingKey, 48 | name: name, 49 | recipient: { 50 | udise: udise, 51 | }, 52 | }, 53 | }, 54 | }, 55 | }; 56 | const token = jwt.sign(payload, privateKey, { 57 | algorithm: "RS256", 58 | expiresIn: "10y", 59 | notBefore: "60", 60 | }); 61 | const generateQR = async (text) => { 62 | try { 63 | return await QRCode.toDataURL(text, { scale: 2 }); 64 | } catch (err) { 65 | console.error(err); 66 | } 67 | }; 68 | const qrcode = await generateQR(token); 69 | const browser = await puppeteer.launch({ 70 | headless: "true", 71 | executablePath: "/usr/bin/chromium-browser", 72 | args: [ 73 | "--no-sandbox", 74 | "--disable-setuid-sandbox", 75 | "--disable-gpu", 76 | // This will write shared memory files into /tmp instead of /dev/shm, 77 | // because Docker’s default for /dev/shm is 64MB 78 | "--disable-dev-shm-usage", 79 | ], 80 | }); 81 | const page = await browser.newPage(); 82 | await page.setContent(template); 83 | await page.evaluate( 84 | (name, qrcode, base64logo, base64footer, base64bahnschrift) => { 85 | donor = document.querySelector("#donor"); 86 | qr = document.querySelector("#qrcode"); 87 | logo = document.querySelector("#logo"); 88 | footer = document.querySelector("#footer"); 89 | 90 | const style = document.createElement("style"); 91 | style.appendChild( 92 | document.createTextNode(` 93 | @font-face { 94 | font-family: Bahnschrift; 95 | src: url('data:font/ttf;base64,${base64bahnschrift}'); 96 | }`) 97 | ); 98 | document.head.appendChild(style); 99 | 100 | donor.innerHTML = `${name}`; 101 | qr.src = qrcode; 102 | logo.src = `data:image/png;base64,${base64logo}`; 103 | footer.src = `data:image/png;base64,${base64footer}`; 104 | }, 105 | name, 106 | qrcode, 107 | base64logo, 108 | base64footer, 109 | base64bahnschrift 110 | ); 111 | await page.evaluateHandle("document.fonts.ready"); 112 | const buffer = await page.pdf({ printBackground: true }); 113 | const base64 = buffer.toString("base64"); 114 | await browser.close(); 115 | return base64; 116 | } 117 | -------------------------------------------------------------------------------- /portal/components/react-admin/layout/verticalCollapse.js: -------------------------------------------------------------------------------- 1 | import React, { createElement, useEffect, useState } from "react"; 2 | import { 3 | Collapse, 4 | IconButton, 5 | ListItem, 6 | ListItemText, 7 | } from "@material-ui/core"; 8 | import { makeStyles } from "@material-ui/core/styles"; 9 | import clsx from "clsx"; 10 | import VerticalItem from "./verticalItem"; 11 | import { KeyboardArrowDownIcon, KeyboardArrowUpIcon } from "@material-ui/icons"; 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | root: { 15 | padding: 0, 16 | "&.open": { 17 | backgroundColor: 18 | theme.palette.type === "dark" 19 | ? "rgba(255,255,255,.015)" 20 | : "rgba(0,0,0,.05)", 21 | }, 22 | item: (props) => ({ 23 | height: 40, 24 | width: "calc(100% - 16px)", 25 | borderRadius: "0 20px 20px 0", 26 | paddingRight: 12, 27 | flex: 1, 28 | paddingLeft: props.itemPadding > 60 ? 60 : props.itemPadding, 29 | color: theme.palette.text.primary, 30 | "&.active > .list-item-text > span": { 31 | fontWeight: 600, 32 | }, 33 | "& .list-item-icon": { 34 | marginRight: 16, 35 | }, 36 | }), 37 | }, 38 | })); 39 | 40 | const isUrlInChildren = (parent, url) => { 41 | if (!parent.children) { 42 | return false; 43 | } 44 | 45 | for (let i = 0; i < parent.children.length; i += 1) { 46 | if (parent.children[i].children) { 47 | if (isUrlInChildren(parent.children[i], url)) { 48 | return true; 49 | } 50 | } 51 | 52 | if ( 53 | parent.children[i].url === url || 54 | url.includes(parent.children[i].url) 55 | ) { 56 | return true; 57 | } 58 | } 59 | 60 | return false; 61 | }; 62 | 63 | const needsToBeOpened = (location, item) => { 64 | return location && isUrlInChildren(item, location.pathname); 65 | }; 66 | 67 | function VerticalCollapse({ activePath, ...props }) { 68 | const [open, setOpen] = useState(() => 69 | needsToBeOpened(window.location, props.item) 70 | ); 71 | const { item, nestedLevel, publicity } = props; 72 | 73 | const classes = useStyles({ 74 | itemPadding: nestedLevel > 0 ? 40 + nestedLevel * 16 : 24, 75 | }); 76 | 77 | useEffect(() => { 78 | if (needsToBeOpened(window.location, item)) { 79 | setOpen(true); 80 | } 81 | }, [item]); 82 | 83 | function handleClick() { 84 | setOpen(!open); 85 | } 86 | 87 | return ( 88 |
    89 | 99 | {/* {item.icon && createElement(CustomIcons[item.icon])} */} 100 | 105 | 106 | ev.preventDefault()} 111 | > 112 | {createElement(!open ? KeyboardArrowDownIcon : KeyboardArrowUpIcon)} 113 | 114 | 115 | 116 | {item.children && ( 117 | 122 | {item.children.map((i, index) => { 123 | if (i.children) { 124 | return ( 125 | 133 | ); 134 | } 135 | return ( 136 | 142 | ); 143 | })} 144 | 145 | )} 146 |
147 | ); 148 | } 149 | 150 | export default VerticalCollapse; 151 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | # postgres: 5 | # image: postgres:11.9-alpine 6 | # environment: 7 | # PGDATA: /var/lib/postgresql/data/pgdata 8 | # POSTGRES_USER: ${POSTGRES_USER} 9 | # POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 10 | # # Un-comment to access the db service directly 11 | # # ports: 12 | # # - 5432:5432 13 | # restart: unless-stopped 14 | # volumes: 15 | # - db_data:/var/lib/postgresql/data 16 | 17 | # fusionauth: 18 | # image: fusionauth/fusionauth-app:latest 19 | # depends_on: 20 | # - postgres 21 | # environment: 22 | # DATABASE_URL: jdbc:postgresql://postgres:5432/fusionauth 23 | # # Prior to version 1.19.0, use this deprecated name 24 | # # DATABASE_ROOT_USER: ${POSTGRES_USER} 25 | # DATABASE_ROOT_USERNAME: ${POSTGRES_USER} 26 | # DATABASE_ROOT_PASSWORD: ${POSTGRES_PASSWORD} 27 | # # Prior to version 1.19.0, use this deprecated name 28 | # # DATABASE_USER: ${DATABASE_USER} 29 | # DATABASE_USERNAME: ${DATABASE_USERNAME} 30 | # DATABASE_PASSWORD: ${DATABASE_PASSWORD} 31 | # # Prior to version 1.19.0, use this deprecated names 32 | # # FUSIONAUTH_MEMORY: ${FUSIONAUTH_MEMORY} 33 | # # FUSIONAUTH_SEARCH_ENGINE_TYPE: database 34 | # # FUSIONAUTH_URL: http://fusionauth:9011 35 | # # FUSIONAUTH_RUNTIME_MODE: development 36 | # FUSIONAUTH_APP_MEMORY: ${FUSIONAUTH_APP_MEMORY} 37 | # FUSIONAUTH_APP_RUNTIME_MODE: development 38 | # FUSIONAUTH_APP_URL: http://fusionauth:9011 39 | # SEARCH_TYPE: database 40 | 41 | # restart: unless-stopped 42 | # ports: 43 | # - 9011:9011 44 | # volumes: 45 | # - fa_config:/usr/local/fusionauth/config 46 | 47 | # graphql-engine: 48 | # image: hasura/graphql-engine:v2.0.0-beta.2 49 | # ports: 50 | # - "8080:8080" 51 | # depends_on: 52 | # - postgres 53 | # restart: always 54 | # environment: 55 | # HASURA_GRAPHQL_DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/postgres 56 | # ## enable the console served by server 57 | # HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console 58 | # ## enable debugging mode. It is recommended to disable this in production 59 | # HASURA_GRAPHQL_DEV_MODE: "true" 60 | # HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log 61 | # ## uncomment next line to set an admin secret 62 | # HASURA_GRAPHQL_ADMIN_SECRET: S#CREtp@55W0rd 63 | # HASURA_GRAPHQL_JWT_SECRET: '{"type": "RS512", "jwk_url": "http://fusionauth:9011/.well-known/jwks"}' 64 | 65 | mongo: 66 | image: mongo 67 | ports: 68 | - "27017:27017" 69 | environment: 70 | MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER} 71 | MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD} 72 | 73 | api: 74 | build: 75 | context: ./api 76 | depends_on: 77 | - mongo 78 | environment: 79 | MONGO_DATASOURCE_USER: ${MONGO_USER} 80 | MONGO_DATASOURCE_PASSWORD: ${MONGO_PASSWORD} 81 | HASURA_URL: ${HASURA_URL} 82 | HASURA_ADMIN_SECRET: ${HASURA_ADMIN_SECRET} 83 | GUPSHUP_USERNAME: ${GUPSHUP_USERNAME} 84 | GUPSHUP_PASSWORD: ${GUPSHUP_PASSWORD} 85 | GUPSHUP_PRINCIPAL_ENTITY_ID: ${GUPSHUP_PRINCIPAL_ENTITY_ID} 86 | SLACK_ADMIN_LOGGER_AUTH_TOKEN: ${SLACK_ADMIN_LOGGER_AUTH_TOKEN} 87 | SLACK_ADMIN_LOGS_CHANNEL_ID: ${SLACK_ADMIN_LOGS_CHANNEL_ID} 88 | ports: 89 | - "3001:3000" 90 | 91 | portal: 92 | build: 93 | context: ./portal 94 | ports: 95 | - "3000:3000" 96 | environment: 97 | HASURA_URL: ${HASURA_URL} 98 | HASURA_ADMIN_SECRET: ${HASURA_ADMIN_SECRET} 99 | FUSIONAUTH_DOMAIN: ${FUSIONAUTH_DOMAIN} 100 | FUSIONAUTH_API_KEY: ${FUSIONAUTH_API_KEY} 101 | GUPSHUP_USERNAME: ${GUPSHUP_USERNAME} 102 | GUPSHUP_PASSWORD: ${GUPSHUP_PASSWORD} 103 | GUPSHUP_PRINCIPAL_ENTITY_ID: ${GUPSHUP_PRINCIPAL_ENTITY_ID} 104 | NEXTAUTH_URL: ${NEXTAUTH_URL} 105 | SLACK_ADMIN_LOGGER_AUTH_TOKEN: ${SLACK_ADMIN_LOGGER_AUTH_TOKEN} 106 | CERTIFICATE_URL: ${CERTIFICATE_URL} 107 | CAPTCHA_URL: ${CAPTCHA_URL} 108 | 109 | certificate: 110 | build: 111 | context: ./certificate 112 | 113 | captcha-service: 114 | build: 115 | context: ./captcha-service 116 | # volumes: 117 | # db_data: 118 | # fa_config: 119 | -------------------------------------------------------------------------------- /api/src/controllers/donate-device.controller.ts: -------------------------------------------------------------------------------- 1 | import {repository} from '@loopback/repository'; 2 | import {getModelSchemaRef, post, requestBody, response} from '@loopback/rest'; 3 | import {DonateDevice} from '../models'; 4 | import {DonateDeviceRepository} from '../repositories'; 5 | import sendSMS from './../utils/sendSMS'; 6 | import {DonateDevice as DonateDeviceType} from './donate-device-graphQL-model'; 7 | import {graphQLHelper} from './graphQL-helper'; 8 | 9 | export class DonateDeviceController { 10 | constructor( 11 | @repository(DonateDeviceRepository) 12 | public donateDeviceRepository: DonateDeviceRepository, 13 | ) {} 14 | 15 | @post('/donate-devices') 16 | @response(200, { 17 | description: 'DonateDevice model instance', 18 | content: {'application/json': {schema: getModelSchemaRef(DonateDevice)}}, 19 | }) 20 | async create( 21 | @requestBody({ 22 | content: { 23 | 'application/json': { 24 | schema: getModelSchemaRef(DonateDevice, { 25 | title: 'NewDonateDevice', 26 | exclude: ['id'], 27 | }), 28 | }, 29 | }, 30 | }) 31 | donateDevice: Omit, 32 | ): Promise { 33 | const instanceID = donateDevice.data[0]?.instanceID; 34 | const trackingKey = instanceID 35 | .split(':')?.[1] 36 | ?.split('-')?.[0] 37 | .toUpperCase(); 38 | const data = donateDevice?.data?.[0]; 39 | const smsBody = `You have successfully registered for donating your smartphone as part of "Baccho ka Sahara, Phone Humara" campaign. Your tracking ID is ${trackingKey}. You can use this ID to track the status of delivery for your donated device.\n\n- Samagra Shiksha, Himachal Pradesh`; 40 | const contactNumber = data.contact; 41 | const smsDispatchResponse = sendSMS(smsBody, trackingKey, contactNumber); 42 | 43 | data.trackingKey = trackingKey; 44 | const donateDeviceType = new DonateDeviceType(data); 45 | const gQLHelper = new graphQLHelper(); 46 | if (donateDeviceType.phone_number) { 47 | const {errors, data: gqlResponse} = await gQLHelper.startExecuteInsert( 48 | donateDeviceType, 49 | ); 50 | if (errors) { 51 | console.error(errors); 52 | } else { 53 | console.log(gqlResponse); 54 | } 55 | } 56 | return this.donateDeviceRepository.create(donateDevice); 57 | } 58 | 59 | // @get('/donate-devices/count') 60 | // @response(200, { 61 | // description: 'DonateDevice model count', 62 | // content: { 'application/json': { schema: CountSchema } }, 63 | // }) 64 | // async count( 65 | // @param.where(DonateDevice) where?: Where, 66 | // ): Promise { 67 | // return this.donateDeviceRepository.count(where); 68 | // } 69 | 70 | // @patch('/donate-devices') 71 | // @response(200, { 72 | // description: 'DonateDevice PATCH success count', 73 | // content: { 'application/json': { schema: CountSchema } }, 74 | // }) 75 | // async updateAll( 76 | // @requestBody({ 77 | // content: { 78 | // 'application/json': { 79 | // schema: getModelSchemaRef(DonateDevice, { partial: true }), 80 | // }, 81 | // }, 82 | // }) 83 | // donateDevice: DonateDevice, 84 | // @param.where(DonateDevice) where?: Where, 85 | // ): Promise { 86 | // return this.donateDeviceRepository.updateAll(donateDevice, where); 87 | // } 88 | 89 | // @get('/donate-devices/{id}') 90 | // @response(200, { 91 | // description: 'DonateDevice model instance', 92 | // content: { 93 | // 'application/json': { 94 | // schema: getModelSchemaRef(DonateDevice, { includeRelations: true }), 95 | // }, 96 | // }, 97 | // }) 98 | // async findById( 99 | // @param.path.string('id') id: string, 100 | // @param.filter(DonateDevice, { exclude: 'where' }) filter?: FilterExcludingWhere 101 | // ): Promise { 102 | // return this.donateDeviceRepository.findById(id, filter); 103 | // } 104 | 105 | // @patch('/donate-devices/{id}') 106 | // @response(204, { 107 | // description: 'DonateDevice PATCH success', 108 | // }) 109 | // async updateById( 110 | // @param.path.string('id') id: string, 111 | // @requestBody({ 112 | // content: { 113 | // 'application/json': { 114 | // schema: getModelSchemaRef(DonateDevice, { partial: true }), 115 | // }, 116 | // }, 117 | // }) 118 | // donateDevice: DonateDevice, 119 | // ): Promise { 120 | // await this.donateDeviceRepository.updateById(id, donateDevice); 121 | // } 122 | 123 | // @put('/donate-devices/{id}') 124 | // @response(204, { 125 | // description: 'DonateDevice PUT success', 126 | // }) 127 | // async replaceById( 128 | // @param.path.string('id') id: string, 129 | // @requestBody() donateDevice: DonateDevice, 130 | // ): Promise { 131 | // await this.donateDeviceRepository.replaceById(id, donateDevice); 132 | // } 133 | 134 | // @del('/donate-devices/{id}') 135 | // @response(204, { 136 | // description: 'DonateDevice DELETE success', 137 | // }) 138 | // async deleteById(@param.path.string('id') id: string): Promise { 139 | // await this.donateDeviceRepository.deleteById(id); 140 | // } 141 | } 142 | -------------------------------------------------------------------------------- /api/src/controllers/request-device.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | FilterExcludingWhere, 6 | repository, 7 | Where, 8 | } from '@loopback/repository'; 9 | import { 10 | post, 11 | param, 12 | get, 13 | getModelSchemaRef, 14 | patch, 15 | put, 16 | del, 17 | requestBody, 18 | response, 19 | } from '@loopback/rest'; 20 | import {RequestDevice} from '../models'; 21 | import {RequestDeviceRepository} from '../repositories'; 22 | import {RequestDevice as RequestDeviceType} from './request-device-graphQL-model'; 23 | import {graphQLHelper} from './graphQL-helper'; 24 | 25 | export class RequestDeviceController { 26 | constructor( 27 | @repository(RequestDeviceRepository) 28 | public requestDeviceRepository: RequestDeviceRepository, 29 | ) {} 30 | 31 | @post('/request-devices') 32 | @response(200, { 33 | description: 'RequestDevice model instance', 34 | content: {'application/json': {schema: getModelSchemaRef(RequestDevice)}}, 35 | }) 36 | async create( 37 | @requestBody({ 38 | content: { 39 | 'application/json': { 40 | schema: getModelSchemaRef(RequestDevice, { 41 | title: 'NewRequestDevice', 42 | exclude: ['id'], 43 | }), 44 | }, 45 | }, 46 | }) 47 | requestDevice: Omit, 48 | ): Promise { 49 | const instanceID = requestDevice.data[0]?.instanceID; 50 | const filter = {where: {'data.instanceID': instanceID}}; 51 | const existingRecord = await this.requestDeviceRepository.findOne(filter); 52 | if (!existingRecord) { 53 | const data = requestDevice?.data?.[0]; 54 | const requestDeviceType = new RequestDeviceType(data); 55 | const gQLHelper = new graphQLHelper(); 56 | const {errors, data: gqlResponse} = await gQLHelper.startExecuteInsert( 57 | requestDeviceType, 58 | ); 59 | if (errors) { 60 | console.error(errors); 61 | } else { 62 | console.log(gqlResponse); 63 | } 64 | return this.requestDeviceRepository.create(requestDevice); 65 | } else return existingRecord; 66 | } 67 | /* 68 | @get('/request-devices/count') 69 | @response(200, { 70 | description: 'RequestDevice model count', 71 | content: {'application/json': {schema: CountSchema}}, 72 | }) 73 | async count( 74 | @param.where(RequestDevice) where?: Where, 75 | ): Promise { 76 | return this.requestDeviceRepository.count(where); 77 | } 78 | 79 | @get('/request-devices') 80 | @response(200, { 81 | description: 'Array of RequestDevice model instances', 82 | content: { 83 | 'application/json': { 84 | schema: { 85 | type: 'array', 86 | items: getModelSchemaRef(RequestDevice, {includeRelations: true}), 87 | }, 88 | }, 89 | }, 90 | }) 91 | async find( 92 | @param.filter(RequestDevice) filter?: Filter, 93 | ): Promise { 94 | return this.requestDeviceRepository.find(filter); 95 | } 96 | 97 | @patch('/request-devices') 98 | @response(200, { 99 | description: 'RequestDevice PATCH success count', 100 | content: {'application/json': {schema: CountSchema}}, 101 | }) 102 | async updateAll( 103 | @requestBody({ 104 | content: { 105 | 'application/json': { 106 | schema: getModelSchemaRef(RequestDevice, {partial: true}), 107 | }, 108 | }, 109 | }) 110 | requestDevice: RequestDevice, 111 | @param.where(RequestDevice) where?: Where, 112 | ): Promise { 113 | return this.requestDeviceRepository.updateAll(requestDevice, where); 114 | } 115 | 116 | @get('/request-devices/{id}') 117 | @response(200, { 118 | description: 'RequestDevice model instance', 119 | content: { 120 | 'application/json': { 121 | schema: getModelSchemaRef(RequestDevice, {includeRelations: true}), 122 | }, 123 | }, 124 | }) 125 | async findById( 126 | @param.path.string('id') id: string, 127 | @param.filter(RequestDevice, {exclude: 'where'}) 128 | filter?: FilterExcludingWhere, 129 | ): Promise { 130 | return this.requestDeviceRepository.findById(id, filter); 131 | } 132 | 133 | @patch('/request-devices/{id}') 134 | @response(204, { 135 | description: 'RequestDevice PATCH success', 136 | }) 137 | async updateById( 138 | @param.path.string('id') id: string, 139 | @requestBody({ 140 | content: { 141 | 'application/json': { 142 | schema: getModelSchemaRef(RequestDevice, {partial: true}), 143 | }, 144 | }, 145 | }) 146 | requestDevice: RequestDevice, 147 | ): Promise { 148 | await this.requestDeviceRepository.updateById(id, requestDevice); 149 | } 150 | 151 | @put('/request-devices/{id}') 152 | @response(204, { 153 | description: 'RequestDevice PUT success', 154 | }) 155 | async replaceById( 156 | @param.path.string('id') id: string, 157 | @requestBody() requestDevice: RequestDevice, 158 | ): Promise { 159 | await this.requestDeviceRepository.replaceById(id, requestDevice); 160 | } 161 | 162 | @del('/request-devices/{id}') 163 | @response(204, { 164 | description: 'RequestDevice DELETE success', 165 | }) 166 | async deleteById(@param.path.string('id') id: string): Promise { 167 | await this.requestDeviceRepository.deleteById(id); 168 | } 169 | */ 170 | } 171 | -------------------------------------------------------------------------------- /portal/components/react-admin/base/resources/request-device.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | List, 4 | SimpleList, 5 | Datagrid, 6 | TextField, 7 | BooleanField, 8 | FunctionField, 9 | Edit, 10 | SimpleForm, 11 | NullableBooleanInput, 12 | Filter, 13 | SearchInput, 14 | } from "react-admin"; 15 | 16 | import { Typography, makeStyles, useMediaQuery } from "@material-ui/core"; 17 | import EditNoDeleteToolbar from "../components/EditNoDeleteToolbar"; 18 | import BackButton from "../components/BackButton"; 19 | import blueGrey from "@material-ui/core/colors/blueGrey"; 20 | 21 | const useStyles = makeStyles((theme) => ({ 22 | searchBar: { 23 | "& > div": { 24 | fontSize: "1rem", 25 | }, 26 | }, 27 | smSearchBar: { 28 | "& > div": { 29 | fontSize: "1.2rem", 30 | }, 31 | }, 32 | smList: { 33 | margin: "1rem 4rem", 34 | "& > div": { 35 | paddingLeft: 0, 36 | backgroundColor: "unset", 37 | "&:first-child > div": { 38 | backgroundColor: "unset", 39 | }, 40 | "&:last-child > div": { 41 | backgroundColor: "#FFF", 42 | boxShadow: 43 | "0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%)", 44 | }, 45 | }, 46 | }, 47 | list: { 48 | margin: "0rem 2rem", 49 | }, 50 | filter: { 51 | paddingLeft: 0, 52 | }, 53 | grid: { 54 | display: "grid", 55 | width: "100%", 56 | gridTemplateColumns: "1fr 1fr 1fr", 57 | gridRowGap: "1ch", 58 | gridColumnGap: "1ch", 59 | margin: "1rem 0", 60 | "& > td": theme.overrides.MuiTableCell.head, 61 | "& > span": { 62 | fontSize: "1.1rem", 63 | }, 64 | }, 65 | fullWidthGrid: { 66 | gridTemplateColumns: "1fr", 67 | }, 68 | heading: { 69 | fontSize: "1.4rem", 70 | lineHeight: "0.5rem", 71 | fontWeight: 700, 72 | fontVariant: "all-small-caps", 73 | }, 74 | select: { 75 | "& > div > div": { 76 | fontSize: "1.1rem", 77 | }, 78 | }, 79 | warning: { 80 | margin: "0", 81 | padding: "0", 82 | paddingBottom: "1rem", 83 | textAlign: "center", 84 | width: "100%", 85 | fontStyle: "oblique", 86 | }, 87 | grey: { 88 | color: blueGrey[300], 89 | }, 90 | })); 91 | 92 | /** 93 | * Donate Device Request List 94 | * @param {*} props 95 | */ 96 | export const RequestDeviceList = (props) => { 97 | const isSmall = useMediaQuery((theme) => theme.breakpoints.down("sm")); 98 | const classes = useStyles(); 99 | return ( 100 | 106 | {isSmall ? ( 107 | record.name} 109 | secondaryText={(record) => record.district} 110 | tertiaryText={(record) => record.student_count_no_smartphone} 111 | linkType="edit" 112 | /> 113 | ) : ( 114 | 115 | 116 | 117 | { 120 | if (record) { 121 | return record.district 122 | ? record.district 123 | : record.other_district; 124 | } 125 | }} 126 | /> 127 | 128 | 129 | 133 | 134 | )} 135 | 136 | ); 137 | }; 138 | 139 | export const RequestDeviceEdit = (props) => { 140 | const classes = useStyles(); 141 | const Title = ({ record }) => { 142 | return ( 143 | 144 | School #{record.udise} details 145 | 146 | ); 147 | }; 148 | return ( 149 |
150 | }> 151 | }> 152 | 153 | School Details 154 |
155 | Name 156 | Phone Number 157 | District 158 | 159 | 165 | 171 | Block 172 | Pincode 173 | School Name 174 | 180 | 186 | 192 |
193 | Demand Overview 194 |
195 | Total Students 196 | Students without smartphone 197 | 198 | 199 | 203 | 204 |
205 |
206 |
207 |
208 | ); 209 | }; 210 | -------------------------------------------------------------------------------- /portal/components/config.js: -------------------------------------------------------------------------------- 1 | const resourceConfig = { 2 | personas: [ 3 | // { 4 | // consonant: true, 5 | // en: "school head", 6 | // hi: "स्कूल प्रमुख", 7 | // credentials: "e-Samwad", 8 | // applicationId: process.env.NEXT_PUBLIC_FUSIONAUTH_SCHOOL_APP_ID, 9 | // redirectUrl: "school", 10 | // }, 11 | { 12 | consonant: false, 13 | en: "official", 14 | hi: "अधिकारी", 15 | credentials: "Shiksha Saathi", 16 | applicationId: process.env.NEXT_PUBLIC_FUSIONAUTH_STATE_APP_ID, 17 | redirectUrl: `admin#/candidate_profile`, 18 | }, 19 | ], 20 | homepageCards: [ 21 | { 22 | // title: { 23 | // en: "Donate your smartphone", 24 | // hi: "अपना स्मार्टफ़ोन दान करें", 25 | // }, 26 | // target: "/donate", 27 | // icon: "volunteer_activism", 28 | // colour: "primary", 29 | // },{ 30 | // title: { 31 | // en: "Donate a Smartphone as an Individual Donor", 32 | // hi: "व्यक्तिगत दाता", 33 | // }, 34 | // target: process.env.NEXT_PUBLIC_DONATE_DEVICE_INDIV_FORM_URL, 35 | // icon: "volunteer_activism", 36 | // colour: "primary", 37 | // }, 38 | // { 39 | // title: { 40 | // en: "Donate a smartphone as a Corporate Donor", 41 | // hi: "कॉर्पोरेट दाता", 42 | // }, 43 | // target: process.env.NEXT_PUBLIC_DONATE_DEVICE_CORP_FORM_URL, 44 | // icon: "corporate_fare", 45 | // colour: "primary", 46 | // }, 47 | // { 48 | // title: { 49 | // en: "Frequently Asked Questions", 50 | // hi: "जानकारी", 51 | // }, 52 | // target: process.env.NEXT_PUBLIC_FAQ_DOCUMENT_URL, 53 | // icon: "quiz", 54 | // colour: "primary", 55 | // }, 56 | // { 57 | title: { 58 | en: "Login for state officials", 59 | hi: "राज्य के अधिकारियों के लिए लॉग इन", 60 | }, 61 | target: "/login", 62 | icon: "login", 63 | colour: "secondary", 64 | }, 65 | // { 66 | // title: { 67 | // en: "Track your smartphone and get your Digi Saathi certificate", 68 | // hi: "अपने स्मार्टफ़ोन को ट्रैक करें और अपना Digi साथी प्रशंसा पत्र लें", 69 | // }, 70 | // target: "/track", 71 | // icon: "grading", 72 | // colour: "secondary", 73 | // }, 74 | ], 75 | donatePageCards: [ 76 | { 77 | title: { 78 | en: "Individual donor", 79 | hi: "व्यक्तिगत दाता ", 80 | }, 81 | target: process.env.NEXT_PUBLIC_DONATE_DEVICE_INDIV_FORM_URL, 82 | icon: "volunteer_activism", 83 | colour: "primary", 84 | }, 85 | { 86 | title: { 87 | en: "Corporate donor", 88 | hi: "कॉर्पोरेट दाता", 89 | }, 90 | target: process.env.NEXT_PUBLIC_DONATE_DEVICE_CORP_FORM_URL, 91 | icon: "corporate_fare", 92 | colour: "primary", 93 | }, 94 | ], 95 | schoolPageCards: [ 96 | { 97 | title: { 98 | en: "Demand estimation form", 99 | hi: "स्मार्टफ़ोन लागत अनुमान प्रपत्र भरें ", 100 | }, 101 | target: process.env.NEXT_PUBLIC_REQUEST_DEVICE_FORM_URL, 102 | icon: "smartphone", 103 | colour: "primary", 104 | }, 105 | { 106 | title: { 107 | en: "Update donee data", 108 | hi: "लाभार्थी जानकारी भरें", 109 | }, 110 | target: "/admin", 111 | icon: "login", 112 | colour: "secondary", 113 | }, 114 | ], 115 | statusChoices: [ 116 | { 117 | id: "no-action-taken", 118 | name: "Donation in Progress", //No Action Taken 119 | icon: "warning", 120 | style: "error", 121 | }, 122 | { 123 | id: "donor-no-init", 124 | name: "Delivery Not Initiated", 125 | icon: "pending_actions", 126 | style: "error", 127 | }, 128 | { 129 | id: "donor-init", 130 | name: "Delivery Initiated", 131 | icon: "inventory", 132 | style: "pending", 133 | }, 134 | { 135 | id: "received-state", 136 | name: "Received by state", 137 | icon: "real_estate_agent", 138 | style: "success", 139 | templateId: "1007356590433077522", 140 | template: 141 | "Congratulations! Your donated device with the tracking ID {#var#} has been successfully received by Samagra Shiksha, Himachal Pradesh.\nYou can visit the donation portal to download your DigitalSaathi eCertificate.\n\n\n- Samagra Shiksha, Himachal Pradesh", 142 | variables: ["device_tracking_key"], 143 | }, 144 | { 145 | id: "delivered-child", 146 | name: "Delivered to child", 147 | icon: "check_circle", 148 | style: "success", 149 | templateId: "1007267945817846297", 150 | template: 151 | "Congratulations! Your donated device with the tracking ID {#var#} has been successfully donated to a child-in-need from {#var#},({#var#}) . Thank you for your contribution to a student's online learning.\n\n\n- Samagra Shiksha, Himachal Pradesh", 152 | variables: [ 153 | "device_tracking_key", 154 | "recipient_school.name", 155 | "recipient_school.location.district", 156 | ], 157 | }, 158 | { 159 | id: "cancelled", 160 | name: "Cancelled", 161 | icon: "disabled_by_default", 162 | style: "error", 163 | }, 164 | ], 165 | deliveryTypeChoices: [ 166 | { id: "hand", name: "Hand Off", filterable: true }, 167 | { id: "pickup", name: "Pick Up", filterable: true }, 168 | { id: "courier", name: "Courier", filterable: true }, 169 | { id: "handnonhp", name: "Hand Off (outside HP)" }, 170 | { id: "couriernonhp", name: "Courier (outside HP)" }, 171 | ], 172 | gradeChoices: [ 173 | { 174 | id: 1, 175 | name: "1", 176 | }, 177 | { 178 | id: 2, 179 | name: "2", 180 | }, 181 | { 182 | id: 3, 183 | name: "3", 184 | }, 185 | { 186 | id: 4, 187 | name: "4", 188 | }, 189 | { 190 | id: 5, 191 | name: "5", 192 | }, 193 | { 194 | id: 6, 195 | name: "6", 196 | }, 197 | { 198 | id: 7, 199 | name: "7", 200 | }, 201 | { 202 | id: 8, 203 | name: "8", 204 | }, 205 | { 206 | id: 9, 207 | name: "9", 208 | }, 209 | { 210 | id: 10, 211 | name: "10", 212 | }, 213 | { 214 | id: 11, 215 | name: "11", 216 | }, 217 | { 218 | id: 12, 219 | name: "12", 220 | }, 221 | ], 222 | }; 223 | 224 | export default resourceConfig; 225 | -------------------------------------------------------------------------------- /portal/components/react-admin/base/resources/candidateProfile/candidate-show.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | List, 4 | Datagrid, 5 | Pagination, 6 | TextField, 7 | DateField, 8 | FunctionField, 9 | TopToolbar, 10 | sanitizeListRestProps, 11 | UrlField, 12 | BooleanField, 13 | RichTextField, 14 | ArrayField, 15 | SingleFieldList, 16 | FileField, 17 | Show, 18 | Tab, 19 | TabbedShowLayout, 20 | SimpleShowLayout, 21 | Filter, 22 | SelectInput, 23 | SearchInput, 24 | ExportButton, 25 | AutocompleteInput, 26 | useQuery, 27 | downloadCSV, 28 | } from "react-admin"; 29 | import { makeStyles, Typography } from "@material-ui/core"; 30 | 31 | function getAge({ start, end }) { 32 | var today = end ? new Date(end) : new Date(); 33 | var birthDate = new Date(start); 34 | var age = today.getFullYear() - birthDate.getFullYear(); 35 | var m = today.getMonth() - birthDate.getMonth(); 36 | let roundedDownAge = age; 37 | if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { 38 | roundedDownAge--; 39 | } 40 | if (today < birthDate) { 41 | return { years: "Invalid Date", months: "Invalid Date" }; 42 | } 43 | return { years: roundedDownAge, months: age * 12 + m }; 44 | } 45 | 46 | const useStyles = makeStyles((theme) => ({ 47 | root: { 48 | width: "calc(100% - 0px)", 49 | height: "86vh", 50 | marginTop: theme.spacing.unit * 3, 51 | overflowX: "auto", 52 | overflowY: "scroll", 53 | marginLeft: "1rem", 54 | }, 55 | })); 56 | 57 | export const CandidateShow = (props) => { 58 | const CustomFileField = ({ record, ...props }) => { 59 | const url = generateResumeLink(record?.data?.[0]?.resume?.url); 60 | return ( 61 |
68 | {url ? ( 69 | 70 | Resume 71 | 72 | ) : ( 73 | No Resume uploaded 74 | )} 75 |
76 | ); 77 | }; 78 | return ( 79 | 80 | 81 | 82 | 83 | 84 | { 87 | if (record) { 88 | return getAge({ 89 | start: record.DOB, 90 | end: null, 91 | }).years; 92 | } 93 | }} 94 | /> 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 107 | 111 | 112 | 113 | 114 | 118 | 122 | { 125 | if (record) { 126 | console.log("Role:", record); 127 | if (record.current_employed_status === 1) { 128 | return `${record.job_role}, ${record.employer_organization_name}`; 129 | } else return "N/A"; 130 | } 131 | }} 132 | /> 133 | 134 | { 137 | if (record && record.work_experience_details) { 138 | return `${record.work_experience_details.work_experience_choices}`; 139 | } else return "N/A"; 140 | }} 141 | /> 142 | { 145 | if (record) { 146 | if (record.current_employed_status === 1) { 147 | return `₹${record.monthly_salary_details.salary_range}`; 148 | } else return "N/A"; 149 | } 150 | }} 151 | /> 152 | 153 | 154 | 158 | 162 | 163 | 167 | 171 | 172 | { 175 | if (record) { 176 | return `${ 177 | record.sector_preference_1.sector_preference_name || "None" 178 | }, ${ 179 | record.sector_preference_2.sector_preference_name || "None" 180 | }, ${ 181 | record.sector_preference_3.sector_preference_name || "None" 182 | }`; 183 | } 184 | }} 185 | /> 186 | { 189 | if (record) { 190 | return `${record.skill_1 || "None"}, ${ 191 | record.skill_2 || "None" 192 | }, ${record.skill_3 || "None"}, ${record.skill_4 || "None"}`; 193 | } 194 | }} 195 | /> 196 | 200 | 201 | {/* */} 202 | 203 | 204 | 205 | ); 206 | }; 207 | -------------------------------------------------------------------------------- /portal/components/react-admin/base/resources/candidateProfile/candidate-profile.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | List, 4 | Datagrid, 5 | Pagination, 6 | FunctionField, 7 | TopToolbar, 8 | sanitizeListRestProps, 9 | Filter, 10 | SearchInput, 11 | ExportButton, 12 | downloadCSV, 13 | } from "react-admin"; 14 | import { makeStyles, Typography } from "@material-ui/core"; 15 | import jsonExport from "jsonexport/dist"; 16 | 17 | const SearchFilter = (props) => { 18 | console.log("Props:", props); 19 | return ( 20 | 21 | 27 | 28 | ); 29 | }; 30 | const exporter = (records) => { 31 | const recordsForExport = records.map((record) => { 32 | let age = getAge({ 33 | start: record.DOB, 34 | end: null, 35 | }).years; 36 | 37 | return { 38 | "Candidate Name": record.name ? record.name : "NONE", 39 | "Mobile Number": record.mobile_number ? record.mobile_number : "NONE", 40 | "Whatsapp Number": record.whatsapp_mobile_number 41 | ? record.whatsapp_mobile_number 42 | : "NONE", 43 | District: record.district_name.name ? record.district_name.name : "NONE", 44 | Pincode: record.pincode ? record.pincode : "NONE", 45 | "Max Qualification": record.highest_level_qualification 46 | .highest_level_qualification_name 47 | ? record.highest_level_qualification.highest_level_qualification_name 48 | : "NONE", 49 | Qualification: record.qualification_detail.qualification_name 50 | ? record.qualification_detail.qualification_name 51 | : "NONE", 52 | Marks: record.final_score_highest_qualification 53 | ? record.final_score_highest_qualification 54 | : "NONE", 55 | "Date of Birth": record.DOB, 56 | Age: age, 57 | Gender: record.gender.gender_name, 58 | "Have you ever been employed": record.ever_employment.employment_status 59 | ? record.ever_employment.employment_status 60 | : "NONE", 61 | "Job Role": record.job_role ? record.job_role : "NONE", 62 | "Company Name": record.employer_organization_name 63 | ? record.employer_organization_name 64 | : "NONE", 65 | "Total work experience (months)": record.work_experience_details 66 | ? record.work_experience_details.work_experience_choices 67 | : "NONE", 68 | "Monthly salary (Rs.)": record.monthly_salary_details 69 | ? record.monthly_salary_details.salary_range 70 | : "NONE", 71 | "Driving License": record.driver_license.driver_license_choice, 72 | "Distance willing to travel": 73 | record.district_travel.district_travel_choice, 74 | "PAN Card Availability": record.pan_card.pan_card_choice, 75 | "English speaking competency": 76 | record.english_knowledge_choice.english_choice, 77 | "Computer operating competencies": 78 | record.computer_operator.computer_operator_choices, 79 | // "Preferred Sectors": `${record.sector_preference_1.sector_preference_name},${record.sector_preference_2.sector_preference_name},${record.sector_preference_3.sector_preference_name}`, 80 | "Preferred Skill #1": record.skill_1, 81 | "Preferred Skill #2": record.skill_2, 82 | "Preferred Skill #3": record.skill_3, 83 | "Preferred Skill #4": record.skill_4, 84 | "Expected Salary": record.expected_salary.salary_range, 85 | "Resume (URL)": "", 86 | }; 87 | }); 88 | jsonExport(recordsForExport, (err, csv) => { 89 | downloadCSV( 90 | csv, 91 | `candidateData_${new Date(Date.now()).toLocaleDateString()}` 92 | ); 93 | }); 94 | }; 95 | 96 | function getAge({ start, end }) { 97 | var today = end ? new Date(end) : new Date(); 98 | var birthDate = new Date(start); 99 | var age = today.getFullYear() - birthDate.getFullYear(); 100 | var m = today.getMonth() - birthDate.getMonth(); 101 | let roundedDownAge = age; 102 | if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { 103 | roundedDownAge--; 104 | } 105 | if (today < birthDate) { 106 | return { years: "Invalid Date", months: "Invalid Date" }; 107 | } 108 | return { years: roundedDownAge, months: age * 12 + m }; 109 | } 110 | 111 | const CandidateActions = (props) => ( 112 | 113 | 114 | 115 | ); 116 | 117 | const useStyles = makeStyles((theme) => ({ 118 | root: { 119 | width: "calc(100% - 0px)", 120 | height: "86vh", 121 | marginTop: theme.spacing.unit * 3, 122 | overflowX: "auto", 123 | overflowY: "scroll", 124 | marginLeft: "1rem", 125 | }, 126 | })); 127 | export const CandidateList = (props) => { 128 | console.log("Entered Candidate"); 129 | const classes = useStyles(); 130 | 131 | return ( 132 |
133 | } 137 | bulkActionButtons={false} 138 | filters={} 139 | pagination={} 140 | exporter={exporter} 141 | > 142 | 143 | `${record.name}`} /> 144 | { 147 | if (record && record.DOB) { 148 | console.log(record); 149 | return getAge({ 150 | start: record.DOB, 151 | end: null, 152 | }).years; 153 | } 154 | }} 155 | /> 156 | { 159 | if (record && record.gender) { 160 | return record.gender.gender_name; 161 | } 162 | }} 163 | /> 164 | `${record.whatsapp_mobile_number}`} 167 | /> 168 | { 171 | if (record && record.district_name) { 172 | return record.district_name.name; 173 | } 174 | }} 175 | /> 176 | `${record.pincode}`} 179 | /> 180 | { 183 | if (record && record.highest_level_qualification) { 184 | return record.highest_level_qualification 185 | .highest_level_qualification_name; 186 | } 187 | }} 188 | /> 189 | { 192 | if (record && record.qualification_detail) { 193 | return record.qualification_detail.qualification_name; 194 | } 195 | }} 196 | /> 197 | `${record.final_score_highest_qualification}`} 200 | /> 201 | 202 | 203 |
204 | ); 205 | }; 206 | -------------------------------------------------------------------------------- /captcha-service/captchapng.js: -------------------------------------------------------------------------------- 1 | /** 2 | * captchapng 3 | * Captcha PNG generator 4 | * @Author: George Chan 5 | * @Email: gchan@21cn.com 6 | * @Version: 0.0.1 7 | * @Date: 2013-08-18 8 | * @license http://www.opensource.org/licenses/bsd-license.php BSD License 9 | */ 10 | 11 | var pnglib = require('pnglib'); 12 | this.numMask = []; 13 | this.numMask[0]=[]; 14 | this.numMask[0]=loadNumMask0(); 15 | this.numMask[1]=loadNumMask1(); 16 | myself = this; 17 | 18 | function loadNumMask0() { 19 | var numbmp=[]; 20 | numbmp[0]=["0011111000","0111111110","0111111110","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","0111111111"," 111111110","0011111100"]; 21 | numbmp[1]=["0000011","0000111","0011111","1111111","1111111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111"]; 22 | numbmp[2]=["001111100","011111110","111111111","111001111","111001111","111001111","111001111","000011111","000011110","000111110","000111100","000111100","001111000","001111000","011110000","011110000","111111111","111111111","111111111"]; 23 | numbmp[3]=["0011111100","0111111110","1111111111","1111001111","1111001111","1111001111","0000001111","0001111110","0001111100","0001111111","0000001111","1111001111","1111001111","1111001111","1111001111","1111001111","1111111111","0111111110","0011111100"]; 24 | numbmp[4]=["00001111110","00001111110","00011111110","00011111110","00011111110","00111011110","00111011110","00111011110","01110011110","01110011110","01110011110","11100011110","11111111111","11111111111","11111111111","11111111111","00000011110","00000011110","00000011110"]; 25 | numbmp[5]=["1111111111","1111111111","1111111111","1111000000","1111000000","1111011100","1111111110","1111111111","1111001111","1111001111","0000001111","0000001111","1111001111","1111001111","1111001111","1111001111","1111111111","0111111110","0011111100"]; 26 | numbmp[6]=["0011111100","0111111110","0111111111","1111001111","1111001111","1111000000","1111011100","1111111110","1111111111","1111001111","1111001111","1111001111","1111001111","1111001111","1111001111","1111001111","0111111111","0111111110","0011111100"]; 27 | numbmp[7]=["11111111","11111111","11111111","00001111","00001111","00001111","00001110","00001110","00011110","00011110","00011110","00011100","00111100","00111100","00111100","00111100","00111000","01111000","01111000"]; 28 | numbmp[8]=["0011111100","0111111110","1111111111","1111001111","1111001111","1111001111","1111001111","0111111110","0011111100","0111111110","1111001111","1111001111","1111001111","1111001111","1111001111","1111001111","1111111111","0111111110","0011111100"]; 29 | numbmp[9]=["0011111100","0111111110","1111111111","1111001111","1111001111","1111001111","1111001111","1111001111","1111001111","1111001111","1111111111","0111111111","0011101111","0000001111","1111001111","1111001111","1111111110","0111111110","0011111000"]; 30 | 31 | return numbmp; 32 | } 33 | 34 | function loadNumMask1() { 35 | var numbmp=[]; 36 | numbmp[0] = ["000000001111000","000000111111110","000001110000110","000011000000011","000110000000011","001100000000011","011100000000011","011000000000011","111000000000110","110000000000110","110000000001110","110000000001100","110000000011000","110000000111000","011000011110000","011111111000000","000111110000000"]; 37 | numbmp[1] = ["00000111","00001111","00011110","00010110","00001100","00001100","00011000","00011000","00110000","00110000","00110000","01100000","01100000","01100000","11000000","11000000","11000000"]; 38 | numbmp[2] = ["00000011111000","00001111111110","00011100000110","00011000000011","00000000000011","00000000000011","00000000000011","00000000000110","00000000001110","00000000011100","00000001110000","00000111100000","00001110000000","00111100000000","01110000000000","11111111110000","11111111111110","00000000011110"]; 39 | numbmp[3] = ["000000111111000","000011111111110","000111100000111","000110000000011","000000000000011","000000000000011","000000000001110","000000111111000","000000111111000","000000000011100","000000000001100","000000000001100","110000000001100","111000000011100","111100000111000","001111111110000","000111111000000"]; 40 | numbmp[4] = ["00000011000001","00000110000011","00001100000010","00011000000110","00111000000110","00110000001100","01100000001100","01100000001000","11000000011000","11111111111111","11111111111111","00000000110000","00000000110000","00000000100000","00000001100000","00000001100000","00000001100000"]; 41 | numbmp[5] = ["0000001111111111","0000011111111111","0000111000000000","0000110000000000","0000110000000000","0001110000000000","0001101111100000","0001111111111000","0001110000011000","0000000000001100","0000000000001100","0000000000001100","1100000000001100","1110000000011000","1111000001111000","0111111111100000","0001111110000000"]; 42 | numbmp[6] = ["000000001111100","000000111111110","000011110000111","000111000000011","000110000000000","001100000000000","011001111100000","011111111111000","111110000011000","111000000001100","110000000001100","110000000001100","110000000001100","111000000011000","011100001110000","001111111100000","000111110000000"]; 43 | numbmp[7] = ["1111111111111","1111111111111","0000000001110","0000000011100","0000000111000","0000000110000","0000001100000","0000011100000","0000111000000","0000110000000","0001100000000","0011100000000","0011000000000","0111000000000","1110000000000","1100000000000","1100000000000"]; 44 | numbmp[8] = ["0000000111110000","0000011111111100","0000011000001110","0000110000000111","0000110000011111","0000110001111000","0000011111100000","0000011110000000","0001111111000000","0011100011100000","0111000001110000","1110000000110000","1100000000110000","1100000001110000","1110000011100000","0111111111000000","0001111100000000"]; 45 | numbmp[9] = ["0000011111000","0001111111110","0011100000110","0011000000011","0110000000011","0110000000011","0110000000011","0110000000111","0011000011110","0011111111110","0000111100110","0000000001100","0000000011000","0000000111000","0000011110000","1111111000000","1111110000000"]; 46 | return numbmp; 47 | } 48 | 49 | 50 | function captchapng(width,height,dispNumber) { 51 | this.width = width; 52 | this.height = height; 53 | this.depth = 8; 54 | this.dispNumber = ""+dispNumber.toString(); 55 | this.widthAverage = parseInt(this.width/this.dispNumber.length); 56 | 57 | var p = new pnglib(this.width,this.height,this.depth); 58 | 59 | for (var numSection=0;numSection=myself.numMask.length?0:font); 65 | //var random_x_offs = 0, random_y_offs = 0; 66 | var random_x_offs = parseInt(Math.random()*(this.widthAverage - myself.numMask[font][dispNum][0].length)); 67 | var random_y_offs = parseInt(Math.random()*(this.height - myself.numMask[font][dispNum].length)); 68 | random_x_offs = (random_x_offs<0?0:random_x_offs); 69 | random_y_offs = (random_y_offs<0?0:random_y_offs); 70 | 71 | for (var i=0;(i { 18 | console.log("Props:", props); 19 | return ( 20 | 21 | 27 | 28 | ); 29 | }; 30 | const exporter = (records) => { 31 | const recordsForExport = records.map((record) => { 32 | let age = getAge({ 33 | start: record.DOB, 34 | end: null, 35 | }).years; 36 | 37 | return { 38 | "Candidate Name": record.name ? record.name : "NONE", 39 | "Mobile Number": record.mobile_number ? record.mobile_number : "NONE", 40 | "Whatsapp Number": record.whatsapp_mobile_number 41 | ? record.whatsapp_mobile_number 42 | : "NONE", 43 | District: record.district_name.name ? record.district_name.name : "NONE", 44 | Pincode: record.pincode ? record.pincode : "NONE", 45 | "Max Qualification": record.highest_level_qualification 46 | .highest_level_qualification_name 47 | ? record.highest_level_qualification.highest_level_qualification_name 48 | : "NONE", 49 | Qualification: record.qualification_detail.qualification_name 50 | ? record.qualification_detail.qualification_name 51 | : "NONE", 52 | Marks: record.final_score_highest_qualification 53 | ? record.final_score_highest_qualification 54 | : "NONE", 55 | "Date of Birth": record.DOB, 56 | Age: age, 57 | Gender: record.gender.gender_name, 58 | "Have you ever been employed": record.ever_employment.employment_status 59 | ? record.ever_employment.employment_status 60 | : "NONE", 61 | "Job Role": record.job_role ? record.job_role : "NONE", 62 | "Company Name": record.employer_organization_name 63 | ? record.employer_organization_name 64 | : "NONE", 65 | "Total work experience (months)": record.work_experience_details 66 | ? record.work_experience_details.work_experience_choices 67 | : "NONE", 68 | "Monthly salary (Rs.)": record.monthly_salary_details 69 | ? record.monthly_salary_details.salary_range 70 | : "NONE", 71 | "Driving License": record.driver_license.driver_license_choice, 72 | "Distance willing to travel": 73 | record.district_travel.district_travel_choice, 74 | "PAN Card Availability": record.pan_card.pan_card_choice, 75 | "English speaking competency": 76 | record.english_knowledge_choice.english_choice, 77 | "Computer operating competencies": 78 | record.computer_operator.computer_operator_choices, 79 | // "Preferred Sectors": `${record.sector_preference_1.sector_preference_name},${record.sector_preference_2.sector_preference_name},${record.sector_preference_3.sector_preference_name}`, 80 | "Preferred Skill #1": record.skill_1, 81 | "Preferred Skill #2": record.skill_2, 82 | "Preferred Skill #3": record.skill_3, 83 | "Preferred Skill #4": record.skill_4, 84 | "Expected Salary": record.expected_salary.salary_range, 85 | "Resume (URL)": "", 86 | }; 87 | }); 88 | jsonExport(recordsForExport, (err, csv) => { 89 | downloadCSV( 90 | csv, 91 | `candidateData_${new Date(Date.now()).toLocaleDateString()}` 92 | ); 93 | }); 94 | }; 95 | 96 | function getAge({ start, end }) { 97 | var today = end ? new Date(end) : new Date(); 98 | var birthDate = new Date(start); 99 | var age = today.getFullYear() - birthDate.getFullYear(); 100 | var m = today.getMonth() - birthDate.getMonth(); 101 | let roundedDownAge = age; 102 | if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { 103 | roundedDownAge--; 104 | } 105 | if (today < birthDate) { 106 | return { years: "Invalid Date", months: "Invalid Date" }; 107 | } 108 | return { years: roundedDownAge, months: age * 12 + m }; 109 | } 110 | 111 | const CandidateActions = (props) => ( 112 | 113 | 114 | 115 | ); 116 | 117 | const useStyles = makeStyles((theme) => ({ 118 | root: { 119 | width: "calc(100% - 0px)", 120 | height: "86vh", 121 | marginTop: theme.spacing.unit * 3, 122 | overflowX: "auto", 123 | overflowY: "scroll", 124 | marginLeft: "1rem", 125 | }, 126 | })); 127 | export const RecruiterData = (props) => { 128 | console.log("Entered Recruiter"); 129 | const classes = useStyles(); 130 | 131 | return ( 132 |
133 | } 137 | bulkActionButtons={false} 138 | filters={} 139 | pagination={} 140 | exporter={exporter} 141 | > 142 | 143 | 144 | 145 | 146 | 147 | 148 | `${record.name}`} /> 149 | { 152 | if (record && record.DOB) { 153 | console.log(record); 154 | return getAge({ 155 | start: record.DOB, 156 | end: null, 157 | }).years; 158 | } 159 | }} 160 | /> 161 | { 164 | if (record && record.gender) { 165 | return record.gender.gender_name; 166 | } 167 | }} 168 | /> 169 | `${record.whatsapp_mobile_number}`} 172 | /> 173 | { 176 | if (record && record.district_name) { 177 | return record.district_name.name; 178 | } 179 | }} 180 | /> 181 | `${record.pincode}`} 184 | /> 185 | { 188 | if (record && record.highest_level_qualification) { 189 | return record.highest_level_qualification 190 | .highest_level_qualification_name; 191 | } 192 | }} 193 | /> 194 | { 197 | if (record && record.qualification_detail) { 198 | return record.qualification_detail.qualification_name; 199 | } 200 | }} 201 | /> 202 | `${record.final_score_highest_qualification}`} 205 | /> 206 | 207 | 208 |
209 | ); 210 | }; 211 | -------------------------------------------------------------------------------- /portal/components/track/track.js: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { useState, useRef, useEffect } from "react"; 3 | import styles from "../../styles/Track.module.css"; 4 | import controls from "./track.config"; 5 | import axios from "axios"; 6 | import { useToasts } from "react-toast-notifications"; 7 | import config from "@/components/config"; 8 | 9 | const Track = () => { 10 | const [trackingKey, setTrackingKey] = useState(null); 11 | const [captcha, setCaptcha] = useState(null); 12 | const [captchaToken, setCaptchaToken] = useState(null); 13 | const [refreshToken, setRefreshToken] = useState(null); 14 | const [captchaImg, setCaptchaImg] = useState(null); 15 | const [trackingKeyValid, setTrackingKeyValid] = useState(false); 16 | const [trackingResponse, setTrackingResponse] = useState(null); 17 | const [deliveryStatus, setDeliveryStatus] = useState(false); 18 | const [displayCertificate, setDisplayCertificate] = useState(false); 19 | const captchaRef = useRef(null); 20 | 21 | useEffect(() => { 22 | const obj = config.statusChoices.find( 23 | (elem) => elem.id === trackingResponse?.delivery_status 24 | ); 25 | if (obj) { 26 | setDeliveryStatus(obj); 27 | if (["received-state", "delivered-child"].includes(obj.id)) 28 | setDisplayCertificate(true); 29 | else setDisplayCertificate(false); 30 | } 31 | }, [trackingResponse]); 32 | 33 | const handleInput = (e) => { 34 | setTrackingKey(e.target.value); 35 | setTrackingKeyValid(e.target.value != "" && captcha && captcha != ""); 36 | }; 37 | 38 | const handleInputCaptcha = (e) => { 39 | setCaptcha(e.target.value); 40 | setTrackingKeyValid( 41 | e.target.value != "" && trackingKey && trackingKey != "" 42 | ); 43 | }; 44 | 45 | const { addToast } = useToasts(); 46 | 47 | useEffect(() => { 48 | const response = axios 49 | .get(process.env.NEXT_PUBLIC_CAPTCHA_URL) 50 | .then((resp) => { 51 | const { blob } = resp.data; 52 | const { token } = resp.headers; 53 | setCaptchaImg(blob); 54 | setCaptchaToken(token); 55 | }) 56 | .catch((err) => { 57 | addToast(err.response?.data?.errors || err.message, { 58 | appearance: "error", 59 | }); 60 | }); 61 | }, [refreshToken]); 62 | 63 | const handleSubmit = async (e) => { 64 | e.preventDefault(); 65 | try { 66 | const response = await axios.post( 67 | `${process.env.NEXT_PUBLIC_API_URL}/track`, 68 | { 69 | id: trackingKey, 70 | captcha: captcha, 71 | captchaToken: captchaToken, 72 | } 73 | ); 74 | const { error, success } = response.data; 75 | 76 | if (success) setTrackingResponse(success.data); 77 | 78 | if (error) { 79 | addToast(error, { appearance: "error" }); 80 | var rightNow = new Date(); 81 | setRefreshToken(rightNow.toISOString()); 82 | } 83 | } catch (err) { 84 | addToast(JSON.stringify(err), { appearance: "error" }); 85 | } 86 | }; 87 | 88 | const fetchCertificate = async (e) => { 89 | e.preventDefault(); 90 | try { 91 | const response = await axios.post( 92 | `${process.env.NEXT_PUBLIC_API_URL}/certificate`, 93 | { 94 | name: trackingResponse.name, 95 | trackingKey: trackingKey, 96 | udise: trackingResponse.recipient_school?.udise, 97 | } 98 | ); 99 | const { error, success } = response.data; 100 | if (error) { 101 | addToast(error, { appearance: "error" }); 102 | } 103 | if (success) { 104 | const pdfData = success.base64String; 105 | const byteCharacters = atob(pdfData); 106 | let byteNumbers = new Array(byteCharacters.length); 107 | for (let i = 0; i < byteCharacters.length; i++) { 108 | byteNumbers[i] = byteCharacters.charCodeAt(i); 109 | } 110 | const byteArray = new Uint8Array(byteNumbers); 111 | const file = new Blob([byteArray], { type: "application/pdf;base64" }); 112 | const fileURL = URL.createObjectURL(file); 113 | window.open(fileURL); 114 | } 115 | } catch (err) { 116 | addToast(err.message, { appearance: "error" }); 117 | } 118 | }; 119 | 120 | return ( 121 | <> 122 |

Track / ट्रैक

123 |
124 |
125 |

126 | Enter tracking ID / ट्रैकिंग आईडी भरें 127 |

128 |
129 | {controls.map((control) => ( 130 | 140 | ))} 141 |
142 | captcha 148 | 156 |
157 | 165 |
166 |
167 |
172 |

Status / स्थिति

173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | {trackingResponse ? ( 182 | <> 183 | 186 | 196 | 197 | ) : ( 198 | <> 199 | )} 200 | 201 | 202 | 218 | 219 | 220 |
Donor Mobile No.Status
184 | {trackingResponse.phone_number} 185 | 187 | 192 | {deliveryStatus.icon} 193 | 194 | {deliveryStatus.name} 195 |
203 | {displayCertificate && ( 204 | 216 | )} 217 |
221 |
222 |
223 | 224 | ); 225 | }; 226 | 227 | export default Track; 228 | --------------------------------------------------------------------------------