diff --git a/Makefile b/Makefile index 21186f2b4..f7e920895 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ IMAGES := dockerhub/sunbird-rc-core dockerhub/sunbird-rc-nginx dockerhub/sunbird dockerhub/sunbird-rc-public-key-service dockerhub/sunbird-rc-keycloak dockerhub/sunbird-rc-certificate-api \ dockerhub/sunbird-rc-certificate-signer dockerhub/sunbird-rc-notification-service dockerhub/sunbird-rc-claim-ms \ dockerhub/sunbird-rc-digilocker-certificate-api dockerhub/sunbird-rc-bulk-issuance dockerhub/sunbird-rc-metrics \ - dockerhub/sunbird-rc-credentials-service dockerhub/sunbird-rc-identity-service + dockerhub/sunbird-rc-identity-service dockerhub/sunbird-rc-credential-schema dockerhub/sunbird-rc-credentials-service build: java/registry/target/registry.jar echo ${SOURCES} @@ -23,8 +23,10 @@ build: java/registry/target/registry.jar make -C services/digilocker-certificate-api docker make -C services/bulk_issuance docker docker build -t dockerhub/sunbird-rc-nginx . - make -C services/credentials-service/ docker make -C services/identity-service/ docker + make -C services/credential-schema docker + make -C services/credentials-service/ docker + java/registry/target/registry.jar: $(SOURCES) echo $(SOURCES) @@ -71,8 +73,9 @@ test: build make -C services/public-key-service test make -C services/context-proxy-service test make -C services/bulk_issuance test - make -C services/credentials-service test make -C services/identity-service test + make -C services/credential-schema test + make -C services/credentials-service test clean: @rm -rf target || true diff --git a/services/credential-schema/.dockerignore b/services/credential-schema/.dockerignore new file mode 100644 index 000000000..8f00ef230 --- /dev/null +++ b/services/credential-schema/.dockerignore @@ -0,0 +1,3 @@ +node_modules +.env +dist \ No newline at end of file diff --git a/services/credential-schema/.env.sample b/services/credential-schema/.env.sample new file mode 100644 index 000000000..425eb0038 --- /dev/null +++ b/services/credential-schema/.env.sample @@ -0,0 +1,13 @@ +DATABASE_URL="" +SHADOW_DATABASE_URL="" + +# Auth related vars +JWKS_URI="" +ENABLE_AUTH=false + +# Core Service Vars +IDENTITY_BASE_URL= # URL of the identity service to facilitate DID creation + +# Service VARS +PORT=3000 +SCHEMA_BASE_URL= diff --git a/services/credential-schema/.eslintrc.js b/services/credential-schema/.eslintrc.js new file mode 100644 index 000000000..8f5aedb71 --- /dev/null +++ b/services/credential-schema/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir : __dirname, + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, +}; diff --git a/services/credential-schema/.gitignore b/services/credential-schema/.gitignore new file mode 100644 index 000000000..0a5f2324d --- /dev/null +++ b/services/credential-schema/.gitignore @@ -0,0 +1,38 @@ +# compiled output +/dist +/node_modules + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +#Environment files +.env \ No newline at end of file diff --git a/services/credential-schema/.prettierrc b/services/credential-schema/.prettierrc new file mode 100644 index 000000000..bfff89fb8 --- /dev/null +++ b/services/credential-schema/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "tabWidth": 2 +} \ No newline at end of file diff --git a/services/credential-schema/Dockerfile b/services/credential-schema/Dockerfile new file mode 100644 index 000000000..c57f7aa0e --- /dev/null +++ b/services/credential-schema/Dockerfile @@ -0,0 +1,21 @@ +FROM node:16 AS install +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn + +FROM node:16 as build +WORKDIR /app +COPY prisma ./prisma/ +COPY --from=install /app/node_modules ./node_modules +RUN npx prisma generate +COPY . . +RUN yarn build + +FROM node:16 +WORKDIR /app +COPY --from=build /app/dist ./dist +COPY --from=build /app/package*.json ./ +COPY --from=build /app/prisma ./prisma +COPY --from=build /app/node_modules ./node_modules +EXPOSE 3000 +CMD [ "npm", "run", "start:migrate:prod" ] \ No newline at end of file diff --git a/services/credential-schema/Dockerfile.test b/services/credential-schema/Dockerfile.test new file mode 100644 index 000000000..f6d97a3cb --- /dev/null +++ b/services/credential-schema/Dockerfile.test @@ -0,0 +1,13 @@ +FROM node:16 AS install +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn + +FROM node:16 as test +WORKDIR /app +COPY prisma ./prisma/ +COPY --from=install /app/node_modules ./node_modules +RUN npx prisma generate +COPY . . +EXPOSE 3000 +CMD [ "yarn", "test:migrate"] \ No newline at end of file diff --git a/services/credential-schema/Makefile b/services/credential-schema/Makefile new file mode 100644 index 000000000..36c725393 --- /dev/null +++ b/services/credential-schema/Makefile @@ -0,0 +1,22 @@ +IMAGE:=dockerhub/sunbird-rc-credential-schema + +.PHONY: docker publish test + +docker: + @docker build -t $(IMAGE) . + +publish: + @docker push $(IMAGE) + +test: +# Resetting vault of identity-service before running the tests + make -C ../identity-service stop + make -C ../identity-service vault-reset +# Creating an external docker network to connnect services in different compose + @docker network create rcw-test || echo "" +# Starting dependent services + make -C ../identity-service compose-init + @docker-compose -f docker-compose-test.yml down + @docker-compose -f docker-compose-test.yml up --build --abort-on-container-exit + make -C ../identity-service stop + make -C ../identity-service vault-reset \ No newline at end of file diff --git a/services/credential-schema/README.md b/services/credential-schema/README.md new file mode 100644 index 000000000..c7a18f1e7 --- /dev/null +++ b/services/credential-schema/README.md @@ -0,0 +1,86 @@ +# Credential Schema Microservice + +## Description + +[Nest](https://github.com/nestjs/nest) based CRUD APIs for handling the schemas of different verifiable credentials used for implementing the [Unified Learner's Passbook](https://github.com/Unified-Learners-Passbook) in the state of Uttar Pradesh, India. + +## Installation + +```bash + +$ npm install + +``` +OR +```bash + +$ npx yarn + +``` +## Running the app + +```bash +# development +$ npm run start + +# watch mode +$ npm run start:dev + +# production mode +$ npm run start:prod +``` + +OR + +```bash +# development +$ npx yarn run start + +# watch mode +$ npx yarn run start:dev + +# production mode +$ npx yarn run start:prod +``` + +## Test + +```bash +# unit tests +$ npm run test + +# e2e tests +$ npm run test:e2e + +# test coverage +$ npm run test:cov +``` + +OR + +```bash +# unit tests +$ npx yarn run test + +# e2e tests +$ npx yarn run test:e2e + +# test coverage +$ npx yarn run test:cov +``` + +## Stay in touch + +- Authors - [Yash Mittal](https://github.com/techsavvyash) and [Chakshu Gautam](https://github.com/ChakshuGautam) +- Website - [https://nestjs.com](https://nestjs.com/) + +## License + +Nest is [MIT licensed](LICENSE). + +## Reference Links + +- [W3C Credential Schema (Currently being used)](https://w3c-ccg.github.io/vc-json-schemas/schema/2.0/schema.json) +- [W3C Credential Schema (initially used)](https://w3c-ccg.github.io/vc-json-schemas/schema/1.0/schema.json) +- [W3C JSON Schemas Website](https://w3c-ccg.github.io/vc-json-schemas/) +- [API Documentation](https://github.com/Sunbird-RC/sunbird-rc-core/tree/main/api-documentation) diff --git a/services/credential-schema/docker-compose-test.yml b/services/credential-schema/docker-compose-test.yml new file mode 100644 index 000000000..53aa4210b --- /dev/null +++ b/services/credential-schema/docker-compose-test.yml @@ -0,0 +1,35 @@ +version: '3' + +services: + db-test: + image: postgres:12 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U postgres" ] + interval: 10s + timeout: 5s + retries: 5 + networks: + test: + + credential-schema-test: + build: + context: . + dockerfile: Dockerfile.test + depends_on: + db-test: + condition: service_healthy + environment: + DATABASE_URL: postgres://postgres:postgres@db-test:5432/postgres + IDENTITY_BASE_URL: "http://identity-service:3332" + ENABLE_AUTH: "false" + networks: + test: + rcw-test: + +networks: + rcw-test: + external: true + test: \ No newline at end of file diff --git a/services/credential-schema/docker-compose.yml b/services/credential-schema/docker-compose.yml new file mode 100644 index 000000000..53d491fb9 --- /dev/null +++ b/services/credential-schema/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3' + +services: + db: + image: postgres:12 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + volumes: + - data:/var/lib/postgresql/data + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U postgres" ] + interval: 10s + timeout: 5s + retries: 5 + + credential-schema: + image: cred + build: . + depends_on: + db: + condition: service_healthy + ports: + - '3333:3333' + environment: + DATABASE_URL: postgres://postgres:postgres@db:5432/postgres + IDENTITY_BASE_URL: "http://identity-service:3332" + ENABLE_AUTH: "false" + networks: + rcw-test: + default: + + +networks: + rcw-test: + external: true + +volumes: + data: \ No newline at end of file diff --git a/services/credential-schema/nest-cli.json b/services/credential-schema/nest-cli.json new file mode 100644 index 000000000..256648114 --- /dev/null +++ b/services/credential-schema/nest-cli.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src" +} diff --git a/services/credential-schema/package.json b/services/credential-schema/package.json new file mode 100644 index 000000000..211ecd1ae --- /dev/null +++ b/services/credential-schema/package.json @@ -0,0 +1,94 @@ +{ + "name": "cred-schema-ms", + "version": "1.0.0", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "prebuild": "rimraf dist", + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/src/main", + "start:migrate:prod": "npx prisma migrate deploy && node dist/src/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest --coverage && jest --config ./test/jest-e2e.json", + "test:migrate": "npx prisma migrate deploy && jest --coverage", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@fastify/static": "^6.6.0", + "@handlebars/parser": "^2.1.0", + "@nestjs/axios": "^3.0.0", + "@nestjs/common": "^9.0.0", + "@nestjs/config": "^2.2.0", + "@nestjs/core": "^9.0.0", + "@nestjs/passport": "^9.0.0", + "@nestjs/platform-express": "^9.0.0", + "@nestjs/platform-fastify": "^9.2.1", + "@nestjs/swagger": "^6.1.4", + "@nestjs/terminus": "^10.0.1", + "@prisma/client": "4.7.1", + "ajv": "^8.11.2", + "axios": "^1.4.0", + "cache-manager": "^5.1.4", + "fastify-swagger": "^5.2.0", + "flat": "^5.0.2", + "handlebars": "4.7.7", + "json-schema-to-typescript": "^11.0.2", + "jwks-rsa": "^3.0.0", + "passport": "^0.6.0", + "passport-http": "^0.3.0", + "passport-jwt": "^4.0.1", + "prisma": "4.7.1", + "reflect-metadata": "^0.1.13", + "rimraf": "^3.0.2", + "rxjs": "^7.2.0" + }, + "devDependencies": { + "@nestjs/cli": "^9.0.0", + "@nestjs/schematics": "^9.0.0", + "@nestjs/testing": "^9.0.0", + "@types/express": "^4.17.13", + "@types/jest": "28.1.8", + "@types/node": "^16.0.0", + "@types/supertest": "^2.0.11", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^8.0.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0", + "jest": "28.1.3", + "prettier": "^2.3.2", + "source-map-support": "^0.5.20", + "supertest": "^6.1.3", + "ts-jest": "28.0.8", + "ts-loader": "^9.2.3", + "ts-node": "^10.0.0", + "tsconfig-paths": "4.1.0", + "typescript": "^4.7.4" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/services/credential-schema/prisma/migrations/20221129080801_initial/migration.sql b/services/credential-schema/prisma/migrations/20221129080801_initial/migration.sql new file mode 100644 index 000000000..c7569e42f --- /dev/null +++ b/services/credential-schema/prisma/migrations/20221129080801_initial/migration.sql @@ -0,0 +1,43 @@ +-- CreateTable +CREATE TABLE "VerifiableCredentialSchema" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "version" TEXT NOT NULL, + "type" TEXT NOT NULL, + "schema" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "tags" TEXT[], + + CONSTRAINT "VerifiableCredentialSchema_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VerifiablePresentationSchema" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "version" TEXT NOT NULL, + "type" TEXT NOT NULL, + "schema" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "tags" TEXT[], + + CONSTRAINT "VerifiablePresentationSchema_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "VerifiableCredentialSchema_type_idx" ON "VerifiableCredentialSchema" USING HASH ("type"); + +-- CreateIndex +CREATE INDEX "VerifiableCredentialSchema_name_idx" ON "VerifiableCredentialSchema" USING HASH ("name"); + +-- CreateIndex +CREATE INDEX "VerifiablePresentationSchema_type_idx" ON "VerifiablePresentationSchema" USING HASH ("type"); + +-- CreateIndex +CREATE INDEX "VerifiablePresentationSchema_name_idx" ON "VerifiablePresentationSchema" USING HASH ("name"); diff --git a/services/credential-schema/prisma/migrations/20221129084416_update_version_type/migration.sql b/services/credential-schema/prisma/migrations/20221129084416_update_version_type/migration.sql new file mode 100644 index 000000000..d9608506a --- /dev/null +++ b/services/credential-schema/prisma/migrations/20221129084416_update_version_type/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - Changed the type of `version` on the `VerifiableCredentialSchema` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + - Changed the type of `version` on the `VerifiablePresentationSchema` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- AlterTable +ALTER TABLE "VerifiableCredentialSchema" DROP COLUMN "version", +ADD COLUMN "version" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "VerifiablePresentationSchema" DROP COLUMN "version", +ADD COLUMN "version" INTEGER NOT NULL; diff --git a/services/credential-schema/prisma/migrations/20221227191932_update_schemas_to_v2/migration.sql b/services/credential-schema/prisma/migrations/20221227191932_update_schemas_to_v2/migration.sql new file mode 100644 index 000000000..d24cdfae8 --- /dev/null +++ b/services/credential-schema/prisma/migrations/20221227191932_update_schemas_to_v2/migration.sql @@ -0,0 +1,24 @@ +/* + Warnings: + + - You are about to drop the column `description` on the `VerifiableCredentialSchema` table. All the data in the column will be lost. + - You are about to drop the column `description` on the `VerifiablePresentationSchema` table. All the data in the column will be lost. + - Added the required column `author` to the `VerifiableCredentialSchema` table without a default value. This is not possible if the table is not empty. + - Added the required column `authored` to the `VerifiableCredentialSchema` table without a default value. This is not possible if the table is not empty. + - Added the required column `proof` to the `VerifiableCredentialSchema` table without a default value. This is not possible if the table is not empty. + - Added the required column `author` to the `VerifiablePresentationSchema` table without a default value. This is not possible if the table is not empty. + - Added the required column `authored` to the `VerifiablePresentationSchema` table without a default value. This is not possible if the table is not empty. + - Added the required column `proof` to the `VerifiablePresentationSchema` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "VerifiableCredentialSchema" DROP COLUMN "description", +ADD COLUMN "author" TEXT NOT NULL, +ADD COLUMN "authored" TIMESTAMP(3) NOT NULL, +ADD COLUMN "proof" JSONB NOT NULL; + +-- AlterTable +ALTER TABLE "VerifiablePresentationSchema" DROP COLUMN "description", +ADD COLUMN "author" TEXT NOT NULL, +ADD COLUMN "authored" TIMESTAMP(3) NOT NULL, +ADD COLUMN "proof" JSONB NOT NULL; diff --git a/services/credential-schema/prisma/migrations/20221227192117_update_version_data_type/migration.sql b/services/credential-schema/prisma/migrations/20221227192117_update_version_data_type/migration.sql new file mode 100644 index 000000000..06bb10234 --- /dev/null +++ b/services/credential-schema/prisma/migrations/20221227192117_update_version_data_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "VerifiableCredentialSchema" ALTER COLUMN "version" SET DATA TYPE TEXT; diff --git a/services/credential-schema/prisma/migrations/20230214060951_rendering_templates/migration.sql b/services/credential-schema/prisma/migrations/20230214060951_rendering_templates/migration.sql new file mode 100644 index 000000000..9aaa77a59 --- /dev/null +++ b/services/credential-schema/prisma/migrations/20230214060951_rendering_templates/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "Template" ( + "id" TEXT NOT NULL, + "schema" TEXT NOT NULL, + "template" TEXT NOT NULL, + "type" TEXT NOT NULL, + + CONSTRAINT "Template_pkey" PRIMARY KEY ("id") +); diff --git a/services/credential-schema/prisma/migrations/20230620145819_add_create_update_fields_and_rename_schema_id/migration.sql b/services/credential-schema/prisma/migrations/20230620145819_add_create_update_fields_and_rename_schema_id/migration.sql new file mode 100644 index 000000000..81ad48705 --- /dev/null +++ b/services/credential-schema/prisma/migrations/20230620145819_add_create_update_fields_and_rename_schema_id/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the column `schema` on the `Template` table. All the data in the column will be lost. + - Added the required column `schemaId` to the `Template` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Template" DROP COLUMN "schema", +ADD COLUMN "createdBy" TEXT, +ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "schemaId" TEXT NOT NULL, +ADD COLUMN "updatedBy" TEXT, +ADD COLUMN "updated_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP; diff --git a/services/credential-schema/prisma/migrations/20230620184446_add_template_id/migration.sql b/services/credential-schema/prisma/migrations/20230620184446_add_template_id/migration.sql new file mode 100644 index 000000000..d20721094 --- /dev/null +++ b/services/credential-schema/prisma/migrations/20230620184446_add_template_id/migration.sql @@ -0,0 +1,13 @@ +/* + Warnings: + + - The primary key for the `Template` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `id` on the `Template` table. All the data in the column will be lost. + - The required column `templateId` was added to the `Template` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. + +*/ +-- AlterTable +ALTER TABLE "Template" DROP CONSTRAINT "Template_pkey", +DROP COLUMN "id", +ADD COLUMN "templateId" TEXT NOT NULL, +ADD CONSTRAINT "Template_pkey" PRIMARY KEY ("templateId"); diff --git a/services/credential-schema/prisma/migrations/20230620185144_update_create_update_types/migration.sql b/services/credential-schema/prisma/migrations/20230620185144_update_create_update_types/migration.sql new file mode 100644 index 000000000..68b17e941 --- /dev/null +++ b/services/credential-schema/prisma/migrations/20230620185144_update_create_update_types/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `created_at` on the `Template` table. All the data in the column will be lost. + - You are about to drop the column `updated_at` on the `Template` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Template" DROP COLUMN "created_at", +DROP COLUMN "updated_at", +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3); diff --git a/services/credential-schema/prisma/migrations/20230620192530_remove_presentation_schemas/migration.sql b/services/credential-schema/prisma/migrations/20230620192530_remove_presentation_schemas/migration.sql new file mode 100644 index 000000000..063dd7752 --- /dev/null +++ b/services/credential-schema/prisma/migrations/20230620192530_remove_presentation_schemas/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the `VerifiablePresentationSchema` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE "VerifiablePresentationSchema"; diff --git a/services/credential-schema/prisma/migrations/20230620192832_add_status_for_cred_schema/migration.sql b/services/credential-schema/prisma/migrations/20230620192832_add_status_for_cred_schema/migration.sql new file mode 100644 index 000000000..501aa6aa6 --- /dev/null +++ b/services/credential-schema/prisma/migrations/20230620192832_add_status_for_cred_schema/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "SchemaStatus" AS ENUM ('DRAFT'); + +-- AlterTable +ALTER TABLE "VerifiableCredentialSchema" ADD COLUMN "status" "SchemaStatus" NOT NULL DEFAULT 'DRAFT'; diff --git a/services/credential-schema/prisma/migrations/20230620193330_add_created_by_updatedby/migration.sql b/services/credential-schema/prisma/migrations/20230620193330_add_created_by_updatedby/migration.sql new file mode 100644 index 000000000..e0cf0325e --- /dev/null +++ b/services/credential-schema/prisma/migrations/20230620193330_add_created_by_updatedby/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "VerifiableCredentialSchema" ADD COLUMN "createdBy" TEXT, +ADD COLUMN "updatedBy" TEXT; diff --git a/services/credential-schema/prisma/migrations/20230621063230_make_proof_optional/migration.sql b/services/credential-schema/prisma/migrations/20230621063230_make_proof_optional/migration.sql new file mode 100644 index 000000000..2f1e73d0a --- /dev/null +++ b/services/credential-schema/prisma/migrations/20230621063230_make_proof_optional/migration.sql @@ -0,0 +1,13 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "SchemaStatus" ADD VALUE 'ACTIVE'; +ALTER TYPE "SchemaStatus" ADD VALUE 'DEPRECATED'; + +-- AlterTable +ALTER TABLE "VerifiableCredentialSchema" ALTER COLUMN "proof" DROP NOT NULL; diff --git a/services/credential-schema/prisma/migrations/20230621070453_add_deprecated_id/migration.sql b/services/credential-schema/prisma/migrations/20230621070453_add_deprecated_id/migration.sql new file mode 100644 index 000000000..48b7b0eb4 --- /dev/null +++ b/services/credential-schema/prisma/migrations/20230621070453_add_deprecated_id/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "VerifiableCredentialSchema" ADD COLUMN "deprecatedId" TEXT; diff --git a/services/credential-schema/prisma/migrations/20230621151259_update_status_types/migration.sql b/services/credential-schema/prisma/migrations/20230621151259_update_status_types/migration.sql new file mode 100644 index 000000000..a84a4b5e7 --- /dev/null +++ b/services/credential-schema/prisma/migrations/20230621151259_update_status_types/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - The values [ACTIVE,DEPRECATED] on the enum `SchemaStatus` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "SchemaStatus_new" AS ENUM ('DRAFT', 'PUBLISHED', 'REVOKED'); +ALTER TABLE "VerifiableCredentialSchema" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "VerifiableCredentialSchema" ALTER COLUMN "status" TYPE "SchemaStatus_new" USING ("status"::text::"SchemaStatus_new"); +ALTER TYPE "SchemaStatus" RENAME TO "SchemaStatus_old"; +ALTER TYPE "SchemaStatus_new" RENAME TO "SchemaStatus"; +DROP TYPE "SchemaStatus_old"; +ALTER TABLE "VerifiableCredentialSchema" ALTER COLUMN "status" SET DEFAULT 'DRAFT'; +COMMIT; diff --git a/services/credential-schema/prisma/migrations/20230707095947_make_primary_key_composite/migration.sql b/services/credential-schema/prisma/migrations/20230707095947_make_primary_key_composite/migration.sql new file mode 100644 index 000000000..caceda782 --- /dev/null +++ b/services/credential-schema/prisma/migrations/20230707095947_make_primary_key_composite/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - The primary key for the `VerifiableCredentialSchema` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- AlterTable +ALTER TABLE "VerifiableCredentialSchema" DROP CONSTRAINT "VerifiableCredentialSchema_pkey", +ADD CONSTRAINT "VerifiableCredentialSchema_pkey" PRIMARY KEY ("id", "version"); diff --git a/services/credential-schema/prisma/migrations/20230707102219_fix_schema_status/migration.sql b/services/credential-schema/prisma/migrations/20230707102219_fix_schema_status/migration.sql new file mode 100644 index 000000000..a67cc9c4a --- /dev/null +++ b/services/credential-schema/prisma/migrations/20230707102219_fix_schema_status/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "SchemaStatus" ADD VALUE 'DEPRECATED'; diff --git a/services/credential-schema/prisma/migrations/migration_lock.toml b/services/credential-schema/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..fbffa92c2 --- /dev/null +++ b/services/credential-schema/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/services/credential-schema/prisma/schema.prisma b/services/credential-schema/prisma/schema.prisma new file mode 100644 index 000000000..b038f3712 --- /dev/null +++ b/services/credential-schema/prisma/schema.prisma @@ -0,0 +1,53 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") +} + +enum SchemaStatus { + DRAFT + PUBLISHED + DEPRECATED + REVOKED +} + +model VerifiableCredentialSchema { + id String + type String + version String + name String + author String + authored DateTime + schema Json + proof Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy String? + updatedBy String? + deletedAt DateTime? + tags String[] + status SchemaStatus @default(DRAFT) + deprecatedId String? + + @@id([id, version]) + @@index([type], type: Hash) + @@index([name], type: Hash) +} + +model Template { + templateId String @id @default(cuid()) + schemaId String + template String + type String + createdAt DateTime @default(now()) + updatedAt DateTime? @updatedAt + createdBy String? + updatedBy String? +} diff --git a/services/credential-schema/samples/marksheet.json b/services/credential-schema/samples/marksheet.json new file mode 100644 index 000000000..a9dbefa76 --- /dev/null +++ b/services/credential-schema/samples/marksheet.json @@ -0,0 +1,99 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://playground.chapi.io/examples/alumni/alumni-v1.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": "https://w3c-ccg.github.io/vc-json-schemas/", + "version": "1.0", + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "name": "UP Board of Secondary school Education class 12th Marksheet", + "author": "did:example:c276e12ec21ebfeb1f712ebc6f1", + "authored": "2022-12-19T09:22:23.064Z", + "schema": { + "$id": "UP-Board-Class-12-Marksheet-Credential-1.0", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "description": "The holder has scored the in UP Board 12 exams.", + "type": "object", + "properties": { + "roll_number": { + "type": "string", + "description": "Roll number of the holder in the examination." + }, + "name": { + "type": "string", + "description": "Name of the holder." + }, + "fathers_name": { + "type": "string", + "description": "Name of the father of the holder." + }, + "mothers_name": { + "type": "string", + "description": "Name of the mother of the holder." + }, + "DOB": { + "type": "date-time", + "description": "date of birth of the holder" + }, + "result": { + "type": "string", + "description": "Result of the holder in the examination" + }, + "score_details": { + "type": "array", + "description": "Marks scored by the holder in various subjects.", + "items": { + "type": "object", + "properties": { + "subject": { + "type": "string", + "description": "Name of the subject" + }, + "theory_score": { + "type": "number", + "description": "Marks scored by the holder in the subject" + }, + "practical_score": { + "type": "number", + "description": "Marks scored by the holder in the practicals of the subject" + }, + "total_score": { + "type": "number", + "description": "Total marks scored by the holder in the subject" + }, + "grade": { + "type": "string", + "description": "Grade scored by the holder in the subject" + } + }, + "required": [ + "subject", + "theory_score", + "practical_score", + "total_score", + "grade" + ] + } + } + }, + "required": [ + "roll_number", + "name", + "fathers_name", + "mothers_name", + "DOB", + "result", + "score_details" + ], + "additionalProperties": false + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2022-12-19T09:22:23Z", + "verificationMethod": "did:key:z6MkqYDbJ5yVgg5UvfRt5DAsk5dvPTgo6H9CZcenziWdHTqN#z6MkqYDbJ5yVgg5UvfRt5DAsk5dvPTgo6H9CZcenziWdHTqN", + "proofPurpose": "assertionMethod", + "proofValue": "z5iBktnPCr3hPqN7FViY948ds5yMhrL1qujMmVD1GmzsbtXw5RUCdu4GKrQZw8U9c4G78SUNmPLTS87tz6kGAHgXB" + } +} diff --git a/services/credential-schema/samples/proof_of_academic_evaluation.json b/services/credential-schema/samples/proof_of_academic_evaluation.json new file mode 100644 index 000000000..70a4223a1 --- /dev/null +++ b/services/credential-schema/samples/proof_of_academic_evaluation.json @@ -0,0 +1,52 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://playground.chapi.io/examples/alumni/alumni-v1.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": "https://w3c-ccg.github.io/vc-json-schemas/", + "version": "1.0", + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "name": "Proof of Academic Evaluation Credential", + "author": "did:example:c276e12ec21ebfeb1f712ebc6f1", + "authored": "2022-12-19T09:22:23.064Z", + "schema": { + "$id": "Proof-of-Academic-Evaluation-Credential-1.0", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "description": "The holder has secured the in from .", + "type": "object", + "properties": { + "grade": { + "type": "string", + "description": "Grade (%age, GPA, etc.) secured by the holder." + }, + "programme": { + "type": "string", + "description": "Name of the programme pursed by the holder." + }, + "certifyingInstitute": { + "type": "string", + "description": "Name of the instute which certified the said grade in the said skill" + }, + "evaluatingInstitute": { + "type": "string", + "description": "Name of the institute which ran the programme and evaluated the holder." + } + }, + "required": [ + "grade", + "programme", + "certifyingInstitute", + "evaluatingInstitute" + ], + "additionalProperties": false + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2022-12-19T09:22:23Z", + "verificationMethod": "did:key:z6MkqYDbJ5yVgg5UvfRt5DAsk5dvPTgo6H9CZcenziWdHTqN#z6MkqYDbJ5yVgg5UvfRt5DAsk5dvPTgo6H9CZcenziWdHTqN", + "proofPurpose": "assertionMethod", + "proofValue": "z5iBktnPCr3hPqN7FViY948ds5yMhrL1qujMmVD1GmzsbtXw5RUCdu4GKrQZw8U9c4G78SUNmPLTS87tz6kGAHgXB" + } +} diff --git a/services/credential-schema/samples/proof_of_alumni.json b/services/credential-schema/samples/proof_of_alumni.json new file mode 100644 index 000000000..a9348018d --- /dev/null +++ b/services/credential-schema/samples/proof_of_alumni.json @@ -0,0 +1,46 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://playground.chapi.io/examples/alumni/alumni-v1.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": "https://w3c-ccg.github.io/vc-json-schemas/", + "version": "1.0", + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "name": "Alumni Credential", + "author": "did:example:c276e12ec21ebfeb1f712ebc6f1", + "authored": "2022-12-19T09:22:23.064Z", + "schema": { + "$id": "Alumni-Credential-1.0", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "description": "The holder is an alumni of Example University.", + "type": "object", + "properties": { + "alumniOf": { + "type": "object", + "properties": { + "identifier": { + "type": "string", + "format": "did", + "description": "The did for the issuing university (issuer)." + }, + "name": { + "type": "string", + "description": "Name of the issuing university (issuer)." + } + }, + "required": ["identifier", "name"] + } + }, + "required": ["alumniOf"], + "additionalProperties": false + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2022-12-19T09:22:23Z", + "verificationMethod": "did:key:z6MkqYDbJ5yVgg5UvfRt5DAsk5dvPTgo6H9CZcenziWdHTqN#z6MkqYDbJ5yVgg5UvfRt5DAsk5dvPTgo6H9CZcenziWdHTqN", + "proofPurpose": "assertionMethod", + "proofValue": "z5iBktnPCr3hPqN7FViY948ds5yMhrL1qujMmVD1GmzsbtXw5RUCdu4GKrQZw8U9c4G78SUNmPLTS87tz6kGAHgXB" + } +} diff --git a/services/credential-schema/samples/proof_of_marks.json b/services/credential-schema/samples/proof_of_marks.json new file mode 100644 index 000000000..7ad2eff85 --- /dev/null +++ b/services/credential-schema/samples/proof_of_marks.json @@ -0,0 +1,52 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://playground.chapi.io/examples/alumni/alumni-v1.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": "https://w3c-ccg.github.io/vc-json-schemas/", + "version": "1.0", + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "name": "Marks Credential", + "author": "did:example:c276e12ec21ebfeb1f712ebc6f1", + "authored": "2022-12-19T09:22:23.064Z", + "schema": { + "$id": "Marks-Credential-1.0", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "description": "The holder has scored the in ABC Examination.", + "type": "object", + "properties": { + "score": { + "type": "number", + "description": "Marks scored by the holder in ABC Examination." + }, + "examinationName": { + "type": "string", + "description": "Name of the examination taken" + }, + "releasingInstitution": { + "type": "string", + "description": "Name of the instute which released the score" + }, + "organisingInstitute": { + "type": "string", + "description": "Name of the institute which organised the examination." + } + }, + "required": [ + "score", + "examinationName", + "releasingInstitution", + "organisingInstitute" + ], + "additionalProperties": false + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2022-12-19T09:22:23Z", + "verificationMethod": "did:key:z6MkqYDbJ5yVgg5UvfRt5DAsk5dvPTgo6H9CZcenziWdHTqN#z6MkqYDbJ5yVgg5UvfRt5DAsk5dvPTgo6H9CZcenziWdHTqN", + "proofPurpose": "assertionMethod", + "proofValue": "z5iBktnPCr3hPqN7FViY948ds5yMhrL1qujMmVD1GmzsbtXw5RUCdu4GKrQZw8U9c4G78SUNmPLTS87tz6kGAHgXB" + } +} diff --git a/services/credential-schema/samples/proof_of_training.json b/services/credential-schema/samples/proof_of_training.json new file mode 100644 index 000000000..ba554699e --- /dev/null +++ b/services/credential-schema/samples/proof_of_training.json @@ -0,0 +1,43 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://playground.chapi.io/examples/alumni/alumni-v1.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": "https://w3c-ccg.github.io/vc-json-schemas/", + "version": "1.0", + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "name": "Proof of Training Credential", + "author": "did:example:c276e12ec21ebfeb1f712ebc6f1", + "authored": "2022-12-19T09:22:23.064Z", + "schema": { + "$id": "Proof-of-Training-Credential-1.0", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "description": "The holder has completed training for in .", + "type": "object", + "properties": { + "skill": { + "type": "string", + "description": "The skill for which the holder was trained." + }, + "certifyingInstitute": { + "type": "string", + "description": "Name of the instute which certified the holder being skilled in the said skill" + }, + "trainingInstitute": { + "type": "string", + "description": "Name of the institute which organised the training." + } + }, + "required": ["skill", "releasingInstitute", "trainingInstitute"], + "additionalProperties": false + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2022-12-19T09:22:23Z", + "verificationMethod": "did:key:z6MkqYDbJ5yVgg5UvfRt5DAsk5dvPTgo6H9CZcenziWdHTqN#z6MkqYDbJ5yVgg5UvfRt5DAsk5dvPTgo6H9CZcenziWdHTqN", + "proofPurpose": "assertionMethod", + "proofValue": "z5iBktnPCr3hPqN7FViY948ds5yMhrL1qujMmVD1GmzsbtXw5RUCdu4GKrQZw8U9c4G78SUNmPLTS87tz6kGAHgXB" + } +} diff --git a/services/credential-schema/schema.json b/services/credential-schema/schema.json new file mode 100644 index 000000000..2eee5fe61 --- /dev/null +++ b/services/credential-schema/schema.json @@ -0,0 +1,71 @@ +{ + "$id": "credential-schema-2.0", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "description": "JSON Schema for W3C Verifiable Credential JSON Schema", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authored": { + "type": "string" + }, + "schema": { + "type": "object", + "properties": { + "$id": { + "type": "string" + }, + "$schema": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "properties": { + "type": "object" + }, + "required": { + "type": "array", + "items": [ + { + "type": "string" + } + ] + }, + "additionalProperties": { + "type": "boolean" + } + }, + "required": [ + "$id", + "$schema", + "description", + "type", + "properties", + "required", + "additionalProperties" + ] + } + }, + "required": ["type", "name", "author", "authored", "schema"] +} diff --git a/services/credential-schema/src/app.controller.ts b/services/credential-schema/src/app.controller.ts new file mode 100644 index 000000000..089486b2f --- /dev/null +++ b/services/credential-schema/src/app.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Get } from '@nestjs/common'; +import { HealthCheck, HealthCheckService, HttpHealthIndicator } from '@nestjs/terminus'; +import { PrismaHealthIndicator } from './utils/prisma.health'; + +@Controller() +export class AppController { + constructor( + private readonly healthCheckService: HealthCheckService, + private readonly prismaIndicator: PrismaHealthIndicator, + private readonly http: HttpHealthIndicator + ) {} + @Get('/health') + @HealthCheck() + public async checkHealth() { + return this.healthCheckService.check([ + async () => this.prismaIndicator.isHealthy('db'), + async () => this.http.responseCheck('identity-service', `${process.env.IDENTITY_BASE_URL}/health`, (res) => res.status === 200) + ]); + } +} diff --git a/services/credential-schema/src/app.module.ts b/services/credential-schema/src/app.module.ts new file mode 100644 index 000000000..1b496a625 --- /dev/null +++ b/services/credential-schema/src/app.module.ts @@ -0,0 +1,41 @@ +import { CacheModule, Module } from '@nestjs/common'; +import { AppController } from './app.controller'; +import { SchemaService } from './schema/schema.service'; +import { SchemaModule } from './schema/schema.module'; +import { ConfigModule } from '@nestjs/config'; +import { RenderingTemplatesModule } from './rendering-templates/rendering-templates.module'; +import { HttpModule } from '@nestjs/axios'; +import { UtilsService } from './utils/utils.service'; +import { TerminusModule } from '@nestjs/terminus'; +import { PrismaHealthIndicator } from './utils/prisma.health'; +import { PrismaClient } from '@prisma/client'; +import { APP_GUARD } from '@nestjs/core'; +import { AuthGuard } from './auth/auth.guard'; + +@Module({ + imports: [ + SchemaModule, + ConfigModule.forRoot({ + isGlobal: true, + }), + CacheModule.register({ + isGlobal: true, + max: 1000, + }), // using in memory cache for now + RenderingTemplatesModule, + HttpModule, + TerminusModule, + ], + controllers: [AppController], + providers: [ + SchemaService, + UtilsService, + PrismaHealthIndicator, + PrismaClient, + { + provide: APP_GUARD, + useClass: AuthGuard, + }, + ], +}) +export class AppModule {} diff --git a/services/credential-schema/src/auth/auth.guard.ts b/services/credential-schema/src/auth/auth.guard.ts new file mode 100644 index 000000000..d659805d6 --- /dev/null +++ b/services/credential-schema/src/auth/auth.guard.ts @@ -0,0 +1,64 @@ +import { Logger, CanActivate, Injectable, Inject } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Reflector } from '@nestjs/core'; +import * as jwt from 'jsonwebtoken'; +import * as jwksClient from 'jwks-rsa'; + +@Injectable() +export class AuthGuard implements CanActivate { + private client: any; + private getKey: any; + private readonly logger = new Logger(AuthGuard.name); + + public constructor( + private readonly reflector: Reflector, + private readonly configService: ConfigService, + ) { + this.client = jwksClient({ + jwksUri: process.env.JWKS_URI, + requestHeaders: {}, // Optional + timeout: 30000, // Defaults to 30s + }); + + this.getKey = (header, callback) => { + this.client.getSigningKey(header.kid, function (err, key) { + if (err) callback(err, null); + const signingKey = key?.publicKey || key?.rsaPublicKey; + callback(null, signingKey); + }); + }; + } + + async canActivate(context: any): Promise { + const isPublic = this.reflector.get( + 'isPublic', + context.getHandler(), + ); + if (isPublic) return true; + + if (process.env.ENABLE_AUTH === undefined) { + this.logger.warn('ENABLE_AUTH is not set, defaulting to true'); + } + if (process.env.ENABLE_AUTH && process.env.ENABLE_AUTH.trim() === 'false') + return true; + + const request = context.switchToHttp().getRequest(); + const authHeader = request.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer')) { + this.logger.log('No Bearer token found'); + return false; + } + const bearerToken = authHeader.substring(7, authHeader.length); + return new Promise((resolve) => { + jwt.verify(bearerToken, this.getKey, (err, decoded) => { + if (err) { + this.logger.log(err); + resolve(false); + } + if (decoded) resolve(true); + resolve(false); + }); + }); + } +} diff --git a/services/credential-schema/src/main.ts b/services/credential-schema/src/main.ts new file mode 100644 index 000000000..5ee6f2a6e --- /dev/null +++ b/services/credential-schema/src/main.ts @@ -0,0 +1,38 @@ +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { + FastifyAdapter, + NestFastifyApplication, +} from '@nestjs/platform-fastify'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create( + AppModule, + new FastifyAdapter(), + { + logger: + process.env.NODE_ENV && process.env.NODE_ENV.toLowerCase() === 'debug' + ? ['log', 'debug', 'error', 'verbose', 'warn'] + : ['error', 'warn', 'log'], + }, + ); + + const config = new DocumentBuilder() + .setTitle('Credential Schema API') + .setDescription( + 'APIs for creating and managing Verifiable Credential Schemas', + ) + .setVersion(process.env.npm_package_version) + .addTag('VC-Schemas') + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); + + const port = process.env.PORT || 3333; + await app.listen(port, '0.0.0.0'); + Logger.log(`🚀 Application is running on: http://0.0.0.0:${port}/`); +} +bootstrap(); diff --git a/services/credential-schema/src/rendering-templates/dto/addTemplate.dto.ts b/services/credential-schema/src/rendering-templates/dto/addTemplate.dto.ts new file mode 100644 index 000000000..58b1ada73 --- /dev/null +++ b/services/credential-schema/src/rendering-templates/dto/addTemplate.dto.ts @@ -0,0 +1,6 @@ +export class AddTemplateDTO { + schemaId: string; + schemaVersion: string; + template: string; + type: string; +} diff --git a/services/credential-schema/src/rendering-templates/dto/updateTemplate.dto.ts b/services/credential-schema/src/rendering-templates/dto/updateTemplate.dto.ts new file mode 100644 index 000000000..af27222d4 --- /dev/null +++ b/services/credential-schema/src/rendering-templates/dto/updateTemplate.dto.ts @@ -0,0 +1,6 @@ +export class UpdateTemplateDTO { + schemaId: string; + schemaVersion: string; + template: string; + type: string; +} diff --git a/services/credential-schema/src/rendering-templates/entities/Template.entity.ts b/services/credential-schema/src/rendering-templates/entities/Template.entity.ts new file mode 100644 index 000000000..c46cad49e --- /dev/null +++ b/services/credential-schema/src/rendering-templates/entities/Template.entity.ts @@ -0,0 +1,21 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +// represents the Template model created in Prisma +export class Template { + @ApiProperty({ type: String, description: 'Unique CUID' }) + templateId: string; + @ApiProperty({ type: String, description: 'Schema ID' }) + schemaId: string; + @ApiProperty({ type: String, description: 'HTML template' }) + template: string; + @ApiProperty({ type: String, description: 'type' }) + type: string; + @ApiProperty({ type: Date, description: 'createdAt' }) + createdAt: Date; + @ApiPropertyOptional({ type: Date, description: 'updatedAt' }) + updatedAt: Date; + @ApiPropertyOptional({ type: Date, description: 'createdBy' }) + createdBy: Date; + @ApiPropertyOptional({ type: Date, description: 'updatedBy' }) + updatedBy: Date; +} diff --git a/services/credential-schema/src/rendering-templates/entities/TemplateBody.entity.ts b/services/credential-schema/src/rendering-templates/entities/TemplateBody.entity.ts new file mode 100644 index 000000000..ca43820f6 --- /dev/null +++ b/services/credential-schema/src/rendering-templates/entities/TemplateBody.entity.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; + +ApiProperty; +export class TemplateBody { + @ApiProperty({ type: String, description: 'Schema ID' }) + schemaId: string; + @ApiProperty({ type: String, description: 'Template' }) + template: string; + @ApiProperty({ type: String, description: 'Type of Template' }) + type: string; +} diff --git a/services/credential-schema/src/rendering-templates/rendering-templates.controller.spec.ts b/services/credential-schema/src/rendering-templates/rendering-templates.controller.spec.ts new file mode 100644 index 000000000..9639acb1a --- /dev/null +++ b/services/credential-schema/src/rendering-templates/rendering-templates.controller.spec.ts @@ -0,0 +1,34 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RenderingTemplatesController } from './rendering-templates.controller'; +import { RenderingTemplatesService } from './rendering-templates.service'; +import { HttpModule } from '@nestjs/axios'; +import { ValidateTemplateService } from './validate-template.service'; +import { SchemaService } from '../schema/schema.service'; +import { UtilsService } from '../utils/utils.service'; +import { PrismaClient } from '@prisma/client'; + +describe('RenderingTemplatesController', () => { + let controller: RenderingTemplatesController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [RenderingTemplatesController], + imports: [HttpModule], + providers: [ + RenderingTemplatesService, + PrismaClient, + ValidateTemplateService, + SchemaService, + UtilsService, + ], + }).compile(); + + controller = module.get( + RenderingTemplatesController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/services/credential-schema/src/rendering-templates/rendering-templates.controller.ts b/services/credential-schema/src/rendering-templates/rendering-templates.controller.ts new file mode 100644 index 000000000..05a46ccf2 --- /dev/null +++ b/services/credential-schema/src/rendering-templates/rendering-templates.controller.ts @@ -0,0 +1,119 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, +} from '@nestjs/common'; +import { AddTemplateDTO } from './dto/addTemplate.dto'; +import { UpdateTemplateDTO } from './dto/updateTemplate.dto'; +import { RenderingTemplatesService } from './rendering-templates.service'; +import { + ApiBadRequestResponse, + ApiBody, + ApiCreatedResponse, + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiQuery, +} from '@nestjs/swagger'; +import { Template } from './entities/Template.entity'; +import { TemplateBody } from './entities/TemplateBody.entity'; + +@Controller('template') +export class RenderingTemplatesController { + constructor( + private readonly renderingTemplateService: RenderingTemplatesService, + ) {} + + @Get() + @ApiQuery({ name: 'schemaId', required: true, type: String }) + @ApiOperation({ summary: 'Get a Template by schemaId' }) + @ApiOkResponse({ + status: 200, + description: 'The record was found.', + type: Template, + }) + @ApiNotFoundResponse({ + status: 404, + description: 'The record has not been found.', + }) + getTemplateBySchemaID(@Query('schemaId') schemaId: string) { + return this.renderingTemplateService.getTemplateBySchemaID(schemaId); + } + + @ApiQuery({ name: 'templateId', required: true, type: String }) + @ApiOperation({ summary: 'Get a Template by templateId' }) + @ApiOkResponse({ + status: 200, + description: 'The record has been successfully created.', + type: Template, + }) + @ApiNotFoundResponse({ + status: 404, + description: 'The record has not been found.', + }) + @Get(':templateId') + getTemplateById(@Param('templateId') id: string) { + return this.renderingTemplateService.getTemplateById(id); + } + + @ApiOperation({ summary: 'Create a new Template' }) + @ApiBody({ + type: TemplateBody, + }) + @ApiCreatedResponse({ + status: 201, + description: 'The record has been successfully created.', + type: Template, + }) + @ApiBadRequestResponse({ + status: 400, + description: 'There was some problem with the request.', + }) + @Post() + addTemplate(@Body() addTemplateDto: AddTemplateDTO) { + return this.renderingTemplateService.addTemplate(addTemplateDto); + } + + @ApiOperation({ summary: 'Update the template by templateId' }) + @ApiBody({ + type: TemplateBody, + }) + @ApiOkResponse({ + status: 201, + description: 'The record has been successfully udpated.', + type: Template, + }) + @ApiBadRequestResponse({ + status: 400, + description: 'There was some problem with the request.', + }) + @ApiQuery({ name: 'templateId', required: true, type: String }) + @Put(':templateId') + updateTemplate( + @Body() updateTemplateDto: UpdateTemplateDTO, + @Param('templateId') id: string, + ) { + return this.renderingTemplateService.updateTemplate(id, updateTemplateDto); + } + + @ApiOperation({ summary: 'Delete the template by templateId' }) + @ApiOkResponse({ + status: 200, + description: 'The record has been successfully deleted.', + type: Template, + }) + @ApiBadRequestResponse({ + status: 400, + description: 'There was some problem with the request.', + }) + @ApiQuery({ name: 'templateId', required: true, type: String }) + @Delete(':templateId') + deleteTemplate(@Param('templateId') id: string) { + return this.renderingTemplateService.deleteTemplate(id); + } +} diff --git a/services/credential-schema/src/rendering-templates/rendering-templates.fixtures.ts b/services/credential-schema/src/rendering-templates/rendering-templates.fixtures.ts new file mode 100644 index 000000000..87b2c882c --- /dev/null +++ b/services/credential-schema/src/rendering-templates/rendering-templates.fixtures.ts @@ -0,0 +1,14 @@ +import { AddTemplateDTO } from './dto/addTemplate.dto'; + +export const templatePayloadGenerator = ( + schemaID: string, + schemaVersion: string, +): AddTemplateDTO => { + return { + schemaId: schemaID, + schemaVersion: schemaVersion, + template: + "Certificate

