diff --git a/README.md b/README.md index e6070e0f..04211448 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,7 @@ The application allows configuring the following parameters using environment va #### Events tracking -1. `ENABLE_DATADOG`: enables sending information about events to datadog -2. `LOG_LEVEL`: specifies log level, like 'trace', 'debug', 'info', 'warn' or 'error'; +1. `LOG_LEVEL`: specifies log level, like 'trace', 'debug', 'info', 'warn' or 'error'; #### Network API endpoints diff --git a/docker/Dockerfile b/docker/Dockerfile index 5eb787ed..29630d59 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -68,7 +68,6 @@ ARG LOGTO_M2M_APP_SECRET ARG LOGTO_MANAGEMENT_API ARG LOGTO_DEFAULT_ROLE_ID ARG LOGTO_WEBHOOK_SECRET -ARG ENABLE_DATADOG=false ARG LOG_LEVEL=info # API generation @@ -118,7 +117,6 @@ ENV LOGTO_M2M_APP_SECRET ${LOGTO_M2M_APP_SECRET} ENV LOGTO_MANAGEMENT_API ${LOGTO_MANAGEMENT_API} ENV LOGTO_DEFAULT_ROLE_ID ${LOGTO_DEFAULT_ROLE_ID} ENV LOGTO_WEBHOOK_SECRET ${LOGTO_WEBHOOK_SECRET} -ENV ENABLE_DATADOG ${ENABLE_DATADOG} ENV LOG_LEVEL ${LOG_LEVEL} # API generatioin diff --git a/package-lock.json b/package-lock.json index 3dc77be0..5e6ee9f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8473,6 +8473,133 @@ "node": ">= 6" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@motionone/animation": { "version": "10.17.0", "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.17.0.tgz", diff --git a/src/app.ts b/src/app.ts index b8e45cff..c996c566 100644 --- a/src/app.ts +++ b/src/app.ts @@ -32,6 +32,7 @@ import { SubscriptionController } from './controllers/admin/subscriptions.js'; import { PriceController } from './controllers/admin/prices.js'; import { WebhookController } from './controllers/admin/webhook.js'; import { APIKeyController } from './controllers/admin/api-key.js'; +import { OrganisationController } from './controllers/admin/organisation.js'; let swaggerOptions = {}; if (process.env.ENABLE_AUTHENTICATION === 'true') { @@ -282,6 +283,14 @@ class App { // Webhook app.post('/admin/webhook', new WebhookController().handleWebhook); + + // Customer + app.post( + '/admin/organisation/update', + OrganisationController.organisationUpdatevalidator, + new OrganisationController().update + ); + app.get('/admin/organisation/get', new OrganisationController().get); } // 404 for all other requests diff --git a/src/controllers/admin/api-key.ts b/src/controllers/admin/api-key.ts index 88f3fce1..82edd873 100644 --- a/src/controllers/admin/api-key.ts +++ b/src/controllers/admin/api-key.ts @@ -20,7 +20,7 @@ import type { APIKeyUpdateResponseBody, APIKeyUpdateUnsuccessfulResponseBody, APIServiceOptions, -} from '../../types/portal.js'; +} from '../../types/admin.js'; import { EventTracker, eventTracker } from '../../services/track/tracker.js'; import { OperationNameEnum } from '../../types/constants.js'; diff --git a/src/controllers/admin/organisation.ts b/src/controllers/admin/organisation.ts new file mode 100644 index 00000000..49c210f7 --- /dev/null +++ b/src/controllers/admin/organisation.ts @@ -0,0 +1,141 @@ +import * as dotenv from 'dotenv'; +import type { Request, Response } from 'express'; +import { check } from 'express-validator'; +import { validate } from '../validator/decorator.js'; +import { CustomerService } from '../../services/api/customer.js'; +import { StatusCodes } from 'http-status-codes'; +import type { + AdminOrganisationGetUnsuccessfulResponseBody, + AdminOrganisationUpdateResponseBody, + AdminOrganisationUpdateUnsuccessfulResponseBody, +} from '../../types/admin.js'; +import { PaymentAccountService } from '../../services/api/payment-account.js'; + +dotenv.config(); + +export class OrganisationController { + static organisationUpdatevalidator = [ + check('name').optional().isString().withMessage('name should be a valid string'), + check('email').optional().isEmail().withMessage('email value should a well-formatted string'), + check('description').optional().isString().withMessage('description should be a valid string'), + ]; + + /** + * @openapi + * + * /admin/organisation/update: + * post: + * summary: Update an organisation + * description: Update an organisation + * tags: [Organisation] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * example: Cheqd + * email: + * type: string + * example: cheqd@example.com + * format: email + * description: + * type: string + * example: Cheqd organisation + * responses: + * 200: + * description: A successful response + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrganisationResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + * */ + @validate + async update(request: Request, response: Response) { + const { name, email, description } = request.body; + try { + const customer = await CustomerService.instance.update( + response.locals.customer.customerId, + name, + email, + description + ); + const paymentAccount = await PaymentAccountService.instance.find({ customer: customer }); + + if (!customer || paymentAccount.length === 0) { + response.status(StatusCodes.NOT_FOUND).json({ + error: 'Customer for updating not found', + } satisfies AdminOrganisationUpdateUnsuccessfulResponseBody); + } + + return response.status(StatusCodes.OK).json({ + name: customer.name, + email: customer.email, + description: customer.description, + cosmosAddress: paymentAccount[0].address as string, + } satisfies AdminOrganisationUpdateResponseBody); + } catch (error) { + return response.status(500).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies AdminOrganisationUpdateUnsuccessfulResponseBody); + } + } + + /** + * @openapi + * + * /admin/organisation/get: + * get: + * summary: Get an organisation + * description: Get an organisation + * tags: [Organisation] + * responses: + * 200: + * description: A successful response + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrganisationResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ + async get(request: Request, response: Response) { + try { + const customer = response.locals.customer; + const paymentAccount = await PaymentAccountService.instance.find({ customer: response.locals.customer }); + + if (!customer || paymentAccount.length === 0) { + response.status(StatusCodes.NOT_FOUND).json({ + error: 'Customer for current user was not found or did not setup properly. Please contact administrator.', + } satisfies AdminOrganisationGetUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + name: customer.name, + email: customer.email, + description: customer.description, + cosmosAddress: paymentAccount[0].address as string, + }); + } catch (error) { + return response.status(500).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies AdminOrganisationGetUnsuccessfulResponseBody); + } + } +} diff --git a/src/controllers/admin/portal-customer.ts b/src/controllers/admin/portal-customer.ts deleted file mode 100644 index 4c1d1c4d..00000000 --- a/src/controllers/admin/portal-customer.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as dotenv from 'dotenv'; -import type { Request, Response } from 'express'; -import { check } from 'express-validator'; -import { validationResult } from '../validator'; -import type { PortalCustomerGetUnsuccessfulResponseBody } from '../../types/portal.js'; - -dotenv.config(); - -export class PortalCustomerController { - static portalCustomerGetValidator = [ - check('logToUserId').optional().isString().withMessage('logToUserId should be a string').bail(), - ]; - - async get(request: Request, response: Response) { - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(400).json({ - error: result.array().pop()?.msg, - } satisfies PortalCustomerGetUnsuccessfulResponseBody); - } - - return response.status(500).json({ - error: 'Not implemented yet', - }); - } -} diff --git a/src/controllers/admin/prices.ts b/src/controllers/admin/prices.ts index 935b554d..9a463257 100644 --- a/src/controllers/admin/prices.ts +++ b/src/controllers/admin/prices.ts @@ -1,7 +1,7 @@ import type { Stripe } from 'stripe'; import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; -import type { PriceListResponseBody, PriceListUnsuccessfulResponseBody } from '../../types/portal.js'; +import type { PriceListResponseBody, PriceListUnsuccessfulResponseBody } from '../../types/admin.js'; import { StatusCodes } from 'http-status-codes'; import { check } from '../validator/index.js'; import { validate } from '../validator/decorator.js'; diff --git a/src/controllers/admin/product.ts b/src/controllers/admin/product.ts index 920695a0..31c36102 100644 --- a/src/controllers/admin/product.ts +++ b/src/controllers/admin/product.ts @@ -7,7 +7,7 @@ import type { ProductListResponseBody, ProductListUnsuccessfulResponseBody, ProductWithPrices, -} from '../../types/portal.js'; +} from '../../types/admin.js'; import { StatusCodes } from 'http-status-codes'; import { check } from '../validator/index.js'; import { validate } from '../validator/decorator.js'; diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index 9c21b2bf..0e822833 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -18,7 +18,7 @@ import type { SubscriptionResumeResponseBody, SubscriptionResumeRequestBody, SubscriptionCancelRequestBody, -} from '../../types/portal.js'; +} from '../../types/admin.js'; import { StatusCodes } from 'http-status-codes'; import { check } from '../validator/index.js'; import { SubscriptionService } from '../../services/admin/subscription.js'; diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index d08b8736..398fb173 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -359,6 +359,7 @@ export class AccountController { operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, data: { name: customer.name, + email: customer.email, customerId: customer.customerId, } satisfies ISubmitStripeCustomerCreateData, } satisfies ISubmitOperation); @@ -473,6 +474,7 @@ export class AccountController { operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, data: { name: customer.name, + email: customer.email, customerId: customer.customerId, } satisfies ISubmitStripeCustomerCreateData, } satisfies ISubmitOperation); diff --git a/src/database/entities/customer.entity.ts b/src/database/entities/customer.entity.ts index de774c1f..fd0a6ba7 100644 --- a/src/database/entities/customer.entity.ts +++ b/src/database/entities/customer.entity.ts @@ -14,6 +14,18 @@ export class CustomerEntity { }) name!: string; + @Column({ + type: 'text', + nullable: true, + }) + email?: string; + + @Column({ + type: 'text', + nullable: true, + }) + description?: string; + @Column({ type: 'timestamptz', nullable: false, @@ -42,9 +54,11 @@ export class CustomerEntity { this.updatedAt = new Date(); } - constructor(customerId: string, name: string) { + constructor(customerId: string, name: string, email?: string, description?: string) { this.customerId = customerId; this.name = name; + this.email = email; + this.description = description; } public isEqual(customer: CustomerEntity): boolean { diff --git a/src/database/migrations/AlterCustomerTableAddEmail.ts b/src/database/migrations/AlterCustomerTableAddEmail.ts new file mode 100644 index 00000000..fcf40bef --- /dev/null +++ b/src/database/migrations/AlterCustomerTableAddEmail.ts @@ -0,0 +1,29 @@ +import { TableColumn, type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AlterCustomerTableAddEmail1695740346005 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const table_name = 'customer'; + + await queryRunner.addColumn( + table_name, + new TableColumn({ + name: 'email', + type: 'text', + isNullable: true, + }) + ); + + await queryRunner.addColumn( + table_name, + new TableColumn({ + name: 'description', + type: 'text', + isNullable: true, + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + throw new Error('illegal_operation: cannot roll back initial migration'); + } +} diff --git a/src/database/types/types.ts b/src/database/types/types.ts index b708d107..ac357d54 100644 --- a/src/database/types/types.ts +++ b/src/database/types/types.ts @@ -38,6 +38,7 @@ import { AlterOperationTable1695740346001 } from '../migrations/AlterOperationTa import { SubscriptionEntity } from '../entities/subscription.entity.js'; import { CreateSubscritpionTable1695740346003 } from '../migrations/CreateSubscriptionTable.js'; import { AlterAPIKeyTable1695740346004 } from '../migrations/AlterAPIKeyTable.js'; +import { AlterCustomerTableAddEmail1695740346005 } from '../migrations/AlterCustomerTableAddEmail.js'; dotenv.config(); const { EXTERNAL_DB_CONNECTION_URL, EXTERNAL_DB_CERT } = process.env; @@ -112,6 +113,8 @@ export class Postgres implements AbstractDatabase { CreateSubscritpionTable1695740346003, // Add revoked field to APIKey table AlterAPIKeyTable1695740346004, + // Add email and description fields + AlterCustomerTableAddEmail1695740346005, ], entities: [ ...Entities, diff --git a/src/middleware/auth/auth-gaurd.ts b/src/middleware/auth/auth-gaurd.ts index 4227e657..0aac2f4b 100644 --- a/src/middleware/auth/auth-gaurd.ts +++ b/src/middleware/auth/auth-gaurd.ts @@ -10,7 +10,6 @@ import { PortalUserInfoFetcher } from './user-info-fetcher/portal-token.js'; import { IdTokenUserInfoFetcher } from './user-info-fetcher/idtoken.js'; import { M2MCredsTokenUserInfoFetcher } from './user-info-fetcher/m2m-creds-token.js'; import { APITokenUserInfoFetcher } from './user-info-fetcher/api-token.js'; -import { decodeJwt } from 'jose'; export class APIGuard { private authRuleRepository: AuthRuleRepository; @@ -72,30 +71,28 @@ export class APIGuard { * @return {void} This function does not return a value. */ private chooseUserFetcherStrategy(request: Request): void { - const authorizationHeader = request.headers.authorization as string; - const apiKeyHeader = request.headers['x-api-key'] as string; + const bearerToken = APIGuard.extractBearerTokenFromHeaders(request.headers) as string; + const portalToken = request.headers['id-token'] as string; + const m2mCreds = request.headers['customer-id'] as string; + const apiToken = request.headers['x-api-key'] as string; - if (authorizationHeader && apiKeyHeader) { - const token = authorizationHeader.replace('Bearer ', ''); - this.setUserInfoStrategy(new PortalUserInfoFetcher(token, apiKeyHeader, this.oauthProvider)); + if (apiToken) { + this.setUserInfoStrategy(new APITokenUserInfoFetcher(apiToken, this.oauthProvider)); return; } - if (authorizationHeader) { - const token = authorizationHeader.replace('Bearer ', ''); - const payload = decodeJwt(token); - - if (payload.aud === process.env.LOGTO_APP_ID) { - this.setUserInfoStrategy(new IdTokenUserInfoFetcher(token, this.oauthProvider)); - } else { - this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(token, this.oauthProvider)); - } + if (m2mCreds) { + this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(m2mCreds, this.oauthProvider)); + return; + } + if (portalToken && bearerToken) { + this.setUserInfoStrategy(new PortalUserInfoFetcher(bearerToken, portalToken, this.oauthProvider)); return; } - if (apiKeyHeader) { - this.setUserInfoStrategy(new APITokenUserInfoFetcher(apiKeyHeader, this.oauthProvider)); + if (bearerToken) { + this.setUserInfoStrategy(new IdTokenUserInfoFetcher(bearerToken, this.oauthProvider)); return; } diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index b2c720b4..cebd79ff 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -7,61 +7,39 @@ export class AdminAuthRuleProvider extends AuthRuleProvider { constructor() { super(); // Main swagger route - this.registerRule('/admin/swagger', 'GET', 'admin:swagger:testnet', { skipNamespace: true }); - this.registerRule('/admin/swagger', 'GET', 'admin:swagger:mainnet', { skipNamespace: true }); + this.registerRule('/admin/swagger', 'GET', 'admin:swagger', { skipNamespace: true }); // Subscriptions // skipNamespace is set to true cause we don't have the information about the namespace in the request - this.registerRule('/admin/subscription/create', 'POST', 'admin:subscription:create:testnet', { + this.registerRule('/admin/subscription/create', 'POST', 'admin:subscription:create', { skipNamespace: true, }); - this.registerRule('/admin/subscription/create', 'POST', 'admin:subscription:create:mainnet', { + this.registerRule('/admin/subscription/list', 'GET', 'admin:subscription:list', { skipNamespace: true, }); - this.registerRule('/admin/subscription/list', 'GET', 'admin:subscription:list:testnet', { + this.registerRule('/admin/subscription/update', 'POST', 'admin:subscription:update', { skipNamespace: true, }); - this.registerRule('/admin/subscription/list', 'GET', 'admin:subscription:list:mainnet', { + this.registerRule('/admin/subscription/cancel', 'POST', 'admin:subscription:cancel', { skipNamespace: true, }); - this.registerRule('/admin/subscription/update', 'POST', 'admin:subscription:update:testnet', { + this.registerRule('/admin/subscription/resume', 'POST', 'admin:subscription:resume', { skipNamespace: true, }); - this.registerRule('/admin/subscription/update', 'POST', 'admin:subscription:update:mainnet', { - skipNamespace: true, - }); - this.registerRule('/admin/subscription/cancel', 'POST', 'admin:subscription:cancel:testnet', { - skipNamespace: true, - }); - this.registerRule('/admin/subscription/cancel', 'POST', 'admin:subscription:cancel:mainnet', { - skipNamespace: true, - }); - this.registerRule('/admin/subscription/resume', 'POST', 'admin:subscription:resume:testnet', { - skipNamespace: true, - }); - this.registerRule('/admin/subscription/resume', 'POST', 'admin:subscription:resume:mainnet', { - skipNamespace: true, - }); - this.registerRule('/admin/subscription/get', 'GET', 'admin:subscription:get:testnet', { skipNamespace: true }); - this.registerRule('/admin/subscription/get', 'GET', 'admin:subscription:get:mainnet', { skipNamespace: true }); + this.registerRule('/admin/subscription/get', 'GET', 'admin:subscription:get', { skipNamespace: true }); // Prices - this.registerRule('/admin/price/list', 'GET', 'admin:price:list:testnet', { skipNamespace: true }); - this.registerRule('/admin/price/list', 'GET', 'admin:price:list:mainnet', { skipNamespace: true }); + this.registerRule('/admin/price/list', 'GET', 'admin:price:list', { skipNamespace: true }); // Products - this.registerRule('/admin/product/list', 'GET', 'admin:product:list:testnet', { skipNamespace: true }); - this.registerRule('/admin/product/list', 'GET', 'admin:product:list:mainnet', { skipNamespace: true }); - this.registerRule('/admin/product/get', 'GET', 'admin:product:get:testnet', { skipNamespace: true }); - this.registerRule('/admin/product/get', 'GET', 'admin:product:get:mainnet', { skipNamespace: true }); + this.registerRule('/admin/product/list', 'GET', 'admin:product:list', { skipNamespace: true }); + this.registerRule('/admin/product/get', 'GET', 'admin:product:get', { skipNamespace: true }); // API Key - this.registerRule('/admin/api-key/create', 'POST', 'admin:api-key:create:testnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/create', 'POST', 'admin:api-key:create:mainnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/update', 'POST', 'admin:api-key:update:testnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/update', 'POST', 'admin:api-key:update:mainnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/revoke', 'DELETE', 'admin:api-key:revoke:testnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/revoke', 'DELETE', 'admin:api-key:revoke:mainnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/get', 'GET', 'admin:api-key:get:testnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/get', 'GET', 'admin:api-key:get:mainnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/list', 'GET', 'admin:api-key:list:testnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/list', 'GET', 'admin:api-key:list:mainnet', { skipNamespace: true }); + this.registerRule('/admin/api-key/create', 'POST', 'admin:api-key:create', { skipNamespace: true }); + this.registerRule('/admin/api-key/update', 'POST', 'admin:api-key:update', { skipNamespace: true }); + this.registerRule('/admin/api-key/revoke', 'DELETE', 'admin:api-key:revoke', { skipNamespace: true }); + this.registerRule('/admin/api-key/get', 'GET', 'admin:api-key:get', { skipNamespace: true }); + this.registerRule('/admin/api-key/list', 'GET', 'admin:api-key:list', { skipNamespace: true }); + // Customer + this.registerRule('/admin/organisation/update', 'POST', 'admin:organisation:update', { skipNamespace: true }); + this.registerRule('/admin/organisation/get', 'GET', 'admin:organisation:get', { skipNamespace: true }); } } diff --git a/src/middleware/auth/user-info-fetcher/swagger-ui.ts b/src/middleware/auth/user-info-fetcher/swagger-ui.ts index 0b02f21c..297ad47d 100644 --- a/src/middleware/auth/user-info-fetcher/swagger-ui.ts +++ b/src/middleware/auth/user-info-fetcher/swagger-ui.ts @@ -11,7 +11,6 @@ export class SwaggerUserInfoFetcher extends UserInfoHelper implements IUserInfoF super(); this.oauthProvider = oauthProvider; } - /** * Tries to fetch user information based on the request and sets the appropriate response. * diff --git a/src/services/admin/api-key.ts b/src/services/admin/api-key.ts index adc843b7..59075bcd 100644 --- a/src/services/admin/api-key.ts +++ b/src/services/admin/api-key.ts @@ -10,7 +10,7 @@ import { APIKeyEntity } from '../../database/entities/api.key.entity.js'; import type { UserEntity } from '../../database/entities/user.entity.js'; import { SecretBox } from '@veramo/kms-local'; import { API_SECRET_KEY_LENGTH, API_KEY_PREFIX, API_KEY_EXPIRATION } from '../../types/constants.js'; -import type { APIServiceOptions } from '../../types/portal.js'; +import type { APIServiceOptions } from '../../types/admin.js'; dotenv.config(); export class APIKeyService { diff --git a/src/services/api/customer.ts b/src/services/api/customer.ts index 254290c4..10191648 100644 --- a/src/services/api/customer.ts +++ b/src/services/api/customer.ts @@ -20,7 +20,7 @@ export class CustomerService { this.customerRepository = Connection.instance.dbConnection.getRepository(CustomerEntity); } - public async create(name: string) { + public async create(name: string, email?: string, description?: string) { // The sequence for creating a customer is supposed to be: // 1. Create a new customer entity in the database; // 2. Create new cosmos keypair @@ -30,7 +30,7 @@ export class CustomerService { if (await this.isExist({ name: name })) { throw new Error(`Cannot create a new customer since the customer with same name ${name} already exists`); } - const customerEntity = new CustomerEntity(uuidv4(), name); + const customerEntity = new CustomerEntity(uuidv4(), name, email, description); await this.customerRepository.insert(customerEntity); // Create a new Cosmos account for the customer and make a link with customer entity; @@ -42,7 +42,13 @@ export class CustomerService { }; } - public async update(customerId: string, name?: string, paymentProviderId?: string) { + public async update( + customerId: string, + name?: string, + email?: string, + description?: string, + paymentProviderId?: string + ) { const existingCustomer = await this.customerRepository.findOneBy({ customerId }); if (!existingCustomer) { throw new Error(`CustomerId not found`); @@ -51,6 +57,14 @@ export class CustomerService { existingCustomer.name = name; } + if (email) { + existingCustomer.email = email; + } + + if (description) { + existingCustomer.description = description; + } + if (paymentProviderId) { existingCustomer.paymentProviderId = paymentProviderId; } diff --git a/src/services/identity/postgres.ts b/src/services/identity/postgres.ts index 2b1c3fe9..02204553 100644 --- a/src/services/identity/postgres.ts +++ b/src/services/identity/postgres.ts @@ -55,7 +55,7 @@ import type { AbstractIdentifierProvider } from '@veramo/did-manager'; import type { CheqdProviderError } from '@cheqd/did-provider-cheqd'; import type { TPublicKeyEd25519 } from '@cheqd/did-provider-cheqd'; import { toTPublicKeyEd25519 } from '../helpers.js'; -import type { APIServiceOptions } from '../../types/portal.js'; +import type { APIServiceOptions } from '../../types/admin.js'; dotenv.config(); diff --git a/src/services/track/tracker.ts b/src/services/track/tracker.ts index b854f2f5..864b5b87 100644 --- a/src/services/track/tracker.ts +++ b/src/services/track/tracker.ts @@ -1,6 +1,6 @@ import EventEmitter from 'node:events'; import type { INotifyMessage, ITrackOperation } from '../../types/track.js'; -import { DatadogNotifier, LoggerNotifier } from './notifiers.js'; +import { LoggerNotifier } from './notifiers.js'; import { DBOperationSubscriber } from './operation-subscriber.js'; import { ResourceSubscriber } from './api/resource-subscriber.js'; import { PresentationSubscriber } from './api/presentation-subscriber.js'; @@ -8,7 +8,6 @@ import { CredentialStatusSubscriber } from './api/credential-status-subscriber.j import { DIDSubscriber } from './api/did-subscriber.js'; import { CredentialSubscriber } from './api/credential-subscriber.js'; import type { ITrackType } from './types.js'; -import { ENABLE_DATADOG } from '../../types/constants.js'; import { SubmitSubject, TrackSubject } from './observer.js'; import type { ISubmitOperation } from './submitter.js'; import { PortalAccountCreateSubmitter } from './admin/account-submitter.js'; @@ -49,9 +48,6 @@ export class EventTracker { setupDefaultNotifiers() { this.notifier.attach(new LoggerNotifier()); - if (ENABLE_DATADOG) { - this.notifier.attach(new DatadogNotifier()); - } } setupDefaultSubmitters() { diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index f8d03627..8dd4307f 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -222,6 +222,97 @@ } } }, + "/admin/organisation/update": { + "post": { + "summary": "Update an organisation", + "description": "Update an organisation", + "tags": [ + "Organisation" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Cheqd" + }, + "email": { + "type": "string", + "example": "cheqd@example.com", + "format": "email" + }, + "description": { + "type": "string", + "example": "Cheqd organisation" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "A successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganisationResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/organisation/get": { + "get": { + "summary": "Get an organisation", + "description": "Get an organisation", + "tags": [ + "Organisation" + ], + "responses": { + "200": { + "description": "A successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganisationResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, "/admin/price/list": { "get": { "summary": "Get a list of prices", @@ -1034,6 +1125,37 @@ "ref": "#/components/schemas/APIKeyResponse" } }, + "OrganisationResponseBody": { + "description": "The response body for an organisation", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the organisation", + "example": "Cheqd" + }, + "email": { + "type": "string", + "description": "The email of the organisation", + "example": "cheqd@example.com", + "format": "email", + "nullable": true, + "default": null + }, + "description": { + "type": "string", + "description": "The description of the organisation", + "example": "Cheqd organisation", + "nullable": true, + "default": null + }, + "cosmosAddress": { + "type": "string", + "description": "The cosmos address of the organisation", + "example": "cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u" + } + } + }, "NotFoundError": { "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", "type": "object", diff --git a/src/types/portal.ts b/src/types/admin.ts similarity index 86% rename from src/types/portal.ts rename to src/types/admin.ts index 8ab6f0ba..6836476c 100644 --- a/src/types/portal.ts +++ b/src/types/admin.ts @@ -147,6 +147,25 @@ export type APIKeyGetRequestBody = APIKeyResponseBody; export type APIKeyGetResponseBody = APIKeyCreateResponseBody; export type APIKeyGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; +// Organisation +export type AdminOrganisationResponseBody = { + name: string; + email?: string; + description?: string; + cosmosAddress?: string; +}; + +export type AdminOrganisationGetResponseBody = AdminOrganisationResponseBody; +export type AdminOrganisationGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; + +export type AdminOrganisationUpdateRequestBody = { + name?: string; + email?: string; + description?: string; +}; +export type AdminOrganisationUpdateResponseBody = AdminOrganisationGetResponseBody; +export type AdminOrganisationUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; + // Utils export type PaymentBehavior = Stripe.SubscriptionCreateParams.PaymentBehavior; diff --git a/src/types/constants.ts b/src/types/constants.ts index 34c30e24..2279f964 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -14,8 +14,6 @@ export const CORS_ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || APPLICAT export const API_KEY_PREFIX = 'caas'; export const API_SECRET_KEY_LENGTH = 64; export const API_KEY_EXPIRATION = 30; -// By default we don't send events to datadog -export const ENABLE_DATADOG = process.env.ENABLE_DATADOG === 'true' ? true : false; // Possible cases 'trace' 'debug' 'info' 'warn' 'error'; export const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; diff --git a/src/types/environment.d.ts b/src/types/environment.d.ts index f5584c72..3e4d3065 100644 --- a/src/types/environment.d.ts +++ b/src/types/environment.d.ts @@ -7,7 +7,6 @@ declare global { NODE_ENV: EnvironmentType; PORT: string; NPM_CONFIG_LOGLEVEL: string; - ENABLE_DATADOG: string | 'false'; LOG_LEVEL: string | 'info'; // Network API endpoints diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 8f320a75..b0f66e22 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -316,6 +316,31 @@ * type: object * schema: * ref: '#/components/schemas/APIKeyResponse' + * OrganisationResponseBody: + * description: The response body for an organisation + * type: object + * properties: + * name: + * type: string + * description: The name of the organisation + * example: Cheqd + * email: + * type: string + * description: The email of the organisation + * example: cheqd@example.com + * format: email + * nullable: true + * default: null + * description: + * type: string + * description: The description of the organisation + * example: Cheqd organisation + * nullable: true + * default: null + * cosmosAddress: + * type: string + * description: The cosmos address of the organisation + * example: cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u * NotFoundError: * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. * type: object