Commit 5152dcd0 authored by Daffa 's avatar Daffa

initial commit for boiler plate

parent 7d14eb9e
Pipeline #2158 failed
# Database
DATABASE_DIALECT="mysql"
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USERNAME=root
# # Database
DATABASE_DIALECT="postgres"
DATABASE_HOST="localhost"
DATABASE_PORT=5432
DATABASE_USERNAME=
DATABASE_PASSWORD=
DATABASE_NAME=nestjs_development
DATABASE_TIMEZONE="+07:00"
DATABASE_BENCHMARK=true
# only for managed DB
DATABASE_NAME=
DB_SSL=false
# App
......
# NestJS Starter
# NestJS Boilerplate
## Requirement
1. Node.js 16.15.0
2. Git
3. PostgreSQL
A production-ready NestJS boilerplate with authentication, PostgreSQL, Sequelize ORM, and Redis integration.
## Features
- **Authentication**: JWT-based authentication with refresh tokens
- **Database**: PostgreSQL with Sequelize ORM
- **Caching**: Redis integration
- **Validation**: Class-validator for request validation
- **API Documentation**: Swagger/OpenAPI documentation
- **Email**: Mailgun integration for sending emails
- **File Upload**: Built-in file upload support
- **Security**: Helmet, CORS, rate limiting
- **Code Quality**: ESLint, Prettier
## Requirements
- Node.js 16.17.0+
- npm 8.15.0+
- PostgreSQL 12+
- Redis (optional, for caching)
- Git
## Installation
1. Clone this project
### 1. Clone the repository
```bash
git clone <your-repo-url>
cd nest-boiler-plate
npm install
```
### 2. Environment Setup
Copy the example environment file and update the configuration:
```bash
cp .env.example .env
```
Update the following variables in `.env`:
- **Database**: `DATABASE_DIALECT`, `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `DATABASE_NAME`
- **Redis**: `REDIS_HOST`, `REDIS_PORT`
- **JWT**: Keys are already generated, but you can replace them
- **Mail**: `MAIL_API_KEY`, `MAIL_DOMAIN` (if using Mailgun)
### 3. Database Setup
Create the database:
```bash
npm run db:create
```
Run migrations:
```bash
$ git clone git@git.seni.cloud:nodejs/nestjs-starter.git
$ cd nestjs-starter
$ npm install
npm run db:migrate
```
2. Create Environment (Update Database dan Email Environment)
(Optional) Seed the database:
```bash
npm run db:seed
```
## Development
Start the development server with hot-reload:
```bash
npm run start:dev
```
The API will be available at `http://localhost:3000`
## Production
Build the application:
```bash
npm run build
```
Start the production server:
```bash
npm run start:prod
```
## Database Commands
### Migrations
Create a new migration:
```bash
$ cd nestjs-starter
$ cp .env.example .env
npx sequelize migration:generate --name create-your-table-name
```
3. Create Database
Run all pending migrations:
```bash
$ npm run db:create
npm run db:migrate
```
4. Migrate Database
Check migration status:
```bash
$ npm run db:migrate
$ npm run db:seed
npm run db:migrate:status
```
## How To Start Develop
Undo the last migration:
```bash
npm run db:migrate:undo
```
1. Run Application
Reset database (undo all migrations and re-run them):
```bash
$ npm run start:dev
npm run db:reset
```
## How To Start Production
### Seeds
Create a new seed file:
```bash
npx sequelize seed:generate --name seed-name
```
1. Run Application
Run all seeds:
```bash
$ npm run start:prod
npm run db:seed
```
## API Documentation
Open the API documentation from browser: [http://localhost:3000/api-documentation](http://localhost:3000/api-documentation)
Once the application is running, access the Swagger documentation at:
```
http://localhost:3000/api-documentation
```
## Available Scripts
- `npm run start:dev` - Start development server with hot-reload
- `npm run start:prod` - Start production server
- `npm run build` - Build the application
- `npm run lint` - Run ESLint
- `npm run format` - Format code with Prettier
- `npm run test` - Run unit tests
- `npm run test:watch` - Run tests in watch mode
- `npm run test:cov` - Run tests with coverage
- `npm run db:create` - Create database
- `npm run db:migrate` - Run migrations
- `npm run db:migrate:status` - Check migration status
- `npm run db:migrate:undo` - Undo last migration
- `npm run db:reset` - Reset database (undo all and re-run)
- `npm run db:seed` - Run seeders
## Project Structure
```
src/
├── common/ # Shared utilities, constants, decorators
├── database/ # Database models, migrations, seeders
│ ├── migrations/ # Sequelize migrations
│ ├── models/ # Sequelize models
│ └── seeders/ # Database seeders
├── interceptors/ # Global interceptors
├── middleware/ # Custom middleware
├── modules/ # Feature modules
│ ├── auth/ # Authentication module
│ └── users/ # Users module
├── share/ # Shared services and guards
├── app.module.ts # Root module
└── main.ts # Application entry point
```
## Creating New Migrations
### Example: Create a products table
1. Generate migration file:
```bash
npx sequelize migration:generate --name create-products-table
```
2. Edit the generated file in `src/database/migrations/`:
```javascript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('products', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
name: {
type: Sequelize.STRING,
allowNull: false
},
description: {
type: Sequelize.TEXT,
allowNull: true
},
price: {
type: Sequelize.DECIMAL(10, 2),
allowNull: false
},
createdAt: {
type: Sequelize.DATE,
field: 'created_at',
allowNull: false,
defaultValue: Sequelize.fn('NOW')
},
updatedAt: {
type: Sequelize.DATE,
field: 'updated_at',
allowNull: false,
defaultValue: Sequelize.fn('NOW')
}
});
await queryInterface.addIndex('products', ['name']);
},
async down(queryInterface) {
await queryInterface.dropTable('products');
}
};
```
3. Run the migration:
```bash
npm run db:migrate
```
## Contributors
- Daffa Praramadhana - [daffa.praramadhana@mdd.co.id](mailto:daffa.praramadhana@mdd.co.id)
## License
Copyright © 2025 Multidaya Dinamika. All rights reserved.
......@@ -139,6 +139,30 @@
}
}
},
"node_modules/@angular-devkit/core/node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@angular-devkit/core/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/@angular-devkit/core/node_modules/rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
......@@ -889,28 +913,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"node_modules/@grpc/grpc-js": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.4.tgz",
......@@ -3040,13 +3042,14 @@
}
},
"node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
......@@ -3071,6 +3074,30 @@
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
......@@ -5001,22 +5028,6 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/eslint/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/eslint/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
......@@ -5067,12 +5078,6 @@
"node": ">=10.13.0"
}
},
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"node_modules/espree": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
......@@ -5434,6 +5439,23 @@
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fastq": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
......@@ -7714,9 +7736,10 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
......@@ -9464,6 +9487,8 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
......@@ -9657,22 +9682,6 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/schema-utils/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/schema-utils/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
......@@ -9682,12 +9691,6 @@
"ajv": "^6.9.1"
}
},
"node_modules/schema-utils/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
......@@ -11393,21 +11396,6 @@
"acorn": "^8"
}
},
"node_modules/webpack/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/webpack/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
......@@ -11443,11 +11431,6 @@
"node": ">= 10.13.0"
}
},
"node_modules/webpack/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/webpack/node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
......
'use strict';
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.createTable('roles', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
name: {
type: Sequelize.STRING(50),
allowNull: false
},
displayName: {
type: Sequelize.STRING,
field: 'display_name',
allowNull: true,
},
active: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
createdAt: {
type: Sequelize.DATE,
field: 'created_at',
allowNull: false,
defaultValue:Sequelize.fn('NOW'),
},
updatedAt: {
type: Sequelize.DATE,
field: 'updated_at',
allowNull: false,
defaultValue:Sequelize.fn('NOW'),
},
deletedAt: {
type: Sequelize.DATE,
field: 'deleted_at',
allowNull: true,
defaultValue: null
}
});
},
async down (queryInterface) {
await queryInterface.dropTable('roles');
}
};
......@@ -29,11 +29,6 @@ module.exports = {
allowNull: false,
defaultValue: false,
},
roleId: {
type: Sequelize.BIGINT,
field: 'role_id',
references: { model: 'roles', key: 'id' }
},
emailVerificationSentAt: {
type: Sequelize.DATE,
field: 'email_verification_sent_at',
......@@ -63,9 +58,8 @@ module.exports = {
});
await queryInterface.addIndex('users', ['email']);
await queryInterface.addIndex('users', ['phone_number']);
await queryInterface.addIndex('users', ['phone_number']);
await queryInterface.addIndex('users', ['active']);
await queryInterface.addIndex('users', ['role_id']);
},
async down (queryInterface) {
......
......@@ -2,32 +2,17 @@ import {
AllowNull,
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DefaultScope,
DeletedAt,
ForeignKey,
HasMany,
HasOne,
Scopes,
Table,
Unique,
} from "sequelize-typescript";
import { generateHash } from "src/common/hash";
import { TimestampModel } from "src/database/models/timestamp.model";
import { Role } from "./role.model";
@DefaultScope(() => ({
attributes: { exclude: ["password","deletedAt"] },
include: [Role],
}))
@Scopes(() => ({
role: () => ({
include: [
{model: Role.unscoped(), attributes: ['id', 'name', 'displayName', 'active']}
],
})
}))
@Table({ timestamps: true, tableName: "users" })
......@@ -54,10 +39,6 @@ export class User extends TimestampModel {
@Column({ defaultValue: false })
active: boolean;
@ForeignKey(() => Role)
@Column({ field: "role_id" })
roleId: number;
@Column({ field: "email_verification_sent_at" })
emailVerificationSentAt: Date;
......@@ -68,9 +49,6 @@ export class User extends TimestampModel {
@Column({ field: "deleted_at" })
deletedAt?: Date;
@BelongsTo(() => Role)
role: Role;
@BeforeCreate
static hashPassword(instance: User) {
if (instance.password) {
......
......@@ -48,7 +48,6 @@ export class AuthController {
const token = await this.authService.createAccessToken({
user: user.id,
role: user.roleId,
});
const data_user = await this.userService.findOne(user.id);
......
......@@ -77,7 +77,6 @@ export class AuthService {
}
async createAccessToken(data: {
role: number;
user: number;
}): Promise<TokenDto> {
......@@ -89,7 +88,6 @@ export class AuthService {
accessToken: await this.jwtService.signAsync({
type: this.appConfigService.jwt().accessTokenType,
user: data.user,
role: data.role,
}, {
expiresIn: expiresIn,
}),
......
import { ApiProperty } from "@nestjs/swagger";
import { IsDefined } from "class-validator";
export class assignPermissionDto {
@ApiProperty({
example: [1, 2],
required: true,
})
@IsDefined()
permissionIds: any;
}
import {
IsDefined,
IsString,
IsBoolean,
IsEnum,
IsOptional,
} from "class-validator";
import { Transform } from "class-transformer";
import { ApiProperty } from "@nestjs/swagger";
import { toBoolean } from "src/common/cast";
import { ROLE_EXCEPT_SUPER_ENUM } from "./../../../common/constant";
export class CreateRoleDto {
@ApiProperty({ example: "Admin", description: "The role name" })
@IsDefined()
@IsString()
@IsEnum(ROLE_EXCEPT_SUPER_ENUM, {
message: "Nama role harus bernilai Admin atau Patient"
})
name: ROLE_EXCEPT_SUPER_ENUM;
@ApiProperty({ example: "Admin", description: "The role display name" })
@IsDefined()
@IsString()
displayName: string;
@ApiProperty({
example: true,
description: "Set true if you want to activate this role",
required: false,
})
@Transform(({ value }) => toBoolean(value))
@IsOptional()
@IsBoolean()
active?: boolean;
}
import { ApiProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsOptional } from "class-validator";
import { Op } from "sequelize";
import { QueryDto } from "src/common/dto/query.dto";
export class PermissionQueryDto extends QueryDto {
@ApiProperty({ required: false })
@IsOptional()
@Transform(({ value }) => ({ [Op.iLike]: `%${value}%` }))
name: string;
}
import { ApiProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsOptional } from "class-validator";
import { Op } from "sequelize";
import { toBoolean } from "src/common/cast";
import { QueryDto } from "src/common/dto/query.dto";
export class RoleQueryDto extends QueryDto {
@ApiProperty({ required: false })
@IsOptional()
@Transform(({ value }) => ({ [Op.iLike]: `%${value}%` }))
name: string;
@ApiProperty({ required: false })
@Transform(({ value }) => toBoolean(value))
@IsOptional()
active?: boolean;
}
import {
IsDefined,
IsInt,
IsBoolean,
} from "class-validator";
import { Transform, Type } from "class-transformer";
import { ApiProperty } from "@nestjs/swagger";
import { toBoolean } from "src/common/cast";
export class UpdateRolePermissionDto {
@ApiProperty({ example: "1", description: "id of permissions you want add" })
@IsDefined()
@IsInt()
permission_id: number;
@ApiProperty({
example: true,
description: "Set true if you want to activate this role",
required: false,
})
@Transform(({ value }) => toBoolean(value))
@IsDefined()
@IsBoolean()
active?: boolean;
}
import {
IsString,
IsBoolean,
IsEnum,
IsOptional,
} from "class-validator";
import { Transform, Type } from "class-transformer";
import { ApiProperty } from "@nestjs/swagger";
import { toBoolean } from "src/common/cast";
import { ROLE_ENUM } from "./../../../common/constant";
export class UpdateRoleDto {
@ApiProperty({ example: "admin", description: "The role name" })
@IsOptional()
@IsString()
@IsEnum(ROLE_ENUM)
name: ROLE_ENUM;
@ApiProperty({ example: "admin", description: "The role display name" })
@IsOptional()
@IsString()
displayName: string;
@ApiProperty({
example: true,
description: "Set true if you want to activate this role",
required: false,
})
@Transform(({ value }) => toBoolean(value))
@IsOptional()
@IsBoolean()
active?: boolean;
}
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Put,
Query,
UseGuards
} from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard";
import { IMeta } from "src/common/interfaces/IMeta";
import { CreateRoleDto } from "./dto/create-role.dto";
import { RoleQueryDto } from "./dto/role.query.dto";
import { Roles } from "../../share/decorators/roles.decorator";
import { RolesGuard } from "../../share/guards/roles.guard";
import { RolesService } from "./roles.service";
import { Role } from "src/database/models/role.model";
import { UpdateRoleDto } from "./dto/update-role.dto";
import { UpdateRolePermissionDto } from "./dto/update-role-permission.dto";
import { assignPermissionDto } from "./dto/assign-permission.dto";
@ApiTags("roles")
@ApiBearerAuth()
@Controller("roles")
export class RolesController {
constructor(
private readonly rolesService: RolesService
) {}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get()
@Roles("role.view")
@HttpCode(HttpStatus.OK)
async findAll(
@Query() query: RoleQueryDto
): Promise<{ meta: IMeta; data: Role[] }> {
const data = await this.rolesService.findAll(query);
const count = await this.rolesService.count(query);
const { page, limit, orderBy, order } = query;
return {
meta: {
totalData: count,
totalPage: Math.ceil(count / query.limit),
page,
limit,
orderBy,
order,
},
data,
};
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get(":id")
@Roles("role.view")
@HttpCode(HttpStatus.OK)
find(@Param("id") id: number): Promise<Role> {
return this.rolesService.findOne(+id);
}
}
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { InjectModel } from "@nestjs/sequelize";
import { Op } from "sequelize";
import * as _ from "lodash";
import { CreateRoleDto } from "./dto/create-role.dto";
import { RoleQueryDto } from "./dto/role.query.dto";
import { UsersService } from "./users.service";
import { Role } from "src/database/models/role.model";
import { Permission } from "src/database/models/permission.model";
import { UpdateRoleDto } from "./dto/update-role.dto";
import { UpdateRolePermissionDto } from "./dto/update-role-permission.dto";
import { assignPermissionDto } from "./dto/assign-permission.dto";
import { ROLE_ENUM } from "./../../common/constant";
import { error_message } from "src/common/error_message";
@Injectable()
export class RolesService {
constructor(
@InjectModel(Role)
private readonly roleModel: typeof Role,
@InjectModel(Permission)
private readonly permissionModel: typeof Permission,
private readonly usersService: UsersService
) { }
async create(payload: CreateRoleDto): Promise<Role> {
if (payload.displayName) {
await this.validateRole(payload)
}
return this.roleModel.create({
name: payload.name,
displayName: payload.displayName,
active: payload.active,
});
}
async count(query?: RoleQueryDto): Promise<number> {
return this.roleModel.unscoped().count({ where: this.createWhere(query) });
}
findAll(query?: RoleQueryDto): Promise<Role[]> {
const { page, limit, order, orderBy } = query;
return this.roleModel.findAll({
where: this.createWhere(query),
limit,
offset: (page - 1) * limit,
order: [[orderBy, order]],
});
}
async findOne(id: number): Promise<Role> {
const data = await this.roleModel.findOne({
where: {
id,
},
});
if (!data) throw new NotFoundException(error_message.not_found);
return data;
}
private async validateRole(payload) {
const whereRole = [];
if (payload.role) {
whereRole[0] = {
id: {[Op.ne]: payload.role.id}
}
whereRole[1] = {
displayName: {[Op.eq]: payload.displayName}
}
} else {
whereRole[0] = {
displayName: {[Op.eq]: payload.displayName}
}
}
const existRoleByName = await this.roleModel.count({
where: {
[Op.and]: whereRole,
}
});
if (existRoleByName) {
throw new BadRequestException('Nama display role tidak boleh sama.');
}
}
async update(id: number, payload: UpdateRoleDto): Promise<Role> {
const role = await this.findOne(id);
if (!role) throw new NotFoundException("Role not found");
if (payload.displayName != role.displayName) {
Object.assign(payload, {role: role})
await this.validateRole(payload)
}
await role.update(payload);
return role;
}
async assignPermission(id: number, payload: assignPermissionDto): Promise<Role> {
const permissionData = await this.permissionModel.findAll({
where: {
id: payload.permissionIds,
superadmin: false
}
}).then((permissions) => {
const result = [];
permissions.forEach(async permission => {
result.push({
permissionId: permission.id,
roleId: id
})
});
return result;
});
return await this.findOne(id);
}
async findSuperAdmin(): Promise<Role> {
const superAdmin = await this.roleModel.findOne({where: {name: ROLE_ENUM.SUPERADMIN}})
return superAdmin;
}
async delete(id: number): Promise<void> {
// user
const users = await this.usersService.findNoScope({
where: {
roleId: id,
},
});
if (users && users.length) {
throw new NotFoundException("Role telah digunakan");
}
const role = await this.findOne(id);
if (!role) throw new NotFoundException("Role not found");
await role.destroy();
}
private createWhere(conditions) {
const { page, limit, order, orderBy, ...rest } = conditions; // eslint-disable-line
const where = _.isEmpty(rest) ? {} : { [Op.and]: { ...rest } };
return where;
}
}
import { Module } from "@nestjs/common";
import { SequelizeModule } from "@nestjs/sequelize";
import { Permission } from "src/database/models/permission.model";
import { Role } from "src/database/models/role.model";
import { User } from "../../database/models/user.model";
import { RolesController } from "./roles.controller";
import { RolesService } from "./roles.service";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
import { IsUserEmailUniqueRule } from "./validators/is_user_email_unique";
......@@ -14,21 +10,17 @@ import { IsUserPhoneUniqueRule } from "./validators/is_user_phone_unique";
imports: [
SequelizeModule.forFeature([
User,
Role,
Permission,
])
],
providers: [
UsersService,
RolesService,
// Validator
IsUserEmailUniqueRule,
IsUserPhoneUniqueRule
],
controllers: [
UsersController,
RolesController,
UsersController,
],
exports: [UsersService],
})
......
import { Injectable } from "@nestjs/common";
import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
import { Role } from "src/database/models/role.model";
@ValidatorConstraint({ name: 'IsExistRole', async: true })
@Injectable()
export class IsExistRoleRule implements ValidatorConstraintInterface {
async validate(roleId: number): Promise<boolean> {
return await Role.findByPk(roleId).then(role => {
return role
}) !== null
}
defaultMessage() {
return `Role tidak ditemukan`;
}
}
export function IsExistRole(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsExistRoleRule,
})
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment