├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── front.ts ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── eicrud.config.service.ts ├── main.ts ├── roles.ts └── services │ ├── article │ ├── article.entity.ts │ ├── article.hooks.ts │ ├── article.security.ts │ ├── article.service.spec.ts │ ├── article.service.ts │ └── cmds.ts │ ├── comment │ ├── cmds.ts │ ├── comment.entity.ts │ ├── comment.hooks.ts │ ├── comment.security.ts │ ├── comment.service.spec.ts │ └── comment.service.ts │ ├── email │ ├── cmds.ts │ ├── email.entity.ts │ ├── email.hooks.ts │ ├── email.security.ts │ ├── email.service.spec.ts │ └── email.service.ts │ ├── index.ts │ └── user │ ├── cmds.ts │ ├── cmds │ ├── change_password │ │ └── change_password.security.ts │ ├── check_jwt │ │ └── check_jwt.security.ts │ ├── create_account │ │ └── create_account.security.ts │ ├── login │ │ └── login.security.ts │ ├── logout │ │ └── logout.security.ts │ ├── logout_everywhere │ │ └── logout_everywhere.security.ts │ ├── reset_password │ │ └── reset_password.security.ts │ ├── send_password_reset_email │ │ └── send_password_reset_email.security.ts │ ├── send_verification_email │ │ └── send_verification_email.security.ts │ ├── timeout_user │ │ └── timeout_user.security.ts │ └── verify_email │ │ └── verify_email.security.ts │ ├── user.entity.ts │ ├── user.hooks.ts │ ├── user.security.ts │ ├── user.service.spec.ts │ └── user.service.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 eicrud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build your CRUD backend in 20min with Eicrud + MongoDB 2 | 3 | In this tutorial, I'm going to demonstrate how [Eicrud](eicrud.com) can save you tons of time when building your Node.js backend. 4 | 5 | We're going to make a simple blogging platform where users can read, publish and comment on articles. 6 | 7 | ## Prerequisites 8 | 9 | - [Node.js](https://nodejs.org/en/download/package-manager) >= v18 10 | - [MongoDB](https://www.mongodb.com/docs/manual/installation/) listening on localhost:27017 11 | - [ts-node](https://www.npmjs.com/package/ts-node) or your favorite frontend framework 12 | 13 | --- 14 | 15 | ## Installation 16 | 17 | First, let's set up our app by following [Eicrud's documentation](https://docs.eicrud.com/installation/). 18 | 19 | We create a new [NestJS](https://nestjs.com/) application. 20 | 21 | ```bash 22 | npm i -g @nestjs/cli 23 | nest new myblog 24 | cd myblog 25 | ``` 26 | Then install [Eicrud](eicrud.com) on top of it. 27 | ```bash 28 | npm i -g @eicrud/cli 29 | eicrud setup mongo myblog 30 | ``` 31 | Finally, we generate a secret key and store it in a `.env` file at the root of our project. 32 | 33 | ```bash 34 | echo "JWT_SECRET=$(node -e "console.log(require('crypto').randomBytes(256).toString('base64'));")" > .env 35 | ``` 36 | Now we already have a well-structured project! You can check that everything is working. 37 | ```bash 38 | npm run start 39 | ``` 40 | 41 | ## Services 42 | 43 | The [CLI](https://www.npmjs.com/package/@eicrud/cli) already generated a `User` service for us in `src\services\user`. All we need to add to our application is an `Article` service and a `Comment` service. 44 | 45 | 46 | ```bash 47 | eicrud generate service Article 48 | eicrud generate service Comment 49 | ``` 50 | 51 | 52 | ## Model definition 53 | 54 | Let's edit our entities to our liking. 55 | 56 | Update the file `src\services\article\article.entity.ts` with the following. 57 | 58 | ```typescript 59 | import { Entity, PrimaryKey, Property, 60 | ManyToOne } from "@mikro-orm/core"; 61 | import { IsString, IsOptional } from "class-validator"; 62 | import { CrudEntity } from "@eicrud/core/crud"; 63 | import { User } from "../user/user.entity"; 64 | import { $MaxSize } from "@eicrud/core/validation"; 65 | 66 | 67 | @Entity() 68 | export class Article implements CrudEntity { 69 | 70 | @PrimaryKey({ name: '_id' }) 71 | @IsString() 72 | @IsOptional() 73 | id: string; 74 | 75 | @ManyToOne(() => User) 76 | @IsString() 77 | author: User | string; 78 | 79 | @Property() 80 | @IsString() 81 | title: string; 82 | 83 | @Property() 84 | @IsString() 85 | @$MaxSize(1000) 86 | content: string; 87 | 88 | @Property() 89 | createdAt: Date; 90 | 91 | @Property() 92 | updatedAt: Date; 93 | 94 | } 95 | ``` 96 | 97 | And don't forget our `Comment` entity in `src\services\comment\comment.entity.ts`. 98 | 99 | ```typescript 100 | import { Entity, PrimaryKey, Property, ManyToOne } from "@mikro-orm/core"; 101 | import { IsString, IsOptional } from "class-validator"; 102 | import { CrudEntity } from "@eicrud/core/crud"; 103 | import { Article } from "../article/article.entity"; 104 | import { User } from "../user/user.entity"; 105 | import { $MaxSize } from "@eicrud/core/validation"; 106 | 107 | 108 | @Entity() 109 | export class Comment implements CrudEntity { 110 | 111 | @PrimaryKey({ name: '_id' }) 112 | @IsString() 113 | @IsOptional() 114 | id: string; 115 | 116 | @ManyToOne(() => Article) 117 | @IsString() 118 | article: Article | string; 119 | 120 | @ManyToOne(() => User) 121 | @IsString() 122 | author: User | string; 123 | 124 | @Property() 125 | @IsString() 126 | @$MaxSize(200) 127 | content: string; 128 | 129 | @Property() 130 | createdAt: Date; 131 | 132 | @Property() 133 | updatedAt: Date; 134 | 135 | } 136 | ``` 137 | 138 | ## Access rules 139 | Now that we have our model, let's update the access rules in `src\services\article\article.security.ts`. We want everyone to read our articles, but only their author should be able to modify them. 140 | 141 | ```typescript 142 | guest: { 143 | async defineCRUDAbility(can, cannot, ctx) { 144 | // guests can read all articles 145 | can('read', article) 146 | } 147 | }, 148 | user: { 149 | async defineCRUDAbility(can, cannot, ctx) { 150 | // user can manage their own articles 151 | can('crud', article, { author: ctx.userId }) 152 | } 153 | } 154 | ``` 155 | It's the same idea for comments, except this time we don't want users to modify their comments after creation. Let's change the rules in `src\services\comment\comment.security.ts`. 156 | 157 | ```typescript 158 | guest: { 159 | async defineCRUDAbility(can, cannot, ctx) { 160 | // guests can read all comments 161 | can('read', comment) 162 | } 163 | }, 164 | user: { 165 | async defineCRUDAbility(can, cannot, ctx) { 166 | // user create and delete their own comments 167 | can('cd', comment, {author: ctx.userId}) 168 | } 169 | } 170 | ``` 171 | ## Commands 172 | Eicrud has [prebuilt commands](https://docs.eicrud.com/user/service/#account-creation) for account creation and login. We just need to allow their usage in `src\services\user\cmds\create_account\create_account.security.ts`. 173 | 174 | ```typescript 175 | guest: { 176 | async defineCMDAbility(can, cannot, ctx) { 177 | // can create an account with the role 'user' 178 | can(create_account, user, {role: 'user'}) 179 | } 180 | } 181 | ``` 182 | 183 | And in `src\services\user\cmds\login\login.security.ts`. 184 | ```typescript 185 | guest: { 186 | async defineCMDAbility(can, cannot, ctx) { 187 | // Define abilities for user 188 | can(login, user) 189 | } 190 | } 191 | ``` 192 | Now our backend is ready for action! Let's start the server and switch to another terminal. 193 | ```bash 194 | npm run start 195 | ``` 196 | 197 | ## Calling the API 198 | To simulate our frontend we're going to use [ts-node](https://www.npmjs.com/package/ts-node), but you can use your favorite framework or tools like [esbuild](https://esbuild.github.io/) if you prefer. 199 | 200 | ```bash 201 | npm i -g ts-node # make sur to install it globally 202 | ``` 203 | 204 | Let's install [Eicrud's client](https://docs.eicrud.com/client/setup). 205 | ```bash 206 | npm i @eicrud/client 207 | ``` 208 | Create a file `front.ts` and start with a helper class to make Eicrud's client dynamic. 209 | 210 | ```typescript 211 | import { ClientConfig, CrudClient } from "@eicrud/client"; 212 | 213 | export class DynamicClient { 214 | crudClient: CrudClient; 215 | 216 | constructor() { 217 | const initalConfig: ClientConfig = { 218 | url: 'http://localhost:3000', 219 | serviceName: 'user', 220 | } 221 | this.crudClient = new CrudClient(initalConfig); 222 | } 223 | 224 | get(serviceName){ 225 | this.crudClient.config.serviceName = serviceName; 226 | return this.crudClient; 227 | } 228 | } 229 | ``` 230 | Now we can create our first account and publish an article. 231 | 232 | ```typescript 233 | // ... following in front.ts 234 | 235 | import { ICreateAccountDto } from '@eicrud/shared/interfaces'; 236 | import { ILoginDto } from '@eicrud/shared/interfaces'; 237 | import { Article } from "./src/services/article/article.entity"; 238 | 239 | async function main() { 240 | const client = new DynamicClient(); 241 | 242 | // Create account and publish article 243 | const dto: ICreateAccountDto = { 244 | email: 'new.user@mail.com', 245 | password: 'p4ssw0rd', 246 | role: 'user', 247 | }; 248 | const { userId } = await client.get('user').cmdS('create_account', dto); 249 | 250 | console.log('User created!', userId); 251 | 252 | const loginDto: ILoginDto = { 253 | email: 'new.user@mail.com', 254 | password: 'p4ssw0rd', 255 | }; 256 | 257 | await client.get('user').login(loginDto); 258 | 259 | const newArticle: Partial
= { 260 | title: 'New article', 261 | content: 'This is a new article', 262 | author: userId, 263 | }; 264 | 265 | await client.get('article').create(newArticle); 266 | 267 | // Let's check that our article was created 268 | const userArticles = await client.get('article').find({ author: userId }); 269 | console.log('User articles:', userArticles); 270 | } 271 | 272 | main(); 273 | ``` 274 | And let's run our frontend to test our API. 275 | ```bash 276 | ts-node front.ts 277 | ``` 278 | That's all for the basics! 279 | 280 | Take note of all the [CRUD operations](https://docs.eicrud.com/client/operations) available to the client. You can use any of them to query the `'article'` or `'comment'` service. 281 | 282 | ## Going further 283 | 284 | Eicrud is in active development and has many features to make your life easier. You can learn all about it in the [documentation](https://docs.eicrud.com). If you find any issue don't hesitate to [create one](https://github.com/eicrud/eicrud/issues) and I'll look into it. 285 | 286 | Happy coding! 287 | 288 | --- 289 | 290 | Original article: https://dev.to/acrosett/build-your-crud-backend-in-20min-with-eicrud-mongodb-4n3p 291 | -------------------------------------------------------------------------------- /front.ts: -------------------------------------------------------------------------------- 1 | import { ClientConfig, CrudClient } from "@eicrud/client"; 2 | 3 | export class DynamicClient { 4 | crudClient: CrudClient; 5 | 6 | constructor() { 7 | const initalConfig: ClientConfig = { 8 | url: 'http://localhost:3000', 9 | serviceName: 'user', 10 | } 11 | this.crudClient = new CrudClient(initalConfig); 12 | } 13 | 14 | get(serviceName){ 15 | this.crudClient.config.serviceName = serviceName; 16 | return this.crudClient; 17 | } 18 | } 19 | 20 | import { ICreateAccountDto } from '@eicrud/shared/interfaces'; 21 | import { ILoginDto } from '@eicrud/shared/interfaces'; 22 | import { Article } from "./src/services/article/article.entity"; 23 | 24 | async function main() { 25 | const client = new DynamicClient(); 26 | 27 | // Create account and publish article 28 | const dto: ICreateAccountDto = { 29 | email: 'new.user@mail.com', 30 | password: 'p4ssw0rd', 31 | role: 'user', 32 | }; 33 | const { userId } = await client.get('user').cmdS('create_account', dto); 34 | 35 | console.log('User created!', userId); 36 | 37 | const loginDto: ILoginDto = { 38 | email: 'new.user@mail.com', 39 | password: 'p4ssw0rd', 40 | }; 41 | 42 | await client.get('user').login(loginDto); 43 | 44 | const newArticle: Partial
= { 45 | title: 'New article', 46 | content: 'This is a new article', 47 | author: userId, 48 | }; 49 | 50 | await client.get('article').create(newArticle); 51 | 52 | // Let's check that our article was created 53 | const userArticles = await client.get('article').find({ author: userId }); 54 | console.log('User articles:', userArticles); 55 | } 56 | 57 | main(); -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myblog", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@eicrud/client": "^0.7.6", 24 | "@eicrud/core": "^0.7.6", 25 | "@eicrud/mongodb": "^0.7.6", 26 | "@nestjs/common": "^10.0.0", 27 | "@nestjs/config": "^3.2.2", 28 | "@nestjs/core": "^10.0.0", 29 | "@nestjs/platform-express": "^10.0.0", 30 | "reflect-metadata": "^0.2.0", 31 | "rxjs": "^7.8.1" 32 | }, 33 | "devDependencies": { 34 | "@nestjs/cli": "^10.0.0", 35 | "@nestjs/schematics": "^10.0.0", 36 | "@nestjs/testing": "^10.0.0", 37 | "@types/express": "^4.17.17", 38 | "@types/jest": "^29.5.2", 39 | "@types/node": "^20.3.1", 40 | "@types/supertest": "^6.0.0", 41 | "@typescript-eslint/eslint-plugin": "^6.0.0", 42 | "@typescript-eslint/parser": "^6.0.0", 43 | "eslint": "^8.42.0", 44 | "eslint-config-prettier": "^9.0.0", 45 | "eslint-plugin-prettier": "^5.0.0", 46 | "jest": "^29.5.0", 47 | "prettier": "^3.0.0", 48 | "source-map-support": "^0.5.21", 49 | "supertest": "^6.3.3", 50 | "ts-jest": "^29.1.0", 51 | "ts-loader": "^9.4.3", 52 | "ts-node": "^10.9.2", 53 | "tsconfig-paths": "^4.2.0", 54 | "typescript": "^5.1.3" 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "js", 59 | "json", 60 | "ts" 61 | ], 62 | "rootDir": "src", 63 | "testRegex": ".*\\.spec\\.ts$", 64 | "transform": { 65 | "^.+\\.(t|j)s$": "ts-jest" 66 | }, 67 | "collectCoverageFrom": [ 68 | "**/*.(t|j)s" 69 | ], 70 | "coverageDirectory": "../coverage", 71 | "testEnvironment": "node" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { CRUDServices } from './services/index'; 2 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 3 | import { EICRUDModule } from '@eicrud/core'; 4 | import { CRUDEntities } from './services/index'; 5 | import { CRUD_CONFIG_KEY } from '@eicrud/core/config'; 6 | import { MyConfigService } from './eicrud.config.service'; 7 | import { ConfigModule } from '@nestjs/config'; 8 | import { MongoDriver } from '@mikro-orm/mongodb'; 9 | import { Module } from '@nestjs/common'; 10 | import { AppController } from './app.controller'; 11 | import { AppService } from './app.service'; 12 | 13 | @Module({ 14 | imports: [ 15 | ConfigModule.forRoot(), 16 | MikroOrmModule.forRoot({ 17 | entities: [...CRUDEntities], 18 | driver: MongoDriver, 19 | dbName: "myblog-db", 20 | }), 21 | EICRUDModule.forRoot(), 22 | ], 23 | controllers: [AppController], 24 | providers: [ 25 | ...CRUDServices, 26 | { 27 | provide: CRUD_CONFIG_KEY, 28 | useClass: MyConfigService, 29 | }, 30 | AppService 31 | ], 32 | }) 33 | export class AppModule {} 34 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/eicrud.config.service.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager, MikroORM } from "@mikro-orm/core"; 2 | import { Injectable } from "@nestjs/common"; 3 | import { BasicMemoryCache, CrudConfigService } from "@eicrud/core/config"; 4 | import { EmailService } from "./services/email/email.service"; 5 | import { UserService } from "./services/user/user.service"; 6 | import { MongoDbAdapter } from '@eicrud/mongodb' 7 | import { roles } from "./roles"; 8 | 9 | 10 | @Injectable() 11 | export class MyConfigService extends CrudConfigService { 12 | 13 | constructor( 14 | public userService: UserService, 15 | public entityManager: EntityManager, 16 | public emailService: EmailService, 17 | protected orm: MikroORM 18 | ) { 19 | super({ 20 | userService, 21 | entityManager, 22 | emailService, 23 | jwtSecret: process.env.JWT_SECRET, 24 | cacheManager: new BasicMemoryCache(), 25 | orm, 26 | id_field: 'id', 27 | dbAdapter: new MongoDbAdapter(), 28 | }); 29 | 30 | this.addRoles(roles); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { FastifyAdapter } from '@nestjs/platform-fastify'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule, new FastifyAdapter()); 7 | await app.listen(3000); 8 | } 9 | bootstrap(); 10 | -------------------------------------------------------------------------------- /src/roles.ts: -------------------------------------------------------------------------------- 1 | import { CrudRole } from "@eicrud/core/config"; 2 | 3 | export const roles: CrudRole[] = [ 4 | { 5 | name: 'admin', 6 | isAdminRole: true, 7 | canMock: true, 8 | inherits: ['user'] 9 | }, 10 | { 11 | name: 'user', 12 | inherits: ['guest'] 13 | }, 14 | { 15 | name: 'guest' 16 | }, 17 | ] -------------------------------------------------------------------------------- /src/services/article/article.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryKey, Property, 2 | ManyToOne } from "@mikro-orm/core"; 3 | import { IsString, IsOptional } from "class-validator"; 4 | import { CrudEntity } from "@eicrud/core/crud"; 5 | import { User } from "../user/user.entity"; 6 | import { $MaxSize } from "@eicrud/core/validation"; 7 | 8 | 9 | @Entity() 10 | export class Article implements CrudEntity { 11 | 12 | @PrimaryKey({ name: '_id' }) 13 | @IsString() 14 | @IsOptional() 15 | id: string; 16 | 17 | @ManyToOne(() => User) 18 | @IsString() 19 | author: User | string; 20 | 21 | @Property() 22 | @IsString() 23 | title: string; 24 | 25 | @Property() 26 | @IsString() 27 | @$MaxSize(1000) 28 | content: string; 29 | 30 | @Property() 31 | createdAt: Date; 32 | 33 | @Property() 34 | updatedAt: Date; 35 | 36 | } -------------------------------------------------------------------------------- /src/services/article/article.hooks.ts: -------------------------------------------------------------------------------- 1 | import { CrudContext, CrudHooks } from "@eicrud/core/crud"; 2 | import { Article } from "./article.entity"; 3 | import { ArticleService } from "./article.service"; 4 | import { FindResponseDto } from "@eicrud/shared/interfaces"; 5 | 6 | export class ArticleHooks extends CrudHooks
{ 7 | 8 | override async beforeCreateHook(this: ArticleService, data: Partial
[], ctx: CrudContext): Promise[]> { 9 | // before Article creation 10 | 11 | return data; 12 | } 13 | 14 | override async afterCreateHook(this: ArticleService, result: any[], data: Partial
[], ctx: CrudContext): Promise { 15 | // after Article creation 16 | 17 | return result; 18 | } 19 | 20 | override async beforeReadHook(this: ArticleService, query: Partial
, ctx: CrudContext): Promise> { 21 | // before Article read 22 | 23 | return query; 24 | } 25 | 26 | override async afterReadHook(this: ArticleService, result, query: Partial
, ctx: CrudContext): Promise> { 27 | // after Article read 28 | 29 | return result; 30 | } 31 | 32 | override async beforeUpdateHook(this: ArticleService, 33 | updates: { query: Partial
; data: Partial
}[], 34 | ctx: CrudContext, 35 | ): Promise<{ query: Partial
; data: Partial
}[]> { 36 | // before Article update 37 | 38 | return updates; 39 | } 40 | 41 | override async afterUpdateHook(this: ArticleService, 42 | results: any[], 43 | updates: { query: Partial
; data: Partial
}[], 44 | ctx: CrudContext, 45 | ): Promise { 46 | // after Article update 47 | 48 | return results; 49 | } 50 | 51 | override async beforeDeleteHook(this: ArticleService, query: Partial
, ctx: CrudContext): Promise> { 52 | // before Article delete 53 | 54 | return query; 55 | } 56 | 57 | override async afterDeleteHook(this: ArticleService, result: any, query: Partial
, ctx: CrudContext): Promise { 58 | // after Article delete 59 | 60 | return result; 61 | } 62 | 63 | override async errorControllerHook(this: ArticleService, error: any, ctx: CrudContext): Promise { 64 | //after Article error 65 | 66 | } 67 | }; 68 | 69 | export const hooks = new ArticleHooks(); 70 | 71 | -------------------------------------------------------------------------------- /src/services/article/article.security.ts: -------------------------------------------------------------------------------- 1 | import { CrudSecurity } from "@eicrud/core/config"; 2 | import { serviceCmds } from "./cmds"; 3 | 4 | export function getSecurity(article: string): CrudSecurity { 5 | return { 6 | rolesRights: { 7 | guest: { 8 | async defineCRUDAbility(can, cannot, ctx) { 9 | // guests can read all articles 10 | can('read', article) 11 | } 12 | }, 13 | user: { 14 | async defineCRUDAbility(can, cannot, ctx) { 15 | // user can manage their own articles 16 | can('crud', article, {author: ctx.userId}) 17 | } 18 | 19 | } 20 | }, 21 | 22 | cmdSecurityMap: Object.keys(serviceCmds).reduce((acc, cmd) => { 23 | acc[cmd] = serviceCmds[cmd].getCmdSecurity(cmd, article); return acc; 24 | }, {}) 25 | } 26 | } -------------------------------------------------------------------------------- /src/services/article/article.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { MongoDriver } from '@mikro-orm/mongodb'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { ArticleService } from './article.service'; 4 | import { EICRUDModule } from '@eicrud/core'; 5 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 6 | import { Article } from './article.entity'; 7 | import { MyConfigService } from '../../eicrud.config.service'; 8 | import { CRUD_CONFIG_KEY } from '@eicrud/core/config'; 9 | 10 | describe('AppController', () => { 11 | let myService: ArticleService; 12 | 13 | beforeEach(async () => { 14 | const app: TestingModule = await Test.createTestingModule({ 15 | imports: [ 16 | MikroOrmModule.forRoot({ 17 | entities: [Article], 18 | driver: MongoDriver, 19 | dbName: 'test-article', 20 | }), 21 | EICRUDModule.forRoot(), 22 | ], 23 | providers: [ 24 | ArticleService, 25 | { 26 | provide: CRUD_CONFIG_KEY, 27 | useClass: MyConfigService, 28 | }, 29 | ], 30 | }).compile(); 31 | 32 | myService = app.get(ArticleService); 33 | }); 34 | 35 | describe('root', () => { 36 | it('should be defined"', () => { 37 | expect(myService).toBeDefined(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/services/article/article.service.ts: -------------------------------------------------------------------------------- 1 | import { ModuleRef } from "@nestjs/core"; 2 | import { Article } from "./article.entity"; 3 | import { Injectable } from "@nestjs/common"; 4 | import { getSecurity } from "./article.security"; 5 | import { CrudService, Inheritance, CrudContext } from "@eicrud/core/crud"; 6 | import { serviceCmds } from "./cmds"; 7 | import { hooks } from "./article.hooks"; 8 | 9 | @Injectable() 10 | export class ArticleService extends CrudService
{ 11 | constructor(protected moduleRef: ModuleRef) { 12 | const serviceName = CrudService.getName(Article); 13 | super(moduleRef, Article, getSecurity(serviceName), { hooks }); 14 | } 15 | 16 | // GENERATED START - do not remove 17 | 18 | } -------------------------------------------------------------------------------- /src/services/article/cmds.ts: -------------------------------------------------------------------------------- 1 | 2 | //Auto generated file 3 | 4 | export const serviceCmds = { 5 | } -------------------------------------------------------------------------------- /src/services/comment/cmds.ts: -------------------------------------------------------------------------------- 1 | 2 | //Auto generated file 3 | 4 | export const serviceCmds = { 5 | } -------------------------------------------------------------------------------- /src/services/comment/comment.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryKey, Property, 2 | ManyToOne } from "@mikro-orm/core"; 3 | import { IsString, IsOptional } from "class-validator"; 4 | import { CrudEntity } from "@eicrud/core/crud"; 5 | import { Article } from "../article/article.entity"; 6 | import { User } from "../user/user.entity"; 7 | import { $MaxSize } from "@eicrud/core/validation"; 8 | 9 | 10 | @Entity() 11 | export class Comment implements CrudEntity { 12 | 13 | @PrimaryKey({ name: '_id' }) 14 | @IsString() 15 | @IsOptional() 16 | id: string; 17 | 18 | @ManyToOne(() => Article) 19 | @IsString() 20 | article: Article | string; 21 | 22 | @ManyToOne(() => User) 23 | @IsString() 24 | author: User | string; 25 | 26 | @Property() 27 | @IsString() 28 | @$MaxSize(200) 29 | content: string; 30 | 31 | @Property() 32 | createdAt: Date; 33 | 34 | @Property() 35 | updatedAt: Date; 36 | 37 | } -------------------------------------------------------------------------------- /src/services/comment/comment.hooks.ts: -------------------------------------------------------------------------------- 1 | import { CrudContext, CrudHooks } from "@eicrud/core/crud"; 2 | import { Comment } from "./comment.entity"; 3 | import { CommentService } from "./comment.service"; 4 | import { FindResponseDto } from "@eicrud/shared/interfaces"; 5 | 6 | export class CommentHooks extends CrudHooks { 7 | 8 | override async beforeCreateHook(this: CommentService, data: Partial[], ctx: CrudContext): Promise[]> { 9 | // before Comment creation 10 | 11 | return data; 12 | } 13 | 14 | override async afterCreateHook(this: CommentService, result: any[], data: Partial[], ctx: CrudContext): Promise { 15 | // after Comment creation 16 | 17 | return result; 18 | } 19 | 20 | override async beforeReadHook(this: CommentService, query: Partial, ctx: CrudContext): Promise> { 21 | // before Comment read 22 | 23 | return query; 24 | } 25 | 26 | override async afterReadHook(this: CommentService, result, query: Partial, ctx: CrudContext): Promise> { 27 | // after Comment read 28 | 29 | return result; 30 | } 31 | 32 | override async beforeUpdateHook(this: CommentService, 33 | updates: { query: Partial; data: Partial }[], 34 | ctx: CrudContext, 35 | ): Promise<{ query: Partial; data: Partial }[]> { 36 | // before Comment update 37 | 38 | return updates; 39 | } 40 | 41 | override async afterUpdateHook(this: CommentService, 42 | results: any[], 43 | updates: { query: Partial; data: Partial }[], 44 | ctx: CrudContext, 45 | ): Promise { 46 | // after Comment update 47 | 48 | return results; 49 | } 50 | 51 | override async beforeDeleteHook(this: CommentService, query: Partial, ctx: CrudContext): Promise> { 52 | // before Comment delete 53 | 54 | return query; 55 | } 56 | 57 | override async afterDeleteHook(this: CommentService, result: any, query: Partial, ctx: CrudContext): Promise { 58 | // after Comment delete 59 | 60 | return result; 61 | } 62 | 63 | override async errorControllerHook(this: CommentService, error: any, ctx: CrudContext): Promise { 64 | //after Comment error 65 | 66 | } 67 | }; 68 | 69 | export const hooks = new CommentHooks(); 70 | 71 | -------------------------------------------------------------------------------- /src/services/comment/comment.security.ts: -------------------------------------------------------------------------------- 1 | import { CrudSecurity } from "@eicrud/core/config"; 2 | import { serviceCmds } from "./cmds"; 3 | 4 | export function getSecurity(comment: string): CrudSecurity { 5 | return { 6 | rolesRights: { 7 | guest: { 8 | async defineCRUDAbility(can, cannot, ctx) { 9 | // guests can read all comments 10 | can('read', comment) 11 | } 12 | }, 13 | user: { 14 | async defineCRUDAbility(can, cannot, ctx) { 15 | // user create and delete their own comments 16 | can('cd', comment, {author: ctx.userId}) 17 | } 18 | } 19 | }, 20 | 21 | cmdSecurityMap: Object.keys(serviceCmds).reduce((acc, cmd) => { 22 | acc[cmd] = serviceCmds[cmd].getCmdSecurity(cmd, comment); return acc; 23 | }, {}) 24 | } 25 | } -------------------------------------------------------------------------------- /src/services/comment/comment.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { MongoDriver } from '@mikro-orm/mongodb'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { CommentService } from './comment.service'; 4 | import { EICRUDModule } from '@eicrud/core'; 5 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 6 | import { Comment } from './comment.entity'; 7 | import { MyConfigService } from '../../eicrud.config.service'; 8 | import { CRUD_CONFIG_KEY } from '@eicrud/core/config'; 9 | 10 | describe('AppController', () => { 11 | let myService: CommentService; 12 | 13 | beforeEach(async () => { 14 | const app: TestingModule = await Test.createTestingModule({ 15 | imports: [ 16 | MikroOrmModule.forRoot({ 17 | entities: [Comment], 18 | driver: MongoDriver, 19 | dbName: 'test-comment', 20 | }), 21 | EICRUDModule.forRoot(), 22 | ], 23 | providers: [ 24 | CommentService, 25 | { 26 | provide: CRUD_CONFIG_KEY, 27 | useClass: MyConfigService, 28 | }, 29 | ], 30 | }).compile(); 31 | 32 | myService = app.get(CommentService); 33 | }); 34 | 35 | describe('root', () => { 36 | it('should be defined"', () => { 37 | expect(myService).toBeDefined(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/services/comment/comment.service.ts: -------------------------------------------------------------------------------- 1 | import { ModuleRef } from "@nestjs/core"; 2 | import { Comment } from "./comment.entity"; 3 | import { Injectable } from "@nestjs/common"; 4 | import { getSecurity } from "./comment.security"; 5 | import { CrudService, Inheritance, CrudContext } from "@eicrud/core/crud"; 6 | import { serviceCmds } from "./cmds"; 7 | import { hooks } from "./comment.hooks"; 8 | 9 | @Injectable() 10 | export class CommentService extends CrudService { 11 | constructor(protected moduleRef: ModuleRef) { 12 | const serviceName = CrudService.getName(Comment); 13 | super(moduleRef, Comment, getSecurity(serviceName), { hooks }); 14 | } 15 | 16 | // GENERATED START - do not remove 17 | 18 | } -------------------------------------------------------------------------------- /src/services/email/cmds.ts: -------------------------------------------------------------------------------- 1 | 2 | //Auto generated file 3 | 4 | export const serviceCmds = { 5 | } -------------------------------------------------------------------------------- /src/services/email/email.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryKey, Property } from "@mikro-orm/core"; 2 | import { IsString, IsOptional } from "class-validator"; 3 | import { CrudEntity } from "@eicrud/core/crud"; 4 | 5 | 6 | @Entity() 7 | export class Email implements CrudEntity { 8 | 9 | @PrimaryKey({ name: '_id' }) 10 | @IsString() 11 | @IsOptional() 12 | id: string; 13 | 14 | @Property() 15 | createdAt: Date; 16 | 17 | @Property() 18 | updatedAt: Date; 19 | 20 | } -------------------------------------------------------------------------------- /src/services/email/email.hooks.ts: -------------------------------------------------------------------------------- 1 | import { CrudContext, CrudHooks } from "@eicrud/core/crud"; 2 | import { Email } from "./email.entity"; 3 | import { EmailService } from "./email.service"; 4 | import { FindResponseDto } from "@eicrud/shared/interfaces"; 5 | 6 | export class EmailHooks extends CrudHooks { 7 | 8 | override async beforeCreateHook(this: EmailService, data: Partial[], ctx: CrudContext): Promise[]> { 9 | // before Email creation 10 | 11 | return data; 12 | } 13 | 14 | override async afterCreateHook(this: EmailService, result: any[], data: Partial[], ctx: CrudContext): Promise { 15 | // after Email creation 16 | 17 | return result; 18 | } 19 | 20 | override async beforeReadHook(this: EmailService, query: Partial, ctx: CrudContext): Promise> { 21 | // before Email read 22 | 23 | return query; 24 | } 25 | 26 | override async afterReadHook(this: EmailService, result, query: Partial, ctx: CrudContext): Promise> { 27 | // after Email read 28 | 29 | return result; 30 | } 31 | 32 | override async beforeUpdateHook(this: EmailService, 33 | updates: { query: Partial; data: Partial }[], 34 | ctx: CrudContext, 35 | ): Promise<{ query: Partial; data: Partial }[]> { 36 | // before Email update 37 | 38 | return updates; 39 | } 40 | 41 | override async afterUpdateHook(this: EmailService, 42 | results: any[], 43 | updates: { query: Partial; data: Partial }[], 44 | ctx: CrudContext, 45 | ): Promise { 46 | // after Email update 47 | 48 | return results; 49 | } 50 | 51 | override async beforeDeleteHook(this: EmailService, query: Partial, ctx: CrudContext): Promise> { 52 | // before Email delete 53 | 54 | return query; 55 | } 56 | 57 | override async afterDeleteHook(this: EmailService, result: any, query: Partial, ctx: CrudContext): Promise { 58 | // after Email delete 59 | 60 | return result; 61 | } 62 | 63 | override async errorControllerHook(this: EmailService, error: any, ctx: CrudContext): Promise { 64 | //after Email error 65 | 66 | } 67 | }; 68 | 69 | export const hooks = new EmailHooks(); 70 | 71 | -------------------------------------------------------------------------------- /src/services/email/email.security.ts: -------------------------------------------------------------------------------- 1 | import { CrudSecurity } from "@eicrud/core/config"; 2 | import { serviceCmds } from "./cmds"; 3 | 4 | export function getSecurity(email: string): CrudSecurity { 5 | return { 6 | rolesRights: { 7 | guest: { 8 | async defineCRUDAbility(can, cannot, ctx) { 9 | // Define abilities for guest 10 | 11 | } 12 | } 13 | }, 14 | 15 | cmdSecurityMap: Object.keys(serviceCmds).reduce((acc, cmd) => { 16 | acc[cmd] = serviceCmds[cmd].getCmdSecurity(cmd, email); return acc; 17 | }, {}) 18 | } 19 | } -------------------------------------------------------------------------------- /src/services/email/email.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { MongoDriver } from '@mikro-orm/mongodb'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { EmailService } from './email.service'; 4 | import { EICRUDModule } from '@eicrud/core'; 5 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 6 | import { Email } from './email.entity'; 7 | import { MyConfigService } from '../../eicrud.config.service'; 8 | import { CRUD_CONFIG_KEY } from '@eicrud/core/config'; 9 | 10 | describe('AppController', () => { 11 | let myService: EmailService; 12 | 13 | beforeEach(async () => { 14 | const app: TestingModule = await Test.createTestingModule({ 15 | imports: [ 16 | MikroOrmModule.forRoot({ 17 | entities: [Email], 18 | driver: MongoDriver, 19 | dbName: 'test-email', 20 | }), 21 | EICRUDModule.forRoot(), 22 | ], 23 | providers: [ 24 | EmailService, 25 | { 26 | provide: CRUD_CONFIG_KEY, 27 | useClass: MyConfigService, 28 | }, 29 | ], 30 | }).compile(); 31 | 32 | myService = app.get(EmailService); 33 | }); 34 | 35 | describe('root', () => { 36 | it('should be defined"', () => { 37 | expect(myService).toBeDefined(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/services/email/email.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { ModuleRef } from "@nestjs/core"; 3 | import { CrudService, CrudContext, Inheritance } from "@eicrud/core/crud"; 4 | import { Email } from "./email.entity"; 5 | import { getSecurity } from "./email.security"; 6 | import { EmailService as BaseEmailService, CrudUser } from "@eicrud/core/config"; 7 | 8 | @Injectable() 9 | export class EmailService extends CrudService implements BaseEmailService { 10 | constructor( 11 | protected moduleRef: ModuleRef 12 | ) { 13 | const serviceName = CrudService.getName(Email); 14 | super(moduleRef, Email, getSecurity(serviceName)); 15 | } 16 | 17 | sendVerificationEmail(to: string, token: string, ctx: CrudContext): Promise { 18 | console.log('Sending verification email to', to, 'with token', token); 19 | return Promise.resolve(); 20 | } 21 | 22 | sendTwoFactorEmail(to: string, code: string, ctx: CrudContext): Promise { 23 | console.log('Sending two factor email to', to, 'with code', code); 24 | return Promise.resolve(); 25 | } 26 | 27 | sendPasswordResetEmail(to: string, token: string, ctx: CrudContext): Promise { 28 | console.log('Sending password reset email to', to, 'with token', token); 29 | return Promise.resolve(); 30 | } 31 | 32 | sendAccountCreationEmail(to: string, user: CrudUser, ctx: CrudContext): Promise { 33 | console.log('Sending account creation email to', to); 34 | return Promise.resolve(); 35 | } 36 | } -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Comment } from './comment/comment.entity'; 2 | import { CommentService } from './comment/comment.service'; 3 | import { Article } from './article/article.entity'; 4 | import { ArticleService } from './article/article.service'; 5 | import { Email } from './email/email.entity'; 6 | import { EmailService } from './email/email.service'; 7 | import { User } from './user/user.entity'; 8 | import { UserService } from './user/user.service'; 9 | 10 | //Auto generated file 11 | 12 | export const CRUDServices = [ 13 | CommentService, 14 | ArticleService, 15 | EmailService, 16 | UserService, 17 | ] 18 | 19 | export const CRUDEntities = [ 20 | Comment, 21 | Article, 22 | User, 23 | ] 24 | 25 | -------------------------------------------------------------------------------- /src/services/user/cmds.ts: -------------------------------------------------------------------------------- 1 | import { logoutSecurity } from './cmds/logout/logout.security'; 2 | import { timeoutUserSecurity } from './cmds/timeout_user/timeout_user.security'; 3 | import { checkJwtSecurity } from './cmds/check_jwt/check_jwt.security'; 4 | import { loginSecurity } from './cmds/login/login.security'; 5 | import { logoutEverywhereSecurity } from './cmds/logout_everywhere/logout_everywhere.security'; 6 | import { createAccountSecurity } from './cmds/create_account/create_account.security'; 7 | import { resetPasswordSecurity } from './cmds/reset_password/reset_password.security'; 8 | import { changePasswordSecurity } from './cmds/change_password/change_password.security'; 9 | import { sendPasswordResetEmailSecurity } from './cmds/send_password_reset_email/send_password_reset_email.security'; 10 | import { verifyEmailSecurity } from './cmds/verify_email/verify_email.security'; 11 | import { sendVerificationEmailSecurity } from './cmds/send_verification_email/send_verification_email.security'; 12 | 13 | //Auto generated file 14 | 15 | export const serviceCmds = { 16 | logout: logoutSecurity, 17 | timeout_user: timeoutUserSecurity, 18 | check_jwt: checkJwtSecurity, 19 | login: loginSecurity, 20 | logout_everywhere: logoutEverywhereSecurity, 21 | create_account: createAccountSecurity, 22 | reset_password: resetPasswordSecurity, 23 | change_password: changePasswordSecurity, 24 | send_password_reset_email: sendPasswordResetEmailSecurity, 25 | verify_email: verifyEmailSecurity, 26 | send_verification_email: sendVerificationEmailSecurity, 27 | } -------------------------------------------------------------------------------- /src/services/user/cmds/change_password/change_password.security.ts: -------------------------------------------------------------------------------- 1 | import { CmdSecurity, baseCmds } from "@eicrud/core/config"; 2 | 3 | 4 | const getCmdSecurity = (change_password, user): CmdSecurity => { 5 | return { 6 | minTimeBetweenCmdCallMs: 1000, 7 | dto: baseCmds.changePassword.dto, 8 | rolesRights: { 9 | user: { 10 | async defineCMDAbility(can, cannot, ctx) { 11 | // Define abilities for user 12 | 13 | } 14 | } 15 | }, 16 | } 17 | } 18 | 19 | export const changePasswordSecurity = { 20 | getCmdSecurity, 21 | } -------------------------------------------------------------------------------- /src/services/user/cmds/check_jwt/check_jwt.security.ts: -------------------------------------------------------------------------------- 1 | import { CmdSecurity, baseCmds } from "@eicrud/core/config"; 2 | 3 | 4 | const getCmdSecurity = (check_jwt, user): CmdSecurity => { 5 | return { 6 | minTimeBetweenCmdCallMs: 1000, 7 | dto: baseCmds.checkJwt.dto, 8 | rolesRights: { 9 | user: { 10 | async defineCMDAbility(can, cannot, ctx) { 11 | // Define abilities for user 12 | 13 | } 14 | } 15 | }, 16 | } 17 | } 18 | 19 | export const checkJwtSecurity = { 20 | getCmdSecurity, 21 | } -------------------------------------------------------------------------------- /src/services/user/cmds/create_account/create_account.security.ts: -------------------------------------------------------------------------------- 1 | import { CmdSecurity, baseCmds } from "@eicrud/core/config"; 2 | 3 | 4 | const getCmdSecurity = (create_account, user): CmdSecurity => { 5 | return { 6 | minTimeBetweenCmdCallMs: 1000, 7 | dto: baseCmds.createAccount.dto, 8 | rolesRights: { 9 | guest: { 10 | async defineCMDAbility(can, cannot, ctx) { 11 | // can create an account with the role 'user' 12 | can(create_account, user, {role: 'user'}) 13 | } 14 | } 15 | }, 16 | } 17 | } 18 | 19 | export const createAccountSecurity = { 20 | getCmdSecurity, 21 | } -------------------------------------------------------------------------------- /src/services/user/cmds/login/login.security.ts: -------------------------------------------------------------------------------- 1 | import { CmdSecurity, baseCmds } from "@eicrud/core/config"; 2 | 3 | 4 | const getCmdSecurity = (login, user): CmdSecurity => { 5 | return { 6 | minTimeBetweenCmdCallMs: 1000, 7 | dto: baseCmds.login.dto, 8 | rolesRights: { 9 | guest: { 10 | async defineCMDAbility(can, cannot, ctx) { 11 | // Define abilities for user 12 | can(login, user) 13 | } 14 | } 15 | }, 16 | } 17 | } 18 | 19 | export const loginSecurity = { 20 | getCmdSecurity, 21 | } -------------------------------------------------------------------------------- /src/services/user/cmds/logout/logout.security.ts: -------------------------------------------------------------------------------- 1 | import { CmdSecurity, baseCmds } from "@eicrud/core/config"; 2 | 3 | 4 | const getCmdSecurity = (logout, user): CmdSecurity => { 5 | return { 6 | minTimeBetweenCmdCallMs: 1000, 7 | dto: baseCmds.logout.dto, 8 | rolesRights: { 9 | user: { 10 | async defineCMDAbility(can, cannot, ctx) { 11 | // Define abilities for user 12 | 13 | } 14 | } 15 | }, 16 | } 17 | } 18 | 19 | export const logoutSecurity = { 20 | getCmdSecurity, 21 | } -------------------------------------------------------------------------------- /src/services/user/cmds/logout_everywhere/logout_everywhere.security.ts: -------------------------------------------------------------------------------- 1 | import { CmdSecurity, baseCmds } from "@eicrud/core/config"; 2 | 3 | 4 | const getCmdSecurity = (logout_everywhere, user): CmdSecurity => { 5 | return { 6 | minTimeBetweenCmdCallMs: 1000, 7 | dto: baseCmds.logoutEverywhere.dto, 8 | rolesRights: { 9 | user: { 10 | async defineCMDAbility(can, cannot, ctx) { 11 | // Define abilities for user 12 | 13 | } 14 | } 15 | }, 16 | } 17 | } 18 | 19 | export const logoutEverywhereSecurity = { 20 | getCmdSecurity, 21 | } -------------------------------------------------------------------------------- /src/services/user/cmds/reset_password/reset_password.security.ts: -------------------------------------------------------------------------------- 1 | import { CmdSecurity, baseCmds } from "@eicrud/core/config"; 2 | 3 | 4 | const getCmdSecurity = (reset_password, user): CmdSecurity => { 5 | return { 6 | minTimeBetweenCmdCallMs: 1000, 7 | dto: baseCmds.resetPassword.dto, 8 | rolesRights: { 9 | user: { 10 | async defineCMDAbility(can, cannot, ctx) { 11 | // Define abilities for user 12 | 13 | } 14 | } 15 | }, 16 | } 17 | } 18 | 19 | export const resetPasswordSecurity = { 20 | getCmdSecurity, 21 | } -------------------------------------------------------------------------------- /src/services/user/cmds/send_password_reset_email/send_password_reset_email.security.ts: -------------------------------------------------------------------------------- 1 | import { CmdSecurity, baseCmds } from "@eicrud/core/config"; 2 | 3 | 4 | const getCmdSecurity = (send_password_reset_email, user): CmdSecurity => { 5 | return { 6 | minTimeBetweenCmdCallMs: 1000, 7 | dto: baseCmds.sendPasswordResetEmail.dto, 8 | rolesRights: { 9 | user: { 10 | async defineCMDAbility(can, cannot, ctx) { 11 | // Define abilities for user 12 | 13 | } 14 | } 15 | }, 16 | } 17 | } 18 | 19 | export const sendPasswordResetEmailSecurity = { 20 | getCmdSecurity, 21 | } -------------------------------------------------------------------------------- /src/services/user/cmds/send_verification_email/send_verification_email.security.ts: -------------------------------------------------------------------------------- 1 | import { CmdSecurity, baseCmds } from "@eicrud/core/config"; 2 | 3 | 4 | const getCmdSecurity = (send_verification_email, user): CmdSecurity => { 5 | return { 6 | minTimeBetweenCmdCallMs: 1000, 7 | dto: baseCmds.sendVerificationEmail.dto, 8 | rolesRights: { 9 | user: { 10 | async defineCMDAbility(can, cannot, ctx) { 11 | // Define abilities for user 12 | 13 | } 14 | } 15 | }, 16 | } 17 | } 18 | 19 | export const sendVerificationEmailSecurity = { 20 | getCmdSecurity, 21 | } -------------------------------------------------------------------------------- /src/services/user/cmds/timeout_user/timeout_user.security.ts: -------------------------------------------------------------------------------- 1 | import { CmdSecurity, baseCmds } from "@eicrud/core/config"; 2 | 3 | 4 | const getCmdSecurity = (timeout_user, user): CmdSecurity => { 5 | return { 6 | minTimeBetweenCmdCallMs: 1000, 7 | dto: baseCmds.timeoutUser.dto, 8 | rolesRights: { 9 | user: { 10 | async defineCMDAbility(can, cannot, ctx) { 11 | // Define abilities for user 12 | 13 | } 14 | } 15 | }, 16 | } 17 | } 18 | 19 | export const timeoutUserSecurity = { 20 | getCmdSecurity, 21 | } -------------------------------------------------------------------------------- /src/services/user/cmds/verify_email/verify_email.security.ts: -------------------------------------------------------------------------------- 1 | import { CmdSecurity, baseCmds } from "@eicrud/core/config"; 2 | 3 | 4 | const getCmdSecurity = (verify_email, user): CmdSecurity => { 5 | return { 6 | minTimeBetweenCmdCallMs: 1000, 7 | dto: baseCmds.verifyEmail.dto, 8 | rolesRights: { 9 | user: { 10 | async defineCMDAbility(can, cannot, ctx) { 11 | // Define abilities for user 12 | 13 | } 14 | } 15 | }, 16 | } 17 | } 18 | 19 | export const verifyEmailSecurity = { 20 | getCmdSecurity, 21 | } -------------------------------------------------------------------------------- /src/services/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryKey, Property, Unique } from "@mikro-orm/core"; 2 | import { CrudUser } from "@eicrud/core/config"; 3 | 4 | @Entity() 5 | export class User implements CrudUser { 6 | 7 | @PrimaryKey({ name: '_id'}) 8 | id: string; 9 | 10 | @Unique() 11 | @Property() 12 | email: string; 13 | 14 | @Property() 15 | password: string; 16 | 17 | @Property() 18 | role: string; 19 | 20 | @Property({ nullable: true }) 21 | lastLoginAttempt: Date; 22 | 23 | @Property({ nullable: true }) 24 | failedLoginCount: number; 25 | 26 | @Property({ nullable: true }) 27 | lastResetEmailSent: Date; 28 | 29 | @Property({ nullable: true }) 30 | rvkd: number; 31 | 32 | @Property({ nullable: true }) 33 | nextEmail: string; 34 | 35 | @Property({ nullable: true }) 36 | verifiedEmail: boolean; 37 | 38 | @Property({ nullable: true }) 39 | emailVerificationToken: string; 40 | 41 | @Property({ nullable: true }) 42 | lastEmailVerificationSent: Date; 43 | 44 | @Property({ nullable: true }) 45 | verifiedEmailAttempCount: number; 46 | 47 | @Property({ nullable: true }) 48 | lastPasswordResetSent: Date; 49 | 50 | @Property({ nullable: true }) 51 | passwordResetToken: string; 52 | 53 | @Property({ nullable: true }) 54 | passwordResetAttempCount: number; 55 | 56 | @Property({ type: 'json', nullable: true }) 57 | crudUserCountMap: Record | string; 58 | 59 | @Property({ type: 'json', nullable: true }) 60 | cmdUserCountMap: Record | string; 61 | 62 | @Property({ type: 'json', nullable: true }) 63 | cmdUserLastUseMap: Record | string; 64 | 65 | @Property({ nullable: true }) 66 | errorCount: number; 67 | 68 | @Property({ nullable: true }) 69 | incidentCount: number; 70 | 71 | @Property({ nullable: true }) 72 | highTrafficCount: number; 73 | 74 | @Property({ nullable: true }) 75 | trust: number; 76 | 77 | @Property({ nullable: true }) 78 | lastComputedTrust: Date; 79 | 80 | @Property({ nullable: true }) 81 | timeout: Date; 82 | 83 | @Property({ nullable: true }) 84 | timeoutCount: number; 85 | 86 | @Property({ nullable: true }) 87 | didCaptcha: boolean; 88 | 89 | @Property({ nullable: true }) 90 | captchaRequested: boolean; 91 | 92 | @Property({ nullable: true }) 93 | twoFA: boolean; 94 | 95 | @Property({ nullable: true }) 96 | lastTwoFACode: string; 97 | 98 | @Property({ nullable: true }) 99 | lastTwoFACodeSent: Date; 100 | 101 | @Property({ nullable: true }) 102 | twoFACodeCount: number; 103 | 104 | @Property({ nullable: true }) 105 | saltRounds: number; 106 | 107 | @Property({ nullable: true }) 108 | createdAt: Date; 109 | 110 | @Property({ nullable: true }) 111 | updatedAt: Date; 112 | 113 | @Property({ nullable: true }) 114 | noTokenRefresh: boolean; 115 | 116 | } -------------------------------------------------------------------------------- /src/services/user/user.hooks.ts: -------------------------------------------------------------------------------- 1 | import { CrudContext, CrudHooks } from "@eicrud/core/crud"; 2 | import { User } from "./user.entity"; 3 | import { UserService } from "./user.service"; 4 | import { FindResponseDto } from "@eicrud/shared/interfaces"; 5 | 6 | export class UserHooks extends CrudHooks { 7 | 8 | override async beforeCreateHook(this: UserService, data: Partial[], ctx: CrudContext): Promise[]> { 9 | // before User creation 10 | 11 | return data; 12 | } 13 | 14 | override async afterCreateHook(this: UserService, result: any[], data: Partial[], ctx: CrudContext): Promise { 15 | // after User creation 16 | 17 | return result; 18 | } 19 | 20 | override async beforeReadHook(this: UserService, query: Partial, ctx: CrudContext): Promise> { 21 | // before User read 22 | 23 | return query; 24 | } 25 | 26 | override async afterReadHook(this: UserService, result, query: Partial, ctx: CrudContext): Promise> { 27 | // after User read 28 | 29 | return result; 30 | } 31 | 32 | override async beforeUpdateHook(this: UserService, 33 | updates: { query: Partial; data: Partial }[], 34 | ctx: CrudContext, 35 | ): Promise<{ query: Partial; data: Partial }[]> { 36 | // before User update 37 | 38 | return updates; 39 | } 40 | 41 | override async afterUpdateHook(this: UserService, 42 | results: any[], 43 | updates: { query: Partial; data: Partial }[], 44 | ctx: CrudContext, 45 | ): Promise { 46 | // after User update 47 | 48 | return results; 49 | } 50 | 51 | override async beforeDeleteHook(this: UserService, query: Partial, ctx: CrudContext): Promise> { 52 | // before User delete 53 | 54 | return query; 55 | } 56 | 57 | override async afterDeleteHook(this: UserService, result: any, query: Partial, ctx: CrudContext): Promise { 58 | // after User delete 59 | 60 | return result; 61 | } 62 | 63 | override async errorControllerHook(this: UserService, error: any, ctx: CrudContext): Promise { 64 | //after User error 65 | 66 | } 67 | }; 68 | 69 | export const hooks = new UserHooks(); 70 | 71 | -------------------------------------------------------------------------------- /src/services/user/user.security.ts: -------------------------------------------------------------------------------- 1 | import { CrudSecurity } from "@eicrud/core/config"; 2 | import { serviceCmds } from "./cmds"; 3 | 4 | export function getSecurity(user: string): CrudSecurity { 5 | return { 6 | rolesRights: { 7 | guest: { 8 | async defineCRUDAbility(can, cannot, ctx) { 9 | // Define abilities for guest 10 | 11 | } 12 | } 13 | }, 14 | 15 | cmdSecurityMap: Object.keys(serviceCmds).reduce((acc, cmd) => { 16 | acc[cmd] = serviceCmds[cmd].getCmdSecurity(cmd, user); return acc; 17 | }, {}) 18 | } 19 | } -------------------------------------------------------------------------------- /src/services/user/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { MongoDriver } from '@mikro-orm/mongodb'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { UserService } from './user.service'; 4 | import { EICRUDModule } from '@eicrud/core'; 5 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 6 | import { User } from './user.entity'; 7 | import { MyConfigService } from '../../eicrud.config.service'; 8 | import { CRUD_CONFIG_KEY } from '@eicrud/core/config'; 9 | 10 | describe('AppController', () => { 11 | let myService: UserService; 12 | 13 | beforeEach(async () => { 14 | const app: TestingModule = await Test.createTestingModule({ 15 | imports: [ 16 | MikroOrmModule.forRoot({ 17 | entities: [User], 18 | driver: MongoDriver, 19 | dbName: 'test-user', 20 | }), 21 | EICRUDModule.forRoot(), 22 | ], 23 | providers: [ 24 | UserService, 25 | { 26 | provide: CRUD_CONFIG_KEY, 27 | useClass: MyConfigService, 28 | }, 29 | ], 30 | }).compile(); 31 | 32 | myService = app.get(UserService); 33 | }); 34 | 35 | describe('root', () => { 36 | it('should be defined"', () => { 37 | expect(myService).toBeDefined(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/services/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { CrudUserService } from '@eicrud/core/config' 2 | import { ModuleRef } from "@nestjs/core"; 3 | import { User } from "./user.entity"; 4 | import { Injectable } from "@nestjs/common"; 5 | import { getSecurity } from "./user.security"; 6 | import { CrudService, Inheritance, CrudContext } from "@eicrud/core/crud"; 7 | import { serviceCmds } from "./cmds"; 8 | import { hooks } from "./user.hooks"; 9 | 10 | @Injectable() 11 | export class UserService extends CrudUserService { 12 | constructor(protected moduleRef: ModuleRef) { 13 | const serviceName = CrudService.getName(User); 14 | super(moduleRef, User, getSecurity(serviceName), { hooks }); 15 | } 16 | 17 | // GENERATED START - do not remove 18 | 19 | } -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | --------------------------------------------------------------------------------