CERTIFICATE OF COMPLETION

is hereby awarded to

Daniel Vitorrie

for successfully completing the

diploma in Java Developer

Thank you for demonstrating the type of character and integrity that inspire others
", + type: 'Handlebar', + }; +}; diff --git a/services/credential-schema/src/rendering-templates/rendering-templates.module.ts b/services/credential-schema/src/rendering-templates/rendering-templates.module.ts new file mode 100644 index 000000000..ecc150b2d --- /dev/null +++ b/services/credential-schema/src/rendering-templates/rendering-templates.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { RenderingTemplatesService } from './rendering-templates.service'; +import { RenderingTemplatesController } from './rendering-templates.controller'; +import { ValidateTemplateService } from './validate-template.service'; +import { SchemaService } from '../schema/schema.service'; +import { HttpModule } from '@nestjs/axios'; +import { UtilsService } from '../utils/utils.service'; +import { PrismaClient } from '@prisma/client'; + +@Module({ + imports: [HttpModule], + providers: [ + RenderingTemplatesService, + PrismaClient, + ValidateTemplateService, + SchemaService, + UtilsService, + ], + controllers: [RenderingTemplatesController], +}) +export class RenderingTemplatesModule {} diff --git a/services/credential-schema/src/rendering-templates/rendering-templates.service.spec.ts b/services/credential-schema/src/rendering-templates/rendering-templates.service.spec.ts new file mode 100644 index 000000000..e30b7b350 --- /dev/null +++ b/services/credential-schema/src/rendering-templates/rendering-templates.service.spec.ts @@ -0,0 +1,136 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RenderingTemplatesService } from './rendering-templates.service'; +import { ValidateTemplateService } from './validate-template.service'; +import { SchemaService } from '../schema/schema.service'; +import { PrismaClient } from '@prisma/client'; +import { UtilsService } from '../utils/utils.service'; +import { HttpModule } from '@nestjs/axios'; +import { templatePayloadGenerator } from './rendering-templates.fixtures'; +import { + generateCredentialSchemaTestBody, + generateTestDIDBody, +} from '../schema/schema.fixtures'; + +describe('RenderingTemplatesService', () => { + let service: RenderingTemplatesService; + let schemaService: SchemaService; + let utilsService: UtilsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [HttpModule], + providers: [ + RenderingTemplatesService, + PrismaClient, + ValidateTemplateService, + SchemaService, + UtilsService, + ], + }).compile(); + + service = module.get(RenderingTemplatesService); + schemaService = module.get(SchemaService); + utilsService = module.get(UtilsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should create a template and get it by templateID', async () => { + const didBody = generateTestDIDBody(); + const did = await utilsService.generateDID(didBody); + const credSchemaPayload = generateCredentialSchemaTestBody(); + credSchemaPayload.schema.author = did.id; + const schema = await schemaService.createCredentialSchema( + credSchemaPayload, + ); + const templatePayload = templatePayloadGenerator( + schema.schema.id, + schema.schema.version, + ); + const template = await service.addTemplate(templatePayload); + expect(template).toBeDefined(); + const getTemplate = await service.getTemplateById( + template.template.templateId, + ); + expect(getTemplate.schemaId).toEqual(template.template.schemaId); + expect(getTemplate.template).toEqual(template.template.template); + }); + + it('should create a template with wrong schemaID', async () => { + const templatePayload = templatePayloadGenerator('randomID', '1.0.0'); + await expect(service.addTemplate(templatePayload)).rejects.toThrowError(); + }); + + it('should get a template by schemaID', async () => { + const didBody = generateTestDIDBody(); + const did = await utilsService.generateDID(didBody); + const credSchemaPayload = generateCredentialSchemaTestBody(); + credSchemaPayload.schema.author = did.id; + const schema = await schemaService.createCredentialSchema( + credSchemaPayload, + ); + const templatePayload = templatePayloadGenerator( + schema.schema.id, + schema.schema.version, + ); + const template = await service.addTemplate(templatePayload); + expect(template).toBeDefined(); + + const getTemplate = await service.getTemplateBySchemaID( + templatePayload.schemaId, + ); + expect(getTemplate).toBeDefined(); + expect(getTemplate[0].template).toEqual(template.template.template); + }); + + it('should update the template by templateID', async () => { + const didBody = generateTestDIDBody(); + const did = await utilsService.generateDID(didBody); + const credSchemaPayload = generateCredentialSchemaTestBody(); + credSchemaPayload.schema.author = did.id; + const schema = await schemaService.createCredentialSchema( + credSchemaPayload, + ); + const templatePayload = templatePayloadGenerator( + schema.schema.id, + schema.schema.version, + ); + const template = await service.addTemplate(templatePayload); + expect(template).toBeDefined(); + + const newTemplatePayload = templatePayloadGenerator( + schema.schema.id, + schema.schema.version, + ); + newTemplatePayload.template = 'TEST'; + + const ntemplate = await service.updateTemplate( + template.template.templateId, + newTemplatePayload, + ); + expect(ntemplate.template).toEqual(newTemplatePayload.template); + }); + + it('should delete the template by templateID', async () => { + const didBody = generateTestDIDBody(); + const did = await utilsService.generateDID(didBody); + const credSchemaPayload = generateCredentialSchemaTestBody(); + credSchemaPayload.schema.author = did.id; + const schema = await schemaService.createCredentialSchema( + credSchemaPayload, + ); + const templatePayload = templatePayloadGenerator( + schema.schema.id, + schema.schema.version, + ); + const template = await service.addTemplate(templatePayload); + expect(template).toBeDefined(); + + await service.deleteTemplate(template.template.templateId); + await expect( + service.getTemplateById(template.template.templateId), + ).rejects.toThrowError(); + }); +}); diff --git a/services/credential-schema/src/rendering-templates/rendering-templates.service.ts b/services/credential-schema/src/rendering-templates/rendering-templates.service.ts new file mode 100644 index 000000000..b9cbe1b64 --- /dev/null +++ b/services/credential-schema/src/rendering-templates/rendering-templates.service.ts @@ -0,0 +1,100 @@ +import { + Injectable, + InternalServerErrorException, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { PrismaClient, Template } from '@prisma/client'; +import { AddTemplateDTO } from './dto/addTemplate.dto'; +import { ValidateTemplateService } from './validate-template.service'; +import { TemplateWarnings } from './types/TemplateWarnings.interface'; + +@Injectable() +export class RenderingTemplatesService { + constructor( + private prisma: PrismaClient, + private readonly verifier: ValidateTemplateService, + ) {} + private logger = new Logger(RenderingTemplatesService.name); + async getTemplateBySchemaID(schemaId: string): Promise { + try { + return await this.prisma.template.findMany({ + where: { schemaId }, + }); + } catch (err) { + this.logger.error(err); + throw new InternalServerErrorException( + err, + 'Error fetching templates for the schemaID', + ); + } + } + + async getTemplateById(id: string): Promise