diff --git a/apps/server/openapi3.json b/apps/server/openapi3.json index e3f5b714..694cb4af 100644 --- a/apps/server/openapi3.json +++ b/apps/server/openapi3.json @@ -60,40 +60,6 @@ } } }, - "/cbac/initiate": { - "post": { - "operationId": "request-proof-of-possession-of-private-key", - "summary": "Initiate certificate based authentication", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InitiateCertificateBasedAuthentication" - } - } - } - }, - "responses": { - "201": { - "description": "" - } - } - } - }, - "/cbac/verify": { - "post": { - "operationId": "verify-possession-of-private-key", - "summary": "Verify proof of possession of Private Key", - "parameters": [], - "responses": { - "201": { - "description": "" - } - } - } - }, "/account": { "post": { "operationId": "register", @@ -112,76 +78,6 @@ } } }, - "responses": { - "200": { - "description": "Account was successfully registered in system.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Account" - } - } - } - }, - "400": { - "description": "Provided Invalid Data", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpProblem", - "type": "object", - "example": { - "type": "com.methylphenidate.account.invalid-username", - "title": "Invalid Username", - "status": 400 - }, - "examples": [ - { - "type": "com.methylphenidate.account.invalid-email", - "title": "Invalid Email", - "status": 400 - } - ] - } - } - } - }, - "409": { - "description": "Account already exists.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpProblem" - } - } - } - } - } - }, - "patch": { - "operationId": "update-account", - "summary": "", - "description": "Update details of account.", - "tags": [ - "account" - ], - "parameters": [], - "responses": { - "200": { - "description": "" - } - } - } - }, - "/account/delete-account": { - "post": { - "operationId": "delete-account", - "summary": "", - "description": "Deletes the user's account.", - "tags": [ - "account" - ], - "parameters": [], "responses": { "201": { "description": "" @@ -189,184 +85,6 @@ } } }, - "/account/recovery/forgot-password": { - "post": { - "operationId": "forgot-password", - "summary": "Request password reset", - "description": "Sends a password reset email", - "tags": [ - "account-recovery" - ], - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecoverAccount" - } - } - } - }, - "responses": { - "201": { - "description": "" - } - } - } - }, - "/account/recovery/reset-password": { - "post": { - "operationId": "reset-password", - "summary": "Reset password", - "description": "Resets the user's password", - "tags": [ - "account-recovery" - ], - "parameters": [ - { - "name": "code", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "" - } - } - } - }, - "/account/verification/verify-email": { - "post": { - "operationId": "verify-email", - "summary": "Verify Email", - "description": "Verifies the user’s email", - "tags": [ - "account" - ], - "parameters": [ - { - "name": "verification_code", - "required": true, - "in": "query", - "example": "verification_code", - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "" - } - } - } - }, - "/account/verification/resend-verification-email": { - "post": { - "operationId": "resend-verification-email", - "summary": "Re-send verification email", - "description": "Resends the verification email", - "tags": [ - "account" - ], - "parameters": [ - { - "name": "email", - "required": true, - "in": "query", - "description": "Email of created account to which verification email should be sent", - "example": "Raul_Marks@gmail.com", - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "" - } - } - } - }, - "/authenticate": { - "post": { - "operationId": "authenticate", - "summary": "Basic Authentication", - "description": "Logs the user in with usage of a username and password.", - "tags": [ - "authentication" - ], - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Auhenticate" - } - } - } - }, - "responses": { - "201": { - "description": "The user has been successfully authenticated and session was created.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthenticationResponse" - } - } - } - }, - "404": { - "description": "The user could not be found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpProblem" - } - } - } - } - }, - "security": [ - { - "basic": [] - } - ] - }, - "get": { - "operationId": "whoami", - "summary": "Who am I?", - "description": "Returns the current user", - "tags": [ - "authentication" - ], - "parameters": [], - "responses": { - "200": { - "description": "Account was found in system, and returned.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Account" - } - } - } - } - }, - "security": [ - { - "bearer": [] - } - ] - } - }, "/health": { "get": { "operationId": "healthcheck", @@ -578,24 +296,6 @@ "redirectUrl" ] }, - "InitiateCertificateBasedAuthentication": { - "type": "object", - "properties": { - "username": { - "type": "string", - "description": "The account's username", - "example": "Raul_Marks", - "examples": [ - "Mariano_OKeefe21", - "Bette.Ebert69", - "Buster.Beatty50" - ] - } - }, - "required": [ - "username" - ] - }, "RegisterAccountCommand": { "type": "object", "properties": { @@ -635,196 +335,6 @@ "password", "username" ] - }, - "HttpProblem": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "A URI reference that uniquely identifies the problem type only in the context of the provided API. Opposed to the specification in RFC-7807, it is neither recommended to be dereferencable and point to a human-readable documentation nor globally unique for the problem type.", - "example": "com.app.account.already-exists" - }, - "title": { - "type": "string", - "description": "A short, human-readable summary of the problem type.", - "example": "Account already exists" - }, - "status": { - "type": "number", - "minimum": 100, - "maximum": 599, - "enum": [ - 100, - 101, - 200, - 201, - 202, - 203, - 204, - 205, - 206, - 300, - 301, - 302, - 303, - 304, - 305, - 307, - 400, - 401, - 404, - 409, - 500 - ], - "description": "No rocket science there.", - "example": 400 - }, - "detail": { - "type": "string", - "description": "A human-readable explanation specific to this occurrence of the problem.", - "example": "The account you are trying to register, already exists." - }, - "instance": { - "type": "string", - "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced.", - "example": "com.app.account.already-exists" - } - }, - "required": [ - "type", - "title", - "status", - "detail", - "instance" - ] - }, - "RecoverAccount": { - "type": "object", - "properties": { - "username": { - "type": "string", - "description": "The account's username", - "example": "Raul_Marks", - "examples": [ - "Mariano_OKeefe21", - "Bette.Ebert69", - "Buster.Beatty50" - ] - }, - "method": { - "type": "string", - "description": "Recovery Method", - "example": "" - } - }, - "required": [ - "username" - ] - }, - "AuthenticationResponse": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique identifier of given resource." - }, - "mfa": { - "type": "boolean", - "description": "Indicates if domain uses MFA. In case of true, additional steps are supposed to be made to activate access token." - }, - "accessToken": { - "type": "string", - "description": "Access token.", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkIjp7fSwiaWF0IjoxNzA3NzQ1NTE5fQ.ZMuKBcLvQm_Jc4TSx1cp0tWU7TmSrpIfr_Ust_BbcEc", - "examples": [ - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkIjp7fSwiaWF0IjoxNzA3NzQ1NTE5fQ.ZMuKBcLvQm_Jc4TSx1cp0tWU7TmSrpIfr_Ust_BbcEc", - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkIjp7fSwiaWF0IjoxNzA3NzQ1NTE5fQ.ZMuKBcLvQm_Jc4TSx1cp0tWU7TmSrpIfr_Ust_BbcEc", - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkIjp7fSwiaWF0IjoxNzA3NzQ1NTE5fQ.ZMuKBcLvQm_Jc4TSx1cp0tWU7TmSrpIfr_Ust_BbcEc" - ] - }, - "refreshToken": { - "type": "string", - "description": "Refresh token." - } - }, - "required": [ - "id", - "mfa", - "accessToken", - "refreshToken" - ] - }, - "Account": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The domain's unique identifier", - "example": "cjld2cjxh0000qzrmn831i7rn" - }, - "email": { - "type": "string", - "description": "The domain's email address", - "example": "Raul_Marks@gmail.com", - "examples": [ - "Raul_Marks@gmail.com", - "Buddy.Schulist@gmail.com", - "Anibal_Pouros@hotmail.com" - ] - }, - "emailVerified": { - "type": "boolean", - "description": "Indicates whether the email associated with a user domain has been verified", - "example": false - }, - "username": { - "type": "string", - "description": "The account's username", - "example": "Raul_Marks", - "examples": [ - "Mariano_OKeefe21", - "Bette.Ebert69", - "Buster.Beatty50" - ] - } - }, - "required": [ - "id", - "email", - "emailVerified", - "username" - ], - "description": "asdasd" - }, - "Auhenticate": { - "type": "object", - "properties": { - "username": { - "type": "string", - "description": "The account's username", - "example": "Raul_Marks", - "examples": [ - "Mariano_OKeefe21", - "Bette.Ebert69", - "Buster.Beatty50" - ] - }, - "password": { - "type": "string", - "description": "The account's username", - "example": "SuperSecretPassword123!", - "examples": [ - "LaW1KVuo69FG5Pf", - "9srQfkporSDhU76", - "74ETrfy3OQCdUzs" - ] - } - }, - "required": [ - "username", - "password" - ], - "description": "asdasd" } } } diff --git a/apps/server/src/bootstrap.ts b/apps/server/src/bootstrap.ts index f967fdea..a8a61826 100644 --- a/apps/server/src/bootstrap.ts +++ b/apps/server/src/bootstrap.ts @@ -1,206 +1,160 @@ -import { HttpException, Logger, ValidationPipe } from '@nestjs/common'; -import { HttpAdapterHost, NestFactory } from '@nestjs/core'; -import { - ExpressAdapter, - NestExpressApplication, -} from '@nestjs/platform-express'; -import { apiReference } from '@scalar/nestjs-api-reference'; -import Sentry from '@sentry/node'; -import cookieParser from 'cookie-parser'; -import delay from 'delay'; -import express, { Express, NextFunction } from 'express'; -import helmet from 'helmet'; -import ms from 'ms'; -import process from 'node:process'; -import requestIp from 'request-ip'; -import { startTunnel } from 'untun'; -import { HttpExceptionFilter } from './common/filters/exception-filter/http-exception-filter.js'; -import { HttpStatus } from './common/http-status.js'; -import { buildCompodocDocumentation } from './common/modules/documentation/compodoc/compodoc.js'; -import { buildSwaggerDocumentation } from './common/modules/documentation/swagger/swagger.js'; -import { executePrismaRelatedProcesses } from './common/modules/resources/prisma/utils/execute-prisma-related-processes.js'; -import { __appConfig, __config } from './configs/global/__config.js'; -import { isDevelopment } from './configs/helper/is-development.js'; -import { StaticFeatureFlags } from './configs/static-feature-flags.js'; -import { Container } from './container.js'; -import { LoggerNestjsProxy } from './kernel/modules/logger/nestjs-logger-proxy.js'; -import { FingerprintMiddleware } from './kernel/platform/http/middleware/fingerprint.js'; -import { ExpressRequest, ExpressResponse } from './types/express-response.js'; -import { portAllocator } from './utilities/network-utils/port-allocator.js'; +import {HttpException, Logger, ValidationPipe} from '@nestjs/common'; +import {HttpAdapterHost, NestFactory} from '@nestjs/core'; +import {ExpressAdapter, NestExpressApplication} from '@nestjs/platform-express'; +import {apiReference} from '@scalar/nestjs-api-reference'; +import Sentry from '@sentry/node'; +import cookieParser from 'cookie-parser'; +import delay from 'delay'; +import express, {Express, NextFunction} from 'express'; +import helmet from 'helmet'; +import ms from 'ms'; +import process from 'node:process'; +import requestIp from 'request-ip'; +import {HttpExceptionFilter} from './common/filters/exception-filter/http-exception-filter.js'; +import {HttpStatus} from './common/http-status.js'; +import {buildCompodocDocumentation} from './common/modules/documentation/compodoc/compodoc.js'; +import {buildSwaggerDocumentation} from './common/modules/documentation/swagger/swagger.js'; +import {executePrismaRelatedProcesses} from './common/modules/resources/prisma/utils/execute-prisma-related-processes.js'; +import {__appConfig, __config} from './configs/global/__config.js'; +import {isDevelopment} from './configs/helper/is-development.js'; +import {StaticFeatureFlags} from './configs/static-feature-flags.js'; +import {Container} from './container.js'; +import {LoggerNestjsProxy} from "./core/modules/logger/nestjs-logger-proxy.js" +import {FingerprintMiddleware} from './kernel/platform/http/middleware/fingerprint.js'; +import {ExpressRequest, ExpressResponse} from './types/express-response.js'; +import {portAllocator} from './utilities/network-utils/port-allocator.js'; + + export async function bootstrap(): Promise { - const __expressApp: Express = express(); - - // Bootstrap application - const app: NestExpressApplication = - await NestFactory.create( - Container, - new ExpressAdapter(__expressApp), - { - autoFlushLogs: true, - cors: true, - bodyParser: true, - rawBody: true, - preview: false, - bufferLogs: true, - abortOnError: isDevelopment(), - snapshot: isDevelopment(), - logger: new LoggerNestjsProxy(), - }, - ); - - const { httpAdapter } = app.get(HttpAdapterHost); - - app.useGlobalPipes(new ValidationPipe()); - app.useBodyParser('json'); - app.use(helmet({ contentSecurityPolicy: false })); - app.use(requestIp.mw()); - app.use(cookieParser()); - app.use(new FingerprintMiddleware().use); - - // Implement logger used for bootstrapping and notifying about application state - const logger = new Logger('Bootstrap'); - - await executePrismaRelatedProcesses(); - - app.use(helmet({ contentSecurityPolicy: false })); - // Build swagger documentation - const apiSpec = await buildSwaggerDocumentation(app); - - app.use( - '/reference', - apiReference({ - spec: { - content: apiSpec, - }, - showSidebar: true, - isEditable: false, - layout: 'modern', - theme: 'kepler', - }), - ); - - buildCompodocDocumentation(); - - // The error handler must be before any other error middleware and after all controllers - app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost))); - - app.use( - Sentry.Handlers.errorHandler({ - // No better idea how to filter out non-important errors rn - shouldHandleError: function (error: Error | HttpException): boolean { - if (error instanceof HttpException) { - return error.getStatus() >= HttpStatus.INTERNAL_SERVER_ERROR; - } - return true; - }, - }), - ); - - // Optional fallthrough error handler - app.use(function onError( - _err: Error, - _req: ExpressRequest, - res: ExpressResponse, - _next: NextFunction, - ) { - res.statusCode = 500; - res.end((res as any).sentry + '\n'); - }); - - // Enable graceful shutdown hooks - app.enableShutdownHooks(); - - const PORT = __config.get('PORT'); - - // Listen on selected application port (with grace) - let openPort = await portAllocator(PORT); - - if (openPort.wasReplaced) { - logger.warn( - `Application performed port availability check and ::${PORT} is not available, found a new shiny ::${openPort.port} instead. If you believe this is a mistake, please check your environment variables and processes that are running on your machine.`, - ); - } else { - logger.log( - `Port availability check succeeded and requested ::${PORT} is available`, - ); - } - - let isApplicationListening = false; - let retryDelay = ms('5s'); - let retryCount = 3; - - const PROTOCOL = __config.get('PROTOCOL'); - const HOST = __config.get('HOST'); - const NODE_ENV = __config.get('NODE_ENV'); - - const applicationUrl = `${PROTOCOL}://${HOST}:${openPort.port}`; - - while (!isApplicationListening) { - try { - await app.listen(openPort.port, async () => { - logger.verbose(`${'-'.repeat(54)}`); - logger.log( - `πŸš€ Application started on ${applicationUrl} in ${NODE_ENV} mode`, - ); - - logger.verbose(`${'-'.repeat(54)}`); - logger.verbose(`πŸ“„ Compodoc endpoint: ${applicationUrl + '/docs'}`); - logger.verbose( - `πŸ“„ OpenAPI 3.0 Documentation: ${applicationUrl + '/reference'}`, - ); - if (StaticFeatureFlags.isGraphQLRunning) { - logger.verbose( - `🧩 GraphQL is running on: ${applicationUrl + '/graphql'}`, - ); - } - logger.verbose( - `🩺 Healthcheck endpoint: ${ - applicationUrl + __config.get('APPLICATION').HEALTHCHECK_ENDPOINT - }`, - ); - - if (isDevelopment() && StaticFeatureFlags.shouldRunPrismaStudio) { - logger.verbose( - `🧩 Prisma Admin is running on: http://localhost:${__appConfig.PRISMA_ADMIN_PORT}`, - ); - } - - logger.verbose(`${'-'.repeat(54)}`); - - if (__config.get('FEATURE').ENABLE_TUNNEL) { - //const tunnel = await startTunnel({ - // port: openPort.port, - // acceptCloudflareNotice: true, - //}); - // - //if (tunnel) { - // logger.log( - // `πŸš‡ Tunnel is enabled, you can access your application via public URL: ${await tunnel.getURL()}`, - // ); - //} - } - }); - - isApplicationListening = true; - } catch (e) { - logger.error( - `Error while trying to start application: ${ - (e as unknown as any).message - }`, - ); - await delay(retryDelay); - openPort = await portAllocator(PORT); - retryDelay = retryDelay * 2; - } - - if (retryCount === 0) { - logger.error(`Application failed to start after ${retryCount} attempts`); - process.exit(1); - } - - retryCount--; - } - - return app; + const __expressApp: Express = express(); + + // Bootstrap application + const app: NestExpressApplication = await NestFactory.create(Container, new ExpressAdapter(__expressApp), { + autoFlushLogs: true, + cors: true, + bodyParser: true, + rawBody: true, + preview: false, + bufferLogs: true, + abortOnError: isDevelopment(), + snapshot: isDevelopment(), + logger: new LoggerNestjsProxy(), + }); + + const {httpAdapter} = app.get(HttpAdapterHost); + + app.useGlobalPipes(new ValidationPipe()); + app.useBodyParser('json'); + app.use(helmet({contentSecurityPolicy: false})); + app.use(requestIp.mw()); + app.use(cookieParser()); + app.use(new FingerprintMiddleware().use); + + // Implement logger used for bootstrapping and notifying about application state + const logger = new Logger('Bootstrap'); + + await executePrismaRelatedProcesses(); + + app.use(helmet({contentSecurityPolicy: false})); + // Build swagger documentation + const apiSpec = await buildSwaggerDocumentation(app); + + app.use('/reference', apiReference({ + spec: { + content: apiSpec, + }, + showSidebar: true, + isEditable: false, + layout: 'modern', + theme: 'kepler', + })); + + buildCompodocDocumentation(); + + // The error handler must be before any other error middleware and after all controllers + app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost))); + + app.use(Sentry.Handlers.errorHandler({ + // No better idea how to filter out non-important errors rn + shouldHandleError: function (error: Error | HttpException): boolean { + if (error instanceof HttpException) { + return error.getStatus() >= HttpStatus.INTERNAL_SERVER_ERROR; + } + return true; + }, + })); + + // Optional fallthrough error handler + app.use(function onError(_err: Error, _req: ExpressRequest, res: ExpressResponse, _next: NextFunction) { + res.statusCode = 500; + res.end(( + res as any + ).sentry + '\n'); + }); + + // Enable graceful shutdown hooks + app.enableShutdownHooks(); + + const PORT = __config.get('PORT'); + + // Listen on selected application port (with grace) + let openPort = await portAllocator(PORT); + + if (openPort.wasReplaced) { + logger.warn(`Application performed port availability check and ::${PORT} is not available, found a new shiny ::${openPort.port} instead. If you believe this is a mistake, please check your environment variables and processes that are running on your machine.`); + } else { + logger.log(`Port availability check succeeded and requested ::${PORT} is available`); + } + + let isApplicationListening = false; + let retryDelay = ms('5s'); + let retryCount = 3; + + const PROTOCOL = __config.get('PROTOCOL'); + const HOST = __config.get('HOST'); + const NODE_ENV = __config.get('NODE_ENV'); + + const applicationUrl = `${PROTOCOL}://${HOST}:${openPort.port}`; + + while (!isApplicationListening) { + try { + await app.listen(openPort.port, async () => { + logger.verbose(`${'-'.repeat(54)}`); + logger.log(`πŸš€ Application started on ${applicationUrl} in ${NODE_ENV} mode`); + + logger.verbose(`${'-'.repeat(54)}`); + logger.verbose(`πŸ“„ Compodoc endpoint: ${applicationUrl + '/docs'}`); + logger.verbose(`πŸ“„ OpenAPI 3.0 Documentation: ${applicationUrl + '/reference'}`); + if (StaticFeatureFlags.isGraphQLRunning) { + logger.verbose(`🧩 GraphQL is running on: ${applicationUrl + '/graphql'}`); + } + logger.verbose(`🩺 Healthcheck endpoint: ${applicationUrl + __config.get('APPLICATION').HEALTHCHECK_ENDPOINT}`); + + if (isDevelopment() && StaticFeatureFlags.shouldRunPrismaStudio) { + logger.verbose(`🧩 Prisma Admin is running on: http://localhost:${__appConfig.PRISMA_ADMIN_PORT}`); + } + + logger.verbose(`${'-'.repeat(54)}`); + }); + + isApplicationListening = true; + } catch (e) { + logger.error(`Error while trying to start application: ${( + e as unknown as any + ).message}`); + await delay(retryDelay); + openPort = await portAllocator(PORT); + retryDelay = retryDelay * 2; + } + + if (retryCount === 0) { + logger.error(`Application failed to start after ${retryCount} attempts`); + process.exit(1); + } + + retryCount--; + } + + return app; } diff --git a/apps/server/src/common/modules/resources/stripe/README.md b/apps/server/src/common/modules/resources/stripe/README.md deleted file mode 100644 index 18d784bf..00000000 --- a/apps/server/src/common/modules/resources/stripe/README.md +++ /dev/null @@ -1,218 +0,0 @@ -# Stripe Module - -> Fork of https://github.com/golevelup/nestjs/tree/master/packages/stripe - -- πŸ’‰ Injectable Stripe client for interacting with the Stripe API in Controllers - and Providers -- πŸŽ‰ Optionally exposes an API endpoint from your NestJS application at to be - used for webhook event processing from - Stripe. Defaults to `/stripe/webhook/` but can be easily configured -- πŸ”’ Automatically validates that the event payload was actually sent from Stripe - using the configured webhook signing - secret -- πŸ•΅οΈ Discovers providers from your application decorated - with `StripeWebhookHandler` and routes incoming events to them -- 🧭 Route events to logical services easily simply by providing the Stripe - webhook event type - -## Getting Started - -### Import - -Import and add `StripeModule` to the `imports` section of the consuming module ( -most likely `AppModule`). Your Stripe -API key is required, and you can optionally include a webhook configuration if -you plan on consuming Stripe webhook -events inside your app. -Stripe secrets you can get from your -Dashboard’s [Webhooks settings](https://dashboard.stripe.com/webhooks). Select -an -endpoint that you want to obtain the secret for, then click the Click to reveal -button. - -`account` - The webhook secret registered in the Stripe Dashboard for events on -your accounts -`connect` - The webhook secret registered in the Stripe Dashboard for events on -Connected accounts - -```typescript -import {StripeModule} from '@golevelup/nestjs-stripe'; - - -@Module({ - imports: [ - StripeModule.forRoot(StripeModule, { - apiKey: '123', - webhookConfig: { - stripeSecrets: { - account: 'abc', - connect: 'cba', - }, - }, - }), - ], -}) -export class AppModule { - // ... -} -``` - -### Configuration - -The Stripe Module supports both the `forRoot` and `forRootAsync` patterns for -configuration, so you can easily retrieve -the necessary config values from a `ConfigService` or other provider. - -### Injectable Providers - -The module exposes two injectable providers with accompanying decorators for -your convenience. These can be provided to -the constructors of controllers and other providers: - -```typescript - // injects the instantiated Stripe client which can be used to make API calls -export class StripeService { - constructor( - @InjectStripeClient() stripeClient: Stripe - ) { - } -} - -``` - -```typescript -// injects the module configuration -@InjectStripeModuleConfig() -config -: -StripeModuleConfig -``` - -## Consuming Webhooks - -### Included API Endpoint - -This module will automatically add a new API endpoint to your NestJS application -for processing webhooks. By default, -the route for this endpoint will be `stripe/webhook` but you can modify this to -use a different prefix using -the `controllerPrefix` property of the `webhookConfig` when importing the -module. - -### ⚠️ Configure Raw Request Body Handling - -If you would like your NestJS application to be able to process incoming -webhooks, it is essential that Stripe has -access to the raw request payload. - -By default, NestJS is configured to use JSON body parsing middleware which will -transform the request before it can be -validated by the Stripe library. - -You can choose -to [pass the raw request body](https://docs.nestjs.com/faq/raw-body#use-with-express) -into the context of -each Request, -which will not cause side effects to any of your existing project's -architectural design and other APIs. - -```typescript -// main.ts -const app = await NestFactory.create(AppModule, { - rawBody: true, -}); -``` - -You can then manually set up `bodyProperty` to use rawBody: - -```typescript -StripeModule.forRoot(StripeModule, { - apiKey: '', - webhookConfig: { - stripeWebhookSecret: '', - requestBodyProperty: 'rawBody', // <-- Set to 'rawBody' - }, -}); -``` - -### Decorate Methods For Processing Webhook Events - -Exposing provider/service methods to be used for processing Stripe events is -easy! Simply use the provided decorator and -indicate the event type that the handler should receive. - -[Review the Stripe documentation](https://stripe.com/docs/api/events/types) for -more information about the types of -events available. - -```typescript -@Injectable() -class PaymentCreatedService { - @StripeWebhookHandler('payment_intent.created') - handlePaymentIntentCreated(evt: StripeEvent) { - // execute your custom business logic - } -} -``` - -### Webhook Controller Decorators - -You can also pass any class decorator to the `decorators` property of -the `webhookConfig` object as a part of the module -configuration. This could be used in situations like when using -the `@nestjs/throttler` package and needing to apply -the `@SkipThrottle()` decorator, or when you have a global guard but need to -skip routes with certain metadata. - -```typescript -StripeModule.forRoot(StripeModule, { - apiKey: '123', - webhookConfig: { - stripeWebhookSecret: 'super-secret', - decorators: [SkipThrottle()], - }, -}), -``` - -### Usage with Interceptors, Guards and Filters - -This library is built using an underlying NestJS concept -called `External Contexts` which allows for methods to be -included in the NestJS lifecycle. This means that Guards, Interceptors and -Filters (collectively known as "enhancers") -can be used in conjunction with Stripe webhook handlers. However, this can have -unwanted/unintended consequences if you -are using _Global_ enhancers in your application as these will also apply to all -Stripe webhook handlers. If you were -previously expecting all contexts to be regular HTTP contexts, you may need to -add conditional logic to prevent your -enhancers from applying to Stripe webhook handlers. - -You can identify Stripe webhook contexts by their context -type, `'stripe_webhook'`: - -```typescript -@Injectable() -class ExampleInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler) { - const contextType = context.getType<'http' | 'stripe_webhook'>(); - - // Do nothing if this is a Stripe webhook event - if (contextType === 'stripe_webhook') { - return next.handle(); - } - - // Execute custom interceptor logic for HTTP request/response - return next.handle(); - } -} -``` - -### Configure Webhooks in the Stripe Dashboard - -Follow the instructions from -the [Stripe Documentation](https://stripe.com/docs/webhooks) for remaining -integration -steps such as testing your integration with the CLI before you go live and -properly configuring the endpoint from the -Stripe dashboard so that the correct events are sent to your NestJS app. \ No newline at end of file diff --git a/apps/server/src/common/modules/resources/stripe/constraints/stripe-constraints.ts b/apps/server/src/common/modules/resources/stripe/constraints/stripe-constraints.ts deleted file mode 100644 index 53a7ef36..00000000 --- a/apps/server/src/common/modules/resources/stripe/constraints/stripe-constraints.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const STRIPE_MODULE_CONFIG_TOKEN = Symbol( 'STRIPE_MODULE_CONFIG_TOKEN' ) - -export const STRIPE_CLIENT_TOKEN = Symbol( 'STRIPE_CLIENT_TOKEN' ) - -export const STRIPE_WEBHOOK_HANDLER = Symbol( 'STRIPE_WEBHOOK_HANDLER' ) - -export const STRIPE_WEBHOOK_SERVICE = Symbol( 'STRIPE_WEBHOOK_SERVICE' ) \ No newline at end of file diff --git a/apps/server/src/common/modules/resources/stripe/controllers/stripe-webhook-controller.ts b/apps/server/src/common/modules/resources/stripe/controllers/stripe-webhook-controller.ts deleted file mode 100644 index 8556b854..00000000 --- a/apps/server/src/common/modules/resources/stripe/controllers/stripe-webhook-controller.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - Controller, - Headers, - Post, - Req, -} from '@nestjs/common' -import type { Request } from 'express' -import { InjectStripeModuleConfig } from '../decorators/stripe-decorator.js' -import type { StripeModuleConfig } from '../interfaces/stripe-interface.js' -import { StripePayloadService } from '../services/stripe-payload-service.js' -import { StripeWebhookService } from '../services/stripe-webhook-service.js' - - - -@Controller( '/stripe' ) -export class StripeWebhookController - { - - - constructor( - @InjectStripeModuleConfig() private readonly config : StripeModuleConfig, - private readonly stripePayloadService : StripePayloadService, - private readonly stripeWebhookService : StripeWebhookService, - ) - { - } - - - @Post( '/webhook' ) - async handleWebhook( - @Headers( 'stripe-signature' ) sig : string, - @Req() request : Request, - ) - { - if ( !sig ) - { - throw new Error( 'Missing stripe-signature header' ) - } - const rawBody = request.body - - const event = this.stripePayloadService.tryHydratePayload( sig, rawBody ) - - await this.stripeWebhookService.handleWebhook( event ) - } - } \ No newline at end of file diff --git a/apps/server/src/common/modules/resources/stripe/decorators/make-injectable-decorator.ts b/apps/server/src/common/modules/resources/stripe/decorators/make-injectable-decorator.ts deleted file mode 100644 index 793a4116..00000000 --- a/apps/server/src/common/modules/resources/stripe/decorators/make-injectable-decorator.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Inject } from '@nestjs/common' - - - -/** - * Creates a decorator that can be used as a convenience to inject a specific token - * - * Instead of using @Inject(SOME_THING_TOKEN) this can be used to create a new named Decorator - * such as @InjectSomeThing() which will hide the token details from users making APIs easier - * to consume - * @param token - */ -export const makeInjectableDecorator = (token : string | symbol) : ( () => ParameterDecorator ) => () => Inject( - token ) \ No newline at end of file diff --git a/apps/server/src/common/modules/resources/stripe/decorators/stripe-decorator.ts b/apps/server/src/common/modules/resources/stripe/decorators/stripe-decorator.ts deleted file mode 100644 index f8c95871..00000000 --- a/apps/server/src/common/modules/resources/stripe/decorators/stripe-decorator.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { SetMetadata } from '@nestjs/common' -import Stripe from 'stripe' -import { - STRIPE_CLIENT_TOKEN, - STRIPE_MODULE_CONFIG_TOKEN, - STRIPE_WEBHOOK_HANDLER, -} from '../constraints/stripe-constraints.js' -import { makeInjectableDecorator } from './make-injectable-decorator.js' - - - -/** - * Injects the Stripe Module config - */ -export const InjectStripeModuleConfig = makeInjectableDecorator( STRIPE_MODULE_CONFIG_TOKEN ) - -/** - * Injects the Stripe Client instance - */ -export const InjectStripeClient = makeInjectableDecorator( STRIPE_CLIENT_TOKEN ) - -/** - * Binds the decorated service method as a handler for incoming Stripe Webhook events. - * Events will be automatically routed here based on their event type property - * @param eventType - */ -export const StripeWebhookHandler = (eventType : Stripe.WebhookEndpointCreateParams.EnabledEvent) => SetMetadata( - STRIPE_WEBHOOK_HANDLER, eventType ) \ No newline at end of file diff --git a/apps/server/src/common/modules/resources/stripe/interfaces/dynamic-module.ts b/apps/server/src/common/modules/resources/stripe/interfaces/dynamic-module.ts deleted file mode 100644 index 49019090..00000000 --- a/apps/server/src/common/modules/resources/stripe/interfaces/dynamic-module.ts +++ /dev/null @@ -1,222 +0,0 @@ -import type { - DynamicModule, - OptionalFactoryDependency, - Provider, - Type, -} from '@nestjs/common' -import type { ModuleMetadata } from '@nestjs/common/interfaces' -import { - interval, - lastValueFrom, - race, - Subject, -} from 'rxjs' -import { - first, - map, -} from 'rxjs/operators' - - - -type InjectionToken = - string - | symbol - | Type; - - -export interface ModuleConfigFactory - { - createModuleConfig() : Promise | T; - } - - -export type AsyncModuleConfig = - Pick - & ( | { - useExisting : { - value : ModuleConfigFactory; - provide? : InjectionToken; - }; - } | { - useClass : Type> - } | { - useFactory : (...args : any[]) => Promise | T; - inject? : any[]; - } ); - - -export function createModuleConfigProvider( - provide : InjectionToken, - options : AsyncModuleConfig, -) : Provider[] - { - if ( 'useFactory' in options ) - { - return [ - { - provide, - useFactory : options.useFactory, - inject : options.inject ?? [], - }, - ] - } - - const optionsProviderGenerator = (inject : InjectionToken | OptionalFactoryDependency) : Provider => ( - { - provide, - useFactory : async (moduleConfigFactory : ModuleConfigFactory) => { - return moduleConfigFactory.createModuleConfig() - }, - inject : [ inject ], - } - ) - - if ( 'useClass' in options ) - { - return [ - optionsProviderGenerator( options.useClass ), { - provide : options.useClass, - useClass : options.useClass, - }, - ] - } - if ( 'useExisting' in options ) - { - return [ - optionsProviderGenerator( options.useExisting.provide ?? options.useExisting.value.constructor.name ), { - provide : options.useExisting.provide || options.useExisting.value.constructor.name, - useValue : options.useExisting.value, - }, - ] - } - - return [] - } - - -export interface IConfigurableDynamicRootModule - { - moduleSubject : Subject; - - new() : Type; - - forRoot( - moduleCtor : Type, - moduleConfig : U, - ) : DynamicModule; - - forRootAsync( - moduleCtor : Type, - asyncModuleConfig : AsyncModuleConfig, - ) : DynamicModule; - - externallyConfigured( - moduleCtor : Type, - wait : number, - ) : Promise; - } - - -export function createConfigurableDynamicRootModule( - moduleConfigToken : InjectionToken, - moduleProperties : Partial> = { - imports : [], - exports : [], - providers : [], - }, -) - { - abstract class DynamicRootModule - { - static moduleSubject = new Subject() - - - static forRootAsync( - moduleCtor : Type, - asyncModuleConfig : AsyncModuleConfig, - ) : DynamicModule - { - const dynamicModule = { - module : moduleCtor, - imports : [ - ...( - asyncModuleConfig.imports || [] - ), ...( - moduleProperties.imports || [] - ), - ], - exports : [ - ...( - asyncModuleConfig.exports || [] - ), ...( - moduleProperties.exports || [] - ), - ], - providers : [ - ...createModuleConfigProvider( moduleConfigToken, asyncModuleConfig ), ...( - moduleProperties.providers || [] - ), - ], - } - - DynamicRootModule.moduleSubject.next( dynamicModule ) - - return dynamicModule - } - - - static forRoot( - moduleCtor : Type, - moduleConfig : U, - ) : DynamicModule - { - const dynamicModule : DynamicModule = { - module : moduleCtor, - imports : [ - ...( - moduleProperties.imports || [] - ), - ], - exports : [ - ...( - moduleProperties.exports || [] - ), - ], - controllers : [ - ...( - moduleProperties.controllers || [] - ), - ], - providers : [ - { - provide : moduleConfigToken, - useValue : moduleConfig, - }, ...( - moduleProperties.providers || [] - ), - ], - } - - DynamicRootModule.moduleSubject.next( dynamicModule ) - - return dynamicModule - } - - - static async externallyConfigured( - moduleCtor : Type, - wait : number, - ) : Promise - { - const timeout$ = interval( wait ).pipe( first(), map( () => { - throw new Error( - `Expected ${moduleCtor.name} to be configured by at last one Module but it was not configured within ${wait}ms` ) - } ) ) - - return lastValueFrom( race( timeout$, DynamicRootModule.moduleSubject.pipe( first() ) ) ) - } - } - - - return DynamicRootModule as IConfigurableDynamicRootModule - } \ No newline at end of file diff --git a/apps/server/src/common/modules/resources/stripe/interfaces/stripe-interface.ts b/apps/server/src/common/modules/resources/stripe/interfaces/stripe-interface.ts deleted file mode 100644 index 90cc1ed9..00000000 --- a/apps/server/src/common/modules/resources/stripe/interfaces/stripe-interface.ts +++ /dev/null @@ -1,75 +0,0 @@ -import Stripe from 'stripe' - - - -type RequireAtLeastOne = - Pick> - & { - [K in Keys]-? : Required> & Partial>>; - }[Keys]; - - -interface StripeSecrets - { - /** - * The webhook secret registered in the Stripe Dashboard for events on your accounts - */ - account? : string; - - /** - * The webhook secret registered in the Stripe Dashboard for events on Connected accounts - */ - connect? : string; - } - - -export interface StripeModuleConfig - extends Partial - { - readonly apiKey : string; - /** - * Configuration for processing Stripe Webhooks - */ - webhookConfig? : { - stripeSecrets : RequireAtLeastOne; - - /** - * The property on the request that contains the raw message body so that it - * can be validated. Defaults to 'body' - */ - requestBodyProperty? : string; - - /** - * The prefix of the generated webhook handling controller. Defaults to 'stripe' - */ - controllerPrefix? : string; - - /** - * Any metadata specific decorators you want to apply to the webhook handling controller. - * - * Note: these decorators must only set metadata that will be read at request time. Decorators like Nest's - * `@UsePipes()` or `@UseInterceptors()` wll not work, due to the time at which Nest reads the metadata for - * those, but something that uses `SetMetadata` will be fine, because that metadata is read at request time. - */ - decorators? : ClassDecorator[]; - - /** - * Logging configuration - */ - loggingConfiguration? : { - /** - * If enabled will log information regarding event handlers that match incoming webhook events - */ - logMatchingEventHandlers : boolean; - }; - }; - } - - -export interface StripeWebhookHandlerConfig - { - /** - * Event type from Stripe that will be used to match this handler - */ - eventType : string; - } \ No newline at end of file diff --git a/apps/server/src/common/modules/resources/stripe/services/stripe-payload-service.ts b/apps/server/src/common/modules/resources/stripe/services/stripe-payload-service.ts deleted file mode 100644 index 68afa1c7..00000000 --- a/apps/server/src/common/modules/resources/stripe/services/stripe-payload-service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable } from '@nestjs/common' -import { Buffer } from 'node:buffer' -import Stripe from 'stripe' -import { - InjectStripeClient, - InjectStripeModuleConfig, -} from '../decorators/stripe-decorator.js' -import type { StripeModuleConfig } from '../interfaces/stripe-interface.js' - - - -@Injectable() -export class StripePayloadService - { - private readonly stripeWebhookSecret : string - private readonly stripeConnectWebhookSecret : string - - - constructor( - @InjectStripeModuleConfig() private readonly config : StripeModuleConfig, - @InjectStripeClient() private readonly stripeClient : Stripe, - ) - { - this.stripeWebhookSecret = this.config.webhookConfig?.stripeSecrets.account || '' - this.stripeConnectWebhookSecret = this.config.webhookConfig?.stripeSecrets.connect || '' - } - - - tryHydratePayload( - signature : string, - payload : Buffer, - ) : { type : string } - { - const decodedPayload = JSON.parse( Buffer.isBuffer( payload ) ? payload.toString( 'utf8' ) : payload ) - - return this.stripeClient.webhooks.constructEvent( payload, signature, decodedPayload.account ? - this.stripeConnectWebhookSecret : - this.stripeWebhookSecret ) - } - } \ No newline at end of file diff --git a/apps/server/src/common/modules/resources/stripe/services/stripe-webhook-service.ts b/apps/server/src/common/modules/resources/stripe/services/stripe-webhook-service.ts deleted file mode 100644 index 2c5ec79f..00000000 --- a/apps/server/src/common/modules/resources/stripe/services/stripe-webhook-service.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - Injectable, - SetMetadata, -} from '@nestjs/common' -import { STRIPE_WEBHOOK_SERVICE } from '../constraints/stripe-constraints.js' - - - -@Injectable() @SetMetadata( STRIPE_WEBHOOK_SERVICE, true ) -export class StripeWebhookService - { - public handleWebhook(evt : any) : any - { - // The implementation for this method is overriden by the containing module - console.log( evt ) - } - } \ No newline at end of file diff --git a/apps/server/src/common/modules/resources/stripe/stripe-module.ts b/apps/server/src/common/modules/resources/stripe/stripe-module.ts deleted file mode 100644 index 862322d1..00000000 --- a/apps/server/src/common/modules/resources/stripe/stripe-module.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { - Logger, - Module, - OnModuleInit, -} from '@nestjs/common' -import { PATH_METADATA } from '@nestjs/common/constants.js' -import { ExternalContextCreator } from '@nestjs/core' -import { - flatten, - groupBy, -} from 'ramda' -import Stripe from 'stripe' -import { DiscoveryModule } from '../../environment/discovery/discovery-module.js' -import { DiscoveryService } from '../../environment/discovery/discovery-service.js' -import { - STRIPE_CLIENT_TOKEN, - STRIPE_MODULE_CONFIG_TOKEN, - STRIPE_WEBHOOK_HANDLER, - STRIPE_WEBHOOK_SERVICE, -} from './constraints/stripe-constraints.js' -import { StripeWebhookController } from './controllers/stripe-webhook-controller.js' -import { InjectStripeModuleConfig } from './decorators/stripe-decorator.js' -import { createConfigurableDynamicRootModule } from './interfaces/dynamic-module.js' -import { StripeModuleConfig } from './interfaces/stripe-interface.js' -import { StripePayloadService } from './services/stripe-payload-service.js' -import { StripeWebhookService } from './services/stripe-webhook-service.js' - - - -@Module( { - controllers : [ StripeWebhookController ], - } ) -export class StripeModule - extends createConfigurableDynamicRootModule( STRIPE_MODULE_CONFIG_TOKEN, { - imports : [ DiscoveryModule ], - providers : [ - { - provide : Symbol( 'CONTROLLER_HACK' ), - useFactory : (config : StripeModuleConfig) => { - const controllerPrefix = config.webhookConfig?.controllerPrefix || 'stripe' - - Reflect.defineMetadata( PATH_METADATA, controllerPrefix, StripeWebhookController ) - config.webhookConfig?.decorators?.forEach( (deco) => { - deco( StripeWebhookController ) - } ) - }, - inject : [ STRIPE_MODULE_CONFIG_TOKEN ], - }, { - provide : STRIPE_CLIENT_TOKEN, - useFactory : ({ - apiKey, - typescript = true, - apiVersion = '2023-10-16', - webhookConfig, - ...options - } : StripeModuleConfig) : Stripe => { - return new Stripe( apiKey, { - typescript, - apiVersion, ...options, - } ) - }, - inject : [ STRIPE_MODULE_CONFIG_TOKEN ], - }, StripeWebhookService, StripePayloadService, - ], - exports : [ - STRIPE_MODULE_CONFIG_TOKEN, STRIPE_CLIENT_TOKEN, - ], - } ) - implements OnModuleInit - { - private readonly logger = new Logger( StripeModule.name ) - - - constructor( - private readonly discover : DiscoveryService, - private readonly externalContextCreator : ExternalContextCreator, - @InjectStripeModuleConfig() private readonly stripeModuleConfig : StripeModuleConfig, - ) - { - super() - } - - - public async onModuleInit() - { - // If they didn't provide a webhook config secret there's no reason - // to even attempt discovery - if ( !this.stripeModuleConfig.webhookConfig ) - { - return - } - - const noOneSecretProvided = !this.stripeModuleConfig.webhookConfig.stripeSecrets.account - && !this.stripeModuleConfig.webhookConfig.stripeSecrets.connect - - if ( noOneSecretProvided ) - { - const errorMessage = 'missing stripe webhook secret. module is improperly configured and will be unable to process incoming webhooks from Stripe' - this.logger.error( errorMessage ) - throw new Error( errorMessage ) - } - - this.logger.log( 'Initializing Stripe Module for webhooks' ) - - const [ stripeWebhookService ] = ( ( // @ts-ignore - await this.discover.providersWithMetaAtKey( - STRIPE_WEBHOOK_SERVICE as any ) ) || [] ).map( - (x) => x.discoveredClass.instance ) - - if ( !stripeWebhookService || !( stripeWebhookService instanceof StripeWebhookService ) ) - { - throw new Error( 'Could not find instance of Stripe Webhook Service' ) - } - - // @ts-ignore - const eventHandlerMeta = // @ts-ignore - await this.discover.providerMethodsWithMetaAtKey( STRIPE_WEBHOOK_HANDLER ) - - const grouped = groupBy( (x) => x.discoveredMethod.parentClass.name, eventHandlerMeta ) - - const webhookHandlers = flatten( Object.keys( grouped ).map( (x) => { - this.logger.log( `Registering Stripe webhook handlers from ${x}` ) - - return ( grouped[ x ] as { discoveredMethod : any, eventType : string, meta : string }[] ).map( ({ - discoveredMethod : discoveredMethod, - meta : eventType, - }) => { - return ( { - key : eventType, - handler : this.externalContextCreator.create( discoveredMethod.parentClass.instance, - discoveredMethod.handler, discoveredMethod.methodName, - undefined, // metadataKey - undefined, // paramsFactory - undefined, // contextId - undefined, // inquirerId - undefined, // options - 'stripe_webhook', // contextType - ), - } ) - } ) - } ) ) - const handleWebhook = async (webhookEvent : { type : string }) => { - const {type} = webhookEvent - const handlers = webhookHandlers.filter( (x) => x.key === type ) - - if ( handlers.length ) - { - if ( this.stripeModuleConfig?.webhookConfig?.loggingConfiguration?.logMatchingEventHandlers ) - { - this.logger.log( - `Received webhook event for ${type}. Forwarding to ${handlers.length} event handlers` ) - } - await Promise.all( handlers.map( (x) => x.handler( webhookEvent ) ) ) - } - } - - stripeWebhookService.handleWebhook = handleWebhook - } - } \ No newline at end of file diff --git a/apps/server/src/common/notification/entity/notification.ts b/apps/server/src/common/notification/entity/notification.ts index dd1f9c62..0b4b3052 100644 --- a/apps/server/src/common/notification/entity/notification.ts +++ b/apps/server/src/common/notification/entity/notification.ts @@ -23,18 +23,14 @@ * */ -import {randomUUID} from 'node:crypto' -import {EmailMessage} from "../../../kernel/modules/mailer/entity/email-message.js" -import {EmailContent, isEmailContent} from "../../../kernel/modules/mailer/value-object/email-content.js" -import {EmailRecipient} from "../../../kernel/modules/mailer/value-object/email-recipient.js" -import type {AccountId} from '../../../modules/account/shared-kernel/account-id.js' -import { - EntityBase, - type EntityFoundation, -} from '../../libraries/domain/entity/entity-base.js' -import {NotificationQueued} from '../event/notification-queued.js' -import {NotificationChannel} from '../value-object/notification-channel.js' -import {NotificationStatus} from '../value-object/notification-status.js' +import {randomUUID} from 'node:crypto' +import {EmailMessage} from "../../../kernel/modules/mailer/entity/email-message.js" +import {EmailContent, isEmailContent} from "../../../kernel/modules/mailer/value-object/email-content.js" +import {EmailRecipient} from "../../../kernel/modules/mailer/value-object/email-recipient.js" +import {EntityBase, type EntityFoundation} from '../../libraries/domain/entity/entity-base.js' +import {NotificationQueued} from '../event/notification-queued.js' +import {NotificationChannel} from '../value-object/notification-channel.js' +import {NotificationStatus} from '../value-object/notification-status.js' @@ -46,57 +42,48 @@ type ChannelContentMap = { } type ReceipentByChannelMap = { - [NotificationChannel.EMAIL]: EmailRecipient, - [NotificationChannel.SMS]: unknown + [NotificationChannel.EMAIL]: EmailRecipient, [NotificationChannel.SMS]: unknown [NotificationChannel.PUSH]: unknown [NotificationChannel.WEBHOOK]: unknown } -export interface NotificationProperties - extends EntityFoundation -{ +export interface NotificationProperties extends EntityFoundation { type: NotificationChannel content: ChannelContentMap[T] recipient: ReceipentByChannelMap[T] status?: NotificationStatus sentAt?: Date - sentBy: AccountId + sentBy: string priority?: 'LOW' | 'MEDIUM' | 'HIGH' } -export class Notification - extends EntityBase - implements NotificationProperties -{ +export class Notification extends EntityBase implements NotificationProperties { content: ChannelContentMap[T] priority: 'LOW' | 'MEDIUM' | 'HIGH' recipient: ReceipentByChannelMap[T] sentAt: Date | undefined - sentBy: AccountId + sentBy: string status: NotificationStatus type: NotificationChannel - constructor(payload: NotificationProperties) - { + constructor(payload: NotificationProperties) { super({ - id : payload.id ?? randomUUID(), - updatedAt: payload.updatedAt, - createdAt: payload.createdAt, - version : payload.version, - }) + id: payload.id ?? randomUUID(), + updatedAt: payload.updatedAt, + createdAt: payload.createdAt, + version: payload.version, + }) this.type = payload.type // Validate Content Type - if (this.type === NotificationChannel.EMAIL) - { + if (this.type === NotificationChannel.EMAIL) { const isEmail = isEmailContent(payload.content) - if (!isEmail) - { + if (!isEmail) { throw new Error('Invalid Email Message') } @@ -113,8 +100,7 @@ export class Notification } - queue() - { + queue() { this.when(NotificationStatus.FAILED, this.status) this.logger.debug('Queueing notification...') this.status = NotificationStatus.PENDING @@ -125,8 +111,7 @@ export class Notification } - send() - { + send() { this.logger.debug('Sending notification...') this.status = NotificationStatus.SENT // TODO: Append Event @@ -135,25 +120,21 @@ export class Notification } - cancel() - { + cancel() { } - retry() - { + retry() { } - process() - { + process() { this.logger.debug('Processing notification...') this.logger.debug('Actions on notification will be locked until it is processed.') return this } - fail() - { + fail() { } } diff --git a/apps/server/src/common/shared-module.ts b/apps/server/src/common/shared-module.ts index cb10222d..40ad9789 100644 --- a/apps/server/src/common/shared-module.ts +++ b/apps/server/src/common/shared-module.ts @@ -1,9 +1,6 @@ import {Module} from '@nestjs/common' -import {ConfigModule} from '../kernel/core/configuration/config-module.js' +import {ConfigModule} from '../core/modules/configuration/config-module.js' import {XcacheModule} from '../kernel/modules/cache/xcache-module.js' -import {RedisModule} from '../kernel/resource/redis/redis.module.js' -import {AccountModule} from '../modules/account/account.module.js' -import {AuthenticationModule} from '../modules/authentication/authentication-module.js' import {DatabaseModule} from './modules/database/database.module.js' import {AsyncLocalStorageModule} from './modules/environment/local-storage/async-local-storage-module.js' import {ContinuationLocalStorageModule} from './modules/environment/local-storage/continuation-local-storage-module.js' @@ -13,41 +10,24 @@ import {ObservabilityModule} from './modules/observability/observabil @Module({ - imports : [ - AccountModule, - AuthenticationModule, - ContinuationLocalStorageModule, - AsyncLocalStorageModule, - ConfigModule.forRoot(), - ObservabilityModule, - DatabaseModule, - EventBusModule, - RedisModule, - EventBusModule, - XcacheModule, - // SessionMiddlewareModule.forRoot({ - // session: { - // secret : 'secretomitted', - // rolling : false, - // resave : false, - // saveUninitialized: false, - // }, - // }), // - // https://stackoverflow.com/questions/56046527/express-session-req-session-touch-not-a-function - // CookieSessionModule.forRoot({ session: { name: 'session', secret: 'secretomitted', maxAge: 0 } }) - ], - providers: [], - exports : [ - ObservabilityModule, - DatabaseModule, - ContinuationLocalStorageModule, - AsyncLocalStorageModule, - ConfigModule, - EventBusModule, - RedisModule, - XcacheModule, - ], - }) -export class SharedModule -{ + imports: [ + ContinuationLocalStorageModule, AsyncLocalStorageModule, ConfigModule.forRoot(), ObservabilityModule, + DatabaseModule, EventBusModule, EventBusModule, XcacheModule, // SessionMiddlewareModule.forRoot({ + // session: { + // secret : 'secretomitted', + // rolling : false, + // resave : false, + // saveUninitialized: false, + // }, + // }), // + // https://stackoverflow.com/questions/56046527/express-session-req-session-touch-not-a-function + // CookieSessionModule.forRoot({ session: { name: 'session', secret: 'secretomitted', maxAge: 0 } }) + ], + providers: [], + exports: [ + ObservabilityModule, DatabaseModule, ContinuationLocalStorageModule, AsyncLocalStorageModule, ConfigModule, + EventBusModule, XcacheModule, + ], +}) +export class SharedModule { } diff --git a/apps/server/src/container.ts b/apps/server/src/container.ts index f0d7f1f1..66e75d68 100644 --- a/apps/server/src/container.ts +++ b/apps/server/src/container.ts @@ -1,69 +1,59 @@ -import { CacheInterceptor } from '@nestjs/cache-manager'; -import { - Logger, - MiddlewareConsumer, - Module, - OnModuleDestroy, - OnModuleInit, - RequestMethod, -} from '@nestjs/common'; -import { APP_INTERCEPTOR } from '@nestjs/core'; -import Sentry from '@sentry/node'; -import { DocumentationModule } from './common/modules/documentation/documentation-module.js'; -import { DeveloperToolsModule } from './common/modules/environment/dev-tools/developer-tools.module.js'; -import { HealthModule } from './common/modules/observability/healthcheck/health-module.js'; -import { CacheManagerModule } from './common/modules/storage/cache-manager/cache-manager-module.js'; -import { SharedModule } from './common/shared-module.js'; -import { GraphqlModule } from './kernel/platform/gql/graphql-module.js'; -import { FingerprintMiddleware } from './kernel/platform/http/middleware/fingerprint.js'; -import { CertificateBasedAuthenticationController } from './kernel/modules/identity/cbac.js'; -import { SingleSignOnController } from './kernel/modules/identity/sso/sso.js'; -import { AccountController } from './http/v1/account.js'; +import {CacheInterceptor} from '@nestjs/cache-manager'; +import {Logger, MiddlewareConsumer, Module, OnModuleDestroy, OnModuleInit, RequestMethod} from '@nestjs/common'; +import {APP_INTERCEPTOR} from '@nestjs/core'; +import Sentry from '@sentry/node'; +import {DocumentationModule} from './common/modules/documentation/documentation-module.js'; +import {DeveloperToolsModule} from './common/modules/environment/dev-tools/developer-tools.module.js'; +import {HealthModule} from './common/modules/observability/healthcheck/health-module.js'; +import {CacheManagerModule} from './common/modules/storage/cache-manager/cache-manager-module.js'; +import {SharedModule} from './common/shared-module.js'; +import {CertificateBasedAuthenticationController} from './kernel/modules/identity/cbac.js'; +import {SingleSignOnController} from './kernel/modules/identity/sso/sso.js'; +import {GraphqlModule} from './kernel/platform/gql/graphql-module.js'; +import {FingerprintMiddleware} from './kernel/platform/http/middleware/fingerprint.js'; +import {AccountController} from './routes/v1/account.js'; + + @Module({ - imports: [ - GraphqlModule, - SharedModule, - DocumentationModule, - HealthModule, - DeveloperToolsModule, - CacheManagerModule, - ], - controllers: [ - SingleSignOnController, - CertificateBasedAuthenticationController, - AccountController, - ], - providers: [ - { - provide: APP_INTERCEPTOR, - useClass: CacheInterceptor, - }, - ], + imports: [ + GraphqlModule, SharedModule, DocumentationModule, HealthModule, DeveloperToolsModule, CacheManagerModule, + ], + controllers: [ + SingleSignOnController, CertificateBasedAuthenticationController, AccountController, + ], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: CacheInterceptor, + }, + ], }) export class Container implements OnModuleInit, OnModuleDestroy { - configure(consumer: MiddlewareConsumer) { - consumer.apply(Sentry.Handlers.requestHandler()).forRoutes({ - path: '*', - method: RequestMethod.ALL, - }); - consumer.apply(Sentry.Handlers.tracingHandler()).forRoutes({ - path: '*', - method: RequestMethod.ALL, - }); - // TODO: Fingerprinting isn't really needed for all endpoints, and can be optimized to be only used for parts - // where it's really needed like carts, checkouts, and other parts of app where we have synthetic sessions. - consumer.apply(FingerprintMiddleware).forRoutes({ - path: '*', - method: RequestMethod.ALL, - }); - } + configure(consumer: MiddlewareConsumer) { + consumer.apply(Sentry.Handlers.requestHandler()).forRoutes({ + path: '*', + method: RequestMethod.ALL, + }); + consumer.apply(Sentry.Handlers.tracingHandler()).forRoutes({ + path: '*', + method: RequestMethod.ALL, + }); + // TODO: Fingerprinting isn't really needed for all endpoints, and can be optimized to be only used for parts + // where it's really needed like carts, checkouts, and other parts of app where we have synthetic sessions. + consumer.apply(FingerprintMiddleware).forRoutes({ + path: '*', + method: RequestMethod.ALL, + }); + } + + + async onModuleInit() { + new Logger('Container').log(`Container was built successfully! πŸ“‘ `); + } - async onModuleInit() { - new Logger('Container').log(`Container was built successfully! πŸ“‘ `); - } - async onModuleDestroy() { - new Logger('Container').log(`Container was destroyed successfully! πŸ“‘ `); - } + async onModuleDestroy() { + new Logger('Container').log(`Container was destroyed successfully! πŸ“‘ `); + } } diff --git a/apps/server/src/hooks/pre-start/acquire-process-lock.ts b/apps/server/src/core/hooks/pre-start/acquire-process-lock.ts similarity index 77% rename from apps/server/src/hooks/pre-start/acquire-process-lock.ts rename to apps/server/src/core/hooks/pre-start/acquire-process-lock.ts index c2472045..caf66d1e 100644 --- a/apps/server/src/hooks/pre-start/acquire-process-lock.ts +++ b/apps/server/src/core/hooks/pre-start/acquire-process-lock.ts @@ -23,8 +23,8 @@ * */ -import process from 'node:process' -import { ProcessLockManager } from '../../common/process-locking/process-lock.js' +import process from 'node:process' +import {ProcessLockManager} from '../../../common/process-locking/process-lock.js' @@ -32,10 +32,7 @@ const processLocker = new ProcessLockManager() processLocker.initializeTerminationHandlers() - -export async function acquireProcessLock(lockName : string = process.env.SERVICE_NAME - || 'nodejs-application') : Promise - { - await processLocker.checkLock() - await processLocker.createLock() - } \ No newline at end of file +export async function acquireProcessLock(lockName: string = process.env.SERVICE_NAME || 'nodejs-application'): Promise { + await processLocker.checkLock() + await processLocker.createLock() +} \ No newline at end of file diff --git a/apps/server/src/hooks/pre-start/initialize-sentry.ts b/apps/server/src/core/hooks/pre-start/initialize-sentry.ts similarity index 71% rename from apps/server/src/hooks/pre-start/initialize-sentry.ts rename to apps/server/src/core/hooks/pre-start/initialize-sentry.ts index 1e7ae4f2..fb8d1e18 100644 --- a/apps/server/src/hooks/pre-start/initialize-sentry.ts +++ b/apps/server/src/core/hooks/pre-start/initialize-sentry.ts @@ -24,41 +24,34 @@ */ import {setupGlobalHub} from '@sentry/opentelemetry' -import {__sentry} from '../../common/modules/resources/sentry-v2/global/get-sentry.js' -import {SENTRY_CONFIGURATION} from '../../configs/config-set/sentry-configuration.js' -import {__config} from '../../configs/global/__config.js' -import {CombinedLogger} from '../../kernel/modules/logger/logger.js' +import {__sentry} from '../../../common/modules/resources/sentry-v2/global/get-sentry.js' +import {SENTRY_CONFIGURATION} from '../../../configs/config-set/sentry-configuration.js' +import {__config} from '../../../configs/global/__config.js' +import {CombinedLogger} from "../../modules/logger/logger.js" -export function initializeSentry(): void -{ +export function initializeSentry(): void { const logger = new CombinedLogger('sentry') - if (SENTRY_CONFIGURATION.enabled) - { + if (SENTRY_CONFIGURATION.enabled) { logger.debug(`Initializing Sentry... ${__config.get('SENTRY_DSN')}`) } // Turn ON if integrating with OTEL - if (SENTRY_CONFIGURATION.instrumenter === 'otel') - { + if (SENTRY_CONFIGURATION.instrumenter === 'otel') { setupGlobalHub() } // OTEL Configuration - try - { + try { __sentry?.init({ - ...SENTRY_CONFIGURATION, - }) - } - catch - { + ...SENTRY_CONFIGURATION, + }) + } catch { } - if (SENTRY_CONFIGURATION.enabled) - { + if (SENTRY_CONFIGURATION.enabled) { logger.info(`Sentry initialized!`) } } diff --git a/apps/server/src/modules/authentication/README.md b/apps/server/src/core/identity/authn/README.md similarity index 91% rename from apps/server/src/modules/authentication/README.md rename to apps/server/src/core/identity/authn/README.md index 6f84a022..c908f02d 100644 --- a/apps/server/src/modules/authentication/README.md +++ b/apps/server/src/core/identity/authn/README.md @@ -1,4 +1,4 @@ -# Authorization +# Authentication (AuthN) Authentication verifies that a person is who they claim to be. Users are typically authenticated when they log in by entering their username and password. diff --git a/apps/server/src/core/identity/authn/components/authentication-service.ts b/apps/server/src/core/identity/authn/components/authentication-service.ts new file mode 100644 index 00000000..e630dac6 --- /dev/null +++ b/apps/server/src/core/identity/authn/components/authentication-service.ts @@ -0,0 +1,59 @@ +/* + * MIT License + * + * Copyright (c) 2024 Jakub Olan + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import {ForbiddenException, NotFoundException} from '@nestjs/common' +import {Result} from 'neverthrow' +import {AccessToken, RefreshToken} from "../../../../kernel/modules/identity/jwt.js" + + + +export abstract class AuthenticationService { + /** + * Authenticates a user with their username and password. + * + * @param {string} username - The username of the user. + * @param {string} password - The password of the user. + * @param {Object} [metadata] - Additional metadata for the + * authentication. + * @param {string} [metadata.userAgent] - The user agent of the client. + * @param {string} [metadata.ipAddress] - The IP address of the client. + * @returns {Promise>} - The result of the + * authentication. If successful, it contains the domain ID, access + * token, and refresh token. If unsuccessful, it contains the + * error. + */ + public abstract authenticate(username: string, password: string): Promise>; + + + public abstract logout(): Promise; + + + public abstract refreshToken(refreshToken: RefreshToken): Promise<{ + refreshToken: RefreshToken, accessToken: AccessToken + }>; +} \ No newline at end of file diff --git a/apps/server/src/modules/authentication/contract/authentication-strategy/authentication-strategy-type.ts b/apps/server/src/core/identity/authn/components/authentication-strategy/authentication-strategy-type.ts similarity index 100% rename from apps/server/src/modules/authentication/contract/authentication-strategy/authentication-strategy-type.ts rename to apps/server/src/core/identity/authn/components/authentication-strategy/authentication-strategy-type.ts diff --git a/apps/server/src/modules/authentication/contract/authentication-strategy/authentication-strategy.ts b/apps/server/src/core/identity/authn/components/authentication-strategy/authentication-strategy.ts similarity index 100% rename from apps/server/src/modules/authentication/contract/authentication-strategy/authentication-strategy.ts rename to apps/server/src/core/identity/authn/components/authentication-strategy/authentication-strategy.ts diff --git a/apps/server/src/modules/authentication/contract/authentication-strategy/index.ts b/apps/server/src/core/identity/authn/components/authentication-strategy/index.ts similarity index 100% rename from apps/server/src/modules/authentication/contract/authentication-strategy/index.ts rename to apps/server/src/core/identity/authn/components/authentication-strategy/index.ts diff --git a/apps/server/src/core/identity/authn/components/guards/jwt-authorization-guard.ts b/apps/server/src/core/identity/authn/components/guards/jwt-authorization-guard.ts new file mode 100644 index 00000000..8cbb2509 --- /dev/null +++ b/apps/server/src/core/identity/authn/components/guards/jwt-authorization-guard.ts @@ -0,0 +1,8 @@ +import {Injectable} from "@nestjs/common" +import {AuthGuard, IAuthGuard} from "@nestjs/passport" +import {AuthenticationStrategyType} from "../authentication-strategy/authentication-strategy-type.js" + + + +@Injectable() +export class JwtAuthorizationGuard extends AuthGuard(AuthenticationStrategyType.JWT) implements IAuthGuard {} \ No newline at end of file diff --git a/apps/server/src/core/identity/authn/components/guards/local-authorization-guard.ts b/apps/server/src/core/identity/authn/components/guards/local-authorization-guard.ts new file mode 100644 index 00000000..959f96e9 --- /dev/null +++ b/apps/server/src/core/identity/authn/components/guards/local-authorization-guard.ts @@ -0,0 +1,11 @@ +import {Injectable} from "@nestjs/common" +import {AuthGuard, IAuthGuard} from "@nestjs/passport" +import {AuthenticationStrategyType} from "../authentication-strategy/authentication-strategy-type.js" + + + +@Injectable() + +export class LocalAuthorizationGuard extends AuthGuard(AuthenticationStrategyType.LOCAL) implements IAuthGuard { + +} \ No newline at end of file diff --git a/apps/server/src/core/identity/authz/README.md b/apps/server/src/core/identity/authz/README.md new file mode 100644 index 00000000..641d3f9b --- /dev/null +++ b/apps/server/src/core/identity/authz/README.md @@ -0,0 +1,3 @@ +# Authorization (AuthZ) + +Authorization is the process of determining whether a user is allowed to perform a given action. \ No newline at end of file diff --git a/apps/server/src/modules/account/models/account/account-update-model.ts b/apps/server/src/core/identity/identity.ts similarity index 100% rename from apps/server/src/modules/account/models/account/account-update-model.ts rename to apps/server/src/core/identity/identity.ts diff --git a/apps/server/src/modules/audit/README.md b/apps/server/src/core/modules/audit/README.md similarity index 100% rename from apps/server/src/modules/audit/README.md rename to apps/server/src/core/modules/audit/README.md diff --git a/apps/server/src/modules/audit/TODO.txt b/apps/server/src/core/modules/audit/TODO.txt similarity index 100% rename from apps/server/src/modules/audit/TODO.txt rename to apps/server/src/core/modules/audit/TODO.txt diff --git a/apps/server/src/modules/audit/controller/audit-controller.ts b/apps/server/src/core/modules/audit/controller/audit-controller.ts similarity index 100% rename from apps/server/src/modules/audit/controller/audit-controller.ts rename to apps/server/src/core/modules/audit/controller/audit-controller.ts diff --git a/apps/server/src/modules/audit/decorator/auditable-decorator.ts b/apps/server/src/core/modules/audit/decorator/auditable-decorator.ts similarity index 100% rename from apps/server/src/modules/audit/decorator/auditable-decorator.ts rename to apps/server/src/core/modules/audit/decorator/auditable-decorator.ts diff --git a/apps/server/src/core/modules/audit/entities/audit-log.ts b/apps/server/src/core/modules/audit/entities/audit-log.ts new file mode 100644 index 00000000..682f174e --- /dev/null +++ b/apps/server/src/core/modules/audit/entities/audit-log.ts @@ -0,0 +1,14 @@ +import {AuditActor} from '../value-object/audit-actor.js' +import {AuditOperation} from '../value-object/audit-operation.js' +import {AuditResource} from '../value-object/audit-resource.js' + + + +export interface AuditLog { + id: string + /* "Who?" | The actor who performed the action. See below for the description of its type. */ + actor: AuditActor + operation: AuditOperation + resource: AuditResource + date: Date +} \ No newline at end of file diff --git a/apps/server/src/modules/group/repository/prisma-group-repository.ts b/apps/server/src/core/modules/audit/interceptor/audit-interceptor.ts similarity index 58% rename from apps/server/src/modules/group/repository/prisma-group-repository.ts rename to apps/server/src/core/modules/audit/interceptor/audit-interceptor.ts index b19b0bb3..31d1139b 100644 --- a/apps/server/src/modules/group/repository/prisma-group-repository.ts +++ b/apps/server/src/core/modules/audit/interceptor/audit-interceptor.ts @@ -23,38 +23,24 @@ * */ -import { Injectable } from '@nestjs/common' -import { Group } from '../entities/group.js' -import { GroupRepository } from './group-repository.js' +import {CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor} from '@nestjs/common' +import {Reflector} from '@nestjs/core' +import {Observable} from 'rxjs' +import {PrismaService} from "../../../../common/modules/resources/prisma/services/prisma-service.js" @Injectable() -export class PrismaGroupRepository - extends GroupRepository - { - create(entity : Group) : Promise - { - throw Error( 'Not implemented' ) - } - - delete(entity : Group) : Promise - { - throw Error( 'Not implemented' ) - } - - exists(entity : Group) : Promise - { - throw Error( 'Not implemented' ) - } - - findById(id : string) : Promise - { - throw Error( 'Not implemented' ) - } - - update(entity : Group) : Promise - { - throw Error( 'Not implemented' ) - } - } \ No newline at end of file +export class AuditInterceptor implements NestInterceptor { + private logger = new Logger('interceptor:audit') + + + constructor(private readonly reflector: Reflector, private prisma: PrismaService) { + } + + + intercept(context: ExecutionContext, next: CallHandler): Observable | Promise> { + // const auditLog = this.reflector.get("XYZ", context.getHandler()) + return next.handle() + } +} \ No newline at end of file diff --git a/apps/server/src/modules/audit/repository/audit-repository.ts b/apps/server/src/core/modules/audit/repository/audit-repository.ts similarity index 94% rename from apps/server/src/modules/audit/repository/audit-repository.ts rename to apps/server/src/core/modules/audit/repository/audit-repository.ts index 810f5364..f3881069 100644 --- a/apps/server/src/modules/audit/repository/audit-repository.ts +++ b/apps/server/src/core/modules/audit/repository/audit-repository.ts @@ -23,7 +23,7 @@ * */ -import {Repository} from "../../../common/libraries/storage/index.js" +import {Repository} from "../../../../common/libraries/storage/index.js" import {AuditLog} from "../entities/audit-log.js" diff --git a/apps/server/src/modules/audit/services/audit-manager.ts b/apps/server/src/core/modules/audit/services/audit-manager.ts similarity index 100% rename from apps/server/src/modules/audit/services/audit-manager.ts rename to apps/server/src/core/modules/audit/services/audit-manager.ts diff --git a/apps/server/src/modules/audit/value-object/audit-action.ts b/apps/server/src/core/modules/audit/value-object/audit-action.ts similarity index 100% rename from apps/server/src/modules/audit/value-object/audit-action.ts rename to apps/server/src/core/modules/audit/value-object/audit-action.ts diff --git a/apps/server/src/modules/audit/value-object/audit-actor.ts b/apps/server/src/core/modules/audit/value-object/audit-actor.ts similarity index 100% rename from apps/server/src/modules/audit/value-object/audit-actor.ts rename to apps/server/src/core/modules/audit/value-object/audit-actor.ts diff --git a/apps/server/src/modules/audit/value-object/audit-operation.ts b/apps/server/src/core/modules/audit/value-object/audit-operation.ts similarity index 100% rename from apps/server/src/modules/audit/value-object/audit-operation.ts rename to apps/server/src/core/modules/audit/value-object/audit-operation.ts diff --git a/apps/server/src/modules/audit/value-object/audit-resource.ts b/apps/server/src/core/modules/audit/value-object/audit-resource.ts similarity index 100% rename from apps/server/src/modules/audit/value-object/audit-resource.ts rename to apps/server/src/core/modules/audit/value-object/audit-resource.ts diff --git a/apps/server/src/kernel/core/configuration/README.md b/apps/server/src/core/modules/configuration/README.md similarity index 100% rename from apps/server/src/kernel/core/configuration/README.md rename to apps/server/src/core/modules/configuration/README.md diff --git a/apps/server/src/kernel/core/configuration/config-module.ts b/apps/server/src/core/modules/configuration/config-module.ts similarity index 100% rename from apps/server/src/kernel/core/configuration/config-module.ts rename to apps/server/src/core/modules/configuration/config-module.ts diff --git a/apps/server/src/kernel/modules/logger/README.md b/apps/server/src/core/modules/logger/README.md similarity index 100% rename from apps/server/src/kernel/modules/logger/README.md rename to apps/server/src/core/modules/logger/README.md diff --git a/apps/server/src/kernel/modules/logger/appender/README.md b/apps/server/src/core/modules/logger/appender/README.md similarity index 100% rename from apps/server/src/kernel/modules/logger/appender/README.md rename to apps/server/src/core/modules/logger/appender/README.md diff --git a/apps/server/src/kernel/modules/logger/appender/console-appender.ts b/apps/server/src/core/modules/logger/appender/console-appender.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/appender/console-appender.ts rename to apps/server/src/core/modules/logger/appender/console-appender.ts diff --git a/apps/server/src/kernel/modules/logger/appender/file-appender.ts b/apps/server/src/core/modules/logger/appender/file-appender.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/appender/file-appender.ts rename to apps/server/src/core/modules/logger/appender/file-appender.ts diff --git a/apps/server/src/kernel/modules/logger/appender/rolling-file-appender.ts b/apps/server/src/core/modules/logger/appender/rolling-file-appender.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/appender/rolling-file-appender.ts rename to apps/server/src/core/modules/logger/appender/rolling-file-appender.ts diff --git a/apps/server/src/kernel/modules/logger/appender/syslog-appender.ts b/apps/server/src/core/modules/logger/appender/syslog-appender.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/appender/syslog-appender.ts rename to apps/server/src/core/modules/logger/appender/syslog-appender.ts diff --git a/apps/server/src/kernel/modules/logger/formatter/log-symbols.ts b/apps/server/src/core/modules/logger/formatter/log-symbols.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/formatter/log-symbols.ts rename to apps/server/src/core/modules/logger/formatter/log-symbols.ts diff --git a/apps/server/src/kernel/modules/logger/formatter/pretty-formatter.ts b/apps/server/src/core/modules/logger/formatter/pretty-formatter.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/formatter/pretty-formatter.ts rename to apps/server/src/core/modules/logger/formatter/pretty-formatter.ts diff --git a/apps/server/src/kernel/modules/logger/http-logger.ts b/apps/server/src/core/modules/logger/http-logger.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/http-logger.ts rename to apps/server/src/core/modules/logger/http-logger.ts diff --git a/apps/server/src/kernel/modules/logger/layout/pretty-logger-layout.ts b/apps/server/src/core/modules/logger/layout/pretty-logger-layout.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/layout/pretty-logger-layout.ts rename to apps/server/src/core/modules/logger/layout/pretty-logger-layout.ts diff --git a/apps/server/src/kernel/modules/logger/log-appender.ts b/apps/server/src/core/modules/logger/log-appender.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/log-appender.ts rename to apps/server/src/core/modules/logger/log-appender.ts diff --git a/apps/server/src/kernel/modules/logger/log-level.ts b/apps/server/src/core/modules/logger/log-level.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/log-level.ts rename to apps/server/src/core/modules/logger/log-level.ts diff --git a/apps/server/src/kernel/modules/logger/log.ts b/apps/server/src/core/modules/logger/log.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/log.ts rename to apps/server/src/core/modules/logger/log.ts diff --git a/apps/server/src/kernel/modules/logger/logger-layout.ts b/apps/server/src/core/modules/logger/logger-layout.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/logger-layout.ts rename to apps/server/src/core/modules/logger/logger-layout.ts diff --git a/apps/server/src/kernel/modules/logger/logger.module.ts b/apps/server/src/core/modules/logger/logger.module.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/logger.module.ts rename to apps/server/src/core/modules/logger/logger.module.ts diff --git a/apps/server/src/kernel/modules/logger/logger.ts b/apps/server/src/core/modules/logger/logger.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/logger.ts rename to apps/server/src/core/modules/logger/logger.ts diff --git a/apps/server/src/kernel/modules/logger/nestjs-logger-proxy.ts b/apps/server/src/core/modules/logger/nestjs-logger-proxy.ts similarity index 100% rename from apps/server/src/kernel/modules/logger/nestjs-logger-proxy.ts rename to apps/server/src/core/modules/logger/nestjs-logger-proxy.ts diff --git a/apps/server/src/kernel/core/secret-manager/secret-manager.ts b/apps/server/src/core/modules/secret-manager/secret-manager.ts similarity index 100% rename from apps/server/src/kernel/core/secret-manager/secret-manager.ts rename to apps/server/src/core/modules/secret-manager/secret-manager.ts diff --git a/apps/server/src/http/v1/account.ts b/apps/server/src/http/v1/account.ts deleted file mode 100644 index a8d4bf8a..00000000 --- a/apps/server/src/http/v1/account.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { Body, Controller, Post } from '@nestjs/common'; -import { - ApiBody, - ApiConflictResponse, - ApiOkResponse, - ApiOperation, - ApiProperty, - ApiResponse, - getSchemaPath, -} from '@nestjs/swagger'; -import { argon2i, argon2d, hash, verify } from 'argon2'; -import { PrismaClient, Prisma, Account } from 'db'; -import { SignJWT } from 'jose'; -import { randomUUID } from 'node:crypto'; -import { HttpProblem } from '../../common/error/problem-details/http-problem.js'; -import { PrismaService } from '../../common/modules/resources/prisma/services/prisma-service.js'; -import { __authConfig } from '../../configs/global/__config.js'; -import { AccountViewModel } from '../../modules/account/view-model/account-view-model.js'; -import { ApiModel } from '../../utilities/docs-utils/swagger-api-model.js'; -import { ApiAccountMockup } from '../../utilities/fixtures/api-account-mockup.js'; - -export const ApiPropertyAccountEmail = ApiProperty({ - name: 'email', - description: 'The' + " account's email address", - example: ApiAccountMockup.email, - examples: ApiAccountMockup._examples.emails, - type: String, -}); - -export const ApiPropertyAccountPassword = ApiProperty({ - name: 'password', - description: "The account's username", - example: ApiAccountMockup.password, - examples: ApiAccountMockup._examples.passwords, -}); - -export const ApiPropertyAccountUsername = ApiProperty({ - name: 'username', - description: "The account's username", - example: ApiAccountMockup.username, - examples: ApiAccountMockup._examples.usernames, -}); - -export class RegisterAccountCommand { - /** - * Represents an email address. - * @typedef {string} email - */ - @ApiPropertyAccountEmail email: string; - - /** - * The password variable is a string that represents a user's password. - * - * @type {string} - */ - @ApiPropertyAccountPassword password: string; - - /** - * Represents a username. - * @typedef {string} username - */ - @ApiPropertyAccountUsername username: string; -} - -@ApiModel({ - name: 'Auhenticate', - description: 'asdasd', -}) -export class AuthenticateCommand { - /** - * Represents a username. - * @typedef {string} username - */ - @ApiPropertyAccountUsername username: string; - - /** - * The password variable is a string that represents a user's password. - * - * @type {string} - */ - @ApiPropertyAccountPassword password: string; -} - -export interface RegisterAccount { - username: string; - password: string; -} - -export interface RegisteredAccount { - id: string; - username: string; - password: string; -} - -export interface BasicAuthenticate { - username: string; - password: string; -} - -export interface AuthenticationSucceed { - accessToken: string; -} - -@Controller('account') -export class AccountController { - private readonly accountRepository: Prisma.AccountDelegate; - - constructor(prismaService: PrismaService) { - this.accountRepository = prismaService.account; - } - - @ApiOperation({ - operationId: 'register', - summary: 'Register account', - tags: ['account'], - }) - @ApiBody({ type: RegisterAccountCommand }) - @ApiOkResponse({ - type: AccountViewModel, - description: 'Account was successfully registered in system.', - }) - @Post() - @ApiConflictResponse({ - type: HttpProblem, - description: 'Account already exists.', - content: { - example: { - type: 'https://httpstatuses.com/409', - title: 'Conflict', - status: 409, - detail: 'Account already exists.', - } as any, - }, - }) - @ApiResponse({ - status: 400, - description: 'Provided Invalid Data', - content: { - 'application/json': { - schema: { - $ref: getSchemaPath(HttpProblem), - type: 'object', - example: { - type: 'com.methylphenidate.account.invalid-username', - title: 'Invalid Username', - status: 400, - }, - examples: [ - { - type: 'com.methylphenidate.account.invalid-email', - title: 'Invalid Email', - status: 400, - }, - ], - }, - }, - }, - }) - @ApiBody({ type: RegisterAccountCommand }) - async registerAccount(@Body() body: unknown): Promise { - // TODO: Validate input - const registerAccount = body as RegisterAccount; - - let account: Account | null = await this.accountRepository.findFirst({ - where: { username: registerAccount.username }, - }); - - if (!account) { - const passwordHash = await hash(registerAccount.password); - - account = await this.accountRepository.create({ - data: { - username: registerAccount.username, - email: 'keinsell@protonmail.com', - password: passwordHash, - }, - }); - } else { - throw new Error('Account already exists'); - } - - return { - id: account.id, - username: account.username, - password: account.password, - }; - } - - async basicAuthenticate( - @Body() body: unknown, - ): Promise { - // TODO: Validate this - const basicAuthenticate = body as BasicAuthenticate; - - const account = await this.accountRepository.findFirst({ - where: { username: basicAuthenticate.username }, - }); - - if (!account) { - throw new Error('Account not found'); - } - - if (!(await verify(account.password, basicAuthenticate.password))) { - throw new Error('Invalid password'); - } - - const key = new TextEncoder().encode(__authConfig.JWT_SECRET); - - const payload = { - jti: randomUUID(), - sub: account.id, - aud: 'access', - }; - - const plainAccessToken = new SignJWT({ ...payload }); - plainAccessToken.setExpirationTime('1h'); - plainAccessToken.setProtectedHeader({ - b64: true, - alg: 'HS256', - }); - - const accessToken = await plainAccessToken.sign(key); - - return { - accessToken: accessToken, - }; - } -} diff --git a/apps/server/src/kernel/modules/configuration/oauth-config.ts b/apps/server/src/kernel/modules/configuration/oauth-config.ts index 4c61beb8..11d242cf 100644 --- a/apps/server/src/kernel/modules/configuration/oauth-config.ts +++ b/apps/server/src/kernel/modules/configuration/oauth-config.ts @@ -6,15 +6,12 @@ // and automatically parse them into objects, // then validate them and notify the administrator of the application if things are fine or something gone wrong. - - -import {CombinedLogger} from '../logger/logger.js' +import {CombinedLogger} from "../../../core/modules/logger/logger.js" /** Interface dedicated for adding OAuth 2.0 clients to application be used in SSO for example */ -export interface OAuthClientConfig -{ +export interface OAuthClientConfig { /** @example "github" * @link [GitHub Developer Apps](https://github.com/settings/applications/new) */ @@ -44,20 +41,16 @@ export interface OAuthClientConfig } - -export function discoverSSOClients(): OAuthClientConfig[] -{ +export function discoverSSOClients(): OAuthClientConfig[] { const ENV = process.env const clients: { [idp: string]: OAuthClientConfig } = {} const logger = new CombinedLogger() - for (const [key, value] of Object.entries(ENV)) - { + for (const [key, value] of Object.entries(ENV)) { // OAUTH_CLIENT_{IDP}_{VARIABLE} - if (key.startsWith('OAUTH_CLIENT_') && typeof value === 'string') - { + if (key.startsWith('OAUTH_CLIENT_') && typeof value === 'string') { // Get a IDP name const parts = key.split('_') const idp = parts[2].toLowerCase() @@ -65,13 +58,12 @@ export function discoverSSOClients(): OAuthClientConfig[] if (!( idp in clients - )) - { + )) { clients[idp] = { - IdP : idp, + IdP: idp, discoverEndpoints: false, - scope : 'openid email profile', - redirectUri : `http://localhost:1337/sso/${idp}`, + scope: 'openid email profile', + redirectUri: `http://localhost:1337/sso/${idp}`, } as any } @@ -80,40 +72,39 @@ export function discoverSSOClients(): OAuthClientConfig[] // Parse additional fields const setting = parts.slice(3).join('_') - if (setting === 'CLIENT_ID') - { + if (setting === 'CLIENT_ID') { logger.debug(`${key}=${value}`) config.clientId = value - } - else if (setting === 'SECRET') - { - logger.debug(`${key}=${value}`) - config.clientSecret = value - } - else if (setting === 'AUTHORITY') - { - logger.debug(`${key}=${value}`) - config.authority = value - } - else if (setting === 'USERINFO_ENDPOINT') - { - logger.debug(`${key}=${value}`) - config.userinfoEndpoint = value - } - else if (setting === 'TOKEN_ENDPOINT') - { - logger.debug(`${key}=${value}`) - config.tokenEndpoint = value - } - else if (setting === 'AUTH_ENDPOINT') - { - logger.debug(`${key}=${value}`) - config.authorizationEndpoint = value - } - else if (setting === 'ISSUER') - { - logger.debug(`${key}=${value}`) - config.issuer = value + } else { + if (setting === 'SECRET') { + logger.debug(`${key}=${value}`) + config.clientSecret = value + } else { + if (setting === 'AUTHORITY') { + logger.debug(`${key}=${value}`) + config.authority = value + } else { + if (setting === 'USERINFO_ENDPOINT') { + logger.debug(`${key}=${value}`) + config.userinfoEndpoint = value + } else { + if (setting === 'TOKEN_ENDPOINT') { + logger.debug(`${key}=${value}`) + config.tokenEndpoint = value + } else { + if (setting === 'AUTH_ENDPOINT') { + logger.debug(`${key}=${value}`) + config.authorizationEndpoint = value + } else { + if (setting === 'ISSUER') { + logger.debug(`${key}=${value}`) + config.issuer = value + } + } + } + } + } + } } } } @@ -122,8 +113,8 @@ export function discoverSSOClients(): OAuthClientConfig[] return Object.values(clients).filter(validateSSOClient) } -function validateSSOClient(client: OAuthClientConfig): boolean -{ + +function validateSSOClient(client: OAuthClientConfig): boolean { const logger = new CombinedLogger() let missingParts: string[] = [] diff --git a/apps/server/src/kernel/modules/identity/authorization-strategy/jwt-authorization-strategy.ts b/apps/server/src/kernel/modules/identity/authorization-strategy/jwt-authorization-strategy.ts index 76caa1e6..46841197 100644 --- a/apps/server/src/kernel/modules/identity/authorization-strategy/jwt-authorization-strategy.ts +++ b/apps/server/src/kernel/modules/identity/authorization-strategy/jwt-authorization-strategy.ts @@ -1,12 +1,10 @@ -import {BadRequestException, Injectable, Logger} from '@nestjs/common' -import {PassportStrategy} from '@nestjs/passport' -import {setUser} from '@sentry/node' -import {JwtPayload} from "jsonwebtoken" +import {Injectable, Logger} from '@nestjs/common' +import {PassportStrategy} from '@nestjs/passport' +import {JwtPayload} from "jsonwebtoken" import {ExtractJwt, Strategy} from 'passport-jwt' import {__authConfig} from '../../../../configs/global/__config.js' -import {AccountService} from '../../../../modules/account/services/account-service.js' -import {AuthenticationStrategyType} from '../../../../modules/authentication/contract/authentication-strategy/authentication-strategy-type.js' -import {AuthenticationStrategy} from '../../../../modules/authentication/contract/authentication-strategy/authentication-strategy.js' +import {AuthenticationStrategyType} from '../../../../core/identity/authn/components/authentication-strategy/authentication-strategy-type.js' +import {AuthenticationStrategy} from '../../../../core/identity/authn/components/authentication-strategy/authentication-strategy.js' @@ -16,7 +14,7 @@ export class JwtAuthorizationStrategy extends PassportStrategy(Strategy, Authent private logger: Logger = new Logger('authorization::strategy::jwt') - constructor(private accountService: AccountService) { + constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: new TextEncoder().encode(__authConfig.JWT_SECRET), @@ -25,26 +23,26 @@ export class JwtAuthorizationStrategy extends PassportStrategy(Strategy, Authent async validate(payload: JwtPayload): Promise { - this.logger.verbose(`Decoded jsonwebtoken from authorization header`) - - // Type guard against missing sub - if (!payload.sub) { - throw new BadRequestException('Provided JWT does not contain "sub" property which is required to proceed.') - } - - // Fetch profile associated with token - const account = await this.accountService.getById(payload.sub) - - setUser({ - id: account.id, - username: account.username, - email: account.email.address, - }) - - // TODO: Fetch session associated with token - // TODO: Check if session is valid (not blacklisted and - // existing) TODO: Return User to middleware - - return account + //this.logger.verbose(`Decoded jsonwebtoken from authorization header`) + // + //// Type guard against missing sub + //if (!payload.sub) { + // throw new BadRequestException('Provided JWT does not contain "sub" property which is required to proceed.') + //} + // + //// Fetch profile associated with token + //const account = await this.accountService.getById(payload.sub) + // + //setUser({ + // id: account.id, + // username: account.username, + // email: account.email.address, + //}) + // + //// TODO: Fetch session associated with token + //// TODO: Check if session is valid (not blacklisted and + //// existing) TODO: Return User to middleware + // + //return account } } \ No newline at end of file diff --git a/apps/server/src/kernel/modules/identity/authorization-strategy/local-authorization-strategy.ts b/apps/server/src/kernel/modules/identity/authorization-strategy/local-authorization-strategy.ts index 13a0623d..eb4b8e54 100644 --- a/apps/server/src/kernel/modules/identity/authorization-strategy/local-authorization-strategy.ts +++ b/apps/server/src/kernel/modules/identity/authorization-strategy/local-authorization-strategy.ts @@ -1,73 +1,55 @@ -import { - Injectable, - Logger, -} from '@nestjs/common' -import { PassportStrategy } from '@nestjs/passport' -import { setUser } from '@sentry/node' -import { Strategy } from 'passport-local' -import { OpentelemetryTracer } from '../../../../common/modules/observability/tracing/opentelemetry/provider/tracer/opentelemetry-tracer.js' -import { censorString } from '../../../../utilities/console-utils/censor-string.js' -import { CredentialValidator } from '../../../../modules/account/shared-kernel/credential-validator/credential-validator.js' -import { AuthenticationStrategy } from '../../../../modules/authentication/contract/authentication-strategy/authentication-strategy.js' +import {Injectable, Logger} from '@nestjs/common' +import {PassportStrategy} from '@nestjs/passport' +import {Strategy} from 'passport-local' +import {AuthenticationStrategy} from "../../../../core/identity/authn/components/authentication-strategy/index.js" @Injectable() -export class LocalAuthorizationStrategy - extends PassportStrategy( Strategy ) - implements AuthenticationStrategy - { - private logger : Logger = new Logger( 'authorization::strategy::local' ) - - - constructor(private credentialValidation : CredentialValidator) - { - super() - } - - - async validate( - username : string, - password : string, - ) : Promise - { - const span = new OpentelemetryTracer().startSpan( 'authentication.local', { - attributes : { - 'op' : 'function', - 'name' : 'LocalAuthorizationStrategy.validate', - }, - } ) - - span.setAttribute( 'user_username', username ) - span.setAttribute( 'user.username', username ) - - this.logger.verbose( `User "${username}" is trying to authenticate with "${censorString( password )}"` ) - - const user = await this.credentialValidation.validateCredentials( username.toLowerCase(), password ) - - if ( user.isErr() ) - { - span.end() - this.logger.debug( `Authorization failed: ${JSON.stringify( user.error )}` ) - throw user.error - } - else - { - this.logger.debug( `Authenticated user ${user.value.id}` ) - - span.addEvent( 'authenticated' ) - span.setAttribute( 'user_id', user.value.id ) - span.setAttribute( 'user.id', user.value.id ) - - span.end() - - setUser( { - id : user.value.id, - username : user.value.username, - email : user.value.email.address, - } ) - - return user.value - } - } - } \ No newline at end of file +export class LocalAuthorizationStrategy extends PassportStrategy(Strategy) implements AuthenticationStrategy { + private logger: Logger = new Logger('authorization::strategy::local') + + + constructor() { + super() + } + + + async validate(username: string, password: string): Promise { + //const span = new OpentelemetryTracer().startSpan('authentication.local', { + // attributes: { + // 'op': 'function', + // 'name': 'LocalAuthorizationStrategy.validate', + // }, + //}) + // + //span.setAttribute('user_username', username) + //span.setAttribute('user.username', username) + // + //this.logger.verbose(`User "${username}" is trying to authenticate with "${censorString(password)}"`) + // + //const user = await this.credentialValidation.validateCredentials(username.toLowerCase(), password) + // + //if (user.isErr()) { + // span.end() + // this.logger.debug(`Authorization failed: ${JSON.stringify(user.error)}`) + // throw user.error + //} else { + // this.logger.debug(`Authenticated user ${user.value.id}`) + // + // span.addEvent('authenticated') + // span.setAttribute('user_id', user.value.id) + // span.setAttribute('user.id', user.value.id) + // + // span.end() + // + // setUser({ + // id: user.value.id, + // username: user.value.username, + // email: user.value.email.address, + // }) + // + // return user.value + //} + } +} \ No newline at end of file diff --git a/apps/server/src/kernel/modules/identity/cbac.ts b/apps/server/src/kernel/modules/identity/cbac.ts index 1cdb95ee..675ca0bb 100644 --- a/apps/server/src/kernel/modules/identity/cbac.ts +++ b/apps/server/src/kernel/modules/identity/cbac.ts @@ -1,54 +1,31 @@ -import { - BadRequestException, - Body, - Controller, - NotFoundException, - Post, -} from '@nestjs/common'; -import { - ApiBody, - ApiOperation, -} from '@nestjs/swagger'; -import ms from 'ms'; -import { PrismaService } from '../../../common/modules/resources/prisma/services/prisma-service.js'; -import { CacheManager } from '../../../common/modules/storage/cache-manager/contract/cache-manager.js'; -import {randomBytes} from 'node:crypto'; -import { - ApiPropertyAccountUsername, - createUsername, -} from '../../../modules/account/value-objects/username.js'; - - - -export class InitiateCertificateBasedAuthentication -{ - @ApiPropertyAccountUsername - readonly username: string +import {Controller} from '@nestjs/common'; +import {ApiPropertyAccountUsername} from "../../../routes/v1/account.js" + + + +export class InitiateCertificateBasedAuthentication { + @ApiPropertyAccountUsername readonly username: string } -export interface InitiatedCertificateChallenge -{ +export interface InitiatedCertificateChallenge { readonly ourPublicKey: string readonly yourMessage: string } -export interface GetCertificateBasedAuthenticationChallenge -{ +export interface GetCertificateBasedAuthenticationChallenge { readonly challenge: string } -export interface SolveCertificateAuthenticatonChallenge -{ +export interface SolveCertificateAuthenticatonChallenge { readonly challengeId: string readonly puzzle: string } -export interface CertificateBasedAuthenticationService -{ +export interface CertificateBasedAuthenticationService { // 1. User requests a proof of possession of a private key by providing his username to a server requestProofOfPossessionOfPrivateKey(request: InitiateCertificateBasedAuthentication): Promise @@ -62,79 +39,76 @@ export interface CertificateBasedAuthenticationService @Controller('/cbac') -export class CertificateBasedAuthenticationController -{ - private readonly prisma: PrismaService - private readonly cacheManager: CacheManager - - constructor( - prisma: PrismaService, - cacheManager: CacheManager, - ) - { - this.prisma = prisma - this.cacheManager = cacheManager - } - - @ApiOperation({summary: 'Initiate certificate based authentication', operationId: "request-proof-of-possession-of-private-key"}) - @ApiBody({type: InitiateCertificateBasedAuthentication}) - @Post('/initiate') - async initiateCertificateBasedAuthentication(@Body() initiateCertificateBasedAuthenticationRequest: InitiateCertificateBasedAuthentication): Promise - { - let account: any - let secureChallengeKey: string | null = null - - const username = createUsername(initiateCertificateBasedAuthenticationRequest?.username).mapErr((err) => { - throw err - })._unsafeUnwrap() - - // Find an account requested in body - account = await this.prisma.account.findUnique({ - where: { - username: username - }, include:{ - PGPKey: true - } - }) - - // Generate a secure challenge key - randomBytes(32, (err, buffer) => - { - if (err) - { - throw new Error('Could not generate secure challenge key') - } - secureChallengeKey = buffer.toString('hex') - }) - - // Check if an account exists - - if (!account) - { - throw new NotFoundException('Account not found') - } - - if (!account?.PGPKey) { - throw new BadRequestException('Account do not support certificate-based authentication') - } - - // Store the secure challenge key in cache - await this.cacheManager.set(`cbac:${account.id}`, secureChallengeKey, ms("1h")) - - // Encrypt the key with User's Public Key +export class CertificateBasedAuthenticationController { +// private readonly prisma: PrismaService +// private readonly cacheManager: CacheManager +// +// +// constructor(prisma: PrismaService, cacheManager: CacheManager) { +// this.prisma = prisma +// this.cacheManager = cacheManager +// } +// +// +// @ApiOperation({ +// summary: 'Initiate certificate based authentication', +// operationId: "request-proof-of-possession-of-private-key", +// }) @ApiBody({type: InitiateCertificateBasedAuthentication}) @Post('/initiate') +// async initiateCertificateBasedAuthentication(@Body() initiateCertificateBasedAuthenticationRequest: InitiateCertificateBasedAuthentication): Promise { +// let account: any +// let secureChallengeKey: string | null = null +// +// const username = createUsername(initiateCertificateBasedAuthenticationRequest?.username).mapErr((err) => { +// throw err +// })._unsafeUnwrap() +// +// // Find an account requested in body +// account = await this.prisma.account.findUnique({ +// where: { +// username: username, +// }, +// include: { +// PGPKey: true, +// }, +// }) +// +// // Generate a secure challenge key +// randomBytes(32, (err, buffer) => { +// if (err) { +// throw new Error('Could not generate secure challenge key') +// } +// secureChallengeKey = buffer.toString('hex') +// }) +// +// // Check if an account exists +// +// if (!account) { +// throw new NotFoundException('Account not found') +// } +// +// if (!account?.PGPKey) { +// throw new BadRequestException('Account do not support certificate-based authentication') +// } +// +// // Store the secure challenge key in cache +// await this.cacheManager.set(`cbac:${account.id}`, secureChallengeKey, ms("1h")) +// +// // Encrypt the key with User's Public Key +//// throw new Error('Not implemented') +// +// // Return the encrypted key to the user +// return { +// ourPublicKey: '', +// yourMessage: '', +// } +// } +// +// +// @ApiOperation({ +// summary: 'Verify proof of possession of Private Key', +// operationId: "verify-possession-of-private-key", +// }) @Post('/verify') +// async verifyProofOfPossessionOfPrivateKey(request: InitiateCertificateBasedAuthentication): Promise { // throw new Error('Not implemented') - - // Return the encrypted key to the user - return { - ourPublicKey: '', - yourMessage: '', - } - } - - @ApiOperation({summary: 'Verify proof of possession of Private Key', operationId: "verify-possession-of-private-key"}) - @Post('/verify') - async verifyProofOfPossessionOfPrivateKey(request: InitiateCertificateBasedAuthentication): Promise - { - throw new Error('Not implemented') - } +// } } diff --git a/apps/server/src/kernel/modules/identity/pgp/pgp.ts b/apps/server/src/kernel/modules/identity/pgp/pgp.ts index 9ff99994..8f35a5dd 100644 --- a/apps/server/src/kernel/modules/identity/pgp/pgp.ts +++ b/apps/server/src/kernel/modules/identity/pgp/pgp.ts @@ -1,45 +1,34 @@ -import { - Controller, - Get, - Post, - UseGuards, -} from '@nestjs/common'; -import { ApiProperty } from '@nestjs/swagger'; -import { JwtAuthorizationGuard } from '../../../../modules/authentication/guards/jwt-authorization-guard.js'; +import {Controller, Get, Post, UseGuards} from '@nestjs/common'; +import {ApiProperty} from '@nestjs/swagger'; +import {JwtAuthorizationGuard} from '../../../../core/identity/authn/components/guards/jwt-authorization-guard.js'; + + class CreatePgpKey { - @ApiProperty({ - description: "Public key", - } - ) - readonly publicKey: string + @ApiProperty({ + description: "Public key", + }) readonly publicKey: string } + @Controller("/pgp") export class PgpController { + @Post() @UseGuards(JwtAuthorizationGuard) addKey() { + // Check if the key already exists (User can only have one key) - @Post() - @UseGuards(JwtAuthorizationGuard) - addKey() { - // Check if the key already exists (User can only have one key) - - // Check if the key is valid + // Check if the key is valid - // Add the key to the database + // Add the key to the database - // Return the key + // Return the key + return "Add key"; + } - return "Add key"; - } - @Get() - @UseGuards(JwtAuthorizationGuard) - getKeys( - ) - { - return "Get key"; - } + @Get() @UseGuards(JwtAuthorizationGuard) getKeys() { + return "Get key"; + } } diff --git a/apps/server/src/kernel/platform/gql/graphql-module.ts b/apps/server/src/kernel/platform/gql/graphql-module.ts index 4a8806c5..2e8d5e14 100644 --- a/apps/server/src/kernel/platform/gql/graphql-module.ts +++ b/apps/server/src/kernel/platform/gql/graphql-module.ts @@ -23,71 +23,65 @@ * */ - -import {ApolloServerPluginInlineTrace} from "@apollo/server/plugin/inlineTrace" -import {ApolloServerPluginSubscriptionCallback} from "@apollo/server/plugin/subscriptionCallback" import {ApolloServerPluginCacheControl} from "@apollo/server/plugin/cacheControl" -import {ApolloServerPluginUsageReporting} from "@apollo/server/plugin/usageReporting" +import {ApolloServerPluginInlineTrace} from "@apollo/server/plugin/inlineTrace" import { ApolloServerPluginLandingPageLocalDefault, ApolloServerPluginLandingPageProductionDefault, } from '@apollo/server/plugin/landingPage/default' +import {ApolloServerPluginSubscriptionCallback} from "@apollo/server/plugin/subscriptionCallback" import { - ApolloDriver, - type ApolloDriverConfig, + ApolloDriver, type ApolloDriverConfig, } from '@nestjs/apollo' import { - Module, - OnModuleDestroy, - OnModuleInit, + Module, OnModuleDestroy, OnModuleInit, } from '@nestjs/common' -import {GraphQLModule} from '@nestjs/graphql' -import {join} from 'node:path' -import process from 'node:process' -import {isDevelopment} from '../../../configs/helper/is-development.js' -import {StaticFeatureFlags} from '../../../configs/static-feature-flags.js' -import {FooResolver} from '../../../http/graphql/hello-world-resolver.js' +import {GraphQLModule} from '@nestjs/graphql' +import {join} from 'node:path' +import process from 'node:process' +import {isDevelopment} from '../../../configs/helper/is-development.js' +import {StaticFeatureFlags} from '../../../configs/static-feature-flags.js' +import {FooResolver} from '../../../routes/graphql/hello-world-resolver.js' + + + +const landingPageGraphQLPlayground = isDevelopment() ? + ApolloServerPluginLandingPageLocalDefault() : + ApolloServerPluginLandingPageProductionDefault({}) -const landingPageGraphQLPlayground = isDevelopment() ? ApolloServerPluginLandingPageLocalDefault() : ApolloServerPluginLandingPageProductionDefault({}) @Module({ - imports : [ - GraphQLModule.forRoot({ - driver : ApolloDriver, - autoSchemaFile : join(process.cwd(), 'schema.gql'), - playground : false, - status400ForVariableCoercionErrors: true, - inheritResolversFromInterfaces : true, - plugins : [ - landingPageGraphQLPlayground as any, - ApolloServerPluginCacheControl(), - //ApolloServerPluginUsageReporting({}), - ApolloServerPluginInlineTrace(), - ApolloServerPluginSubscriptionCallback(), - ], - allowBatchedHttpRequests : true, - introspection : true, - transformAutoSchemaFile : true, - autoTransformHttpErrors : true, - sortSchema : true, - installSubscriptionHandlers : true, - cache : 'bounded', - persistedQueries : {}, - stopOnTerminationSignals : true, - includeStacktraceInErrorResponses: isDevelopment(), - }), - ], - providers: [FooResolver], - }) -export class GraphqlModule - implements OnModuleInit, - OnModuleDestroy -{ - public onModuleDestroy(): any - { + imports: [ + GraphQLModule.forRoot({ + driver: ApolloDriver, + autoSchemaFile: join(process.cwd(), 'schema.gql'), + playground: false, + status400ForVariableCoercionErrors: true, + inheritResolversFromInterfaces: true, + plugins: [ + landingPageGraphQLPlayground as any, ApolloServerPluginCacheControl(), + //ApolloServerPluginUsageReporting({}), + ApolloServerPluginInlineTrace(), ApolloServerPluginSubscriptionCallback(), + ], + allowBatchedHttpRequests: true, + introspection: true, + transformAutoSchemaFile: true, + autoTransformHttpErrors: true, + sortSchema: true, + installSubscriptionHandlers: true, + cache: 'bounded', + persistedQueries: {}, + stopOnTerminationSignals: true, + includeStacktraceInErrorResponses: isDevelopment(), + }), + ], + providers: [FooResolver], +}) +export class GraphqlModule implements OnModuleInit, OnModuleDestroy { + public onModuleDestroy(): any { } - public onModuleInit(): any - { + + public onModuleInit(): any { StaticFeatureFlags.isGraphQLRunning = true } } diff --git a/apps/server/src/kernel/platform/http/middleware/fingerprint.ts b/apps/server/src/kernel/platform/http/middleware/fingerprint.ts index 6a146d55..34701648 100644 --- a/apps/server/src/kernel/platform/http/middleware/fingerprint.ts +++ b/apps/server/src/kernel/platform/http/middleware/fingerprint.ts @@ -1,34 +1,24 @@ -import { - createParamDecorator, - ExecutionContext, - Injectable, - NestMiddleware, -} from '@nestjs/common' -import { - NextFunction, - Request, - Response, -} from 'express' -import murmurhash from 'murmurhash3js' -import ua from 'useragent' -import {ExpressRequest} from '../../../../types/express-response.js' -import {__logger} from '../../../modules/logger/logger.js' - - - -function getUserAgentFromRequest(req: Request): ua.Agent -{ +import {createParamDecorator, ExecutionContext, Injectable, NestMiddleware} from '@nestjs/common' +import {NextFunction, Request, Response} from 'express' +import murmurhash from 'murmurhash3js' +import ua from 'useragent' +import {__logger} from "../../../../core/modules/logger/logger.js" +import {ExpressRequest} from '../../../../types/express-response.js' + + + +function getUserAgentFromRequest(req: Request): ua.Agent { return ua.parse(req.headers['user-agent']) } + /** * Generate a fingerprint for the given request and parameters. * * @param {Request} req - The request object. * @return {object} - An object containing the generated fingerprint. */ -export function generateFingerprint(req: Request): string -{ +export function generateFingerprint(req: Request): string { const logger = __logger('fingerprint') logger.debug('Called generateFingerprint') @@ -38,24 +28,24 @@ export function generateFingerprint(req: Request): string // Create a new fingerprint object const fingerprint: any = { - headers : { - accept : req.headers['accept'], + headers: { + accept: req.headers['accept'], language: req.headers['accept-language'], encoding: req.headers['accept-encoding'], }, userAgent: { browser: { - family : ua.family, + family: ua.family, version: ua.major, }, - device : { - family : ua.device.family, + device: { + family: ua.device.family, version: ua.device.major, }, - os : { + os: { family: ua.os.family, - major : ua.os.major, - minor : ua.os.minor, + major: ua.os.major, + minor: ua.os.minor, }, }, ipAddress: req.ip, @@ -67,7 +57,6 @@ export function generateFingerprint(req: Request): string const hash = murmurhash.x86.hash128(JSON.stringify(fingerprint)) logger.debug(`Generated hash for fingerprint: ${hash}`) - logger.debug('Adding fingerprint to request headers...') req.headers['x-fingerprint'] = hash req.fingerprint = hash @@ -82,11 +71,8 @@ export function generateFingerprint(req: Request): string * A middleware to generate a fingerprint for each request. */ @Injectable() -export class FingerprintMiddleware - implements NestMiddleware -{ - use(req: ExpressRequest, _res: Response, next: NextFunction): void - { +export class FingerprintMiddleware implements NestMiddleware { + use(req: ExpressRequest, _res: Response, next: NextFunction): void { // Generate a fingerprint for the request const fingerprint = generateFingerprint(req) @@ -106,10 +92,9 @@ export class FingerprintMiddleware /** * Get fingerprint by request */ -export const Fingerprint = createParamDecorator((_, ctx: ExecutionContext): string | undefined => - { - const request: Request = ctx - .switchToHttp() - .getRequest() - return request.fingerprint - }) +export const Fingerprint = createParamDecorator((_, ctx: ExecutionContext): string | undefined => { + const request: Request = ctx + .switchToHttp() + .getRequest() + return request.fingerprint +}) diff --git a/apps/server/src/kernel/runtime/boostrap.ts b/apps/server/src/kernel/runtime/boostrap.ts index 6b4be644..67e9dd10 100644 --- a/apps/server/src/kernel/runtime/boostrap.ts +++ b/apps/server/src/kernel/runtime/boostrap.ts @@ -1,35 +1,30 @@ -import {NestFactory} from '@nestjs/core' -import { - ExpressAdapter, - NestExpressApplication, -} from '@nestjs/platform-express' -import express, {Express} from 'express' -import helmet from 'helmet' -import {isDevelopment} from '../../configs/helper/is-development.js' -import {Container} from '../../container.js' -import {LoggerNestjsProxy} from '../modules/logger/nestjs-logger-proxy.js' +import {NestFactory} from '@nestjs/core' +import {ExpressAdapter, NestExpressApplication} from '@nestjs/platform-express' +import express, {Express} from 'express' +import helmet from 'helmet' +import {isDevelopment} from '../../configs/helper/is-development.js' +import {Container} from '../../container.js' +import {LoggerNestjsProxy} from "../../core/modules/logger/nestjs-logger-proxy.js" -export interface IBootstrap -{ +export interface IBootstrap { (): Promise } -export async function createExpressApplication(): Promise -{ +export async function createExpressApplication(): Promise { const expressHttpServer: Express = express() const app: NestExpressApplication = await NestFactory.create(Container, new ExpressAdapter(expressHttpServer), { autoFlushLogs: true, //cors: true, //bodyParser: true, //rawBody: true, - preview : false, - bufferLogs : true, + preview: false, + bufferLogs: true, abortOnError: isDevelopment(), - snapshot : isDevelopment(), - logger : new LoggerNestjsProxy(), + snapshot: isDevelopment(), + logger: new LoggerNestjsProxy(), }) app.useBodyParser('json') diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 6cc57a64..617c3a4e 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -1,16 +1,15 @@ -import { bootstrap } from './bootstrap.js'; -import { isProduction } from './configs/helper/is-production.js'; -import { acquireProcessLock } from './hooks/pre-start/acquire-process-lock.js'; -import { initializeSentry } from './hooks/pre-start/initialize-sentry.js'; -import { - prettyPrintServiceInformation, - printSystemInfo, -} from './utilities/console-utils/index.js'; +import {bootstrap} from './bootstrap.js'; +import {isProduction} from './configs/helper/is-production.js'; +import {acquireProcessLock} from './core/hooks/pre-start/acquire-process-lock.js'; +import {initializeSentry} from './core/hooks/pre-start/initialize-sentry.js'; +import {prettyPrintServiceInformation, printSystemInfo} from './utilities/console-utils/index.js'; + + await acquireProcessLock(); if (isProduction()) { - printSystemInfo(); + printSystemInfo(); } prettyPrintServiceInformation(); diff --git a/apps/server/src/modules/account/README.md b/apps/server/src/modules/account/README.md deleted file mode 100644 index a55942ad..00000000 --- a/apps/server/src/modules/account/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Module - diff --git a/apps/server/src/modules/account/account-already-exists.ts b/apps/server/src/modules/account/account-already-exists.ts deleted file mode 100644 index 1395efe3..00000000 --- a/apps/server/src/modules/account/account-already-exists.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { HttpStatus } from '../../common/http-status.js'; -import { HttpProblem } from '../../common/error/problem-details/http-problem.js'; - -export class AccountAlreadyExists extends HttpProblem { - constructor() { - super({ - type: 'com.methylphenidate.account.already-exists', - title: 'Account Already Exists', - status: HttpStatus.CONFLICT, - message: 'The account already exists.', - instance: 'com.methylphenidate.account.already-exists', - }); - } -} diff --git a/apps/server/src/modules/account/account.module.ts b/apps/server/src/modules/account/account.module.ts deleted file mode 100644 index 7745da71..00000000 --- a/apps/server/src/modules/account/account.module.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {Module} from '@nestjs/common' -import {PwnprocModule} from '../../common/libraries/pwnproc/pwnproc-module.js' -import {UnihashModule} from '../../common/libraries/unihash/index.js' -import {DatabaseModule} from '../../common/modules/database/database.module.js' -import {EventBusModule} from '../../common/modules/messaging/event-bus-module.js' -import {OpentelemetryTracer} from '../../common/modules/observability/tracing/opentelemetry/provider/tracer/opentelemetry-tracer.js' -import {CacheManagerModule} from '../../common/modules/storage/cache-manager/cache-manager-module.js' -import {NotificationModule} from '../../common/notification/notification-module.js' -import {MailerModule} from "../../kernel/modules/mailer/mailer-module.js" -import {RegisterAccountUseCase} from './commands/register-account/register-account-usecase.js' -import {AccountRecoveryController} from './controllers/account-recovery.controller.js' -import {AccountVerificationController} from './controllers/account-verification.controller.js' -import {AccountController} from './controllers/account.controller.js' -import {AccountPolicy} from './policies/account-policy.js' -import {AccountRepository} from './repositories/account-repository.js' -import {PrismaAccountRepository} from './repositories/prisma-account-repository.service.js' -import {AccountRecovery} from './services/account-recovery.js' -import {AccountService} from './services/account-service.js' -import {AccountVerification} from './services/account-verification.js' - - - -@Module({ - imports : [ - DatabaseModule, - PwnprocModule, - UnihashModule, - EventBusModule, - CacheManagerModule, - MailerModule, - NotificationModule, - ], - controllers: [ - AccountController, - AccountRecoveryController, - AccountVerificationController, - ], - providers : [ - AccountService, - RegisterAccountUseCase, - AccountPolicy, - { - provide : AccountRepository, - useClass: PrismaAccountRepository, - }, - AccountVerification, - AccountRecovery, - OpentelemetryTracer, - ], - exports : [ - AccountService, - AccountRepository, - ], - }) -export class AccountModule -{ -} diff --git a/apps/server/src/modules/account/commands/register-account/api-register-account-bad-response.ts b/apps/server/src/modules/account/commands/register-account/api-register-account-bad-response.ts deleted file mode 100644 index 86a42ad0..00000000 --- a/apps/server/src/modules/account/commands/register-account/api-register-account-bad-response.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ApiResponse, getSchemaPath } from '@nestjs/swagger'; -import { HttpProblem } from '../../../../common/error/problem-details/http-problem.js'; - -export const ApiRegisterAccountBadResponse = ApiResponse({ - status: 400, - description: 'User could not be registered due to invalid data.', - content: { - 'application/json': { - schema: { - $ref: getSchemaPath(HttpProblem), - type: 'object', - example: { - type: 'com.methylphenidate.account.invalid-username', - title: 'Invalid Username', - status: 400, - }, - examples: [ - { - type: 'com.neuronek.account.invalid-email', - title: 'Invalid Email', - status: 400, - }, - ], - }, - }, - }, -}); diff --git a/apps/server/src/modules/account/commands/register-account/register-account-command.ts b/apps/server/src/modules/account/commands/register-account/register-account-command.ts deleted file mode 100644 index 58dfa36e..00000000 --- a/apps/server/src/modules/account/commands/register-account/register-account-command.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {ApiPropertyAccountEmail} from '../../value-objects/account-email.js' -import {ApiPropertyAccountPassword} from '../../value-objects/password.js' -import {ApiPropertyAccountUsername} from '../../value-objects/username.js' - - - -export class RegisterAccountCommand - { - /** - * Represents an email address. - * @typedef {string} email - */ - @ApiPropertyAccountEmail email: string - - /** - * The password variable is a string that represents a user's password. - * - * @type {string} - */ - @ApiPropertyAccountPassword password: string - - /** - * Represents a username. - * @typedef {string} username - */ - @ApiPropertyAccountUsername username: string - } - diff --git a/apps/server/src/modules/account/commands/register-account/register-account-usecase.ts b/apps/server/src/modules/account/commands/register-account/register-account-usecase.ts deleted file mode 100644 index c4196117..00000000 --- a/apps/server/src/modules/account/commands/register-account/register-account-usecase.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { - Injectable, - Logger, -} from '@nestjs/common' -import {SpanKind} from '@opentelemetry/api' -import {setUser} from '@sentry/node' -import { - ok, - Result, -} from 'neverthrow' -import {PasswordHashing} from '../../../../common/libraries/unihash/index.js' -import {KdfAlgorithm} from '../../../../common/libraries/unihash/key-derivation-functions/key-derivation-function.js' -import {EventBus} from '../../../../common/modules/messaging/event-bus.js' -import {OpentelemetryTracer} from '../../../../common/modules/observability/tracing/opentelemetry/provider/tracer/opentelemetry-tracer.js' -import {createEmailAddress} from "../../../../kernel/modules/mailer/value-object/email-address.js" -import {UseCase} from '../../../../kernel/standard/use-case.js' -import {Account} from '../../entities/account.js' -import {AccountPolicy} from '../../policies/account-policy.js' -import {AccountRepository} from '../../repositories/account-repository.js' -import {AccountEmail} from '../../value-objects/account-email.js' -import {Password} from '../../value-objects/password.js' -import {createUsername} from '../../value-objects/username.js' -import {RegisterAccountCommand} from './register-account-command.js' - - - -@Injectable() -export class RegisterAccountUseCase - extends UseCase -{ - private logger: Logger = new Logger('account::usecase::register-account') - private repository: AccountRepository - private policy: AccountPolicy - private hashing: PasswordHashing - private tracer: OpentelemetryTracer = new OpentelemetryTracer() - private eventbus: EventBus - - - constructor(repository: AccountRepository, policy: AccountPolicy, hashing: PasswordHashing, eventbus: EventBus) - { - super() - this.repository = repository - this.policy = policy - this.hashing = hashing - this.eventbus = eventbus - } - - - public async execute(input: RegisterAccountCommand): Promise> - { - const span = this.tracer.startSpan('com.methylphenidate.account.service.register', { - kind : SpanKind.INTERNAL, - attributes: { - 'op' : 'function', - request: JSON.stringify(RegisterAccountCommand), - }, - }) - - this.logger.debug('Validating inputs...', {command: input}) - - const emailResult = createEmailAddress(input.email) - const usernameResult = createUsername(input.username) - - this.logger.debug('Checking if email is valid...', {email: input.email}) - - if (emailResult.isErr()) - { - span.end() - span.recordException(emailResult.error) - throw emailResult.error - } - - this.logger.debug('Checking if username is valid...', {username: input.username}) - - if (usernameResult.isErr()) - { - span.end() - span.recordException(usernameResult.error) - throw usernameResult.error - } - - const username = usernameResult.value - const email = emailResult.value - - const accountEmail = AccountEmail.create({ - isVerified: false, - address : email, - }) - - const password = await Password.fromPlain(input.password, this.hashing.use(KdfAlgorithm.Argon2id)) - - span.setAttribute('user.password', password.toString()) - - await this.policy.canRegisterAccount({ - email : accountEmail.address, - password: input.password, - username: username, - }) - - this.logger.debug('Creating aggregate...') - - let identity = Account.RegisterAccount({ - username: username, - email : accountEmail, - password: password, - groups : [], - }) - - const events = identity.getUncommittedEvents() - - this.logger.debug('Saving aggregate...') - - identity = await this.repository.save(identity) - - this.logger.debug('Publishing events...') - - await this.eventbus.publishAll(events) - - this.logger.log(`Account ${identity.id} was successfully registered.`) - - setUser({ - email : identity.email.address, - username: identity.username, - id : identity.id, - }) - - span.end() - - return ok(identity) - } - -} diff --git a/apps/server/src/modules/account/commands/register-account/register-account.http b/apps/server/src/modules/account/commands/register-account/register-account.http deleted file mode 100644 index fa2d2a7c..00000000 --- a/apps/server/src/modules/account/commands/register-account/register-account.http +++ /dev/null @@ -1,29 +0,0 @@ -### Register Account - -POST http://{{HOST}}:{{PORT}}/account -Content-Type: application/json - -{ -"email": "{{USER_EMAIL}}", -"username": "{{USER_NICKNAME}}", -"password": "{{USER_PASSWORD}}" -} - -> {% - -client.test("Response content-type is json", function () { -const type = response.contentType.mimeType; -client.assert(type === "application/json", `Expected 'application/json' but received '${type}'`); -}); - -%} - -### Authenticate - -POST http://{{HOST}}:{{PORT}}/authenticate -Content-Type: application/json - -{ -"username": "{{USER_EMAIL}}", -"password": "{{USER_PASSWORD}}" -} diff --git a/apps/server/src/modules/account/commands/register-account/register.md b/apps/server/src/modules/account/commands/register-account/register.md deleted file mode 100644 index 72dc9b6c..00000000 --- a/apps/server/src/modules/account/commands/register-account/register.md +++ /dev/null @@ -1,25 +0,0 @@ -**Register Endpoint** - -This endpoint is responsible for registering a new account in our system. - -Following actions are performed when this path is hit: - -1. A new user account is created in the database with the provided details. -2. A confirmation email is sent to the provided email address containing a token. - -The user must confirm the email address by using the provided token. After that, they can visit /account/email/confirm -with the token as a query parameter to confirm their email. - -### Effects after the operation: - -1. Check your email for a confirmation message from our system which contains the unique token. -2. Our system stores your account information and awaits for your email confirmation. - -### Possible Outcomes: - -- An account gets successfully registered and awaits email confirmation. -- If the provided email already exists in our system, an error will be returned. -- The server will respond with errors in case of bad request data. - -Please note that the registration process is incomplete until the email is confirmed by visiting the -/account/email/confirm an endpoint with the required token. \ No newline at end of file diff --git a/apps/server/src/modules/account/contract/account-recovery.ts b/apps/server/src/modules/account/contract/account-recovery.ts deleted file mode 100644 index a55c8eee..00000000 --- a/apps/server/src/modules/account/contract/account-recovery.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -export interface AccountRecovery { - /** - * # `request-password-reset` - * - * Request password reset is an operation dedicated to requesting password reset. - * - * @param {string} accountId - * @returns {Promise} - */ - requestPasswordReset(accountId : string) : Promise; - - /** - * # `reset-password` - * - * Reset password is an operation dedicated to resetting password. - * - * @param {string} accountId - * @param {string} newPassword - * @returns {Promise} - */ - resetPassword(accountId : string, newPassword : string) : Promise; -} \ No newline at end of file diff --git a/apps/server/src/modules/account/contract/account-self-service.ts b/apps/server/src/modules/account/contract/account-self-service.ts deleted file mode 100644 index bd9df7d2..00000000 --- a/apps/server/src/modules/account/contract/account-self-service.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import type {RegisterAccountCommand} from '../commands/register-account/register-account-command.js' -import {Account} from '../entities/account.js' - - - -export interface AccountSelfService - { - /** # register-account - * - * Register account is an operation dedicated to creating new accounts - * in codebase. - * - * - Will end up in an unverified account (account that his not recoverable as until account provided in - * registration will be confirmed), but an overall account can be used as well as verified account. - * - * @param {RegisterAccountCommand} registerAccount - * @returns {Promise} - */ - register(registerAccount: RegisterAccountCommand): Promise; - - closeAccount(accountId: string): Promise; - - updateAccount( - accountId: string, - updateAccount: RegisterAccountCommand, - ): Promise; - } diff --git a/apps/server/src/modules/account/contract/account-verification.ts b/apps/server/src/modules/account/contract/account-verification.ts deleted file mode 100644 index 05f13a18..00000000 --- a/apps/server/src/modules/account/contract/account-verification.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -export interface AccountVerification - { - /** - * # `verify-email` - * - * Verify email is an operation dedicated to verifying the email address of an account. - * - * @param {string} accountId - * @returns {Promise} - */ - verifyEmail(accountId : string) : Promise; - - /** - * # `request-email-verification` - * - * Request email verification is an operation dedicated to requesting email verification. - * - * @param {string} accountId - * @returns {Promise} - */ - requestEmailVerification(accountId : string) : Promise; - } \ No newline at end of file diff --git a/apps/server/src/modules/account/controllers/account-management.controller.ts b/apps/server/src/modules/account/controllers/account-management.controller.ts deleted file mode 100644 index 7ede6da2..00000000 --- a/apps/server/src/modules/account/controllers/account-management.controller.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - Controller, - Get, -} from '@nestjs/common' -import { - ApiOperation, - ApiQuery, -} from '@nestjs/swagger' -import { OpenapiTags } from '../../../common/modules/documentation/swagger/openapi-tags.js' -import { AccountStatus } from '../value-objects/account-status.js' - - - -@Controller( 'accounts' ) -export class AccountManagementController - { - - // TODO: List all accounts of system - @Get() @ApiOperation( { - operationId : 'list-accounts', - tags : [ OpenapiTags.ACCOUNT_MANAGEMENT ], - } ) @ApiQuery( { - type : String, - enum : AccountStatus, - name : 'status', - allowEmptyValue : false, - required : false, - description : 'Filter accounts by status', - } ) - public async getAccounts() - { - } - - - // TODO: Get single account by ID - - // TODO: Update single account - - // TODO: Bulk update multiple accounts - - // TODO: Force password reset - } \ No newline at end of file diff --git a/apps/server/src/modules/account/controllers/account-recovery.controller.ts b/apps/server/src/modules/account/controllers/account-recovery.controller.ts deleted file mode 100644 index 5760b775..00000000 --- a/apps/server/src/modules/account/controllers/account-recovery.controller.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {Body, Controller, Post, Query, UseGuards} from "@nestjs/common" -import {ApiOperation} from "@nestjs/swagger" -import {OpenapiTags} from "../../../common/modules/documentation/swagger/openapi-tags.js" -import {JwtAuthorizationGuard} from "../../authentication/guards/jwt-authorization-guard.js" -import {AccountRecovery} from "../services/account-recovery.js"; -import {RecoverAccount} from "../dtos/recover-account.js"; -import {AccountService} from "../services/account-service.js"; - - - -@Controller("/account/recovery") -export class AccountRecoveryController { - - constructor( - private accountService : AccountService, - private accountRecovery : AccountRecovery, - ) - {} - - @UseGuards(JwtAuthorizationGuard) @ApiOperation({ - operationId: "forgot-password", - summary : "Request password reset", - description: "Sends a password reset email", - tags : [OpenapiTags.ACCOUNT_RECOVERY], - }) @Post('forgot-password') - async recoverAccount(@Body() body : RecoverAccount) : Promise { - - const account = await this.accountService.getById(body.username) - - await this.accountRecovery.startPasswordRecovery(account.id) - - return "ok" - } - - - @UseGuards(JwtAuthorizationGuard) @ApiOperation({ - operationId: "reset-password", - summary : "Reset password", - description: "Resets the user's password", - tags : [OpenapiTags.ACCOUNT_RECOVERY], - }) @Post('reset-password') - async resetPassword(@Query("code") code : string) : Promise { - // TODO: Check if given parameter is correct password request in cache - // TODO: Find a related account to secret - // TODO: Change password of account - // TODO: Publish event of password changed - // TODO: Return - return "reset-password" - } -} \ No newline at end of file diff --git a/apps/server/src/modules/account/controllers/account-verification.controller.ts b/apps/server/src/modules/account/controllers/account-verification.controller.ts deleted file mode 100644 index 8e875af0..00000000 --- a/apps/server/src/modules/account/controllers/account-verification.controller.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - BadRequestException, - Controller, - Post, - Query, - UseGuards, -} from '@nestjs/common' -import { - ApiOperation, - ApiQuery, -} from '@nestjs/swagger' -import {ApiAccountMockup} from '../../../utilities/fixtures/api-account-mockup.js' -import {JwtAuthorizationGuard} from '../../authentication/guards/jwt-authorization-guard.js' -import {AccountVerification} from '../services/account-verification.js' - - - -@Controller('/account/verification') -export class AccountVerificationController { - - constructor(private accountVerification: AccountVerification) { - } - - - @ApiOperation({ - operationId: 'verify-email', - summary : 'Verify Email', - description: 'Verifies the user’s email', - tags : ['account'], - }) @Post('verify-email') @ApiQuery({ - name : 'verification_code', - example: 'verification_code', - type : String, - }) - async verifyEmail(@Query('verification_code') verificationCode: string): Promise { - const emailVerificationResult = await this.accountVerification.verifyEmail(verificationCode) - - if (emailVerificationResult.isErr()) - { - throw new BadRequestException('Something gone wrong') - } - else - { - return 'ok' - } - } - - - @UseGuards(JwtAuthorizationGuard) @ApiOperation({ - operationId: 'resend-verification-email', - summary : 'Re-send verification email', - description: 'Resends the verification email', - tags : ['account'], - }) @Post('resend-verification-email') @ApiQuery({ - name : 'email', - description: 'Email of created account to which verification email should be sent', - example : ApiAccountMockup.email, - }) - async resendVerificationEmail(@Query('email') email: string): Promise { - email = email.toLowerCase() - await this.accountVerification.resendVerificationEmail(email) - return 'ok' - } - -} diff --git a/apps/server/src/modules/account/controllers/account.controller.ts b/apps/server/src/modules/account/controllers/account.controller.ts deleted file mode 100644 index 493f0e61..00000000 --- a/apps/server/src/modules/account/controllers/account.controller.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Body, Controller, Patch, Post, Req, UseGuards } from '@nestjs/common'; -import { - ApiBody, - ApiConflictResponse, - ApiOkResponse, - ApiOperation, - ApiResponse, - getSchemaPath, -} from '@nestjs/swagger'; -import { getCurrentScope } from '@sentry/node'; -import { Request } from 'express'; -import { readFileSync } from 'node:fs'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { HttpProblem } from '../../../common/error/problem-details/http-problem.js'; -import { JwtAuthorizationGuard } from '../../authentication/guards/jwt-authorization-guard.js'; -import { RegisterAccountCommand } from '../commands/register-account/register-account-command.js'; -import { AccountService } from '../services/account-service.js'; -import { AccountViewModel } from '../view-model/account-view-model.js'; - -function getOperationDocumentation(operation: string): string { - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - const docsDirectory = `${__dirname}/../10-application/docs`; - const operationDirectory = `${docsDirectory}/operations`; - - try { - return readFileSync(`${operationDirectory}/${operation}.md`, 'utf8'); - //return import(`${operationDirectory}/${operation}.md`) as - // string - } catch (e) { - return ''; - } -} - -@Controller('account') -export class AccountController { - constructor(private service: AccountService) {} - - @UseGuards(JwtAuthorizationGuard) - @ApiOperation({ - operationId: 'update-account', - description: 'Update details of account.', - tags: ['account'], - }) - @Patch() - async updateAccount(): Promise { - // Find a account that needs to be updated - return 'change-password'; - } - - @UseGuards(JwtAuthorizationGuard) - @ApiOperation({ - operationId: 'delete-account', - description: "Deletes the user's account.", - tags: ['account'], - }) - @Post('delete-account') - async deleteAccount(): Promise { - return 'delete-domain'; - } -} diff --git a/apps/server/src/modules/account/dtos/create-account-dto.ts b/apps/server/src/modules/account/dtos/create-account-dto.ts deleted file mode 100644 index e915f24a..00000000 --- a/apps/server/src/modules/account/dtos/create-account-dto.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {faker} from '@faker-js/faker' -import {ApiProperty} from '@nestjs/swagger' -import {ApiAccountMockup} from '../../../utilities/fixtures/api-account-mockup.js' -import {ApiPropertyAccountUsername} from '../value-objects/username.js' - - - -export class CreateAccountDto { - /** - * Represents the unique identifier of an entity. - * - * @typedef {string} Id - */ - @ApiProperty({ - name : 'id', - description: 'The domain\'s unique identifier', - example : '', - required : false, - }) id?: string - - /** - * Represents an email address. - * @typedef {string} email - */ - @ApiProperty({ - name : 'email', - description: 'The domain\'s email address', - example : ApiAccountMockup.email, - examples : ApiAccountMockup._examples.emails, - }) email: string - - /** - * Indicates whether the email associated with a user domain has been verified. - * - * @type {boolean} - */ - @ApiProperty({ - name : 'emailVerified', - description: 'Indicates whether the email associated with a user domain has been verified', - example : faker.datatype.boolean(), - }) emailVerified: boolean - - /** - * The password variable is a string that represents a user's password. - * - * @type {string} - */ - @ApiProperty({ - name : 'password', - description: 'The domain\'s password', - example : ApiAccountMockup.password, - examples : ApiAccountMockup._examples.passwords, - }) password: string - - /** - * Represents a username. - * @typedef {string} username - */ - @ApiPropertyAccountUsername username: string -} diff --git a/apps/server/src/modules/account/dtos/recover-account.ts b/apps/server/src/modules/account/dtos/recover-account.ts deleted file mode 100644 index d1ba9b5c..00000000 --- a/apps/server/src/modules/account/dtos/recover-account.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { ApiProperty } from '@nestjs/swagger' -import { ApiPropertyAccountUsername } from '../value-objects/username.js' - - - -export class RecoverAccount - { - @ApiPropertyAccountUsername username : string - - @ApiProperty( { - name : 'method', - description : 'Recovery Method', - example : '', - required : false, - } ) method : 'sms' | 'pgp' | 'email' - } \ No newline at end of file diff --git a/apps/server/src/modules/account/entities/account.ts b/apps/server/src/modules/account/entities/account.ts deleted file mode 100644 index 3fa7e906..00000000 --- a/apps/server/src/modules/account/entities/account.ts +++ /dev/null @@ -1,156 +0,0 @@ -import {BadRequestException} from '@nestjs/common' -import { - AggregateRootProperties, - BaseAggregateRoot, -} from '../../../common/libraries/domain/aggregate.js' -import {AccountEmailConfirmed} from '../events/account-email-confirmed.js' -import {AccountEvent} from '../events/account-event.js' -import {AccountVerificationEmailRequested} from '../events/account-verification-email-requested.js' -import type {AccountId} from '../shared-kernel/account-id.js' -import {AccountEmail} from '../value-objects/account-email.js' -import {AccountGroup} from '../value-objects/account-group.js' -import {AccountStatus} from '../value-objects/account-status.js' -import {Password} from '../value-objects/password.js' -import {Username} from '../value-objects/username.js' - - - -export interface IdentityProperties - extends AggregateRootProperties - { - email: AccountEmail - password: Password - username: Username - status: AccountStatus - groups: AccountGroup[] - } - - -export class Account - extends BaseAggregateRoot - implements IdentityProperties - { - public email: AccountEmail - public groups: AccountGroup[] - public password: Password - public status: AccountStatus - public username: Username - public readonly usernameFields: any = [ - 'username', - 'email', - ] - - - private constructor(payload: IdentityProperties) - { - super({ - id : payload.id, - createdAt: new Date(), - updatedAt: new Date(), - }) - this.email = payload.email - this.username = payload.username - this.password = payload.password - } - - - static RegisterAccount(payload: Omit) - { - let account = new Account({ - ...payload, - status : AccountStatus.INACTIVE, - password: payload.password, - }) - - account = account.register() - - return account - } - - - static build(payload: IdentityProperties) - { - return new Account(payload) - } - - - public changeUsername(username: Username) - { - this.username = username - // TODO: Add UsernameChangedEvent - return this - } - - - public requestPasswordReset() - { - } - - - public requestVerificationEmail() - { - const event = new AccountVerificationEmailRequested(this) - this.appendEvent(event) - return this - } - - - public resetPassword() - { - } - - - public changeEmail(email: AccountEmail) - { - this.email = email - return this - } - - - public changePassword() - { - } - - - public deleteAccount() - { - } - - - public verifyEmail() - { - if (this.email.isVerified) - { - throw new BadRequestException('Email is already verified.') - } - - const verifiedEmail = AccountEmail.create({ - address : this.email.address, - isVerified: true, - }) - - this.email = verifiedEmail - - const event = new AccountEmailConfirmed(this) - - this.appendEvent(event) - - return this - } - - - public register(): this - { - this.logger.debug('Registering account...') - this.status = AccountStatus.ACTIVE - this.appendEvent(new AccountEvent.Registred(this)) - this.logger.log('Account Registered') - return this - } - - - public authenticate(): Account - { - return this - } - } \ No newline at end of file diff --git a/apps/server/src/modules/account/events/account-authenticated.ts b/apps/server/src/modules/account/events/account-authenticated.ts deleted file mode 100644 index 75188c65..00000000 --- a/apps/server/src/modules/account/events/account-authenticated.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {Event} from "../../../common/libraries/message/event.js" -import {Account} from "../entities/account.js" - - - -export class AccountAuthenticated extends Event { - constructor(account: Account) { - super({ - namespace: "domain.authenticated", - body: account, - }) - } -} \ No newline at end of file diff --git a/apps/server/src/modules/account/events/account-deleted.ts b/apps/server/src/modules/account/events/account-deleted.ts deleted file mode 100644 index fa4b1a10..00000000 --- a/apps/server/src/modules/account/events/account-deleted.ts +++ /dev/null @@ -1 +0,0 @@ -export class AccountDeleted {} \ No newline at end of file diff --git a/apps/server/src/modules/account/events/account-email-confirmed.ts b/apps/server/src/modules/account/events/account-email-confirmed.ts deleted file mode 100644 index 632a9ff3..00000000 --- a/apps/server/src/modules/account/events/account-email-confirmed.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DomainEvent } from '../../../common/libraries/domain/domain-event.js' -import { Account } from '../entities/account.js' - - - -export class AccountEmailConfirmed - extends DomainEvent - { - } \ No newline at end of file diff --git a/apps/server/src/modules/account/events/account-event.ts b/apps/server/src/modules/account/events/account-event.ts deleted file mode 100644 index 95799eac..00000000 --- a/apps/server/src/modules/account/events/account-event.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {AccountAuthenticated} from "./account-authenticated.js" -import {AccountRegistered} from "./account-registered.js" - - - -export const AccountEvent = { - Registred: AccountRegistered, - Authenticated: AccountAuthenticated, -} \ No newline at end of file diff --git a/apps/server/src/modules/account/events/account-registered.ts b/apps/server/src/modules/account/events/account-registered.ts deleted file mode 100644 index 29e0a276..00000000 --- a/apps/server/src/modules/account/events/account-registered.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DomainEvent } from '../../../common/libraries/domain/domain-event.js' -import type { Account } from '../entities/account.js' - - - -export class AccountRegistered - extends DomainEvent - { - } \ No newline at end of file diff --git a/apps/server/src/modules/account/events/account-verification-email-requested.ts b/apps/server/src/modules/account/events/account-verification-email-requested.ts deleted file mode 100644 index b00be0cc..00000000 --- a/apps/server/src/modules/account/events/account-verification-email-requested.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DomainEvent } from '../../../common/libraries/domain/domain-event.js' -import { Account } from '../entities/account.js' - - - -export class AccountVerificationEmailRequested - extends DomainEvent - { - } \ No newline at end of file diff --git a/apps/server/src/modules/account/events/account-verification-email-sent.ts b/apps/server/src/modules/account/events/account-verification-email-sent.ts deleted file mode 100644 index aeaf5113..00000000 --- a/apps/server/src/modules/account/events/account-verification-email-sent.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DomainEvent } from '../../../common/libraries/domain/domain-event.js' -import type { Account } from '../entities/account.js' - - - -export class AccountVerificationEmailSent - extends DomainEvent - { - } \ No newline at end of file diff --git a/apps/server/src/modules/account/models/account/account-create-model.ts b/apps/server/src/modules/account/models/account/account-create-model.ts deleted file mode 100644 index ccd1ada1..00000000 --- a/apps/server/src/modules/account/models/account/account-create-model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {DbContextModel} from "../../../../common/modules/database/db-context-model.js" -import {Account} from '../../entities/account.js'; - - - -export function DbContextModelAccountCreatePayloadMapper(account: Account): DbContextModel.Account.CreatePayload { - return { - id : account.id, - email : account.email.address, - email_verified: account.email.isVerified, - password : account.password.hash.serialize(), - username : account.username, - }} diff --git a/apps/server/src/modules/account/models/account/account-entity-model.ts b/apps/server/src/modules/account/models/account/account-entity-model.ts deleted file mode 100644 index d7d11aaf..00000000 --- a/apps/server/src/modules/account/models/account/account-entity-model.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { OptionPredicator } from 'typia/lib/programmers/helpers/OptionPredicator.js'; -import {PhcString} from '../../../../common/libraries/unihash/types/phc-string.js' -import {DbContextModel} from '../../../../common/modules/database/db-context-model.js' -import { DataMapper } from '../../../../common/persistance/data-mapper.js'; -import {EmailAddress} from "../../../../kernel/modules/mailer/value-object/email-address.js" -import {Account} from '../../entities/account.js' -import {AccountEmail} from '../../value-objects/account-email.js' -import {AccountStatus} from '../../value-objects/account-status.js' -import {Password} from '../../value-objects/password.js' -import {type Username} from '../../value-objects/username.js' - -export class AccountEntityMapper extends DataMapper{ - public map(data: DbContextModel.Account.Entity): Account - { - return Account.build({ - email : AccountEmail.create({address: data.email as EmailAddress, isVerified: data.email_verified}), - id : data.id, - status : AccountStatus.ACTIVE, - groups : [], - password: Password.fromHash(PhcString.deserialize(data.password as any)), - username: data.username as Username, - }) - } - - public reverse(data: Account): DbContextModel.Account.Entity - { - throw new Error('Method not implemented.'); - } -} diff --git a/apps/server/src/modules/account/notifcation/account-confirmed-notification.ts b/apps/server/src/modules/account/notifcation/account-confirmed-notification.ts deleted file mode 100644 index b4bff5af..00000000 --- a/apps/server/src/modules/account/notifcation/account-confirmed-notification.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {EmailNotification} from '../../../common/notification/entity/email-notification.js' -import type {AccountId} from '../shared-kernel/account-id.js' -import type {AccountEmail} from '../value-objects/account-email.js' - - - -export class AccountConfirmedNotification extends EmailNotification { - - constructor(email: AccountEmail) { - super({ - sentBy: '' as AccountId, - priority: 'HIGH', - content: { - subject: 'Account Verified', - body: `Account successfully verified`, - }, - recipient: {to: email.address}, - }) - } -} \ No newline at end of file diff --git a/apps/server/src/modules/account/notifcation/confirm-email-notification.ts b/apps/server/src/modules/account/notifcation/confirm-email-notification.ts deleted file mode 100644 index 0f2c34d7..00000000 --- a/apps/server/src/modules/account/notifcation/confirm-email-notification.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { EmailNotification } from '../../../common/notification/entity/email-notification.js' -import type { AccountId } from '../shared-kernel/account-id.js' -import type { AccountEmail } from '../value-objects/account-email.js' - - - -export class ConfirmEmailNotification - extends EmailNotification - { - - constructor( - verificationCode : string, - email : AccountEmail, - ) - { - super( { - sentAt : undefined, - sentBy : '' as AccountId, - priority : 'HIGH', - content : { - subject : 'Account verification', - body : `${verificationCode}`, - }, - recipient : {to : email.address}, - } ) - } - } \ No newline at end of file diff --git a/apps/server/src/modules/account/policies/account-policy.ts b/apps/server/src/modules/account/policies/account-policy.ts deleted file mode 100644 index 069ac293..00000000 --- a/apps/server/src/modules/account/policies/account-policy.ts +++ /dev/null @@ -1,103 +0,0 @@ -import {BadRequestException, ConflictException, Inject, Injectable, Logger} from '@nestjs/common' -import {SpanStatusCode} from '@opentelemetry/api' -import {err, ok} from 'neverthrow' -import {BasePolicy} from '../../../common/libraries/domain/policy/base-policy.js' -import {Pwnproc} from '../../../common/libraries/pwnproc/pwnproc.js' -import {PasswordSecurityLevel} from '../../../common/libraries/pwnproc/report/password-security-level.js' -import {OpentelemetryTracer} from '../../../common/modules/observability/tracing/opentelemetry/provider/tracer/opentelemetry-tracer.js' -import {AccountRepository} from '../repositories/account-repository.js' - - - -@Injectable() -export class AccountPolicy extends BasePolicy { - @Inject(OpentelemetryTracer) tracer: OpentelemetryTracer - private logger: Logger = new Logger('account::policy') - - - constructor(private readonly accountRepository: AccountRepository, private readonly passwordSecurity: Pwnproc) { - super() - } - - - public async canRegisterAccount(registerAccount: { - email: string, username: string, password: string, - }) { - const span = this.tracer.startSpan('com.methylphenidate.account.policy.can_register_account') - - this.logger.debug(`Running CanRegisterAccount policy...`) - - const isUniqueUsername = await this.isUniqueUsername(registerAccount.username) - const isUniqueEmail = await this.isUniqueEmail(registerAccount.email) - const isSecurePassword = await this.isSecurePassword(registerAccount.password) - - const maybePolicy = this.merge(isUniqueUsername, isUniqueEmail, isSecurePassword) - - if (maybePolicy.isErr()) { - span.setStatus({code: SpanStatusCode.ERROR}) - span.recordException(maybePolicy.error) - span.end() - this.logger.warn(`CanRegisterAccount policy failed.`, maybePolicy.error) - throw maybePolicy.error - } else { - span.setStatus({code: SpanStatusCode.OK}) - span.end() - this.logger.verbose(`CanRegisterAccount policy passed.`) - return maybePolicy.value - } - } - - - private async isUniqueUsername(username: string) { - const span = this.tracer.startSpan('com.methylphenidate.account.policy.is_unique_username') - - this.logger.debug(`Validating username uniqueness...`, {username}) - - const identity = await this.accountRepository.findByUsername(username) - - if (identity) { - span.setStatus({code: SpanStatusCode.ERROR}) - span.recordException(new ConflictException('Username is already in use in system, try logging in instead.')) - span.end() - this.logger.warn(`Username is already in use in system.`, {username}) - throw new ConflictException('Username is already in use in system, try logging in instead.') - } - - this.logger.verbose(`Username is unique.`, {username}) - span.setStatus({code: SpanStatusCode.OK}) - span.end() - return ok(true) - } - - - private async isUniqueEmail(email: string) { - this.logger.debug(`Validating email uniqueness...`, {email}) - - const identity = await this.accountRepository.findByEmail(email) - - if (identity) { - this.logger.warn(`Email is already in use in system.`, {email}) - const error = new ConflictException('Email is already in use in system, try logging in instead.') - throw error - } - - this.logger.verbose(`Email is unique.`, {email}) - return ok(true) - } - - - private async isSecurePassword(password: string) { - this.logger.debug(`Validating password security...`) - - const report = await this.passwordSecurity.generateReport(password) - - if (report.isScoreHigherThan(PasswordSecurityLevel.WEAK)) { - this.logger.verbose(`Password is secure.`) - return ok(true) - } else { - this.logger.warn(`Password is insecure enough.`) - const error = new BadRequestException('Password is insecure enough.') - return err(error) - } - } -} \ No newline at end of file diff --git a/apps/server/src/modules/account/repositories/account-repository.ts b/apps/server/src/modules/account/repositories/account-repository.ts deleted file mode 100644 index f9c977f6..00000000 --- a/apps/server/src/modules/account/repositories/account-repository.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Repository } from '../../../common/libraries/storage/index.js' -import { Account } from '../entities/account.js' - - - -export abstract class AccountRepository - extends Repository - { - - abstract findByUsername(username : string) : Promise - - - abstract findByEmail(email : string) : Promise - - - abstract findByUsernameFields(username : string) : Promise - } \ No newline at end of file diff --git a/apps/server/src/modules/account/repositories/account-seeder.ts b/apps/server/src/modules/account/repositories/account-seeder.ts deleted file mode 100644 index e4b0cc4c..00000000 --- a/apps/server/src/modules/account/repositories/account-seeder.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {faker} from '@faker-js/faker' -import { - Injectable, - Logger, -} from '@nestjs/common' -import {$Enums} from 'db' -import {SeederBase} from '../../../common/libraries/seeder/seeder-base.js' -import {PrismaService} from '../../../common/modules/resources/prisma/services/prisma-service.js' -import {ApiAccountMockup} from '../../../utilities/fixtures/api-account-mockup.js' -import type {RegisterAccountCommand} from '../commands/register-account/register-account-command.js' -import {AccountService} from '../services/account-service.js' - - - -@Injectable() -export class AccountSeeder - extends SeederBase -{ - - constructor(private prismaService: PrismaService, private accountService: AccountService) - { - super(new Logger('seeder:domain')) - } - - - public async count(): Promise - { - return this.prismaService.account.count() - } - - - public async exists(input: RegisterAccountCommand): Promise - { - const exists = await this.prismaService.account.findUnique({ - where: { - email: input.email, - }, - }) - - return exists !== null - } - - - public async fabricate(): Promise - { - return { - email : faker.internet.email(), - password: ApiAccountMockup.password, - username: faker.internet.userName(), - } - } - - - public async save(input: RegisterAccountCommand): Promise - { - const account = await this.accountService.register(input) - - await this.prismaService.account.update({ - where: { - id: account.id, - }, - data : { - email_verified: true, - }, - }) - - return account - } -} diff --git a/apps/server/src/modules/account/repositories/prisma-account-repository.service.ts b/apps/server/src/modules/account/repositories/prisma-account-repository.service.ts deleted file mode 100644 index e2aef206..00000000 --- a/apps/server/src/modules/account/repositories/prisma-account-repository.service.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { - Injectable, - Logger, -} from '@nestjs/common'; -import { PhcString } from '../../../common/libraries/unihash/types/phc-string.js'; -import { DbContextModel } from '../../../common/modules/database/db-context-model.js'; -import { PrismaService } from '../../../common/modules/resources/prisma/services/prisma-service.js'; -import { Account } from '../entities/account.js'; -import { DbContextModelAccountCreatePayloadMapper } from '../models/account/account-create-model.js'; -import { AccountEntityMapper } from '../models/account/account-entity-model.js'; -import { AccountEmail } from '../value-objects/account-email.js'; -import { AccountStatus } from '../value-objects/account-status.js'; -import { Password } from '../value-objects/password.js'; -import { Username } from '../value-objects/username.js'; -import { AccountRepository } from './account-repository.js'; - - - -@Injectable() -export class PrismaAccountRepository extends AccountRepository { - private logger: Logger = new Logger('domain:repository:prisma') - - - constructor(private prismaService: PrismaService) { - super() - } - - - public async findByEmail(email: string): Promise { - this.logger.debug(`Searching account entity by email`, {email}) - - const maybeAccount = await this.prismaService.account.findFirst({ - where: { - email: email, - }, - }) - - if (!maybeAccount) { - this.logger.debug(`Searching account entity by email resulted in no results`, {email}) - return null - } - - const account = maybeAccount as DbContextModel.Account.Entity - - this.logger.debug(`Found account entity by email`, { - email, - account, - }) - - return Account.build({ - email: AccountEmail.create({address: account.email as any, isVerified: account.email_verified}), - id: account.id, - status: AccountStatus.ACTIVE, - groups: [], - password: Password.fromHash(account.password as any), - username: account.username as Username, - }) - } - - - public async findByUsername(username: Username): Promise { - this.logger.debug(`findByUsername()`) - - let maybeAccount: DbContextModel.Account.Entity | null = null - - try { - maybeAccount = await this.prismaService.account.findFirst({ - where: { - username: username, - }, - }) as DbContextModel.Account.Entity | null - } catch (e) { - this.logger.error(e) - } - - if (!maybeAccount) { - this.logger.verbose(`findByUsername() => null`) - return null - } - - const account = maybeAccount - - this.logger.debug(`findByUsername() => Account`) - - return Account.build({ - email: AccountEmail.create({address: account.email as any, isVerified: account.email_verified}), - id: account.id, - status: AccountStatus.ACTIVE, - groups: [], - password: Password.fromHash(account.password as any), - username: account.username as Username, - }) - } - - - public async findByUsernameFields(username: string): Promise { - // Find domain by provided username, try to fit it into the username - // and email fields - let maybeAccount: DbContextModel.Account.Entity | null = null - - try { - maybeAccount = await this.prismaService.account.findFirst({ - where: { - OR: [ - {username: username}, {email: username}, - ], - }, - }) as DbContextModel.Account.Entity | null - } catch (e) { - this.logger.error(e) - } - - this.logger.verbose(`findByUsernameFields(${username}) => ${JSON.stringify(maybeAccount)}`) - - if (!maybeAccount) { - return null - } - - const account = maybeAccount - - return new AccountEntityMapper().map(account) - } - - - public async create(identity: Account): Promise { - let entity: DbContextModel.Account.Entity - - try { - entity = await this.prismaService.account.create({ - data: DbContextModelAccountCreatePayloadMapper(identity), - }) - } catch (e) { - this.logger.error(e) - throw e - } - - return new AccountEntityMapper().map(entity) - } - - - public delete(entity: Account): Promise { - return Promise.resolve(undefined) - } - - - async exists(entity: Account): Promise { - const count = await this.prismaService.account.count({ - where: { - OR: [ - {email: entity.email.address}, {username: entity.username}, {id: entity.id}, - ], - }, - }) - - return count > 0 - } - - - public async findById(id: string): Promise { - const entity = await this.prismaService.account.findFirst({ - where: { - id: id, - }, - }) - - if (entity) { - return new AccountEntityMapper().map(entity) - } else { - return null - } - } - - - public async update(entity: Account): Promise { - const account = await this.prismaService.account.update({ - where: { - id: entity.id, - }, - data: { - email: entity.email.address, - }, - }) - - return new AccountEntityMapper().map(account) - } - -} diff --git a/apps/server/src/modules/account/services/README.md b/apps/server/src/modules/account/services/README.md deleted file mode 100644 index 3af98fc0..00000000 --- a/apps/server/src/modules/account/services/README.md +++ /dev/null @@ -1,6 +0,0 @@ - Self-service login and registration: Allow end-users to create and sign in to accounts using username/email and password combinations, social sign-in ("Sign in with Google, GitHub"), passwordless flows, and others. - Multi-factor authentication (MFA/2FA): Support protocols such as TOTP (RFC 6238 and IETF RFC 4226 - better known as Google Authenticator) - Account verification: Verify that an email address, phone number, or physical address actually belongs to the user. - Account recovery: Allow users to recover access to their account using "Forgot Password" flows or security codes. - Profile and account management: Use secure flows to update passwords, personal details, email addresses, and linked social profiles. - Admin APIs: Import, update, and delete user accounts. \ No newline at end of file diff --git a/apps/server/src/modules/account/services/account-recovery.ts b/apps/server/src/modules/account/services/account-recovery.ts deleted file mode 100644 index 7323a5c6..00000000 --- a/apps/server/src/modules/account/services/account-recovery.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Injectable} from "@nestjs/common"; - - - -@Injectable() -export class AccountRecovery { - public async startPasswordRecovery(accountId : string) { - // TODO: Generate Password Reset Request - // TODO: Save PasswordResetRequest in cache for specific user - // TODO: Send verification email for domain - // TODO: Perform such actions only if domain is verified. - } - - public async solvePasswordRecovery(code : string) {} -} \ No newline at end of file diff --git a/apps/server/src/modules/account/services/account-service.ts b/apps/server/src/modules/account/services/account-service.ts deleted file mode 100644 index 3ba49513..00000000 --- a/apps/server/src/modules/account/services/account-service.ts +++ /dev/null @@ -1,54 +0,0 @@ -import {Inject, Injectable, Logger} from '@nestjs/common' -import {ServiceAbstract} from '../../../common/libraries/services/service-abstract.js' -import {PasswordHashing} from '../../../common/libraries/unihash/index.js' -import {EventBus} from '../../../common/modules/messaging/event-bus.js' -import {OpentelemetryTracer} from '../../../common/modules/observability/tracing/opentelemetry/provider/tracer/opentelemetry-tracer.js' -import {RegisterAccountCommand} from '../commands/register-account/register-account-command.js' -import {RegisterAccountUseCase} from "../commands/register-account/register-account-usecase.js" -import {AccountSelfService} from '../contract/account-self-service.js' -import {Account} from '../entities/account.js' -import {AccountPolicy} from '../policies/account-policy.js' -import {AccountRepository} from '../repositories/account-repository.js' - - - -@Injectable() -export class AccountService extends ServiceAbstract implements AccountSelfService { - @Inject(OpentelemetryTracer) private tracer: OpentelemetryTracer - private logger: Logger = new Logger('account::service') - - - constructor(private policy: AccountPolicy, private repository: AccountRepository, private hashing: PasswordHashing, private eventbus: EventBus, private registerAccountUsecase: RegisterAccountUseCase) { - super(repository) - } - - - public closeAccount(accountId: string): Promise { - throw new Error('Method not implemented.') - } - - - /** - * # `register-account` - * - * Register account is an operation dedicated to creating new accounts - * in codebase. - * - * @param {RegisterAccountCommand} registerAccount - * @returns {Promise} - */ - async register(registerAccount: RegisterAccountCommand): Promise { - const account = await this.registerAccountUsecase.execute(registerAccount) - - if (account.isErr()) { - throw account.error - } - - return account.value - } - - - public updateAccount(accountId: string, updateAccount: RegisterAccountCommand): Promise { - throw new Error('Method not implemented.') - } -} \ No newline at end of file diff --git a/apps/server/src/modules/account/services/account-verification.ts b/apps/server/src/modules/account/services/account-verification.ts deleted file mode 100644 index 6d817a3d..00000000 --- a/apps/server/src/modules/account/services/account-verification.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { - BadRequestException, - Injectable, - Logger, - NotFoundException, - UnauthorizedException, -} from '@nestjs/common' -import {OnEvent} from '@nestjs/event-emitter' -import ms from 'ms' -import { - ok, - Result, -} from 'neverthrow' -import {randomUUID} from 'node:crypto' -import {EventBus} from '../../../common/modules/messaging/event-bus.js' -import {CacheManager} from '../../../common/modules/storage/cache-manager/contract/cache-manager.js' -import {NotificationService} from '../../../common/notification/contract/notification-service.js' -import {StaticFeatureFlags} from '../../../configs/static-feature-flags.js' -import {Mailer} from "../../../kernel/modules/mailer/contract/mailer.js" -import {CreateEmailMessagePayload} from "../../../kernel/modules/mailer/dto/create-email-message-payload.js" -import {AccountEmailConfirmed} from '../events/account-email-confirmed.js' -import {AccountRegistered} from '../events/account-registered.js' -import {AccountVerificationEmailSent} from '../events/account-verification-email-sent.js' -import {AccountConfirmedNotification} from '../notifcation/account-confirmed-notification.js' -import {AccountRepository} from '../repositories/account-repository.js' - - - -@Injectable() -export class AccountVerification -{ - private logger: Logger - private accountRepository: AccountRepository - private publisher: EventBus - private cacheManager: CacheManager - private mailer: Mailer - private notificationService: NotificationService - - - constructor(accountRepository: AccountRepository, eventBus: EventBus, cacheManager: CacheManager, mailer: Mailer, notificationService: NotificationService) - { - this.logger = new Logger('account::verification::service') - this.accountRepository = accountRepository - this.publisher = eventBus - this.cacheManager = cacheManager - this.mailer = mailer - this.notificationService = notificationService - } - - - public async sendVerificationEmail(accountId: string): Promise - { - const account = await this.accountRepository.getById(accountId) - - // Create secret code for email - let verificationSecret: string = this.createVerificationSecret() - - // Save verificationSecret - await this.cacheManager.set(`verification_${verificationSecret}`, accountId, ms('15m')) - - // Send verification email - // TODO: Email template may be moved somewhere else - const verificationEmail: CreateEmailMessagePayload = { - recipient: { - to: account.email.address, - }, - subject : 'Account Confirmation', - body : 'Your verification code is: ' + verificationSecret, - } - - await this.mailer.sendEmail(verificationEmail) - - // Emit verification email sent event - const emailVerificationSentEvent = new AccountVerificationEmailSent(account) - this.publisher.publish(emailVerificationSentEvent) - } - - - public async resendVerificationEmail(accountEmail: string): Promise - { - const account = await this.accountRepository.findByEmail(accountEmail) - - if (!account) - { - throw new NotFoundException('Account with associated mail was not found.') - } - - if (account.email.isVerified) - { - throw new BadRequestException('Account email is already verified.') - } - - account.requestVerificationEmail() - - const events = account.getUncommittedEvents() - await this.publisher.publishAll(events) - - await this.accountRepository.save(account) - - await this.sendVerificationEmail(account.id) - } - - - public async verifyEmail(code: string): Promise> - { - const accountId = await this.cacheManager.get(`verification_${code}`) - - if (!accountId) - { - throw new UnauthorizedException('Provided verification code is not valid.') - } - - const account = await this.accountRepository.getById(accountId) - - if (!account) - { - throw new NotFoundException('Account associated with provided code was not found.') - } - - // Verify Account's Email - account.verifyEmail() - - await this.accountRepository.save(account) - - // Delete used verification code - await this.cacheManager.delete(accountId) - - // Emit Account Verified Event - const events = account.getUncommittedEvents() - await this.publisher.publishAll(events) - - return ok(true) - } - - - @OnEvent('account.registered', {async: true}) - private async onAccountRegistered(event: AccountRegistered): Promise - { - this.logger.debug(`Handling ${event.id} with "onAccountRegistered"`) - await this.sendVerificationEmail(event.body!.aggregateId) - this.logger.log(`Handled ${event.id} with "onAccountRegistered"`) - } - - - @OnEvent('account.verification.completed') - private async onEmailVerified(event: AccountEmailConfirmed): Promise - { - this.logger.debug(`Handling ${event.id} with "onEmailVerified": ${JSON.stringify(event)}`) - - if (!event.body?.email) - { - throw new BadRequestException('Email is missing.') - } - - // Create notification - const notification = new AccountConfirmedNotification(event.body?.email) - - // Send notification - await this.notificationService.sendNotification(notification) - - this.logger.debug(`Handled ${event.id} with "onEmailVerified"`) - } - - - private createVerificationSecret(): string - { - let verificationSecret: string - - if (StaticFeatureFlags.shouldUseTestingVerificationCode) - { - verificationSecret = 'verification_code' - } - else - { - verificationSecret = randomUUID() - } - - return verificationSecret - } -} diff --git a/apps/server/src/modules/account/shared-kernel/README.md b/apps/server/src/modules/account/shared-kernel/README.md deleted file mode 100644 index 2d6fbc2d..00000000 --- a/apps/server/src/modules/account/shared-kernel/README.md +++ /dev/null @@ -1 +0,0 @@ -# Shared Kernel \ No newline at end of file diff --git a/apps/server/src/modules/account/shared-kernel/account-id.ts b/apps/server/src/modules/account/shared-kernel/account-id.ts deleted file mode 100644 index 43b5de34..00000000 --- a/apps/server/src/modules/account/shared-kernel/account-id.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Opaque } from 'type-fest' - - - -export type AccountId = Opaque \ No newline at end of file diff --git a/apps/server/src/modules/account/shared-kernel/credential-validator/credential-validator-module.ts b/apps/server/src/modules/account/shared-kernel/credential-validator/credential-validator-module.ts deleted file mode 100644 index d9452d80..00000000 --- a/apps/server/src/modules/account/shared-kernel/credential-validator/credential-validator-module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {Module} from "@nestjs/common" -import {UnihashModule} from "../../../../common/libraries/unihash/unihash-module.js" -import {AccountModule} from "../../account.module.js" -import {CredentialValidator} from "./credential-validator.js" - - - -@Module({ - imports: [AccountModule, UnihashModule], - controllers: [], - providers: [ - CredentialValidator, - ], - exports: [CredentialValidator], -}) -export class CredentialValidatorModule {} \ No newline at end of file diff --git a/apps/server/src/modules/account/shared-kernel/credential-validator/credential-validator.ts b/apps/server/src/modules/account/shared-kernel/credential-validator/credential-validator.ts deleted file mode 100644 index a9de6b13..00000000 --- a/apps/server/src/modules/account/shared-kernel/credential-validator/credential-validator.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {Injectable, NotFoundException, UnauthorizedException} from "@nestjs/common" -import {err, ok, Result} from "neverthrow" -import {EventBus} from "../../../../common/modules/messaging/event-bus.js" -import {PasswordHashing} from "../../../../common/libraries/unihash/password-hashing.service.js" -import {Account} from "../../entities/account.js" -import {AccountRepository} from "../../repositories/account-repository.js" - - - -/** - * This class is responsible for validating credentials. This is exposed by shared-kernel and further used by - * authentication to encapsulate the logic of validating credentials as authentication do not have access to domain itself. - */ -@Injectable() -export class CredentialValidator { - constructor(private repository: AccountRepository, private hashingService: PasswordHashing, private eventBus: EventBus) {} - - - /** - * Validates the credentials of a user. - * - * @param {string} username - The username of the user. - * @param {string} password - The password of the user. - * @returns {Promise} - A promise that resolves with the validation result. - */ - public async validateCredentials(username: string, password: string): Promise> { - // Find the domain by any username field (email, username) - const user = await this.repository.findByUsernameFields(username) - - if (!user) { - return err(new NotFoundException("User not found")) - } - - // Verify the password - - const passwordVerified = await user.password.compare(password, this.hashingService.which(user.password.hash.serialize())) - - if (!passwordVerified) { - return err(new UnauthorizedException("Invalid credentials")) - } - - user.authenticate() - - await this.eventBus.publishAll(user.getUncommittedEvents() as any) - - return ok(user) - } -} \ No newline at end of file diff --git a/apps/server/src/modules/account/tests/account.controller.e2e.spec.ts b/apps/server/src/modules/account/tests/account.controller.e2e.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/server/src/modules/account/value-objects/account-email.ts b/apps/server/src/modules/account/value-objects/account-email.ts deleted file mode 100644 index 47dbcb41..00000000 --- a/apps/server/src/modules/account/value-objects/account-email.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {ApiProperty} from '@nestjs/swagger' -import {ImmutableClass} from '../../../common/libraries/dst/data-class/data-class.js' -import {EmailAddress} from "../../../kernel/modules/mailer/value-object/email-address.js" -import {ApiAccountMockup} from '../../../utilities/fixtures/api-account-mockup.js' - - - -export const ApiPropertyAccountEmail = ApiProperty({ - name : 'email', - description: 'The' + ' account\'s email address', - example : ApiAccountMockup.email, - examples : ApiAccountMockup._examples.emails, - type : String, - }) - - -export class AccountEmail - extends ImmutableClass -{ - address: EmailAddress - isVerified: boolean -} diff --git a/apps/server/src/modules/account/value-objects/account-group.ts b/apps/server/src/modules/account/value-objects/account-group.ts deleted file mode 100644 index 20a1338a..00000000 --- a/apps/server/src/modules/account/value-objects/account-group.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {Group} from "../../group/entities/group.js"; - - - -export type AccountGroup = Group['id'] \ No newline at end of file diff --git a/apps/server/src/modules/account/value-objects/account-status.ts b/apps/server/src/modules/account/value-objects/account-status.ts deleted file mode 100644 index f0a7ff00..00000000 --- a/apps/server/src/modules/account/value-objects/account-status.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum AccountStatus - { - PENDING = 'pending', - ACTIVE = 'active', - INACTIVE = 'inactive', - LOCKED = 'locked', - DEACTIVATED = 'deactivated', - } \ No newline at end of file diff --git a/apps/server/src/modules/account/value-objects/password.ts b/apps/server/src/modules/account/value-objects/password.ts deleted file mode 100644 index f85653ea..00000000 --- a/apps/server/src/modules/account/value-objects/password.ts +++ /dev/null @@ -1,77 +0,0 @@ -import {ApiProperty} from '@nestjs/swagger' -import {Pwnproc} from '../../../common/libraries/pwnproc/pwnproc.js' -import {PasswordSecurityReport} from '../../../common/libraries/pwnproc/report/password-security-report.js' -import {PhcString} from '../../../common/libraries/unihash/types/phc-string.js' -import {UnihashAlgorithm} from '../../../common/libraries/unihash/unihash-algorithm.js' -import {ApiAccountMockup} from '../../../utilities/fixtures/api-account-mockup.js' - - - -export const ApiPropertyAccountPassword = ApiProperty({ - name : 'password', - description: 'The account\'s username', - example : ApiAccountMockup.password, - examples : ApiAccountMockup._examples.passwords, -}) - - -interface PasswordProperties { - hash: PhcString - plain?: string - report?: PasswordSecurityReport -} - - -export class Password - implements PasswordProperties { - hash: PhcString - plain?: string - report?: PasswordSecurityReport - - - private constructor(payload: PasswordProperties) { - this.hash = payload.hash - this.plain = payload.plain - this.report = payload.report - } - - - static fromHash(hash: PhcString): Password { - return new Password({ - hash, - }) - } - - - static async fromPlain(plain: string, hashingService: UnihashAlgorithm): Promise { - const hash = await hashingService.hash(plain) - return new Password({ - hash: PhcString.deserialize(hash), - plain, - }) - } - - - public async compare(plain: string, hashingService: UnihashAlgorithm): Promise { - return hashingService.verify( - this.hash.serialize(), - plain, - ) - } - - - public async generateReport(passwordStrengthEstimator: Pwnproc): Promise { - if (!this.plain) - { - throw new Error('Cannot generate report for a password that was not created from plain text.') - } - - return await passwordStrengthEstimator.generateReport(this.plain) - } - - - public addReport(report: PasswordSecurityReport): Password { - this.report = report - return this - } -} diff --git a/apps/server/src/modules/account/value-objects/username.ts b/apps/server/src/modules/account/value-objects/username.ts deleted file mode 100644 index 120dc67a..00000000 --- a/apps/server/src/modules/account/value-objects/username.ts +++ /dev/null @@ -1,108 +0,0 @@ -import {faker} from '@faker-js/faker' -import {BadRequestException} from '@nestjs/common' -import {ApiProperty} from '@nestjs/swagger' -import { - SpanKind, - SpanStatusCode, -} from '@opentelemetry/api' -import {startInactiveSpan} from '@sentry/opentelemetry' -import { - err, - ok, - Result, -} from 'neverthrow' -import { - createAssert, - createIs, - tags, - TypeGuardError, -} from 'typia' -import type { - Opaque, - UnwrapOpaque, -} from '../../../common/libraries/opaque.js' -import {ApiAccountMockup} from '../../../utilities/fixtures/api-account-mockup.js' - - - -export const ApiPropertyAccountUsername = ApiProperty({ - name : 'username', - description: 'The account\'s username', - example : ApiAccountMockup.username, - examples : [ - faker.internet.userName(), faker.internet.userName(), faker.internet.userName(), - ], -}) - -// The Username should be from 4 to 32 characters. -// It should only contain letters, numbers, and underscores. - -export type Username = Opaque & tags.MaxLength<32> & tags.MinLength<4>, 'username'> - - -export class InvalidUsername - extends BadRequestException { - constructor() { - super('Invalid username') - } -} - - -const _assertUsername = createAssert>() -const _isUsername = createIs>() - -export function isUsername(value: unknown): value is Username { - return _isUsername(value) -} - -export function assertUsername(value: unknown): asserts value is Username { - try - { - _assertUsername(value) - } - catch (e) - { - throw new InvalidUsername() - } -} - -export function createUsername(value: unknown): Result { - const span = startInactiveSpan({ - name : 'create-account-username', - op : 'function', - kind : SpanKind.INTERNAL, - attributes: { - input: value as string, - }, - }) - - try - { - const username = _assertUsername(value) as Username - span.setAttribute( - 'username', - username, - ) - span.setStatus({ - code : SpanStatusCode.OK, - message: 'Username is valid.', - }) - span.end() - return ok(username.toLowerCase() as Username) - } - catch (e: unknown) - { - const error = e as TypeGuardError - span.setStatus({ - code : SpanStatusCode.ERROR, - message: 'Username is invalid.', - }) - span.setAttribute( - 'error', - error.message, - ) - span.recordException(error) - span.end() - return err(new InvalidUsername()) - } -} diff --git a/apps/server/src/modules/account/view-model/account-view-model.ts b/apps/server/src/modules/account/view-model/account-view-model.ts deleted file mode 100644 index 19c59f29..00000000 --- a/apps/server/src/modules/account/view-model/account-view-model.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {faker} from '@faker-js/faker' -import {ApiProperty} from '@nestjs/swagger' -import {ApiModel} from '../../../utilities/docs-utils/swagger-api-model.js' -import {ApiAccountMockup} from '../../../utilities/fixtures/api-account-mockup.js' -import {ApiPropertyAccountUsername} from '../value-objects/username.js' - - - -@ApiModel({ - name : 'Account', - description: 'asdasd', -}) -export class AccountViewModel { - /** - * Represents the unique identifier of an entity. - * - * @typedef {string} Id - */ - @ApiProperty({ - name : 'id', - description: 'The domain\'s unique identifier', - example : 'cjld2cjxh0000qzrmn831i7rn', - required : true, - }) id: string - - /** - * Represents an email address. - * @typedef {string} email - */ - @ApiProperty({ - name : 'email', - description: 'The domain\'s email address', - example : ApiAccountMockup.email, - examples : ApiAccountMockup._examples.emails, - }) email: string - - /** - * Indicates whether the email associated with a user domain has been verified. - * - * @type {boolean} - */ - @ApiProperty({ - name : 'emailVerified', - description: 'Indicates whether the email associated with a user domain has been verified', - example : faker.datatype.boolean(), - }) emailVerified: boolean - - /** - * Represents a username. - * @typedef {string} username - */ - @ApiPropertyAccountUsername username: string -} diff --git a/apps/server/src/modules/audit/entities/audit-log.ts b/apps/server/src/modules/audit/entities/audit-log.ts deleted file mode 100644 index 61e764de..00000000 --- a/apps/server/src/modules/audit/entities/audit-log.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { UUID } from '../../../common/libraries/identification/index.js' -import { AuditActor } from '../value-object/audit-actor.js' -import { AuditOperation } from '../value-object/audit-operation.js' -import { AuditResource } from '../value-object/audit-resource.js' - - - -export interface AuditLog - { - id : UUID - /* "Who?" | The actor who performed the action. See below for the description of its type. */ - actor : AuditActor - operation : AuditOperation - resource : AuditResource - date : Date - } \ No newline at end of file diff --git a/apps/server/src/modules/audit/interceptor/audit-interceptor.ts b/apps/server/src/modules/audit/interceptor/audit-interceptor.ts deleted file mode 100644 index a170cb7d..00000000 --- a/apps/server/src/modules/audit/interceptor/audit-interceptor.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { - CallHandler, - ExecutionContext, - Injectable, - Logger, - NestInterceptor, -} from '@nestjs/common' -import { Reflector } from '@nestjs/core' -import { Observable } from 'rxjs' -import { PrismaService } from '../../../common/modules/resources/prisma/services/prisma-service.js' - - - -@Injectable() -export class AuditInterceptor - implements NestInterceptor - { - private logger = new Logger( 'interceptor:audit' ) - - constructor( - private readonly reflector : Reflector, - private prisma : PrismaService, - ) - { - } - - intercept( - context : ExecutionContext, - next : CallHandler, - ) : Observable | Promise> - { - // const auditLog = this.reflector.get("XYZ", context.getHandler()) - return next.handle() - } - } \ No newline at end of file diff --git a/apps/server/src/modules/authentication/authentication-module.ts b/apps/server/src/modules/authentication/authentication-module.ts deleted file mode 100644 index 10019e62..00000000 --- a/apps/server/src/modules/authentication/authentication-module.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {Module} from '@nestjs/common' -import {PassportModule} from '@nestjs/passport' -import {DatabaseModule} from '../../common/modules/database/database.module.js' -import {__authConfig} from '../../configs/global/__config.js' -import {JwtModule} from "../../kernel/modules/identity/jwt.js" -import {AccountModule} from '../account/account.module.js' -import {CredentialValidatorModule} from '../account/shared-kernel/credential-validator/credential-validator-module.js' -import {AuthenticationController} from './controllers/authentication-controller.js' -import { - JwtAuthorizationStrategy, - LocalAuthorizationStrategy, -} from '../../kernel/modules/identity/authorization-strategy/index.js' -import {LocalAuthenticationService} from './services/local-authentication-service.js' - - - -@Module({ - imports : [ - CredentialValidatorModule, - AccountModule, - JwtModule, - PassportModule.register({ - session: true, - }), - DatabaseModule, - ], - controllers: [AuthenticationController], - providers : [ - LocalAuthenticationService, - LocalAuthorizationStrategy, - JwtAuthorizationStrategy, - ], - exports : [LocalAuthenticationService], - }) -export class AuthenticationModule - { - } \ No newline at end of file diff --git a/apps/server/src/modules/authentication/commands/authenticate/api-authenticate-not-found-response.ts b/apps/server/src/modules/authentication/commands/authenticate/api-authenticate-not-found-response.ts deleted file mode 100644 index 3ff3f035..00000000 --- a/apps/server/src/modules/authentication/commands/authenticate/api-authenticate-not-found-response.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {ApiNotFoundResponse} from '@nestjs/swagger' -import {HttpProblem} from '../../../../common/error/problem-details/http-problem.js' - - - -export const ApiAuthenticateNotFoundResponse = ApiNotFoundResponse({ - description: 'The user could not be found.', - type : HttpProblem, - }) \ No newline at end of file diff --git a/apps/server/src/modules/authentication/commands/authenticate/api-authenticate-ok-response.ts b/apps/server/src/modules/authentication/commands/authenticate/api-authenticate-ok-response.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/server/src/modules/authentication/commands/authenticate/authenticate-command.ts b/apps/server/src/modules/authentication/commands/authenticate/authenticate-command.ts deleted file mode 100644 index f17126e7..00000000 --- a/apps/server/src/modules/authentication/commands/authenticate/authenticate-command.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {ApiModel} from '../../../../utilities/docs-utils/swagger-api-model.js' -import {ApiPropertyAccountPassword} from '../../../account/value-objects/password.js' -import {ApiPropertyAccountUsername} from '../../../account/value-objects/username.js' - - - -@ApiModel({ - name : 'Auhenticate', - description: 'asdasd', - }) -export class AuthenticateCommand - { - /** - * Represents a username. - * @typedef {string} username - */ - @ApiPropertyAccountUsername username: string - - /** - * The password variable is a string that represents a user's password. - * - * @type {string} - */ - @ApiPropertyAccountPassword password: string - } \ No newline at end of file diff --git a/apps/server/src/modules/authentication/commands/initialize-passwordless-authentication.ts b/apps/server/src/modules/authentication/commands/initialize-passwordless-authentication.ts deleted file mode 100644 index ebc71133..00000000 --- a/apps/server/src/modules/authentication/commands/initialize-passwordless-authentication.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** Initialize passwordless authentication. Is a command which will start new passwordless session in our system - * allowing user to perform two-step auth. */ -export interface InitializePasswordlessAuthentication { - /** How to send the code/link to the user. Use email to send the code/link using email, or sms to use SMS. */ - connection: "email" | "sms" | "pgp" - /** The user's email address. */ - email?: string - /** The user's phone number. */ - phoneNumber?: string - /** Use link to send a link or code to send a verification code. If null, a link will be sent. */ - send?: "code" | "link" -} \ No newline at end of file diff --git a/apps/server/src/modules/authentication/contract/authentication-service.ts b/apps/server/src/modules/authentication/contract/authentication-service.ts deleted file mode 100644 index 8e02ae10..00000000 --- a/apps/server/src/modules/authentication/contract/authentication-service.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { - ForbiddenException, - NotFoundException, -} from '@nestjs/common' -import {Result} from 'neverthrow' -import {AccessToken, RefreshToken} from "../../../kernel/modules/identity/jwt.js" - - -export abstract class AuthenticationService - { - /** - * Authenticates a user with their username and password. - * - * @param {string} username - The username of the user. - * @param {string} password - The password of the user. - * @param {Object} [metadata] - Additional metadata for the - * authentication. - * @param {string} [metadata.userAgent] - The user agent of the client. - * @param {string} [metadata.ipAddress] - The IP address of the client. - * @returns {Promise>} - The result of the - * authentication. If successful, it contains the domain ID, access - * token, and refresh token. If unsuccessful, it contains the - * error. - */ - public abstract authenticate( - username: string, - password: string, - ): Promise>; - - public abstract logout(): Promise; - - public abstract refreshToken(refreshToken: RefreshToken): Promise<{ - refreshToken: RefreshToken, - accessToken: AccessToken - }>; - } \ No newline at end of file diff --git a/apps/server/src/modules/authentication/controllers/authentication-controller.ts b/apps/server/src/modules/authentication/controllers/authentication-controller.ts deleted file mode 100644 index 897055ca..00000000 --- a/apps/server/src/modules/authentication/controllers/authentication-controller.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { - Body, - Controller, - Get, - Logger, - Post, - Req, - UseGuards, -} from '@nestjs/common' -import { - ApiBasicAuth, - ApiBearerAuth, - ApiBody, - ApiCreatedResponse, - ApiOkResponse, - ApiOperation, -} from '@nestjs/swagger' -import {Request} from 'express' -import {Account} from '../../account/entities/account.js' -import {AccountViewModel} from '../../account/view-model/account-view-model.js' -import {ApiAuthenticateNotFoundResponse} from '../commands/authenticate/api-authenticate-not-found-response.js' -import {AuthenticateCommand} from '../commands/authenticate/authenticate-command.js' -import {AuthenticationResponse} from '../dtos/authentication-response.js' -import {JwtAuthorizationGuard} from '../guards/jwt-authorization-guard.js' -import {LocalAuthorizationGuard} from '../guards/local-authorization-guard.js' -import {LocalAuthenticationService} from '../services/local-authentication-service.js' - - - -@Controller('authenticate') -export class AuthenticationController - { - private logger: Logger = new Logger('authentication::controller') - - - constructor(private authenticationService: LocalAuthenticationService) - { - } - - - // TODO: This request is using doubled comparison of password which - // extends the time of the request. - @ApiBasicAuth() @UseGuards(LocalAuthorizationGuard) @Post() @ApiOperation({ - operationId: 'authenticate', - summary : 'Basic Authentication', - description: 'Logs the user in with usage of a username and password.', - tags : ['authentication'], - }) @ApiCreatedResponse({ - description: 'The user has been successfully authenticated and session was created.', - type : AuthenticationResponse, - }) @ApiAuthenticateNotFoundResponse @ApiBody({type: AuthenticateCommand}) - async authenticate( - @Req() request: Request, - @Body() body: AuthenticateCommand, - ): Promise - { - const user: Account = request.user as unknown as Account - - this.logger.verbose(`Request performed using local_${user.id} for user ${user.username}`) - - const authenticatedOrException = await this.authenticationService.authenticate( - user.username, - body.password, - ) - - if (authenticatedOrException.isErr()) - { - throw authenticatedOrException.error - } - - const authenticationResult = authenticatedOrException.value - - return { - id : authenticationResult.accountId, - accessToken : authenticationResult.accessToken, - refreshToken: authenticationResult.refreshToken, - mfa : false, - } - } - - - @UseGuards(JwtAuthorizationGuard) @Get() @ApiBearerAuth() @ApiOperation({ - operationId: 'whoami', - summary : 'Who am I?', - description: 'Returns the current user', - tags : ['authentication'], - }) @ApiOkResponse({ - type : AccountViewModel, - description: 'Account was found in system, and returned.', - }) - async whoami(@Req() request: Request): Promise - { - const user: Account = request.user as unknown as Account - - return { - id : user.id, - username : user.username, - email : user.email.address, - emailVerified: user.email.isVerified, - } - } - } \ No newline at end of file diff --git a/apps/server/src/modules/authentication/controllers/passwordless-controller.ts b/apps/server/src/modules/authentication/controllers/passwordless-controller.ts deleted file mode 100644 index 4b83a748..00000000 --- a/apps/server/src/modules/authentication/controllers/passwordless-controller.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {Body, Controller, Post} from "@nestjs/common" -import {InitializePasswordlessAuthentication} from "../commands/initialize-passwordless-authentication.js"; - -// Auth0 is a good example of Authentication API https://auth0.com/docs/api/authentication - -// Passwordless connections do not require the user to remember a password. Instead, another mechanism is used to prove -// identity, such as a one-time code sent through email or SMS, every time the user logs in. - -/** Form of authentication that does not rely on a password as the first factor. */ -@Controller("passwordless") -export class PasswordlessController { - - @Post("start") - async defineAuthenticationChallenge(@Body() intializePasswordlessAuthentication : InitializePasswordlessAuthentication) : Promise { - return intializePasswordlessAuthentication - } - - @Post("solve") - async solveAuthenticationChallenge() : Promise { - return "start" - } -} \ No newline at end of file diff --git a/apps/server/src/modules/authentication/dtos/authentication-response.ts b/apps/server/src/modules/authentication/dtos/authentication-response.ts deleted file mode 100644 index 70820120..00000000 --- a/apps/server/src/modules/authentication/dtos/authentication-response.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {ApiProperty} from '@nestjs/swagger' -import {UniqueIdentifierApiSpecification} from '../../../common/libraries/identification/index.js' -import {ApiAccountMockup} from '../../../utilities/fixtures/api-account-mockup.js' - - -export const refreshTokenApiProperty = ApiProperty({ - name : 'refreshToken', - description: 'Refresh token.', -}) - -export const accessTokenApiProperty = ApiProperty({ - name : 'accessToken', - description: 'Access token.', - example : ApiAccountMockup._examples.jwts[0], - examples : [ - ApiAccountMockup._examples.jwts[0], ApiAccountMockup._examples.jwts[1], ApiAccountMockup._examples.jwts[2], - ], -}) - - -export class AuthenticationResponse { - @ApiProperty(UniqueIdentifierApiSpecification) id: string - - @ApiProperty({ - name : 'mfa', - description: 'Indicates if domain uses MFA. In case of true, additional steps are supposed to be made to activate access token.', - }) mfa: boolean - - @accessTokenApiProperty accessToken: string - - @refreshTokenApiProperty refreshToken: string -} diff --git a/apps/server/src/modules/authentication/guards/jwt-authorization-guard.ts b/apps/server/src/modules/authentication/guards/jwt-authorization-guard.ts deleted file mode 100644 index e435493b..00000000 --- a/apps/server/src/modules/authentication/guards/jwt-authorization-guard.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Injectable} from "@nestjs/common" -import {AuthGuard, IAuthGuard} from "@nestjs/passport" -import {AuthenticationStrategyType} from "../contract/authentication-strategy/authentication-strategy-type.js" - - - -@Injectable() -export class JwtAuthorizationGuard - extends AuthGuard(AuthenticationStrategyType.JWT) - implements IAuthGuard {} \ No newline at end of file diff --git a/apps/server/src/modules/authentication/guards/local-authorization-guard.ts b/apps/server/src/modules/authentication/guards/local-authorization-guard.ts deleted file mode 100644 index a17c985b..00000000 --- a/apps/server/src/modules/authentication/guards/local-authorization-guard.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {Injectable} from "@nestjs/common" -import {AuthGuard, IAuthGuard} from "@nestjs/passport" -import {AuthenticationStrategyType} from "../contract/authentication-strategy/authentication-strategy-type.js" - - - -@Injectable() - -export class LocalAuthorizationGuard - extends AuthGuard(AuthenticationStrategyType.LOCAL) - implements IAuthGuard { - -} \ No newline at end of file diff --git a/apps/server/src/modules/authentication/request-identity.ts b/apps/server/src/modules/authentication/request-identity.ts deleted file mode 100644 index 6c358bb4..00000000 --- a/apps/server/src/modules/authentication/request-identity.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - createParamDecorator, - ExecutionContext, -} from '@nestjs/common' -import {Request} from 'express' - - - -export const RequestIdentity = createParamDecorator((data: unknown, ctx: ExecutionContext) => - { - const request = ctx.switchToHttp().getRequest() - return request.user - }) diff --git a/apps/server/src/modules/authentication/services/local-authentication-service.ts b/apps/server/src/modules/authentication/services/local-authentication-service.ts deleted file mode 100644 index 02554c83..00000000 --- a/apps/server/src/modules/authentication/services/local-authentication-service.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - ForbiddenException, - Injectable, - NotFoundException, -} from '@nestjs/common' -import {setUser} from '@sentry/node' -import ms from 'ms' -import { - ok, - Result, -} from 'neverthrow' -import {AccessToken, JwtService, RefreshToken} from "../../../kernel/modules/identity/jwt.js" -import {PlainText} from "../../../kernel/modules/mailer/value-object/plain-text.js" -import type {AccountId} from '../../account/shared-kernel/account-id.js' -import {CredentialValidator} from '../../account/shared-kernel/credential-validator/credential-validator.js' -import type {Username} from '../../account/value-objects/username.js' -import {AuthenticationService} from '../contract/authentication-service.js' - - - -@Injectable() -export class LocalAuthenticationService - extends AuthenticationService -{ - - constructor(private credentialValidator: CredentialValidator, private jwt: JwtService) - { - super() - } - - - /** - * Authenticates a user with their username and password. - * - * @param {string} username - The username of the user. - * @param {string} password - The password of the user. - * @param {Object} [metadata] - Additional metadata for the - * authentication. - * @param {string} [metadata.userAgent] - The user agent of the client. - * @param {string} [metadata.ipAddress] - The IP address of the client. - * @returns {Promise>} - The result of the - * authentication. If successful, it contains the domain ID, access - * token, and refresh token. If unsuccessful, it contains the - * error. - */ - public async authenticate(username: Username, password: PlainText): Promise> - { - const accountOrException = await this.credentialValidator.validateCredentials(username, password) - - if (accountOrException.isErr()) - { - throw accountOrException.error - } - - const account = accountOrException.value - - setUser({ - id : account.id, - username: account.username, - email : account.email.address, - }) - - const tokens = await this.jwt.issueToken({id: account.id}) - - return ok({ - accountId : account.id, - accessToken : tokens[0], - refreshToken: tokens[1], - }) - } - - public async logout(): Promise - { - return Promise.resolve(undefined) - } - - public async refreshToken(refreshToken: string): Promise<{ - refreshToken: RefreshToken, - accessToken: AccessToken - }> - { - const refreshedToken = this.jwt.refresh(refreshToken as RefreshToken) - - return { - refreshToken: refreshToken as RefreshToken, - accessToken : refreshedToken, - } - } -} diff --git a/apps/server/src/modules/authentication/value-objects/user-agent.ts b/apps/server/src/modules/authentication/value-objects/user-agent.ts deleted file mode 100644 index a3443f1e..00000000 --- a/apps/server/src/modules/authentication/value-objects/user-agent.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {ImmutableClass} from "../../../common/libraries/dst/data-class/data-class.js" - - -// TODO: Add complete typing of user agent -export class UserAgent extends ImmutableClass { - client: { - type: "browser" - name: string - version: string - engine: string - engineVersion: string - } - os: { - name: string - version: string - platform: string - } - device: { - type: string - brand: string - model: string - } - bot: null -} \ No newline at end of file diff --git a/apps/server/src/modules/authentication/view-models/authenticated-account-view-model.ts b/apps/server/src/modules/authentication/view-models/authenticated-account-view-model.ts deleted file mode 100644 index 72f4b391..00000000 --- a/apps/server/src/modules/authentication/view-models/authenticated-account-view-model.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {ApiProperty} from '@nestjs/swagger' - - - -export class AuthenticatedAccountViewModel { - @ApiProperty({ - name : 'id', - description: 'ID of authenticated account.', - }) id: string - - @ApiProperty({ - name : 'mfa', - description: 'Indicates if account uses MFA. In case of true, additional steps are supposed to be made to' + ' activate access token.', - }) mfa: boolean - - @ApiProperty({ - name : 'accessToken', - description: 'Access token.', - }) accessToken: string - - @ApiProperty({ - name : 'refreshToken', - description: 'Refresh token.', - }) refreshToken: string -} \ No newline at end of file diff --git a/apps/server/src/modules/authorization/README.md b/apps/server/src/modules/authorization/README.md deleted file mode 100644 index 6293df8d..00000000 --- a/apps/server/src/modules/authorization/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# RBAC, ABAC, ACL, and PBAC - -Authorization determines whether a known individual can perform a specific action. After logging in, the user has been -authenticated, but they still need to be authorized each time they try to create a new user, assign a permission or -access a sensitive resource. - -- Actor: who is performing the action? User("Ariana") - -- Action: what are they trying to do? "push" - -- Resource: what are they doing it to? Repository("Acme App") - -https://medium.com/@dev.muhammet.ozen/role-based-access-control-in-nestjs-15c15090e47d -https://www.permit.io/blog/how-to-protect-a-url-inside-a-nestjs-app-using-rbac-authorization -https://medium.com/yavar/casl-roles-with-persisted-permissions-in-nestjs-152129f4a6fb -https://medium.com/yavar/casl-roles-with-persisted-permissions-in-nestjs-152129f4a6fb -https://github.com/nestjsx/nest-access-control/blob/master/README.md -https://medium.com/yavar/casl-roles-with-persisted-permissions-in-nestjs-152129f4a6fb -https://www.npmjs.com/package/nest-casl -https://github.com/getjerry/nest-casl/tree/master -https://medium.com/yavar/casl-roles-with-persisted-permissions-in-nestjs-152129f4a6fb -https://pop-code.github.io/nestjs-acl/ \ No newline at end of file diff --git a/apps/server/src/modules/authorization/authoriation.module.ts b/apps/server/src/modules/authorization/authoriation.module.ts deleted file mode 100644 index 044ff9ca..00000000 --- a/apps/server/src/modules/authorization/authoriation.module.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {Module} from "@nestjs/common" - - - -@Module({}) -export class AuthoriationModule {} \ No newline at end of file diff --git a/apps/server/src/modules/authorization/decorator/action-decorator.ts b/apps/server/src/modules/authorization/decorator/action-decorator.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/server/src/modules/authorization/decorator/resource-decorator.ts b/apps/server/src/modules/authorization/decorator/resource-decorator.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/server/src/modules/authorization/entities/grant.ts b/apps/server/src/modules/authorization/entities/grant.ts deleted file mode 100644 index ed4606f2..00000000 --- a/apps/server/src/modules/authorization/entities/grant.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -export interface Grant { - id: string - resourceGroup: string - resource: string - action: string -} \ No newline at end of file diff --git a/apps/server/src/modules/authorization/guard/authorization-guard.ts b/apps/server/src/modules/authorization/guard/authorization-guard.ts deleted file mode 100644 index 43ccc1ab..00000000 --- a/apps/server/src/modules/authorization/guard/authorization-guard.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {CanActivate, ExecutionContext} from "@nestjs/common" -import {Observable} from "rxjs" - - - -export class AuthorizationGuard - implements CanActivate { - public canActivate(context : ExecutionContext) : boolean | Promise | Observable { - // 1. Extract User - // 2. Fetch User groups and permissions - // 3. Define to which resource user is trying to get - // 4. Check if a user has access to such resource - // 5. Pass or Fail - return true - } -} \ No newline at end of file diff --git a/apps/server/src/modules/authorization/service/grant-service.ts b/apps/server/src/modules/authorization/service/grant-service.ts deleted file mode 100644 index 2cd0a06f..00000000 --- a/apps/server/src/modules/authorization/service/grant-service.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -export abstract class GrantService {} \ No newline at end of file diff --git a/apps/server/src/modules/authorization/value-object/access-rule.ts b/apps/server/src/modules/authorization/value-object/access-rule.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/server/src/modules/authorization/value-object/resource.ts b/apps/server/src/modules/authorization/value-object/resource.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/server/src/modules/example/action/README.md b/apps/server/src/modules/example/action/README.md deleted file mode 100644 index 5b6fc699..00000000 --- a/apps/server/src/modules/example/action/README.md +++ /dev/null @@ -1,11 +0,0 @@ -Actions represent the Use Cases of the Application (i.e., the actions that can be performed by a user or software in the -application). - -Actions can hold business logic and/or orchestrate the tasks to perform the business logic. - -Actions take data structures as inputs, manipulate them according to the business rules, and output new data structures. - -Actions should not be concerned with how the data is gathered or how it will be represented. - -By looking at the Actions folder of a Container, you can determine what use cases (features) your Container provides. By -looking at all the Actions, you can tell what an application can do. \ No newline at end of file diff --git a/apps/server/src/modules/example/contract/README.md b/apps/server/src/modules/example/contract/README.md deleted file mode 100644 index 18578ed4..00000000 --- a/apps/server/src/modules/example/contract/README.md +++ /dev/null @@ -1 +0,0 @@ -for defining interfaces to be implemented by classes \ No newline at end of file diff --git a/apps/server/src/modules/example/controller/README.md b/apps/server/src/modules/example/controller/README.md deleted file mode 100644 index 4ab25d17..00000000 --- a/apps/server/src/modules/example/controller/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Controllers are responsible for validating the request, serving the request data, and building a response. Validation -and response happen in separate classes but is triggered from the Controller. - -The Controllers concept is the same as in MVC (They are the C in MVC), but with limited and predefined responsibilities. \ No newline at end of file diff --git a/apps/server/src/modules/example/exception/README.md b/apps/server/src/modules/example/exception/README.md deleted file mode 100644 index a212dc4a..00000000 --- a/apps/server/src/modules/example/exception/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Exceptions are also a form of output that should be expected (like an API exception) and well defined. - -Exceptions are a way to handle errors in a well-defined and expected manner. In a well-designed application, exceptions -should be thrown whenever an error occurs that cannot be handled by the code in its current context. \ No newline at end of file diff --git a/apps/server/src/modules/example/job/README.md b/apps/server/src/modules/example/job/README.md deleted file mode 100644 index 7af14c29..00000000 --- a/apps/server/src/modules/example/job/README.md +++ /dev/null @@ -1 +0,0 @@ -for executing long-running tasks in the background \ No newline at end of file diff --git a/apps/server/src/modules/example/mail/README.md b/apps/server/src/modules/example/mail/README.md deleted file mode 100644 index 99f202cf..00000000 --- a/apps/server/src/modules/example/mail/README.md +++ /dev/null @@ -1 +0,0 @@ -for sending email messages \ No newline at end of file diff --git a/apps/server/src/modules/example/model/README.md b/apps/server/src/modules/example/model/README.md deleted file mode 100644 index 7f7ab23e..00000000 --- a/apps/server/src/modules/example/model/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Models provide an abstraction for data and represent the data in the database. (They are the M in MVC). - -Models are responsible for how the data should be handled and ensure that data is properly stored in the backend (e.g. -Database). \ No newline at end of file diff --git a/apps/server/src/modules/example/request/README.md b/apps/server/src/modules/example/request/README.md deleted file mode 100644 index 1c2ddf8e..00000000 --- a/apps/server/src/modules/example/request/README.md +++ /dev/null @@ -1,6 +0,0 @@ -Requests mainly serve the user input in the application. They are very useful to automatically apply the Validation and -Authorization rules. - -Requests are the best place to apply validations since the validation rules will be related to every request. Requests -can also check the Authorization, e.g., check if this user has access to this controller function (for example, check if -a specific user owns a product before deleting it, or check if this user is an admin to edit something). \ No newline at end of file diff --git a/apps/server/src/modules/example/routes/README.md b/apps/server/src/modules/example/routes/README.md deleted file mode 100644 index fe688d51..00000000 --- a/apps/server/src/modules/example/routes/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Routes are responsible for mapping all incoming HTTP requests to their controller's functions. When an HTTP request hits -the Application, the Endpoints match with the URL pattern and make the call to the corresponding Controller function. - diff --git a/apps/server/src/modules/example/subaction/README.md b/apps/server/src/modules/example/subaction/README.md deleted file mode 100644 index b95178f0..00000000 --- a/apps/server/src/modules/example/subaction/README.md +++ /dev/null @@ -1,9 +0,0 @@ -SubActions are designed to eliminate code duplication in Actions. SubActions allow Actions to share a sequence of Tasks, -while Tasks allows Actions to share a piece of functionality. - -The SubActions are created to solve a problem. Sometimes a big chunk of business logic is reused in multiple Actions, -and that code is already calling some Tasks. In such cases, the solution is to create a SubAction. - -For example, assuming an Action A1 is calling Task1, Task2 and Task3, and another Action A2 is calling Task2, Task3, -Task4, and Task5. Notice both Actions are calling Tasks 2 and 3. To eliminate code duplication, we can create a -SubAction that contains all the common code between both Actions. \ No newline at end of file diff --git a/apps/server/src/modules/example/task/README.md b/apps/server/src/modules/example/task/README.md deleted file mode 100644 index b270d909..00000000 --- a/apps/server/src/modules/example/task/README.md +++ /dev/null @@ -1,15 +0,0 @@ -Tasks are classes that hold shared business logic between multiple Actions across different Containers. - -Each Task is responsible for a small part of the logic, and it usually has a single function called run(). However, -Tasks can have more functions with explicit names if needed, which makes the Task class replace the concept of function -flags. - -Tasks are optional, but in most cases, you find yourself in need of them. For example, if you have Action 1 that needs -to find a record by its ID from the DB, then fires an Event. And you have an Action 2 that needs to find the same record -by its ID, then makes a call to an external API. Since both actions are performing the "find a record by ID" logic, we -can take that business logic and put it in its own class, that class is the Task. This Task is now reusable by both -Actions and any other Action you might create in the future. - -The rule is, whenever you see the possibility of reusing a piece of code from an Action, you should put that piece of -code in a Task. Do not blindly create Tasks for everything. You can always start by writing all the business logic in an -Action and only create a dedicated Task when you need to reuse it. Refactoring is essential to adapt to the code growth. \ No newline at end of file diff --git a/apps/server/src/modules/example/trait/README.md b/apps/server/src/modules/example/trait/README.md deleted file mode 100644 index 7815c4dd..00000000 --- a/apps/server/src/modules/example/trait/README.md +++ /dev/null @@ -1 +0,0 @@ -for sharing code between classes \ No newline at end of file diff --git a/apps/server/src/modules/example/transformer/README.md b/apps/server/src/modules/example/transformer/README.md deleted file mode 100644 index 8f4248d4..00000000 --- a/apps/server/src/modules/example/transformer/README.md +++ /dev/null @@ -1,5 +0,0 @@ -Transformers, short for Response Transformers, are equivalent to Views but for JSON Responses. While Views take data and -represent it in HTML, Transformers take data and represent it in JSON. - -Transformers are responsible for transforming Models into Arrays. They take a Model or a group of Models "Collection" -and convert it to a formatted serializable Array. \ No newline at end of file diff --git a/apps/server/src/modules/example/transporter/README.md b/apps/server/src/modules/example/transporter/README.md deleted file mode 100644 index e09f6026..00000000 --- a/apps/server/src/modules/example/transporter/README.md +++ /dev/null @@ -1 +0,0 @@ -for sending and receiving data between systems \ No newline at end of file diff --git a/apps/server/src/modules/example/value/README.md b/apps/server/src/modules/example/value/README.md deleted file mode 100644 index 4096a8dd..00000000 --- a/apps/server/src/modules/example/value/README.md +++ /dev/null @@ -1 +0,0 @@ -for representing simple value objects \ No newline at end of file diff --git a/apps/server/src/modules/example/view/README.md b/apps/server/src/modules/example/view/README.md deleted file mode 100644 index 654f6a34..00000000 --- a/apps/server/src/modules/example/view/README.md +++ /dev/null @@ -1,7 +0,0 @@ -Views contain the HTML served by your application. - -Their main goal is to separate the application logic from the presentation logic. (They are the V in MVC). - -Views receive data from the Controller and use it to generate the HTML that will be sent to the client's browser. Views -can also include template files that define the structure and layout of the HTML, making it easier to maintain -consistency across multiple pages. \ No newline at end of file diff --git a/apps/server/src/modules/group/controller/group-controller.ts b/apps/server/src/modules/group/controller/group-controller.ts deleted file mode 100644 index a9b4d510..00000000 --- a/apps/server/src/modules/group/controller/group-controller.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { - Body, - Controller, - Delete, - Get, - Param, - Post, - Put, -} from '@nestjs/common' -import { ApiResponse } from '@nestjs/swagger' -import { Group } from '../entities/group.js' - - - -/** - * Controller class for managing groups. - * - * @class - * @controller - * @param {string} path - The base URL path for all group endpoints. - */ -@Controller( 'group' ) -export class GroupController - { - @Post() @ApiResponse( { - status : 201, - description : 'The group has been successfully created.', - } ) - async create(@Body() createGroupDto : any) - { - } - - @Get() @ApiResponse( { - status : 200, - type : Group, - isArray : true, - } ) - async findAll() - { - - } - - @Get( ':id' ) @ApiResponse( { - status : 200, - type : Group, - } ) - async findOne(@Param( 'id' ) id : string) - { - - } - - @Put( ':id' ) @ApiResponse( { - status : 200, - description : 'The group has been successfully updated', - } ) - async update( - @Param( 'id' ) id : string, - @Body() updateGroupDto : any, - ) - { - } - - @Delete( ':id' ) @ApiResponse( { - status : 200, - description : 'The group has been successfully deleted', - } ) - async delete(@Param( 'id' ) id : string) - { - } - } \ No newline at end of file diff --git a/apps/server/src/modules/group/entities/group.ts b/apps/server/src/modules/group/entities/group.ts deleted file mode 100644 index 643f2486..00000000 --- a/apps/server/src/modules/group/entities/group.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { EntityBase } from '../../../common/libraries/domain/entity/entity-base.js' -import { GroupMember } from '../value-object/group-member.js' - - - -/** A group can consist of multiple users that all need similar levels of access to specific resources. By putting these users into a group, you can manage their access controls collectively rather than individually. This can significantly streamline the process of assigning and managing access permissions, especially in larger organizations. */ -export class Group - extends EntityBase - { - name : string - members : GroupMember[] - roles : string[] - } \ No newline at end of file diff --git a/apps/server/src/modules/group/group-module.ts b/apps/server/src/modules/group/group-module.ts deleted file mode 100644 index ae1c1011..00000000 --- a/apps/server/src/modules/group/group-module.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {Module} from "@nestjs/common"; -import {GroupController} from "./controller/group-controller.js"; -import {GroupRepository} from "./repository/group-repository.js"; -import {PrismaGroupRepository} from "./repository/prisma-group-repository.js"; -import {GroupService} from "./service/group-service.js"; - - - -@Module({ - controllers: [GroupController], - providers : [ - { - provide : GroupRepository, - useClass: PrismaGroupRepository, - }, - GroupService, - ], - exports : [GroupService], -}) -export class GroupModule {} \ No newline at end of file diff --git a/apps/server/src/modules/group/repository/group-repository.ts b/apps/server/src/modules/group/repository/group-repository.ts deleted file mode 100644 index 75390c47..00000000 --- a/apps/server/src/modules/group/repository/group-repository.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {Repository} from "../../../common/libraries/storage/index.js"; -import {Group} from "../entities/group.js"; - - - -export abstract class GroupRepository - extends Repository {} \ No newline at end of file diff --git a/apps/server/src/modules/group/service/group-service.ts b/apps/server/src/modules/group/service/group-service.ts deleted file mode 100644 index 625bf5ba..00000000 --- a/apps/server/src/modules/group/service/group-service.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {GroupMember} from "../value-object/group-member.js"; -import {Group} from "../entities/group.js"; -import {GroupRepository} from "../repository/group-repository.js"; -import {Injectable} from "@nestjs/common"; -import {ServiceAbstract} from "../../../common/libraries/services/service-abstract.js"; -import {AccountRegistered} from "../../account/events/account-registered.js"; -import {OnEvent} from "@nestjs/event-emitter"; - - - -@Injectable() -export class GroupService - extends ServiceAbstract { - constructor( - private groupRepository : GroupRepository, - ) - { - super(groupRepository) - } - - async addGroupMember(member : GroupMember, group : Group) : Promise { - throw new Error("Not implemented"); - } - - removeGroupMember(member : GroupMember, group : Group) { - throw new Error("Not implemented"); - } - - createGroup(group : any) { - throw new Error("Not implemented"); - } - - updateGroup(group : any) { - throw new Error("Not implemented"); - } - - deleteGroup(group : any) { - throw new Error("Not implemented"); - } - - @OnEvent("account.registered") - private onAccountRegistred(accountRegistred : AccountRegistered) { - // Add Account to "users" group - } -} \ No newline at end of file diff --git a/apps/server/src/modules/group/value-object/group-member.ts b/apps/server/src/modules/group/value-object/group-member.ts deleted file mode 100644 index 842e9084..00000000 --- a/apps/server/src/modules/group/value-object/group-member.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {AccountId} from "../../account/shared-kernel/account-id.js" - - - -export type GroupMember = AccountId \ No newline at end of file diff --git a/apps/server/src/modules/role/controller/role-controller.ts b/apps/server/src/modules/role/controller/role-controller.ts deleted file mode 100644 index f737123f..00000000 --- a/apps/server/src/modules/role/controller/role-controller.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - - -import { - Controller, - Delete, - Get, - Param, - Patch, - Post, -} from '@nestjs/common' -import { ApiOperation } from '@nestjs/swagger' - - - -@Controller( 'role' ) -export class RoleController - { - @ApiOperation( {operationId : 'list-roles'} ) @Get() getRoles() - { - // code to get roles - } - - @ApiOperation( {operationId : 'get-role'} ) @Get( '/:id' ) getRoleById(@Param( 'id' ) id : string) - { - // code to get role by id - } - - @ApiOperation( {operationId : 'patch-role'} ) @Patch( '/:id' ) updateRole(@Param( 'id' ) id : string) - { - // code to update role by id - } - - @ApiOperation( {operationId : 'create-role'} ) @Post( '' ) createRole() - { - // code to create a new role - } - - @ApiOperation( {operationId : 'delete-role'} ) @Delete( '/:id' ) deleteRole(@Param( 'id' ) id : string) - { - // code to delete role by id - } - } \ No newline at end of file diff --git a/apps/server/src/modules/role/entity/role.ts b/apps/server/src/modules/role/entity/role.ts deleted file mode 100644 index 1b078a54..00000000 --- a/apps/server/src/modules/role/entity/role.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { Grant } from '../../authorization/entities/grant.js' - - - -export interface Role - { - id : string - name : string - grants : Grant[] - } \ No newline at end of file diff --git a/apps/server/src/modules/role/repository/role-repository.ts b/apps/server/src/modules/role/repository/role-repository.ts deleted file mode 100644 index da467b81..00000000 --- a/apps/server/src/modules/role/repository/role-repository.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {Repository} from "../../../common/libraries/storage/index.js"; -import {Role} from "../entity/role.js"; - - - -export abstract class RoleRepository - extends Repository {} \ No newline at end of file diff --git a/apps/server/src/modules/role/role-module.ts b/apps/server/src/modules/role/role-module.ts deleted file mode 100644 index 1eca4be4..00000000 --- a/apps/server/src/modules/role/role-module.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {Module} from "@nestjs/common"; - - - -@Module({}) -export class RoleModule {} \ No newline at end of file diff --git a/apps/server/src/modules/role/seeder/role-seeder.ts b/apps/server/src/modules/role/seeder/role-seeder.ts deleted file mode 100644 index d482641d..00000000 --- a/apps/server/src/modules/role/seeder/role-seeder.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import {faker} from '@faker-js/faker' -import { - Injectable, - Logger, -} from '@nestjs/common' -import type {Prisma} from 'db' -import {SeederBase} from '../../../common/libraries/seeder/seeder-base.js' -import {PrismaService} from '../../../common/modules/resources/prisma/services/prisma-service.js' - - - -@Injectable() -export class RoleSeeder - extends SeederBase -{ - - constructor(private prismaService: PrismaService) - { - super(new Logger('seeder:role')) - - this.provideDataset([ - {name: 'Super Administrator'}, - {name: 'Administrator'}, - {name: 'Customer Support'}, - {name: 'User'}, - ]) - } - - - public async count(): Promise - { - return this.prismaService.role.count() - } - - - public async exists(input: Prisma.RoleCreateInput): Promise - { - const exists = await this.prismaService.role.findFirst({ - where: { - name: input.name, - }, - }) - - return exists !== null - } - - - public async fabricate(): Promise - { - return { - name: faker.word.sample(), - } - } - - - public save(input: Prisma.RoleCreateInput): Promise - { - return this.prismaService.role.create({ - data: input, - }) - } -} diff --git a/apps/server/src/modules/role/service/role-service.ts b/apps/server/src/modules/role/service/role-service.ts deleted file mode 100644 index c8f33d5c..00000000 --- a/apps/server/src/modules/role/service/role-service.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -export abstract class RoleService {} \ No newline at end of file diff --git a/apps/server/src/modules/user/entity/customer.ts b/apps/server/src/modules/user/entity/customer.ts deleted file mode 100644 index 3940e7ec..00000000 --- a/apps/server/src/modules/user/entity/customer.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { User } from './user.js' - - - -export class Customer - extends User - { - } \ No newline at end of file diff --git a/apps/server/src/modules/user/entity/user.ts b/apps/server/src/modules/user/entity/user.ts deleted file mode 100644 index 35ffcb35..00000000 --- a/apps/server/src/modules/user/entity/user.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { EntityBase } from '../../../common/libraries/domain/entity/entity-base.js' - - - -export class User - extends EntityBase - { - } \ No newline at end of file diff --git a/apps/server/src/modules/user/event/user-created.ts b/apps/server/src/modules/user/event/user-created.ts deleted file mode 100644 index 7add902d..00000000 --- a/apps/server/src/modules/user/event/user-created.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { DomainEvent } from '../../../common/libraries/domain/domain-event.js' -import type { User } from '../entity/user.js' - - - -export class UserCreated - extends DomainEvent - { - } \ No newline at end of file diff --git a/apps/server/src/modules/user/event/user-deleted.ts b/apps/server/src/modules/user/event/user-deleted.ts deleted file mode 100644 index a5df4a28..00000000 --- a/apps/server/src/modules/user/event/user-deleted.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { DomainEvent } from '../../../common/libraries/domain/domain-event.js' -import type { User } from '../entity/user.js' - - - -export class UserDeleted - extends DomainEvent - { - } \ No newline at end of file diff --git a/apps/server/src/modules/user/event/user-updated.ts b/apps/server/src/modules/user/event/user-updated.ts deleted file mode 100644 index 92a5b782..00000000 --- a/apps/server/src/modules/user/event/user-updated.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { DomainEvent } from '../../../common/libraries/domain/domain-event.js' -import type { User } from '../entity/user.js' - - - -export class UserUpdated - extends DomainEvent - { - } \ No newline at end of file diff --git a/apps/server/src/modules/user/service/user-service.ts b/apps/server/src/modules/user/service/user-service.ts deleted file mode 100644 index 81395dc4..00000000 --- a/apps/server/src/modules/user/service/user-service.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { OnEvent } from '@nestjs/event-emitter' -import { ServiceAbstract } from '../../../common/libraries/services/service-abstract.js' -import type { User } from '../entity/user.js' -import { UserCreated } from '../event/user-created.js' - - - -export class UserService - extends ServiceAbstract - { - async create(user : any) : Promise - { - throw Error( 'Not implemented' ) - } - - async getById(id : User['id']) : Promise - { - throw Error( 'Not implemented' ) - } - - async update(user : User) : Promise - { - throw Error( 'Not implemented' ) - } - - async delete(user : User) : Promise - { - throw Error( 'Not implemented' ) - } - - @OnEvent( UserCreated.namespace ) onUserCreatedCreateStripeCustomer(event : UserCreated) : void - { - // TODO: Create customer in Stripe. - } - } \ No newline at end of file diff --git a/apps/server/src/modules/user/subscriber/user-event-subscriber.ts b/apps/server/src/modules/user/subscriber/user-event-subscriber.ts deleted file mode 100644 index a4ce2a20..00000000 --- a/apps/server/src/modules/user/subscriber/user-event-subscriber.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Jakub Olan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { Injectable } from '@nestjs/common' -import { OnEvent } from '@nestjs/event-emitter' -import { AccountRegistered } from '../../account/events/account-registered.js' -import type { UserService } from '../service/user-service.js' - - - -@Injectable() -export class UserEventSubscriber - { - private service : UserService - - constructor(service : UserService) - { - this.service = service - } - - @OnEvent( AccountRegistered.namespace ) handleAccountRegistered(event : AccountRegistered) - { - // Create a profile based on account information. - // This is a sample implementation. - this.service.create( { - firstName : event.body, - lastName : event.body.username, - } ) - } - } \ No newline at end of file diff --git a/apps/server/src/routes/authenticate.ts b/apps/server/src/routes/authenticate.ts new file mode 100644 index 00000000..19fdf579 --- /dev/null +++ b/apps/server/src/routes/authenticate.ts @@ -0,0 +1,66 @@ +import {Controller} from '@nestjs/common' + + + +@Controller('authenticate') +export class Authenticate { + //private logger: Logger = new Logger('authentication::controller') + // + // + //constructor(private authenticationService: LocalAuthenticationService) { + //} + // + // + //// TODO: This request is using doubled comparison of password which + //// extends the time of the request. + //@ApiBasicAuth() @UseGuards(LocalAuthorizationGuard) @Post() @ApiOperation({ + // operationId: 'authenticate', + // summary: 'Basic Authentication', + // description: 'Logs the user in with usage of a username and password.', + // tags: ['authentication'], + //}) @ApiCreatedResponse({ + // description: 'The user has been successfully authenticated and session was created.', + // type: AuthenticationResponse, + //}) @ApiAuthenticateNotFoundResponse @ApiBody({type: AuthenticateCommand}) + //async authenticate(@Req() request: Request, @Body() body: AuthenticateCommand): Promise { + // const user: Account = request.user as unknown as Account + // + // this.logger.verbose(`Request performed using local_${user.id} for user ${user.username}`) + // + // const authenticatedOrException = await this.authenticationService.authenticate(user.username, body.password) + // + // if (authenticatedOrException.isErr()) { + // throw authenticatedOrException.error + // } + // + // const authenticationResult = authenticatedOrException.value + // + // return { + // id: authenticationResult.accountId, + // accessToken: authenticationResult.accessToken, + // refreshToken: authenticationResult.refreshToken, + // mfa: false, + // } + //} + // + // + //@UseGuards(JwtAuthorizationGuard) @Get() @ApiBearerAuth() @ApiOperation({ + // operationId: 'whoami', + // summary: 'Who am I?', + // description: 'Returns the current user', + // tags: ['authentication'], + //}) @ApiOkResponse({ + // type: AccountViewModel, + // description: 'Account was found in system, and returned.', + //}) + //async whoami(@Req() request: Request): Promise { + // const user: Account = request.user as unknown as Account + // + // return { + // id: user.id, + // username: user.username, + // email: user.email.address, + // emailVerified: user.email.isVerified, + // } + //} +} \ No newline at end of file diff --git a/apps/server/src/http/graphql/hello-world-resolver.ts b/apps/server/src/routes/graphql/hello-world-resolver.ts similarity index 100% rename from apps/server/src/http/graphql/hello-world-resolver.ts rename to apps/server/src/routes/graphql/hello-world-resolver.ts diff --git a/apps/server/src/modules/authentication/controllers/multi-factor-authenticaton-controller.ts b/apps/server/src/routes/mfa.ts similarity index 96% rename from apps/server/src/modules/authentication/controllers/multi-factor-authenticaton-controller.ts rename to apps/server/src/routes/mfa.ts index a2ba7322..15640013 100644 --- a/apps/server/src/modules/authentication/controllers/multi-factor-authenticaton-controller.ts +++ b/apps/server/src/routes/mfa.ts @@ -28,4 +28,4 @@ import {Controller} from "@nestjs/common"; @Controller("mfa") -export class MultiFactorAuthenticatonController {} \ No newline at end of file +export class Mfa {} \ No newline at end of file diff --git a/apps/server/src/routes/passwordless.ts b/apps/server/src/routes/passwordless.ts new file mode 100644 index 00000000..63b3bfc1 --- /dev/null +++ b/apps/server/src/routes/passwordless.ts @@ -0,0 +1,22 @@ +import {Controller} from "@nestjs/common" + +// Auth0 is a good example of Authentication API https://auth0.com/docs/api/authentication + +// Passwordless connections do not require the user to remember a password. Instead, another mechanism is used to prove +// identity, such as a one-time code sent through email or SMS, every time the user logs in. + +/** Form of authentication that does not rely on a password as the first factor. */ +@Controller("passwordless") +export class Passwordless { + + //@Post("start") + //async defineAuthenticationChallenge(): Promise<> { + // return intializePasswordlessAuthentication + //} + // + // + //@Post("solve") + //async solveAuthenticationChallenge(): Promise { + // return "start" + //} +} \ No newline at end of file diff --git a/apps/server/src/routes/v1/account.ts b/apps/server/src/routes/v1/account.ts new file mode 100644 index 00000000..829bee06 --- /dev/null +++ b/apps/server/src/routes/v1/account.ts @@ -0,0 +1,187 @@ +import {Body, Controller, Post} from '@nestjs/common'; +import {ApiBody, ApiOperation, ApiProperty} from '@nestjs/swagger'; +import {hash, verify} from 'argon2'; +import {Account, Prisma} from 'db'; +import {SignJWT} from 'jose'; +import {randomUUID} from 'node:crypto'; +import {PrismaService} from '../../common/modules/resources/prisma/services/prisma-service.js'; +import {__authConfig} from '../../configs/global/__config.js'; +import {ApiModel} from '../../utilities/docs-utils/swagger-api-model.js'; +import {ApiAccountMockup} from '../../utilities/fixtures/api-account-mockup.js'; + + + +export const ApiPropertyAccountEmail = ApiProperty({ + name: 'email', + description: 'The' + " account's email address", + example: ApiAccountMockup.email, + examples: ApiAccountMockup._examples.emails, + type: String, +}); + +export const ApiPropertyAccountPassword = ApiProperty({ + name: 'password', + description: "The account's username", + example: ApiAccountMockup.password, + examples: ApiAccountMockup._examples.passwords, +}); + +export const ApiPropertyAccountUsername = ApiProperty({ + name: 'username', + description: "The account's username", + example: ApiAccountMockup.username, + examples: ApiAccountMockup._examples.usernames, +}); + + +export class RegisterAccountCommand { + /** + * Represents an email address. + * @typedef {string} email + */ + @ApiPropertyAccountEmail email: string; + + /** + * The password variable is a string that represents a user's password. + * + * @type {string} + */ + @ApiPropertyAccountPassword password: string; + + /** + * Represents a username. + * @typedef {string} username + */ + @ApiPropertyAccountUsername username: string; +} + + +@ApiModel({ + name: 'Auhenticate', + description: 'asdasd', +}) +export class AuthenticateCommand { + /** + * Represents a username. + * @typedef {string} username + */ + @ApiPropertyAccountUsername username: string; + + /** + * The password variable is a string that represents a user's password. + * + * @type {string} + */ + @ApiPropertyAccountPassword password: string; +} + + +export interface RegisterAccount { + username: string; + password: string; +} + + +export interface RegisteredAccount { + id: string; + username: string; + password: string; +} + + +export interface BasicAuthenticate { + username: string; + password: string; +} + + +export interface AuthenticationSucceed { + accessToken: string; +} + + +@Controller('account') +export class AccountController { + private readonly accountRepository: Prisma.AccountDelegate; + + + constructor(prismaService: PrismaService) { + this.accountRepository = prismaService.account; + } + + + @Post() @ApiOperation({ + operationId: 'register', + summary: 'Register account', + tags: ['account'], + }) @ApiBody({type: RegisterAccountCommand}) + async registerAccount(@Body() body: unknown): Promise { + // TODO: Validate input + const registerAccount = body as RegisterAccount; + + let account: Account | null = await this.accountRepository.findFirst({ + where: {username: registerAccount.username}, + }); + + if (!account) { + const passwordHash = await hash(registerAccount.password); + + account = await this.accountRepository.create({ + data: { + username: registerAccount.username, + email: 'keinsell@protonmail.com', + password: passwordHash, + }, + }); + } else { + throw new Error('Account already exists'); + } + + return { + id: account.id, + username: account.username, + password: account.password, + }; + } + + + async basicAuthenticate(@Body() body: unknown): Promise { + // TODO: Validate this + const basicAuthenticate = body as BasicAuthenticate; + + const account = await this.accountRepository.findFirst({ + where: {username: basicAuthenticate.username}, + }); + + if (!account) { + throw new Error('Account not found'); + } + + if (!( + await verify(account.password, basicAuthenticate.password) + )) { + throw new Error('Invalid password'); + } + + const key = new TextEncoder().encode(__authConfig.JWT_SECRET); + + const payload = { + jti: randomUUID(), + sub: account.id, + aud: 'access', + }; + + const plainAccessToken = new SignJWT({...payload}); + plainAccessToken.setExpirationTime('1h'); + plainAccessToken.setProtectedHeader({ + b64: true, + alg: 'HS256', + }); + + const accessToken = await plainAccessToken.sign(key); + + return { + accessToken: accessToken, + }; + } +} diff --git a/apps/server/src/types/process-env.d.ts b/apps/server/src/types/process-env.d.ts index fdbda2ed..24e64904 100644 --- a/apps/server/src/types/process-env.d.ts +++ b/apps/server/src/types/process-env.d.ts @@ -1,10 +1,6 @@ // Extend Node's process.env variables -declare namespace NodeJS -{ - export interface ProcessEnv - { - UV_THREADPOOL_SIZE: number | undefined - +declare namespace NodeJS { + export interface ProcessEnv { // Add your environment variables here [key: string]: string | undefined; } diff --git a/apps/server/src/utilities/testcontainers/run-testcontainers.ts b/apps/server/src/utilities/testcontainers/run-testcontainers.ts index 90806050..c6e35aa7 100644 --- a/apps/server/src/utilities/testcontainers/run-testcontainers.ts +++ b/apps/server/src/utilities/testcontainers/run-testcontainers.ts @@ -1,102 +1,77 @@ -import {$} from 'execa' -import process from 'node:process' -import {Readable} from 'stream' -import { - GenericContainer, - type StartedTestContainer, -} from 'testcontainers' -import {LogWaitStrategy} from 'testcontainers/build/wait-strategies/log-wait-strategy.js' -import {__appConfig} from '../../configs/global/__config.js' -import {CombinedLogger} from '../../kernel/modules/logger/logger.js' +import {$} from 'execa' +import process from 'node:process' +import {Readable} from 'stream' +import {GenericContainer, type StartedTestContainer} from 'testcontainers' +import {LogWaitStrategy} from 'testcontainers/build/wait-strategies/log-wait-strategy.js' +import {__appConfig} from '../../configs/global/__config.js' +import {CombinedLogger} from "../../core/modules/logger/logger.js" const postgres = new GenericContainer('postgres:latest') - .withEnvironment({ - POSTGRES_USER : 'test_user', - POSTGRES_PASSWORD: 'test_password', - POSTGRES_DB : 'test_db', - }) - .withExposedPorts(5432) - .withLogConsumer((stream: Readable) => - { - const logger = new CombinedLogger('container::postgresql') - stream.on('data', (data) => - { - const logLine = data.toString().trim() - if (logLine) - { - logger.debug(logLine) - } - }) - }) - .withWaitStrategy(new LogWaitStrategy('database system is ready to accept connections', 2)) +.withEnvironment({ + POSTGRES_USER: 'test_user', + POSTGRES_PASSWORD: 'test_password', + POSTGRES_DB: 'test_db', +}) +.withExposedPorts(5432) +.withLogConsumer((stream: Readable) => { + const logger = new CombinedLogger('container::postgresql') + stream.on('data', (data) => { + const logLine = data.toString().trim() + if (logLine) { + logger.debug(logLine) + } + }) +}) +.withWaitStrategy(new LogWaitStrategy('database system is ready to accept connections', 2)) const redis = new GenericContainer('redis:latest') - .withExposedPorts(6379) - .withLogConsumer((stream: Readable) => - { - const logger = new CombinedLogger('container::redis') - stream.on('data', (data) => - { - const logLine = data.toString().trim() - if (logLine) - { - logger.debug(logLine) - } - }) - }) - .withWaitStrategy(new LogWaitStrategy('Ready to accept connections', 2)) +.withExposedPorts(6379) +.withLogConsumer((stream: Readable) => { + const logger = new CombinedLogger('container::redis') + stream.on('data', (data) => { + const logLine = data.toString().trim() + if (logLine) { + logger.debug(logLine) + } + }) +}) +.withWaitStrategy(new LogWaitStrategy('Ready to accept connections', 2)) export var __RUNNING_CONTAINER: StartedTestContainer[] = [] -export class ContainerEnvironment -{ +export class ContainerEnvironment { private containers: { - name: string, - container: GenericContainer + name: string, container: GenericContainer }[] = [] - constructor() - { + constructor() { this.containers.push({ - name : 'postgres', - container: postgres, - }) + name: 'postgres', + container: postgres, + }) } - static async run() - { - if (__appConfig.FEATURE_USE_DOCKER_TESTCONTAINERS) - { - new CombinedLogger('environment').debug('Application is running in' - + ' \'testing\' environment' - + ' which means there will' - + ' be mocked up' - + ' dependencies such as' - + ' databases. Please a' - + ' wait a longer while to' - + ' let application setup' - + ' everything.') + static async run() { + if (__appConfig.FEATURE_USE_DOCKER_TESTCONTAINERS) { + new CombinedLogger('environment').debug('Application is running in' + ' \'testing\' environment' + ' which means there will' + ' be mocked up' + ' dependencies such as' + ' databases. Please a' + ' wait a longer while to' + ' let application setup' + ' everything.') await new ContainerEnvironment().startContainers() } } - private async startContainers() - { - for await (const container of this.containers) - { + private async startContainers() { + for await (const container of this.containers) { const runningContainer = await container.container.start() const logger = new CombinedLogger('testcontainer') logger.info(`Started ${container.name}`) - if (container.name === 'postgres') - { + if (container.name === 'postgres') { // Get container information const host = runningContainer.getHost() const port = runningContainer.getMappedPort(5432) @@ -117,27 +92,21 @@ export class ContainerEnvironment } -export async function prepareContainerizedDevelopmentEnvironment() -{ +export async function prepareContainerizedDevelopmentEnvironment() { // We definitely do not want to use test/dev things on production - if (process.env.NODE_ENV === 'production') - { + if (process.env.NODE_ENV === 'production') { return } // Check if we are running in a CI environment - if (process.env.CI) - { + if (process.env.CI) { return } // Check if HOST is running Docker that we can use - try - { + try { await $`docker info` - } - catch (e) - { + } catch (e) { new CombinedLogger('environment').warn('Docker is not running on your machine. Skipping containerized development environment setup.') return } diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 683cb57a..2ec9b551 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": false, @@ -18,9 +22,19 @@ } ], "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ] + }, + "strictNullChecks": true }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] }