Commit 7d14eb9e authored by Geogeoge's avatar Geogeoge

install

parents
# Versioning and metadata
.git
.gitignore
.dockerignore
# Environment (contains sensitive data)
.env
.env.example
# Files not required for production
.editorconfig
Dockerfile
tslint.json
nodemon.json
.gitlab-ci.yml
# Database
DATABASE_DIALECT="mysql"
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USERNAME=root
DATABASE_PASSWORD=
DATABASE_NAME=nestjs_development
DATABASE_TIMEZONE="+07:00"
DATABASE_BENCHMARK=true
# only for managed DB
DB_SSL=false
# App
PORT=3000
APP_HOST="http://localhost:3000"
JWT_EXPIRATION_TIME=604800s # 1 week
ACCESS_TOKEN_TYPE=authentication
RATE_LIMIT_WINDOW_MS=60000 # 1 minute
RATE_LIMIT_MAX=100000 # hits
# openssl genrsa -out privatekey.pem 2048
JWT_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAqdzqLLrKOe4bDNtP2v3JXaj0n/UbMtsbaNu0ZZCRMcnQuHDH\n38ZFJ20hlvNpaKaUC771GADoIZQNnrlKh6MndGdXaBvatm8e06bPDCgsywWu37e2\nzKwY0Tpqbi6JTNvzYeV2leRx8rPKchevpYOnUR6VowVcKElXv/jNrEXNlrSDut+v\nceC74V1uGQIExYyaMEetkpTAum3dHDViWF+sHQoHaCu/Uc3KEosV0hMppwDIfhiq\nISTUeDoUmuGppAvvuTntFnHbWpLml85e0crSCwYuzyBfKuz3xSY0bu2a4TnEUtf4\nKiTNMlEnhfAXW8IT2z6RGhz7QYDYTwtmeJnKqQIDAQABAoIBAHnIArQy7w19YjNj\ngOsWjKnzmf6ccfWSFSKmcb1nWlGByXySjxomQio0GaLpvKbk1rtdGscsz0BTrzge\nzApWtNDFzmq7naCyJj3EGCk1HHgqCOgoBIzUza4j3W5FOva5stJ6+vwYHoHM2NXX\nmXxm2SxVax7HlvzjKGBf9sUN/omOktjoNDSi9lMMcqJZyGPJWq7D2qhWMigBm9SO\nQS+rJmIvuweKt7RBoSbG/qOArh3yCk4ENF8fkaDOf/mDnn+VaeT+4tUO5ev4XTxt\n6gzU3Duh06BoUPIud2cpwMcljslgox2Kk7Ct2ENlufQHvd/dYK8LdKTQPQWISWux\nSUT9ykkCgYEA1LmF+kqKCNdkNw8ML+2HqVRSPRMLIFcllsyrK3kqnh18dNLMFVlQ\n84En19R6ljMD/JBnMiGY/IXpydERAFz2QGa1Kr35y2D8TtSoAsxdtu0YjR+H63KF\nZnNkGSfXbDZhxs6feNMfE+qIy9197b3Y9DfSO/Jf8UCuO8VPFVTC+QcCgYEAzGsy\nYrxx6hyUxPoiwVGpkW9K1JW4DiwwZogmDUHYBFjlUYLqcIn0GL4Kzm/MgGmwaIFV\nAboy7pPIXERPFinZUa7xCAU6tS10rNqz3Q7K47FaAULSxotvuIR0fQ+snSVDICVP\niZ6mZJKDbZLh+c21VrV068rALaMf1RT7lvzEos8CgYAYFm3qWphoyeMKLQUHRFFR\nRkANtGNVJSkXe1OCl7TQtKwMGnaLkFKPUDevw9qhuWMhLUTSM2W8anl/pdHUBbLI\nthibn2lysye+0Wu47WVLOmvGRyS/73bVlAloo89L7JiDx4/dQaLKQYt98jW5Pv8b\nIFsI+LgOfHF+SShcVP3C7wKBgAR/b1Jg1xjRnCxhXJZoV5TNoBXhXXViX4ZCBKRS\nCJC/6/vJJ6NmRaqts8srBpLkwG7sOZSaJji9o7D0B9FzBkcX1FHa25nU8DZFcaOx\niaFM67APalfYBZNHxtBhoeirQzjX2VIr3XllB+NHUhWPAyhvQa1WfrgCWNb1aTu/\nQAxdAoGAZJNb5nOtBpEa5o0j6Q2029/+qeAq1U6pWtTmv/I2DqRPPCFEuirfYMMA\n0fLoGGg9p6QL914xu4h9FUxFRsdgy/UE9d0RXc0H7L3to3Ka+z7Or92rFNN0BS7D\nM7V9KfZ5VpXgbyPsOdxSMzAqAUB2+lz+8CuBCrddUU8dQnhfu+U=\n-----END RSA PRIVATE KEY-----
# openssl rsa -in privatekey.pem -out publickey.pem -pubout -outform PEM
JWT_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqdzqLLrKOe4bDNtP2v3J\nXaj0n/UbMtsbaNu0ZZCRMcnQuHDH38ZFJ20hlvNpaKaUC771GADoIZQNnrlKh6Mn\ndGdXaBvatm8e06bPDCgsywWu37e2zKwY0Tpqbi6JTNvzYeV2leRx8rPKchevpYOn\nUR6VowVcKElXv/jNrEXNlrSDut+vceC74V1uGQIExYyaMEetkpTAum3dHDViWF+s\nHQoHaCu/Uc3KEosV0hMppwDIfhiqISTUeDoUmuGppAvvuTntFnHbWpLml85e0crS\nCwYuzyBfKuz3xSY0bu2a4TnEUtf4KiTNMlEnhfAXW8IT2z6RGhz7QYDYTwtmeJnK\nqQIDAQAB\n-----END PUBLIC KEY-----
# Documentation
ENABLE_DOCUMENTATION=1
# Upload
MAX_SIZE_UPLOAD=20971520 # 20Mb
# Crypto
CRYPTO_SECRET_KEY=
# Ledger
LEDGER_URL=""
# Mailer (mailgun)
MAIL_API_KEY=null
MAIL_DOMAIN=null
MAIL_FROM_NAME="Makna"
MAIL_FROM_ADDRESS="noreply@makna.id"
# Payment
PAYMENT_URL=
PAYMENT_USERNAME=
PAYMENT_PASSWORD=
# Whatsapp
WHATSAPP_MACHINE=
# QRevent
QR_EVENT_URL=
# AWS
AWS_S3_BUCKET=
AWS_S3_ACCESS_KEY=
AWS_S3_KEY_SECRET=
AWS_S3_ENDPOINT=
MEDIA_HOST=
# URL Shortener
URL_SHORTENER_URL=
URL_SHORTENER_USERNAME=
URL_SHORTENER_PASSWORD=
#REDIS
REDIS_HOST=
REDIS_PORT=
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
root: true,
env: {
node: true,
jest: true,
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
# dependencies
/node_modules
# IDE
/.idea
/.awcache
/.vscode
# misc
npm-debug.log
# example
/quick-start
# tests
/test
/coverage
/.nyc_output
# dist
/dist
# env
.env
!.env.example
# uploads
public/qrcode/*
public/uploads/*
!public/uploads/.keep
ecosystem.config.js
*.pem
.DS_Store
stages:
- build-docker-dev
- deploy-dev
variables:
DOCKER_REGISTRY: 492617429530.dkr.ecr.ap-southeast-1.amazonaws.com
APP_NAME: biomedilab
DOCKER_TAGS: bio-svc-user-dev
dev:build:
image:
name: docker:latest
pull_policy: if-not-present
stage: build-docker-dev
only:
- dev
services:
- docker:dind
tags:
- dev
before_script:
- apk add --no-cache curl jq python3 py3-pip
- pip install awscli
- aws ecr get-login-password | docker login --username AWS --password-stdin $DOCKER_REGISTRY
script:
- docker build -t $DOCKER_REGISTRY/$APP_NAME:$DOCKER_TAGS .
- docker push $DOCKER_REGISTRY/$APP_NAME:$DOCKER_TAGS
dev:deploy:
image:
name: alpine:3.16
pull_policy: if-not-present
stage: deploy-dev
only:
- dev
tags:
- dev
before_script:
- echo "setup ssh"
- apk update
- apk add --no-cache openssh
- mkdir -p ~/.ssh
- echo "$dev_ssh_key" > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
script:
- ssh $dev_ssh_user@$dev_ssh_host -p22 "/usr/bin/bash /home/ubuntu/projects/deploy-svc.sh deploy bio-svc-user"
16.15.0
\ No newline at end of file
const path = require('path');
module.exports = {
'config': path.resolve('config', 'sequelize-cli.js'),
// 'models-path': path.resolve('db', 'models'),
'seeders-path': path.resolve('src/database', 'seeders'),
'migrations-path': path.resolve('src/database', 'migrations')
}
ARG NODE_IMAGE=node:16.17.0-alpine
FROM $NODE_IMAGE AS base
RUN apk --no-cache add dumb-init
RUN mkdir -p /home/node/app && chown node:node /home/node/app
WORKDIR /home/node/app
USER node
RUN mkdir tmp
FROM base AS dependencies
COPY --chown=node:node ./package*.json ./
RUN npm ci
FROM dependencies as build
COPY --chown=node:node . .
RUN npm run build
FROM build AS release
COPY --chown=node:node --from=build /home/node/app/ .
CMD [ "npm", "run", "start" ]
# NestJS Starter
## Requirement
1. Node.js 16.15.0
2. Git
3. PostgreSQL
## Installation
1. Clone this project
```bash
$ git clone git@git.seni.cloud:nodejs/nestjs-starter.git
$ cd nestjs-starter
$ npm install
```
2. Create Environment (Update Database dan Email Environment)
```bash
$ cd nestjs-starter
$ cp .env.example .env
```
3. Create Database
```bash
$ npm run db:create
```
4. Migrate Database
```bash
$ npm run db:migrate
$ npm run db:seed
```
## How To Start Develop
1. Run Application
```bash
$ npm run start:dev
```
## How To Start Production
1. Run Application
```bash
$ npm run start:prod
```
## API Documentation
Open the API documentation from browser: [http://localhost:3000/api-documentation](http://localhost:3000/api-documentation)
require("dotenv").config();
module.exports = {
development: {
dialect: process.env.DATABASE_DIALECT,
host: process.env.DATABASE_HOST,
port: process.env.DATABASE_PORT,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
},
// test: {
// dialect: process.env.DATABASE_DIALECT,
// host: process.env.DATABASE_HOST,
// port: process.env.DATABASE_PORT,
// username: process.env.DATABASE_USERNAME,
// password: process.env.DATABASE_PASSWORD,
// database: process.env.DATABASE_NAME,
// },
production: {
dialect: process.env.DATABASE_DIALECT,
host: process.env.DATABASE_HOST,
port: process.env.DATABASE_PORT,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
...(process.env.DB_SSL && {
dialectOptions: {
ssl: {
rejectUnauthorized: false, // very important
},
},
}),
},
};
require("dotenv").config();
const pjson = require('./package')
let port = process.env.PORT
module.exports = {
apps : [
{
name: `${pjson.name}:${port}`,
script: "npm run start",
merge_logs: true,
instances: 1,
env: {
"PORT": port,
"NODE_ENV": "development",
...require('dotenv').config().parsed
}
}
]
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "dolan",
"version": "1.0.0",
"description": "NestJS starter repository",
"license": "MIT",
"engines": {
"node": "16.17.0",
"npm": "8.15.0"
},
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "NODE_ENV=production node dist/main",
"lint": "eslint '{src,apps,libs,test}/**/*.ts' --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "echo 'No e2e tests implemented yet.'",
"db:create": "npx sequelize db:create",
"db:migrate": "npx sequelize db:migrate",
"db:migrate:status": "npx sequelize db:migrate:status",
"db:migrate:undo": "npx sequelize db:migrate:undo",
"db:reset": "npx sequelize db:migrate:undo:all && npx sequelize db:migrate",
"db:seed": "npx sequelize db:seed:all",
"format-lint": "npm run format && npm run lint"
},
"dependencies": {
"@grpc/grpc-js": "^1.8.4",
"@nestjs/axios": "^0.1.0",
"@nestjs/common": "9.0.1",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "9.0.1",
"@nestjs/jwt": "^9.0.0",
"@nestjs/microservices": "^9.2.1",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "9.0.1",
"@nestjs/sequelize": "9.0.0",
"@nestjs/serve-static": "^3.0.0",
"@nestjs/swagger": "^6.0.5",
"@types/hbs": "^4.0.2",
"aws-sdk": "^2.1306.0",
"bcrypt": "^5.0.1",
"cache-manager": "^4.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"compression": "^1.7.4",
"crypto-js": "^4.1.1",
"dotenv": "^16.0.1",
"express": "^4.18.2",
"express-ctx": "^0.1.1",
"express-rate-limit": "^6.5.1",
"hbs": "^4.2.0",
"helmet": "^5.1.1",
"ioredis": "^5.3.2",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
"moment": "^2.29.4",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
"mysql2": "^2.3.3",
"nodemailer": "^6.9.5",
"nodemailer-mailgun-transport": "^2.1.5",
"passport": "^0.6.0",
"passport-jwt": "^4.0.0",
"pg": "^8.7.3",
"pg-hstore": "^2.3.4",
"qrcode": "^1.5.3",
"reflect-metadata": "0.1.13",
"request-ip": "^3.3.0",
"rimraf": "3.0.2",
"rxjs": "7.5.5",
"sequelize": "6.21.2",
"sequelize-cli": "^6.4.1",
"sequelize-typescript": "2.1.3",
"shelljs": "^0.8.5",
"short-uuid": "^4.2.2",
"typescript": "4.7.4",
"ua-parser-js": "^1.0.32",
"uuid": "^9.0.0",
"webpack": "^5.88.2"
},
"devDependencies": {
"@nestjs/cli": "9.0.0",
"@nestjs/schematics": "9.0.1",
"@nestjs/testing": "9.0.1",
"@types/bcrypt": "^5.0.0",
"@types/cache-manager": "^4.0.2",
"@types/ejs": "^3.1.1",
"@types/express": "4.17.13",
"@types/jest": "28.1.4",
"@types/lodash": "^4.14.184",
"@types/moment": "^2.13.0",
"@types/multer": "^1.4.7",
"@types/node": "18.0.3",
"@types/nodemailer": "^6.4.10",
"@types/passport-jwt": "^3.0.6",
"@types/request-ip": "^0.0.37",
"@types/supertest": "2.0.12",
"@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "5.30.5",
"@typescript-eslint/parser": "5.30.5",
"eslint": "8.19.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.26.0",
"jest": "28.1.2",
"prettier": "2.7.1",
"supertest": "6.2.4",
"ts-jest": "28.0.5",
"ts-loader": "9.3.1",
"ts-node": "10.8.2",
"tsconfig-paths": "4.0.0",
"typescript": "4.7.4"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
Not found
\ No newline at end of file
import { HttpModule } from "@nestjs/axios";
import { CacheModule, Logger, MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { SequelizeModule } from "@nestjs/sequelize";
import { ServeStaticModule } from "@nestjs/serve-static";
import { join } from "path";
import { AuthModule } from "./modules/auth/auth.module";
import { AppConfigService } from "./share/app-config.service";
import { ShareModule } from "./share/share.module";
import { UsersModule } from "./modules/users/users.module";
import { MomentTimezoneProvider } from "./common/providers/moment-timezone.provider";
import { LoggingMiddleware } from './middleware/logging.middleware';
import { LoggerService } from "./common/logger/logger.service";
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
import { ExceptionInterceptor } from "./interceptors/exception.interceptor";
import { NotFoundInterceptor } from "./interceptors/not-found.interceptor";
import { UnauthorizedInterceptor } from "./interceptors/unauthorized.interceptor";
import { ForbiddenInterceptor } from "./interceptors/forbidden.interceptor";
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ".env",
}),
SequelizeModule.forRootAsync({
imports: [ShareModule],
inject: [AppConfigService, LoggerService],
useFactory: (appConfigService: AppConfigService, logger: LoggerService) => ({
dialect: appConfigService.db().dialect,
host: appConfigService.db().host,
port: appConfigService.db().port,
username: appConfigService.db().username,
password: appConfigService.db().password,
database: appConfigService.db().database,
autoLoadModels: true, // models will be loaded automatically
synchronize: false, // disable automatically loaded models will be synchronized, use migrate
benchmark: appConfigService.db().logging,
timezone: appConfigService.db().timezone || "+07:00",
logging: (query, options) => {
if (appConfigService.db().logging) {
logger.mysql_log(query, options);
}
},
...(process.env.DB_SSL == 'true' && {
ssl: process.env.NEST_ENV == 'production',
dialectOptions: {
ssl: {
rejectUnauthorized: false,
},
},
}),
}),
}),
ServeStaticModule.forRoot({
rootPath: join(__dirname, "..", "public"),
exclude: ["/api*"],
}),
CacheModule.register({ isGlobal: true }),
HttpModule,
ShareModule,
AuthModule,
UsersModule,
],
providers: [
MomentTimezoneProvider,
Logger,
{
provide: APP_INTERCEPTOR,
useClass: ExceptionInterceptor
},
{
provide: APP_FILTER,
useClass: NotFoundInterceptor
},
{
provide: APP_FILTER,
useClass: UnauthorizedInterceptor
},
{
provide: APP_FILTER,
useClass: ForbiddenInterceptor
}
]
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes('*');
}
}
\ No newline at end of file
import { ConfigService } from "@nestjs/config";
const configService = new ConfigService
interface ToNumberOptions {
default?: number;
min?: number;
max?: number;
}
export function toLowerCase(value: string): string {
return value.toLowerCase();
}
export function trim(value: string): string {
return value.trim();
}
export function toDate(value: string): Date {
return new Date(value);
}
export function toBoolean(value: string): boolean {
if (typeof value === 'boolean') {
return value
}
value = value.toLowerCase();
return value === "true" || value === "1" ? true : false;
}
export function toNumber(value: string, opts: ToNumberOptions = {}): number {
let newValue: number = Number.parseInt(value || String(opts.default), 10);
if (Number.isNaN(newValue)) {
newValue = opts.default;
}
if (opts.min) {
if (newValue < opts.min) {
newValue = opts.min;
}
if (newValue > opts.max) {
newValue = opts.max;
}
}
return newValue;
}
export function defaultBoolean(_default: boolean, value?: any) {
if (value) {
return toBoolean(value);
}
return _default;
}
export function toSplit(value: string) {
if (value) return value.split(",");
return value;
}
export function toUrl(path: string) {
if (path) {
const url = configService.get("AWS_S3_URL") + "/" + path
return url;
}
return null;
}
export function toRupiah(number){
return new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR"
}).format(number);
}
export function numberFormat(val) {
// remove sign if negative
var sign = 1;
if (val < 0) {
sign = -1;
val = -val;
}
// trim the number decimal point if it exists
let num = val.toString().includes(',') ? val.toString().split(',')[0] : val.toString();
let len = num.toString().length;
let result = '';
let count = 1;
for (let i = len - 1; i >= 0; i--) {
result = num.toString()[i] + result;
if (count % 3 === 0 && count !== 0 && i !== 0) {
result = '.' + result;
}
count++;
}
// add number after decimal point
if (val.toString().includes(',')) {
result = result + ',' + val.toString().split(',')[1];
}
// return result with - sign if negative
return sign < 0 ? '-' + result : result;
}
export function rupiahFormat(val) {
// remove sign if negative
var sign = 1;
if (val < 0) {
sign = -1;
val = -val;
}
// trim the number decimal point if it exists
let num = val.toString().includes(',') ? val.toString().split(',')[0] : val.toString();
let len = num.toString().length;
let result = '';
let count = 1;
for (let i = len - 1; i >= 0; i--) {
result = num.toString()[i] + result;
if (count % 3 === 0 && count !== 0 && i !== 0) {
result = '.' + result;
}
count++;
}
// add number after decimal point
if (val.toString().includes(',')) {
result = result + ',' + val.toString().split(',')[1];
}
// return result with - sign if negative
return sign < 0 ? '(Rp. ' + result + ',-)' : 'Rp. ' + result + ',-';
}
\ No newline at end of file
'use strict';
export const INQUIRY = 'inquiry'
export const CART = 'cart'
export const PAYMENT = 'payment'
export const PAID = 'paid'
export const BOOKING = 'booking'
export const ACTIVE = 'active'
export const DEACTIVATE = 'deactivate'
export const DELETED = 'deleted'
export const CANCELLED = 'cancelled'
export const EXPIRED = 'expired'
export const TERMINATE = 'terminate'
export const REDEEMED = 'redeemed'
export const AUDIO = 'audio'
export const IMAGE = 'image'
export const DOCUMENT = 'document'
export const TEXT = 'text'
export const VIDEO = 'video'
export const DEDUCTION = 'deduction'
export const DEPOSIT = 'deposit'
export const DEPOSIT_DEDUCTION = 'deposit_deduction'
export const TRANSACTION = 'transaction'
export const POS = 'pos'
export const TRANSFER = 'transfer'
export const TOPUP = 'topup'
export const TOPUP_TF = 'topup-tf'
export const TOPUP_ADMIN_FEE = 'topup_admin_fee'
export const SERVICE_FEE = 'service_fee'
export const TICKET_PAYMENT = 'ticket_payment'
export const TRANSACTION_ADMIN_FEE = 'transaction_admin_fee'
export const SYSTEM = 'system'
export const ONE_TIME = 'one-time'
export const RECURRING = 'recurring'
export const ADMIN = 'admin'
export const STAFF = 'staff'
export const CUSTOMER = 'customer'
export const OUTLET = 'outlet'
export const WORLD = 'world'
export const MASTER_MERCHANT = 'master_merchant'
export const IDR = 'idr'
enum ROLE_ENUM {
SUPERADMIN = 'Superadmin',
ADMIN = 'Admin',
PATIENT = 'Patient',
}
enum ROLE_EXCEPT_SUPER_ENUM {
ADMIN = 'Admin',
PATIENT = 'Patient',
}
export {
ROLE_ENUM,
ROLE_EXCEPT_SUPER_ENUM
}
\ No newline at end of file
import { InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as CryptoJS from 'crypto-js';
const configService = new ConfigService
const secretKey = CryptoJS.enc.Utf8.parse(configService.get("CRYPTO_SECRET_KEY"))
if (!secretKey) {
throw new InternalServerErrorException('Secret key are required')
}
export function encrypt (data) {
const bytes = CryptoJS.AES.encrypt(data, secretKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString();
return bytes.toString(CryptoJS.enc.Base64);
}
export function decrypt (encryptedData) {
try {
const bytes = CryptoJS.AES.decrypt(encryptedData, secretKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return bytes.toString(CryptoJS.enc.Utf8);
} catch (e) {
return null;
}
}
\ No newline at end of file
import { ConfigService } from "@nestjs/config";
import { Sequelize } from "sequelize-typescript";
const configService = new ConfigService
export function eventAttributes(): any[]{
return [
'id',
'thumbnailType',
[
Sequelize.fn('CONCAT',
`${configService.get("MEDIA_HOST")}/`,
Sequelize.col('thumbnail_file'),
),
'thumbnailFile'
],
'bannerType',
[
Sequelize.fn('CONCAT',
`${configService.get("MEDIA_HOST")}/`,
Sequelize.col('banner_file'),
),
'bannerFile'
],
'name',
'type',
'description',
'termsAndConditions',
'location',
'coordinate',
'deposit',
'registerStartAt',
'registerFinishAt',
'eventStartAt',
'eventFinishAt',
'active'
]
}
export function registrationAttributes(): any[]{
return [
'id',
'customerId',
'eventId',
'transactionId',
'eventAt',
[
Sequelize.fn('CONCAT',
`${configService.get("MEDIA_HOST")}/`,
Sequelize.col('qr'),
),
'qr'
],
'code',
'name',
'phoneNumber',
'eventPriceId',
'eventPriceName',
'eventPriceAmount',
'deposit',
'redeemBy',
'inquiryAt',
'registrationAt',
'redeemAt',
'terminateAt',
'status',
'createdAt',
'updatedAt'
]
}
export function mediaAttributes(): any[]{
return [
'id',
'file_name',
[
Sequelize.fn('CONCAT',
`${configService.get("MEDIA_HOST")}/`,
Sequelize.col('url'),
),
'url'
],
'type',
'status',
'owner_id',
'createdAt',
'updatedAt',
'deletedAt'
]
}
\ No newline at end of file
import { ApiProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsNumber, IsOptional, IsString } from "class-validator";
import { toNumber } from "src/common/cast";
export class QueryDto {
@ApiProperty({
example: 1,
required: false,
default: 1,
description: "Page number"
})
@Transform(({ value }) => toNumber(value, { default: 1, min: 1 }))
@IsNumber()
@IsOptional()
page?: number = 1;
@ApiProperty({
example: 10,
required: false,
default: 10,
description: "Limit page"
})
@Transform(({ value }) => toNumber(value, { default: 10, min: 1 }))
@IsNumber()
@IsOptional()
limit?: number = 10;
@ApiProperty({
required: false,
default: "desc",
description: "Sorting column order (asc, desc)",
enum: ['asc', 'desc']
})
@IsString()
@IsOptional()
order?: string = "asc";
@ApiProperty({
example: "id",
required: false,
default: "id",
description: "Sorting column id"
})
@IsString()
@IsOptional()
orderBy?: string = "id";
}
export const error_message = {
not_found: "Not found",
bad_request: "Bad request",
general_error: "General error",
success: "Success",
unauthorized: "Unauthorized",
forbidden: "Access not allowed",
invalid_parameter: "Invalid or missing parameter",
no_data: "Data not found",
invalid_account: "Account is not valid.",
invalid_pin: "PIN is not valid.",
invalid_code: "Code is not valid.",
invalid_media: "Media and type not match",
expired_pin: "PIN is expired.",
expired_code: "Code is expired.",
account_exist: "Account has been registered",
email_exist: "Email has been registered",
phone_number_exist: "Phone number has been registered",
account_not_active: "Account not active.",
account_banned: "Account has been banned",
account_deleted: "Account has been deleted",
account_pending: "Account need activation",
assigned: "Users has been assigned to this role",
invalid_credentials: "Invalid credentials, either username or password are incorrect",
limit: "Limit for create new data",
invalid_captcha: "Invalid captcha",
ticket_closed: "This conversation has ended. You can not reply to this conversation.",
assigned_ticket: "Ticket has been assigned from {from_name} to {to_name}.",
ticket_reassigned: "This ticket has been assigned to selected agent.",
ticket_closed_delete_channel: "This conversation has ended because you no longer have related channel with this conversation.",
agent_has_ticket: "You can not delete this agent. There is conversation has not been resolved by this agent",
mismatch_password_confirm: "New password and Confirmation password not match",
incorrect_password: "Password incorrect",
insufficient_balance: "Insufficient balance",
failed_upload_media: "Failed upload media to selected channel. Please contact your Administrator.",
duplicated_channel: "Channel is exist in this workspace",
limit_recipient: "Limit recipient on a broadcast is 1000",
invalid_signature: "Invalid signature",
failed_submit_template: "Failed submit template. Please make sure you fill all fields required.",
failed_send_email: "Failed to send email.",
need_approve_new_wappin: "Need to accept terms new WAPPIN",
agent_need_group: "Agents can not have no group.",
connection_refused: "Connection refused.",
once_a_minute: "Only allowed once a minute.",
once_3_times_a_minute: "Only allowed 3 times in one day.",
there_change_in_balance: "There is a change in balance data. please repeat the inquiry process",
no_price_configuration: "There is no price configuration yet",
customer_registered: "Customers have registered for this event",
mandatory: "should not be empty",
product_not_found: "Product not found",
}
\ No newline at end of file
import * as bcrypt from "bcrypt";
/**
* generate hash from password or string
* @param {string} password
* @returns {string}
*/
export function generateHash(password: string): string {
return bcrypt.hashSync(password, 10);
}
/**
* validate text with hash
* @param {string} password
* @param {string} hash
* @returns {Promise<boolean>}
*/
export function validateHash(
password: string | undefined,
hash: string | undefined
): Promise<boolean> {
if (!password || !hash) {
return Promise.resolve(false);
}
return bcrypt.compare(password, hash);
}
export interface IMeta {
totalPage: number,
totalData: number
page: number;
limit: number;
order: string;
orderBy: string;
}
import { Test, TestingModule } from '@nestjs/testing';
import { LoggerService } from './logger.service';
describe('LoggerService', () => {
let service: LoggerService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [LoggerService],
}).compile();
service = module.get<LoggerService>(LoggerService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
const pjson = require('../../../package.json');
import { Injectable, Logger as NestLogger } from '@nestjs/common';
import * as moment from 'moment-timezone';
import { getValue } from 'express-ctx';
var url = require('url');
@Injectable()
export class LoggerService extends NestLogger {
constructor() {
super();
}
static timenow(format = undefined){
if(format){
return moment().utcOffset(7).format(format)
}
return moment().utcOffset(7)
}
replaceAt(string, index, replacement) {
if (index >= string.length) {
return string.valueOf();
}
return string.substring(0, index) + replacement + string.substring(index + 1);
}
mysql_log(sql, timing) {
this.log(sql, `${timing}ms`)
}
jsonString(object, limit = 10000){
if(object == undefined){
return "{}"
}
var str = JSON.stringify(object)
if(str.length > limit){
return `Content too long (${str.length} characters)`
} else {
return str
}
}
log(type, ...any){
let reqId = getValue('session') || "[no session]"
if(["info", "error"].indexOf(type) !== -1){
if(type == "error"){
let str = [
'[ERROR]', this.timenow().toString(), "-", `app=${pjson.name}[${pjson['version']}]`, "-", `session=${reqId}`, "-", ...any
]
console.log(str.join(' '))
} else {
let str = [
'[INFO]', this.timenow().toString(), "-", `app=${pjson.name}[${pjson['version']}]`, "-", `session=${reqId}`, "-", ...any
]
console.log(str.join(' '))
}
} else {
let str = [
'[INFO]', this.timenow().toString(), "-", `app=${pjson.name}[${pjson['version']}]`, "-", `session=${reqId}`, "-", type, ...any
]
console.log(str.join(' '))
}
}
error(...any){
this.log("error", ...any)
}
requestIn(req, res, next){
let fullpath = url.parse(req.originalUrl).path
var str = `Incoming request host=${req.protocol}://${req.hostname}, path=${fullpath}, method=${req.method}, headers=${this.jsonString(req.headers)}, user=${this.jsonString(req.user)}, query=${this.jsonString(req.query)}, params=${this.jsonString(req.params)}, body=${this.jsonString(req.body)}, files=${this.jsonString(req.files || req.file)}`
this.log("info", str)
next()
}
responseOut(payload){
var str = `Send response with http status=${payload.error && payload.statusCode ? (payload.statusCode > 500 ? 400 : payload.statusCode) : 200}, payload=${this.jsonString(payload)}`
this.log("info", str)
}
requestOut(payload, session){
let endpoint = payload.url
if(payload?.baseURL) {
endpoint = payload?.baseURL + payload?.url
if(payload?.baseURL?.endsWith("/") && payload.url?.startsWith("/")) {
endpoint = payload?.baseURL + this.replaceAt(payload?.url, 0, "")
}
}
if(payload?.auth) {
if(payload.headers == undefined) payload.headers = {}
payload.headers['Authorization'] = "Basic " + Buffer.from(payload?.auth?.username + ":" + payload?.auth?.password).toString('base64')
}
var str = `Request out with reqSession=${session}, to=${endpoint}, method=${payload.method || "get"}, headers=${this.jsonString(payload.headers)}, params=${this.jsonString(payload.params)} body=${this.jsonString(payload.data)}`
this.log("info", str)
}
responseIn(type, response, session, elapsed){
if(type == "success"){
var str = `Get response from reqSession=${session}, elapsed=${elapsed} ms, headers=${this.jsonString(response.headers)}, data=${this.jsonString(response.data)}`
this.log("info", str)
} else {
var { payload, message } = response, data = {}, responseData = "{}"
if(payload){
data = payload
}
if(response.response){
if(response.response.data){
let rd = response.response.data
if(typeof rd == "object"){
responseData = this.jsonString(rd)
} else {
responseData = rd
}
}
}
var str = `Error response in from reqSession=${session}, elapsed=${elapsed}, message=${message}, headers=${this.jsonString(response.response?.headers)} response=${responseData}`
this.log("info", str)
}
}
timenow(format = undefined){
if(format){
return moment().utcOffset(7).format(format)
}
return moment().utcOffset(7)
}
}
export const mimetypeImage = [
'image/jpg',
'image/jpeg',
'image/png',
'image/gif'
];
export const mimetypeVideo = [
'video/mp4',
'video/mpeg',
'video/x-sgi-movie',
'video/x-msvideo',
'video/3gpp'
];
export const mimetypeDocument = [
'application/pdf',
'application/msword',
'application/vnd.ms-excel',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel'
];
export const mimetypeAny = [
'image/jpg',
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'application/msword',
'application/vnd.ms-excel',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'video/mp4',
'video/mpeg',
'video/x-sgi-movie',
'video/x-msvideo',
'video/3gpp'
];
export function convertPhoneNumber(phone, countryCode = "62") {
if(phone.substring(0,1) == "0") {
phone = countryCode + phone.substring(1);
} else
if(phone.substring(0,2) == "62") {
phone = countryCode + phone.substring(2);
} else
if(phone.substring(0,3) == "+62") {
phone = countryCode + phone.substring(3);
} else
if(phone.substring(0,1) == "+") {
phone = phone.substring(1);
}
return phone;
}
export function checkPrefixPhoneNumber(phone) {
const countryCode = "62";
if(phone.substring(0,1) == "0") {
phone = countryCode + phone.substring(1);
} else
if(phone.substring(0,1) == "8") {
phone = countryCode + phone;
} else
if(phone.substring(0,1) == "+") {
phone = phone.substring(1);
}
return phone;
}
export function isPhoneNumber(phone) {
if (isNaN(phone)) {
return false;
} else {
return true;
}
}
\ No newline at end of file
import * as moment from 'moment-timezone';
import { Provider } from '@nestjs/common';
export const MomentTimezoneProvider: Provider = {
provide: 'MOMENT_TIMEZONE_CONFIG',
useValue: moment.tz.setDefault('Asia/Jakarta')
};
\ No newline at end of file
import * as _ from "lodash";
export function clearNewLine(key: string): string {
return _.replace(key, new RegExp("\\\\n", "g"), "\n");
}
export function randomString(length) {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
let counter = 0;
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result;
}
export function randomNumberString(length) {
let result = '';
const characters = '0123456789';
const charactersLength = characters.length;
let counter = 0;
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result;
}
export function integerToChar(integer, length) {
const string = integer.toString();
const char = string.padStart(length, '0');
return char;
}
\ No newline at end of file
export function hasTimeComponent(datetimeStr) {
const timeRegex = /\d{2}:\d{2}:\d{2}/;
return timeRegex.test(datetimeStr);
}
\ No newline at end of file
'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');
}
};
'use strict';
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.createTable('users', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
username: {
type: Sequelize.STRING,
field: 'username',
allowNull: true,
},
phoneNumber: {
type: Sequelize.STRING(20),
field: 'phone_number',
allowNull: true
},
email: {
type: Sequelize.STRING(100),
allowNull: true
},
password: Sequelize.STRING,
active: {
type: Sequelize.BOOLEAN,
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',
},
emailVerificationAt: {
type: Sequelize.DATE,
field: 'email_verification_at',
},
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
}
});
await queryInterface.addIndex('users', ['email']);
await queryInterface.addIndex('users', ['phone_number']);
await queryInterface.addIndex('users', ['active']);
await queryInterface.addIndex('users', ['role_id']);
},
async down (queryInterface) {
await queryInterface.dropTable('users');
}
};
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.createTable('user_verifications', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
userId: {
type: Sequelize.BIGINT,
field: 'user_id',
references: { model: 'users', key: 'id' },
allowNull: false
},
type: {
type: Sequelize.ENUM("Email","Phone"),
allowNull: false
},
phoneNumber: {
type: Sequelize.STRING(20),
field: 'phone_number',
allowNull: true,
},
email: {
type: Sequelize.STRING(100),
allowNull: true,
},
token: {
type: Sequelize.STRING,
allowNull: false
},
expiredAt: {
type: Sequelize.DATE,
field: "expired_at",
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'),
},
});
},
async down (queryInterface, Sequelize) {
await queryInterface.dropTable('user_verifications');
}
};
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.createTable('password_resets', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
userId: {
type: Sequelize.BIGINT,
field: 'user_id',
references: { model: 'users', key: 'id' },
allowNull: false
},
phoneNumber: {
type: Sequelize.STRING(20),
field: 'phone_number',
allowNull: true
},
email: {
type: Sequelize.STRING(100),
allowNull: true
},
token: {
type: Sequelize.STRING,
allowNull: false
},
expiredAt: {
type: Sequelize.DATE,
field: 'expired_at',
allowNull: false,
},
status: {
type: Sequelize.STRING(10),
allowNull: true
},
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'),
},
});
},
async down (queryInterface, Sequelize) {
await queryInterface.dropTable('password_resets');
}
};
import {
AllowNull,
BelongsTo,
Column,
DataType,
ForeignKey,
Table,
} from "sequelize-typescript";
import { TimestampModel } from "src/database/models/timestamp.model";
import { User } from "./user.model";
import { ACTIVE, CANCELLED, DEACTIVATE, EXPIRED } from "src/common/constant";
@Table({ timestamps: true, tableName: "password_resets" })
export class PasswordReset extends TimestampModel {
@ForeignKey(() => User)
@Column({ field: "user_id" })
userId: number;
@AllowNull(true)
@Column({ field: "phone_number" })
phoneNumber: string;
@AllowNull(true)
@Column
email: string;
@AllowNull(false)
@Column
token: string;
@AllowNull(false)
@Column({ field: 'expired_at' })
expiredAt: Date;
@AllowNull(false)
@Column({
type: DataType.ENUM(
ACTIVE,
DEACTIVATE,
CANCELLED,
EXPIRED
)
})
status: string;
@BelongsTo(() => User)
user: User;
}
import {
AllowNull,
BelongsToMany,
Column,
Default,
DefaultScope,
DeletedAt,
HasMany,
Table,
} from "sequelize-typescript";
import { TimestampModel } from "src/database/models/timestamp.model";
@DefaultScope(() => ({
attributes: {exclude: ['profilePhotoFile']},
include: [],
}))
@Table({ timestamps: true, tableName: "roles" })
export class Role extends TimestampModel {
@AllowNull(false)
@Column
name: string;
@AllowNull(true)
@Column({ field: "display_name" })
displayName: string;
@AllowNull(false)
@Default(true)
@Column
active: boolean;
@DeletedAt
@Column({ field: "deleted_at" })
deletedAt?: Date;
}
import { Column, CreatedAt, Model, UpdatedAt } from "sequelize-typescript";
export class TimestampModel extends Model {
@CreatedAt
@Column({ field: "created_at" })
createdAt: Date;
@UpdatedAt
@Column({ field: "updated_at" })
updatedAt: Date;
}
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" })
export class User extends TimestampModel {
@Unique(true)
@AllowNull(false)
@Column
username: string;
@Unique(true)
@AllowNull(false)
@Column({ field: "phone_number" })
phoneNumber: string;
@Unique(true)
@AllowNull(false)
@Column
email: string;
@Column
password: string;
@AllowNull(false)
@Column({ defaultValue: false })
active: boolean;
@ForeignKey(() => Role)
@Column({ field: "role_id" })
roleId: number;
@Column({ field: "email_verification_sent_at" })
emailVerificationSentAt: Date;
@Column({ field: "email_verification_at" })
emailVerificationAt: Date;
@DeletedAt
@Column({ field: "deleted_at" })
deletedAt?: Date;
@BelongsTo(() => Role)
role: Role;
@BeforeCreate
static hashPassword(instance: User) {
if (instance.password) {
instance.password = generateHash(instance.password);
}
}
@BeforeUpdate
static hashPasswordIfNeeded(instance: User) {
if (instance.password !== (instance as any)._previousDataValues.password) {
instance.password = generateHash(instance.password);
}
}
}
import {
AllowNull,
BelongsTo,
Column,
DataType,
Default,
ForeignKey,
Table,
} from "sequelize-typescript";
import { TimestampModel } from "src/database/models/timestamp.model";
import { User } from "./user.model";
@Table({ timestamps: true, tableName: "user_verifications" })
export class UserVerification extends TimestampModel {
@ForeignKey(() => User)
@Column({ field: "user_id" })
userId: number;
@AllowNull(false)
@Column(DataType.ENUM("Email","Phone"))
type: string;
@AllowNull(true)
@Column({ field: "phone_number" })
phoneNumber: string;
@AllowNull(true)
@Column
email: string;
@AllowNull(false)
@Column
token: string;
@AllowNull(false)
@Column({ field: "expired_at" })
expiredAt: Date;
@BelongsTo(() => User)
user: User;
}
"use strict";
module.exports = {
async up(queryInterface) {
await queryInterface.bulkDelete('roles', null, {});
await queryInterface.bulkInsert(
"roles",
[
{
id: 1,
name: "superadmin",
display_name: "Superadmin",
active: true,
},
{
id: 2,
name: "admin",
display_name: "Admin",
active: true,
},
{
id: 3,
name: "staff",
display_name: "Staff",
active: true,
},
{
id: 4,
name: "customer",
display_name: "Customer",
active: true,
},
{
id: 5,
name: "api",
display_name: "API",
active: true,
},
],
{}
);
},
async down(queryInterface) {
await queryInterface.bulkDelete('roles', null, {});
},
};
"use strict";
const bcrypt = require("bcrypt");
module.exports = {
async up(queryInterface) {
await queryInterface.bulkDelete('users', null, {});
await queryInterface.bulkInsert(
"users",
[
{
id: 1,
phone_number: "6281111111111",
username: "superadmin@example.com",
email: "superadmin@example.com",
password: bcrypt.hashSync("6~$*N+{g}gPH?q2D", 10),
active: true,
role_id: 1,
},
{
id: 2,
phone_number: "-",
username: "api@example.com",
email: "api@example.com",
password: bcrypt.hashSync("6~$*N+{g}gPH?q2D", 10),
active: true,
role_id: 2,
},
{
id: 3,
phone_number: "628222222222",
username: "admin@example.com",
email: "admin@example.com",
password: bcrypt.hashSync("6~$*N+{g}gPH?q2D", 10),
active: true,
role_id: 2,
},
{
id: 4,
phone_number: "6283333333333",
username: "staff@example.com",
email: "staff@example.com",
password: bcrypt.hashSync("6~$*N+{g}gPH?q2D", 10),
active: true,
role_id: 3,
},
],
{}
);
},
async down(queryInterface) {
await queryInterface.bulkDelete('users', null, {});
},
};
"1","1","11.01","Aceh Selatan","Aceh Selatan"
"2","1","11.02","Aceh Tenggara","Aceh Tenggara"
"3","1","11.03","Aceh Timur","Aceh Timur"
"4","1","11.04","Aceh Tengah","Aceh Tengah"
"5","1","11.05","Aceh Barat","Aceh Barat"
"6","1","11.06","Aceh Besar","Aceh Besar"
"7","1","11.07","Pidie","Pidie"
"8","1","11.08","Aceh Utara","Aceh Utara"
"9","1","11.09","Simeulue","Simeulue"
"10","1","11.10","Aceh Singkil","Aceh Singkil"
"11","1","11.11","Bireuen","Bireuen"
"12","1","11.12","Aceh Barat Daya","Aceh Barat Daya"
"13","1","11.13","Gayo Lues","Gayo Lues"
"14","1","11.14","Aceh Jaya","Aceh Jaya"
"15","1","11.15","Nagan Raya","Nagan Raya"
"16","1","11.16","Aceh Tamiang","Aceh Tamiang"
"17","1","11.17","Bener Meriah","Bener Meriah"
"18","1","11.18","Pidie Jaya","Pidie Jaya"
"19","1","11.71","Banda Aceh","Banda Aceh"
"20","1","11.72","Sabang","Sabang"
"21","1","11.73","Lhokseumawe","Lhokseumawe"
"22","1","11.74","Langsa","Langsa"
"23","1","11.75","Subulussalam","Subulussalam"
"24","2","12.01","Tapanuli Tengah","Tapanuli Tengah"
"25","2","12.02","Tapanuli Utara","Tapanuli Utara"
"26","2","12.03","Tapanuli Selatan","Tapanuli Selatan"
"27","2","12.04","Nias","Nias"
"28","2","12.05","Langkat","Langkat"
"29","2","12.06","Karo","Karo"
"30","2","12.07","Deli Serdang","Deli Serdang"
"31","2","12.08","Simalungun","Simalungun"
"32","2","12.09","Asahan","Asahan"
"33","2","12.10","Labuhanbatu","Labuhanbatu"
"34","2","12.11","Dairi","Dairi"
"35","2","12.12","Toba Samosir","Toba Samosir"
"36","2","12.13","Mandailing Natal","Mandailing Natal"
"37","2","12.14","Nias Selatan","Nias Selatan"
"38","2","12.15","Pakpak Bharat","Pakpak Bharat"
"39","2","12.16","Humbang Hasundutan","Humbang Hasundutan"
"40","2","12.17","Samosir","Samosir"
"41","2","12.18","Serdang Bedagai","Serdang Bedagai"
"42","2","12.19","Batu Bara","Batu Bara"
"43","2","12.20","Padang Lawas Utara","Padang Lawas Utara"
"44","2","12.21","Padang Lawas","Padang Lawas"
"45","2","12.22","Labuhanbatu Selatan","Labuhanbatu Selatan"
"46","2","12.23","Labuhanbatu Utara","Labuhanbatu Utara"
"47","2","12.24","Nias Utara","Nias Utara"
"48","2","12.25","Nias Barat","Nias Barat"
"49","2","12.71","Medan","Medan"
"50","2","12.72","Pematang Siantar","Pematang Siantar"
"51","2","12.73","Sibolga","Sibolga"
"52","2","12.74","Tanjung Balai","Tanjung Balai"
"53","2","12.75","Binjai","Binjai"
"54","2","12.76","Tebing Tinggi","Tebing Tinggi"
"55","2","12.77","Padang Sidempuan","Padang Sidempuan"
"56","2","12.78","Gunungsitoli","Gunungsitoli"
"57","3","13.01","Pesisir Selatan","Pesisir Selatan"
"58","3","13.02","Solok","Kab. Solok"
"59","3","13.03","Sijunjung","Sijunjung"
"60","3","13.04","Tanah Datar","Tanah Datar"
"61","3","13.05","Padang Pariaman","Padang Pariaman"
"62","3","13.06","Agam","Agam"
"63","3","13.07","Lima Puluh Kota","Lima Puluh Kota"
"64","3","13.08","Pasaman","Pasaman"
"65","3","13.09","Kepulauan Mentawai","Kepulauan Mentawai"
"66","3","13.10","Dharmasraya","Dharmasraya"
"67","3","13.11","Solok Selatan","Solok Selatan"
"68","3","13.12","Pasaman Barat","Pasaman Barat"
"69","3","13.71","Padang","Padang"
"70","3","13.72","Solok","Kota Solok"
"71","3","13.73","Sawah Lunto","Sawah Lunto"
"72","3","13.74","Padang Panjang","Padang Panjang"
"73","3","13.75","Bukittinggi","Bukittinggi"
"74","3","13.76","Payakumbuh","Payakumbuh"
"75","3","13.77","Pariaman","Pariaman"
"76","4","14.01","Kampar","Kampar"
"77","4","14.02","Indragiri Hulu","Indragiri Hulu"
"78","4","14.03","Bengkalis","Bengkalis"
"79","4","14.04","Indragiri Hilir","Indragiri Hilir"
"80","4","14.05","Pelalawan","Pelalawan"
"81","4","14.06","Rokan Hulu","Rokan Hulu"
"82","4","14.07","Rokan Hilir","Rokan Hilir"
"83","4","14.08","Siak","Siak"
"84","4","14.09","Kuantan Singingi","Kuantan Singingi"
"85","4","14.10","Kepulauan Meranti","Kepulauan Meranti"
"86","4","14.71","Pekanbaru","Pekanbaru"
"87","4","14.72","Dumai","Dumai"
"88","5","15.01","Kerinci","Kerinci"
"89","5","15.02","Merangin","Merangin"
"90","5","15.03","Sarolangun","Sarolangun"
"91","5","15.04","Batang Hari","Batang Hari"
"92","5","15.05","Muaro Jambi","Muaro Jambi"
"93","5","15.06","Tanjung Jabung Barat","Tanjung Jabung Barat"
"94","5","15.07","Tanjung Jabung Timur","Tanjung Jabung Timur"
"95","5","15.08","Bungo","Bungo"
"96","5","15.09","Tebo","Tebo"
"97","5","15.71","Jambi","Jambi"
"98","5","15.72","Sungaipenuh","Sungaipenuh"
"99","6","16.01","Ogan Komering Ulu","Ogan Komering Ulu"
"100","6","16.02","Ogan Komering Ilir","Ogan Komering Ilir"
"101","6","16.03","Muara Enim","Muara Enim"
"102","6","16.04","Lahat","Lahat"
"103","6","16.05","Musi Rawas","Musi Rawas"
"104","6","16.06","Musi Banyuasin","Musi Banyuasin"
"105","6","16.07","Banyuasin","Banyuasin"
"106","6","16.08","Ogan Komering Ulu Timur","Ogan Komering Ulu Timur"
"107","6","16.09","Ogan Komering Ulu Selatan","Ogan Komering Ulu Selatan"
"108","6","16.10","Ogan Ilir","Ogan Ilir"
"109","6","16.11","Empat Lawang","Empat Lawang"
"110","6","16.12","Penukal Abab Lematang Ilir","Penukal Abab Lematang Ilir"
"111","6","16.13","Musi Rawas Utara","Musi Rawas Utara"
"112","6","16.71","Palembang","Palembang"
"113","6","16.72","Pagar Alam","Pagar Alam"
"114","6","16.73","Lubuk Linggau","Lubuk Linggau"
"115","6","16.74","Prabumulih","Prabumulih"
"116","7","17.01","Bengkulu Selatan","Bengkulu Selatan"
"117","7","17.02","Rejang Lebong","Rejang Lebong"
"118","7","17.03","Bengkulu Utara","Bengkulu Utara"
"119","7","17.04","Kaur","Kaur"
"120","7","17.05","Seluma","Seluma"
"121","7","17.06","Muko Muko","Muko Muko"
"122","7","17.07","Lebong","Lebong"
"123","7","17.08","Kepahiang","Kepahiang"
"124","7","17.09","Bengkulu Tengah","Bengkulu Tengah"
"125","7","17.71","Bengkulu","Bengkulu"
"126","8","18.01","Lampung Selatan","Lampung Selatan"
"127","8","18.02","Lampung Tengah","Lampung Tengah"
"128","8","18.03","Lampung Utara","Lampung Utara"
"129","8","18.04","Lampung Barat","Lampung Barat"
"130","8","18.05","Tulang Bawang","Tulang Bawang"
"131","8","18.06","Tanggamus","Tanggamus"
"132","8","18.07","Lampung Timur","Lampung Timur"
"133","8","18.08","Way Kanan","Way Kanan"
"134","8","18.09","Pesawaran","Pesawaran"
"135","8","18.10","Pringsewu","Pringsewu"
"136","8","18.11","Mesuji","Mesuji"
"137","8","18.12","Tulang Bawang Barat","Tulang Bawang Barat"
"138","8","18.13","Pesisir Barat","Pesisir Barat"
"139","8","18.71","Bandar Lampung","Bandar Lampung"
"140","8","18.72","Metro","Metro"
"141","9","19.01","Bangka","Bangka"
"142","9","19.02","Belitung","Belitung"
"143","9","19.03","Bangka Selatan","Bangka Selatan"
"144","9","19.04","Bangka Tengah","Bangka Tengah"
"145","9","19.05","Bangka Barat","Bangka Barat"
"146","9","19.06","Belitung Timur","Belitung Timur"
"147","9","19.71","Pangkal Pinang","Pangkal Pinang"
"148","10","21.01","Bintan","Bintan"
"149","10","21.02","Karimun","Karimun"
"150","10","21.03","Natuna","Natuna"
"151","10","21.04","Lingga","Lingga"
"152","10","21.05","Kepulauan Anambas","Kepulauan Anambas"
"153","10","21.71","Batam","Batam"
"154","10","21.72","Tanjung Pinang","Tanjung Pinang"
"155","11","31.01","Kepulauan Seribu","Kepulauan Seribu"
"156","11","31.71","Jakarta Pusat","Jakarta Pusat"
"157","11","31.72","Jakarta Utara","Jakarta Utara"
"158","11","31.73","Jakarta Barat","Jakarta Barat"
"159","11","31.74","Jakarta Selatan","Jakarta Selatan"
"160","11","31.75","Jakarta Timur","Jakarta Timur"
"161","12","32.01","Bogor","Kab. Bogor"
"162","12","32.02","Sukabumi","Kab. Sukabumi"
"163","12","32.03","Cianjur","Cianjur"
"164","12","32.04","Bandung","Kab. Bandung"
"165","12","32.05","Garut","Garut"
"166","12","32.06","Tasikmalaya","Kab. Tasikmalaya"
"167","12","32.07","Ciamis","Ciamis"
"168","12","32.08","Kuningan","Kuningan"
"169","12","32.09","Cirebon","Kab. Cirebon"
"170","12","32.10","Majalengka","Majalengka"
"171","12","32.11","Sumedang","Sumedang"
"172","12","32.12","Indramayu","Indramayu"
"173","12","32.13","Subang","Subang"
"174","12","32.14","Purwakarta","Purwakarta"
"175","12","32.15","Karawang","Karawang"
"176","12","32.16","Bekasi","Kab. Bekasi"
"177","12","32.17","Bandung Barat","Bandung Barat"
"178","12","32.18","Pangandaran","Pangandaran"
"179","12","32.71","Bogor","Kota Bogor"
"180","12","32.72","Sukabumi","Kota Sukabumi"
"181","12","32.73","Bandung","Kota Bandung"
"182","12","32.74","Cirebon","Kota Cirebon"
"183","12","32.75","Bekasi","Kota Bekasi"
"184","12","32.76","Depok","Depok"
"185","12","32.77","Cimahi","Cimahi"
"186","12","32.78","Tasikmalaya","Kota Tasikmalaya"
"187","12","32.79","Banjar","Banjar"
"188","13","33.01","Cilacap","Cilacap"
"189","13","33.02","Banyumas","Banyumas"
"190","13","33.03","Purbalingga","Purbalingga"
"191","13","33.04","Banjarnegara","Banjarnegara"
"192","13","33.05","Kebumen","Kebumen"
"193","13","33.06","Purworejo","Purworejo"
"194","13","33.07","Wonosobo","Wonosobo"
"195","13","33.08","Magelang","Kab. Magelang"
"196","13","33.09","Boyolali","Boyolali"
"197","13","33.10","Klaten","Klaten"
"198","13","33.11","Sukoharjo","Sukoharjo"
"199","13","33.12","Wonogiri","Wonogiri"
"200","13","33.13","Karanganyar","Karanganyar"
"201","13","33.14","Sragen","Sragen"
"202","13","33.15","Grobogan","Grobogan"
"203","13","33.16","Blora","Blora"
"204","13","33.17","Rembang","Rembang"
"205","13","33.18","Pati","Pati"
"206","13","33.19","Kudus","Kudus"
"207","13","33.20","Jepara","Jepara"
"208","13","33.21","Demak","Demak"
"209","13","33.22","Semarang","Kab. Semarang"
"210","13","33.23","Temanggung","Temanggung"
"211","13","33.24","Kendal","Kendal"
"212","13","33.25","Batang","Batang"
"213","13","33.26","Pekalongan","Kab. Pekalongan"
"214","13","33.27","Pemalang","Pemalang"
"215","13","33.28","Tegal","Kab. Tegal"
"216","13","33.29","Brebes","Brebes"
"217","13","33.71","Magelang","Kota Magelang"
"218","13","33.72","Surakarta","Surakarta"
"219","13","33.73","Salatiga","Salatiga"
"220","13","33.74","Semarang","Kota Semarang"
"221","13","33.75","Pekalongan","Kota Pekalongan"
"222","13","33.76","Tegal","Kota Tegal"
"223","14","34.01","Kulon Progo","Kulon Progo"
"224","14","34.02","Bantul","Bantul"
"225","14","34.03","Gunung Kidul","Gunung Kidul"
"226","14","34.04","Sleman","Sleman"
"227","14","34.71","Yogyakarta","Yogyakarta"
"228","15","35.01","Pacitan","Pacitan"
"229","15","35.02","Ponorogo","Ponorogo"
"230","15","35.03","Trenggalek","Trenggalek"
"231","15","35.04","Tulungagung","Tulungagung"
"232","15","35.05","Blitar","Kab. Blitar"
"233","15","35.06","Kediri","Kab. Kediri"
"234","15","35.07","Malang","Kab. Malang"
"235","15","35.08","Lumajang","Lumajang"
"236","15","35.09","Jember","Jember"
"237","15","35.10","Banyuwangi","Banyuwangi"
"238","15","35.11","Bondowoso","Bondowoso"
"239","15","35.12","Situbondo","Situbondo"
"240","15","35.13","Probolinggo","Kab. Probolinggo"
"241","15","35.14","Pasuruan","Kab. Pasuruan"
"242","15","35.15","Sidoarjo","Sidoarjo"
"243","15","35.16","Mojokerto","Kab. Mojokerto"
"244","15","35.17","Jombang","Jombang"
"245","15","35.18","Nganjuk","Nganjuk"
"246","15","35.19","Madiun","Kab. Madiun"
"247","15","35.20","Magetan","Magetan"
"248","15","35.21","Ngawi","Ngawi"
"249","15","35.22","Bojonegoro","Bojonegoro"
"250","15","35.23","Tuban","Tuban"
"251","15","35.24","Lamongan","Lamongan"
"252","15","35.25","Gresik","Gresik"
"253","15","35.26","Bangkalan","Bangkalan"
"254","15","35.27","Sampang","Sampang"
"255","15","35.28","Pamekasan","Pamekasan"
"256","15","35.29","Sumenep","Sumenep"
"257","15","35.71","Kediri","Kota Kediri"
"258","15","35.72","Blitar","Kota Blitar"
"259","15","35.73","Malang","Kota Malang"
"260","15","35.74","Probolinggo","Kota Probolinggo"
"261","15","35.75","Pasuruan","Kota Pasuruan"
"262","15","35.76","Mojokerto","Kota Mojokerto"
"263","15","35.77","Madiun","Kota Madiun"
"264","15","35.78","Surabaya","Surabaya"
"265","15","35.79","Batu","Batu"
"266","16","36.01","Pandeglang","Pandeglang"
"267","16","36.02","Lebak","Lebak"
"268","16","36.03","Tangerang","Kab. Tangerang"
"269","16","36.04","Serang","Kab. Serang"
"270","16","36.71","Tangerang","Kota Tangerang"
"271","16","36.72","Cilegon","Cilegon"
"272","16","36.73","Serang","Kota Serang"
"273","16","36.74","Tangerang Selatan","Tangerang Selatan"
"274","17","51.01","Jembrana","Jembrana"
"275","17","51.02","Tabanan","Tabanan"
"276","17","51.03","Badung","Badung"
"277","17","51.04","Gianyar","Gianyar"
"278","17","51.05","Klungkung","Klungkung"
"279","17","51.06","Bangli","Bangli"
"280","17","51.07","Karangasem","Karangasem"
"281","17","51.08","Buleleng","Buleleng"
"282","17","51.71","Denpasar","Denpasar"
"283","18","52.01","Lombok Barat","Lombok Barat"
"284","18","52.02","Lombok Tengah","Lombok Tengah"
"285","18","52.03","Lombok Timur","Lombok Timur"
"286","18","52.04","Sumbawa","Sumbawa"
"287","18","52.05","Dompu","Dompu"
"288","18","52.06","Bima","Kab. Bima"
"289","18","52.07","Sumbawa Barat","Sumbawa Barat"
"290","18","52.08","Lombok Utara","Lombok Utara"
"291","18","52.71","Mataram","Mataram"
"292","18","52.72","Bima","Kota Bima"
"293","19","53.01","Kupang","Kab. Kupang"
"294","19","53.02","Timor Tengah Selatan","Timor Tengah Selatan"
"295","19","53.03","Timor Tengah Utara","Timor Tengah Utara"
"296","19","53.04","Belu","Belu"
"297","19","53.05","Alor","Alor"
"298","19","53.06","Flores Timur","Flores Timur"
"299","19","53.07","Sikka","Sikka"
"300","19","53.08","Ende","Ende"
"301","19","53.09","Ngada","Ngada"
"302","19","53.10","Manggarai","Manggarai"
"303","19","53.11","Sumba Timur","Sumba Timur"
"304","19","53.12","Sumba Barat","Sumba Barat"
"305","19","53.13","Lembata","Lembata"
"306","19","53.14","Rote Ndao","Rote Ndao"
"307","19","53.15","Manggarai Barat","Manggarai Barat"
"308","19","53.16","Nagekeo","Nagekeo"
"309","19","53.17","Sumba Tengah","Sumba Tengah"
"310","19","53.18","Sumba Barat Daya","Sumba Barat Daya"
"311","19","53.19","Manggarai Timur","Manggarai Timur"
"312","19","53.20","Sabu Raijua","Sabu Raijua"
"313","19","53.21","Malaka","Malaka"
"314","19","53.71","Kupang","Kota Kupang"
"315","20","61.01","Sambas","Sambas"
"316","20","61.02","Mempawah","Mempawah"
"317","20","61.03","Sanggau","Sanggau"
"318","20","61.04","Ketapang","Ketapang"
"319","20","61.05","Sintang","Sintang"
"320","20","61.06","Kapuas Hulu","Kapuas Hulu"
"321","20","61.07","Bengkayang","Bengkayang"
"322","20","61.08","Landak","Landak"
"323","20","61.09","Sekadau","Sekadau"
"324","20","61.10","Melawi","Melawi"
"325","20","61.11","Kayong Utara","Kayong Utara"
"326","20","61.12","Kubu Raya","Kubu Raya"
"327","20","61.71","Pontianak","Pontianak"
"328","20","61.72","Singkawang","Singkawang"
"329","21","62.01","Kotawaringin Barat","Kotawaringin Barat"
"330","21","62.02","Kotawaringin Timur","Kotawaringin Timur"
"331","21","62.03","Kapuas","Kapuas"
"332","21","62.04","Barito Selatan","Barito Selatan"
"333","21","62.05","Barito Utara","Barito Utara"
"334","21","62.06","Katingan","Katingan"
"335","21","62.07","Seruyan","Seruyan"
"336","21","62.08","Sukamara","Sukamara"
"337","21","62.09","Lamandau","Lamandau"
"338","21","62.10","Gunung Mas","Gunung Mas"
"339","21","62.11","Pulang Pisau","Pulang Pisau"
"340","21","62.12","Murung Raya","Murung Raya"
"341","21","62.13","Barito Timur","Barito Timur"
"342","21","62.71","Palangka Raya","Palangka Raya"
"343","22","63.01","Tanah Laut","Tanah Laut"
"344","22","63.02","Kotabaru","Kotabaru"
"345","22","63.03","Banjar","Banjar"
"346","22","63.04","Barito Kuala","Barito Kuala"
"347","22","63.05","Tapin","Tapin"
"348","22","63.06","Hulu Sungai Selatan","Hulu Sungai Selatan"
"349","22","63.07","Hulu Sungai Tengah","Hulu Sungai Tengah"
"350","22","63.08","Hulu Sungai Utara","Hulu Sungai Utara"
"351","22","63.09","Tabalong","Tabalong"
"352","22","63.10","Tanah Bumbu","Tanah Bumbu"
"353","22","63.11","Balangan","Balangan"
"354","22","63.71","Banjarmasin","Banjarmasin"
"355","22","63.72","Banjarbaru","Banjarbaru"
"356","23","64.01","Paser","Paser"
"357","23","64.02","Kutai Kartanegara","Kutai Kartanegara"
"358","23","64.03","Berau","Berau"
"359","23","64.07","Kutai Barat","Kutai Barat"
"360","23","64.08","Kutai Timur","Kutai Timur"
"361","23","64.09","Penajam Paser Utara","Penajam Paser Utara"
"362","23","64.11","Mahakam Ulu","Mahakam Ulu"
"363","23","64.71","Balikpapan","Balikpapan"
"364","23","64.72","Samarinda","Samarinda"
"365","23","64.74","Bontang","Bontang"
"366","24","65.01","Bulungan","Bulungan"
"367","24","65.02","Malinau","Malinau"
"368","24","65.03","Nunukan","Nunukan"
"369","24","65.04","Tana Tidung","Tana Tidung"
"370","24","65.71","Tarakan","Tarakan"
"371","25","71.01","Bolaang Mongondow","Bolaang Mongondow"
"372","25","71.02","Minahasa","Minahasa"
"373","25","71.03","Kepulauan Sangihe","Kepulauan Sangihe"
"374","25","71.04","Kepulauan Talaud","Kepulauan Talaud"
"375","25","71.05","Minahasa Selatan","Minahasa Selatan"
"376","25","71.06","Minahasa Utara","Minahasa Utara"
"377","25","71.07","Minahasa Tenggara","Minahasa Tenggara"
"378","25","71.08","Bolaang Mongondow Utara","Bolaang Mongondow Utara"
"379","25","71.09","Kepulauan Siau Tagulandang Biaro (Sitaro)","Kepulauan Siau Tagulandang Biaro (Sitaro)"
"380","25","71.10","Bolaang Mongondow Timur","Bolaang Mongondow Timur"
"381","25","71.11","Bolaang Mongondow Selatan","Bolaang Mongondow Selatan"
"382","25","71.71","Manado","Manado"
"383","25","71.72","Bitung","Bitung"
"384","25","71.73","Tomohon","Tomohon"
"385","25","71.74","Kotamobagu","Kotamobagu"
"386","26","72.01","Banggai","Banggai"
"387","26","72.02","Poso","Poso"
"388","26","72.03","Donggala","Donggala"
"389","26","72.04","Toli-Toli","Toli-Toli"
"390","26","72.05","Buol","Buol"
"391","26","72.06","Morowali","Morowali"
"392","26","72.07","Banggai Kepulauan","Banggai Kepulauan"
"393","26","72.08","Parigi Moutong","Parigi Moutong"
"394","26","72.09","Tojo Una-Una","Tojo Una-Una"
"395","26","72.10","Sigi","Sigi"
"396","26","72.11","Banggai Laut","Banggai Laut"
"397","26","72.12","Morowali Utara","Morowali Utara"
"398","26","72.71","Palu","Palu"
"399","27","73.01","Kepulauan Selayar","Kepulauan Selayar"
"400","27","73.02","Bulukumba","Bulukumba"
"401","27","73.03","Bantaeng","Bantaeng"
"402","27","73.04","Jeneponto","Jeneponto"
"403","27","73.05","Takalar","Takalar"
"404","27","73.06","Gowa","Gowa"
"405","27","73.07","Sinjai","Sinjai"
"406","27","73.08","Bone","Bone"
"407","27","73.09","Maros","Maros"
"408","27","73.10","Pangkajene Kepulauan","Pangkajene Kepulauan"
"409","27","73.11","Barru","Barru"
"410","27","73.12","Soppeng","Soppeng"
"411","27","73.13","Wajo","Wajo"
"412","27","73.14","Sidenreng Rappang","Sidenreng Rappang"
"413","27","73.15","Pinrang","Pinrang"
"414","27","73.16","Enrekang","Enrekang"
"415","27","73.17","Luwu","Luwu"
"416","27","73.18","Tana Toraja","Tana Toraja"
"417","27","73.22","Luwu Utara","Luwu Utara"
"418","27","73.24","Luwu Timur","Luwu Timur"
"419","27","73.26","Toraja Utara","Toraja Utara"
"420","27","73.71","Makassar","Makassar"
"421","27","73.72","Parepare","Parepare"
"422","27","73.73","Palopo","Palopo"
"423","28","74.01","Kolaka","Kolaka"
"424","28","74.02","Konawe","Konawe"
"425","28","74.03","Muna","Muna"
"426","28","74.04","Buton","Buton"
"427","28","74.05","Konawe Selatan","Konawe Selatan"
"428","28","74.06","Bombana","Bombana"
"429","28","74.07","Wakatobi","Wakatobi"
"430","28","74.08","Kolaka Utara","Kolaka Utara"
"431","28","74.09","Konawe Utara","Konawe Utara"
"432","28","74.10","Buton Utara","Buton Utara"
"433","28","74.11","Kolaka Timur","Kolaka Timur"
"434","28","74.12","Konawe Kepulauan","Konawe Kepulauan"
"435","28","74.13","Muna Barat","Muna Barat"
"436","28","74.14","Buton Tengah","Buton Tengah"
"437","28","74.15","Buton Selatan","Buton Selatan"
"438","28","74.71","Kendari","Kendari"
"439","28","74.72","Bau-Bau","Bau-Bau"
"440","29","75.01","Gorontalo","Kab. Gorontalo"
"441","29","75.02","Boalemo","Boalemo"
"442","29","75.03","Bone Bolango","Bone Bolango"
"443","29","75.04","Pohuwato","Pohuwato"
"444","29","75.05","Gorontalo Utara","Gorontalo Utara"
"445","29","75.71","Gorontalo","Kota Gorontalo"
"446","30","76.01","Mamuju Utara","Mamuju Utara"
"447","30","76.02","Mamuju","Mamuju"
"448","30","76.03","Mamasa","Mamasa"
"449","30","76.04","Polewali Mandar","Polewali Mandar"
"450","30","76.05","Majene","Majene"
"451","30","76.06","Mamuju Tengah","Mamuju Tengah"
"452","31","81.01","Maluku Tengah","Maluku Tengah"
"453","31","81.02","Maluku Tenggara","Maluku Tenggara"
"454","31","81.03","Maluku Tenggara Barat","Maluku Tenggara Barat"
"455","31","81.04","Buru","Buru"
"456","31","81.05","Seram Bagian Timur","Seram Bagian Timur"
"457","31","81.06","Seram Bagian Barat","Seram Bagian Barat"
"458","31","81.07","Kepulauan Aru","Kepulauan Aru"
"459","31","81.08","Maluku Barat Daya","Maluku Barat Daya"
"460","31","81.09","Buru Selatan","Buru Selatan"
"461","31","81.71","Ambon","Ambon"
"462","31","81.72","Tual","Tual"
"463","32","82.01","Halmahera Barat","Halmahera Barat"
"464","32","82.02","Halmahera Tengah","Halmahera Tengah"
"465","32","82.03","Halmahera Utara","Halmahera Utara"
"466","32","82.04","Halmahera Selatan","Halmahera Selatan"
"467","32","82.05","Kepulauan Sula","Kepulauan Sula"
"468","32","82.06","Halmahera Timur","Halmahera Timur"
"469","32","82.07","Pulau Morotai","Pulau Morotai"
"470","32","82.08","Pulau Taliabu","Pulau Taliabu"
"471","32","82.71","Ternate","Ternate"
"472","32","82.72","Tidore Kepulauan","Tidore Kepulauan"
"473","33","91.01","Merauke","Merauke"
"474","33","91.02","Jayawijaya","Jayawijaya"
"475","33","91.03","Jayapura","Kab. Jayapura"
"476","33","91.04","Nabire","Nabire"
"477","33","91.05","Kepulauan Yapen","Kepulauan Yapen"
"478","33","91.06","Biak Numfor","Biak Numfor"
"479","33","91.07","Puncak Jaya","Puncak Jaya"
"480","33","91.08","Paniai","Paniai"
"481","33","91.09","Mimika","Mimika"
"482","33","91.10","Sarmi","Sarmi"
"483","33","91.11","Keerom","Keerom"
"484","33","91.12","Pegunungan Bintang","Pegunungan Bintang"
"485","33","91.13","Yahukimo","Yahukimo"
"486","33","91.14","Tolikara","Tolikara"
"487","33","91.15","Waropen","Waropen"
"488","33","91.16","Boven Digoel","Boven Digoel"
"489","33","91.17","Mappi","Mappi"
"490","33","91.18","Asmat","Asmat"
"491","33","91.19","Supiori","Supiori"
"492","33","91.20","Mamberamo Raya","Mamberamo Raya"
"493","33","91.21","Mamberamo Tengah","Mamberamo Tengah"
"494","33","91.22","Yalimo","Yalimo"
"495","33","91.23","Lanny Jaya","Lanny Jaya"
"496","33","91.24","Nduga","Nduga"
"497","33","91.25","Puncak","Puncak"
"498","33","91.26","Dogiyai","Dogiyai"
"499","33","91.27","Intan Jaya","Intan Jaya"
"500","33","91.28","Deiyai","Deiyai"
"501","33","91.71","Jayapura","Kota Jayapura"
"502","34","92.01","Sorong","Kab. Sorong"
"503","34","92.02","Manokwari","Manokwari"
"504","34","92.03","Fakfak","Fakfak"
"505","34","92.04","Sorong Selatan","Sorong Selatan"
"506","34","92.05","Raja Ampat","Raja Ampat"
"507","34","92.06","Teluk Bintuni","Teluk Bintuni"
"508","34","92.07","Teluk Wondama","Teluk Wondama"
"509","34","92.08","Kaimana","Kaimana"
"510","34","92.09","Tambrauw","Tambrauw"
"511","34","92.10","Maybrat","Maybrat"
"512","34","92.11","Manokwari Selatan","Manokwari Selatan"
"513","34","92.12","Pegunungan Arfak","Pegunungan Arfak"
"514","34","92.71","Sorong","Kota Sorong"
"1","11","Aceh (NAD)"
"2","12","Sumatera Utara"
"3","13","Sumatera Barat"
"4","14","Riau"
"5","15","Jambi"
"6","16","Sumatera Selatan"
"7","17","Bengkulu"
"8","18","Lampung"
"9","19","Kepulauan Bangka Belitung"
"10","21","Kepulauan Riau"
"11","31","DKI Jakarta"
"12","32","Jawa Barat"
"13","33","Jawa Tengah"
"14","34","DI Yogyakarta"
"15","35","Jawa Timur"
"16","36","Banten"
"17","51","Bali"
"18","52","Nusa Tenggara Barat (NTB)"
"19","53","Nusa Tenggara Timur (NTT)"
"20","61","Kalimantan Barat"
"21","62","Kalimantan Tengah"
"22","63","Kalimantan Selatan"
"23","64","Kalimantan Timur"
"24","65","Kalimantan Utara"
"25","71","Sulawesi Utara"
"26","72","Sulawesi Tengah"
"27","73","Sulawesi Selatan"
"28","74","Sulawesi Tenggara"
"29","75","Gorontalo"
"30","76","Sulawesi Barat"
"31","81","Maluku"
"32","82","Maluku Utara"
"33","91","Papua"
"34","92","Papua Barat"
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
import { BadRequestException, CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Observable } from "rxjs";
import { mimetypeAny } from "src/common/mimetype";
const configService = new ConfigService;
@Injectable()
export class AnyFile implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const allowType = mimetypeAny;
if (!allowType.includes(req.file.mimetype)){
throw new BadRequestException("Type file tidak");
}
if (req.file.size > configService.get("MAX_SIZE_UPLOAD")) {
throw new BadRequestException("Ukuran file melebihi batas yang diijinkan.")
}
return next.handle();
}
}
import {
BadRequestException,
CallHandler,
ExecutionContext,
Injectable,
InternalServerErrorException,
NestInterceptor,
NotFoundException,
UnauthorizedException,
UnprocessableEntityException,
} from "@nestjs/common";
import { catchError, Observable, throwError } from "rxjs";
import { map } from 'rxjs/operators';
import { LoggerService } from "src/common/logger/logger.service";
const pjson = require('../../package.json');
@Injectable()
export class ExceptionInterceptor implements NestInterceptor {
constructor(
private readonly logger: LoggerService
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isProduction = process.env.NODE_ENV === "production";
return next.handle().pipe(
map(data => {
this.logger.responseOut(data)
return data;
}),
catchError((err) => {
console.error(err)
this.logger.error(err.toString())
if (err instanceof NotFoundException) {
this.logger.responseOut({
error: true,
statusCode: 404,
message: err.message
})
return throwError(() => {
const errorPayload: any = {
error: {
code: 404,
title: "Not found",
message: err.message
},
data: {},
meta: {
version: pjson['version']
}
};
return new NotFoundException(errorPayload);
});
}
if (err.constructor.name === "ValidationError") {
this.logger.responseOut({
error: true,
statusCode: 422,
message: err.message
})
// sequelize
return throwError(() => {
const errorPayload: any = {
error: {
code: 422,
title: "ValidationError",
message: err.message
},
data: {},
meta: {
version: pjson['version']
}
};
return new UnprocessableEntityException(errorPayload);
});
}
if (err instanceof BadRequestException) {
this.logger.responseOut(err.getResponse())
// class-validator
return throwError(() => {
const errorResponse = (err.getResponse() as any);
var code = (err.getResponse() as any).code ?? 400;
var title = (err.getResponse() as any).title ?? "Bad Request";
const { data, ...customError } = errorResponse
delete customError.statusCode;
delete customError.error;
if (Array.isArray(customError.message)) {
customError.message = customError.message.join(", ")
}
const errorPayload: any = {
error: {
code,
title,
...customError
},
data: data ?? {},
meta: {
version: pjson['version']
}
};
return new BadRequestException(errorPayload);
});
}
if (err instanceof UnauthorizedException) {
return throwError(() => {
const errorPayload: any = {
error: {
code: 401,
title: "Unauthorized",
message: (err.getResponse() as any).message,
},
data: {},
meta: {
version: pjson['version']
}
};
return new UnauthorizedException(errorPayload);
});
}
this.logger.responseOut({
error: true,
statusCode: err?.original?.code ?? 500,
message: err.message
})
return throwError(() => {
const errorPayload: any = {
error: {
code: err?.original?.code,
title: "Server error",
message: "Server error",
},
data: {},
meta: {
version: pjson['version']
}
};
if (
err.constructor.name == "UniqueConstraintError" &&
err?.original?.code == "23505"
) {
errorPayload.error.message = err.errors[0].message;
}
return new InternalServerErrorException(errorPayload);
});
})
);
}
}
import { ArgumentsHost, Catch, ExceptionFilter, ForbiddenException, UnauthorizedException } from '@nestjs/common';
import { error_message } from 'src/common/error_message';
import { LoggerService } from 'src/common/logger/logger.service';
const pjson = require('../../package.json');
@Catch(ForbiddenException)
export class ForbiddenInterceptor implements ExceptionFilter {
constructor(
private readonly logger: LoggerService
) {}
catch(exception: ForbiddenException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
const message = error_message.unauthorized;
const exceptionResponse = exception.getResponse() as any;
this.logger.responseOut({
error: true,
statusCode: 403,
message: error_message.forbidden
})
response.status(403).json({
error: {
code: 403,
title: "forbidden",
message: (exception.getResponse() as any).message,
},
data: {},
meta: {
version: pjson['version']
}
});
}
}
\ No newline at end of file
import { BadRequestException, CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Observable } from "rxjs";
const configService = new ConfigService;
@Injectable()
export class ImageFile implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const allowType = [
'image/jpg',
'image/jpeg',
'image/png',
'image/gif',
]
if (!allowType.includes(req.file.mimetype)) {
throw new BadRequestException("Type file tidak");
}
if (req.file.size > configService.get("MAX_SIZE_UPLOAD")) {
throw new BadRequestException("Ukuran file melebihi batas yang diijinkan.")
}
return next.handle();
}
}
\ No newline at end of file
import {
ArgumentsHost,
Catch,
ExceptionFilter,
NotFoundException,
} from "@nestjs/common";
@Catch(NotFoundException)
export class NotFoundInterceptor implements ExceptionFilter {
constructor() {}
catch(exception: NotFoundException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const exceptionResponse = exception.getResponse() as any;
if (exceptionResponse.custom) {
delete exceptionResponse.custom;
return response.status(404).json(exceptionResponse);
}
response.status(404).json(exceptionResponse);
}
}
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
RequestTimeoutException,
} from "@nestjs/common";
import {
catchError,
Observable,
throwError,
timeout,
TimeoutError,
} from "rxjs";
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(1000),
catchError((err) => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => new Error());
})
);
}
}
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from "@nestjs/common";
import { map, Observable } from "rxjs";
import * as _ from "lodash";
const pjson = require('../../package.json');
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformationInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<Response<T>> {
const methodName = _.lowerCase(context.getHandler().name);
const className = _.lowerCase(
context.getClass().name.replace("Controller", "")
);
return next.handle().pipe(
map((data) => {
return {
data,
meta: {
version: pjson['version']
}
};
})
);
}
}
import { ArgumentsHost, Catch, ExceptionFilter, UnauthorizedException } from '@nestjs/common';
import { error_message } from 'src/common/error_message';
import { LoggerService } from 'src/common/logger/logger.service';
const pjson = require('../../package.json');
@Catch(UnauthorizedException)
export class UnauthorizedInterceptor implements ExceptionFilter {
constructor(
private readonly logger: LoggerService
) {}
catch(exception: UnauthorizedException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
const message = error_message.unauthorized;
const exceptionResponse = exception.getResponse() as any;
this.logger.responseOut({
error: true,
statusCode: 401,
message: error_message.unauthorized
})
response.status(401).json({
error: {
code: 401,
title: "Unauthorized",
message: (exception.getResponse() as any).message,
},
data: {},
meta: {
version: pjson['version']
}
});
}
}
\ No newline at end of file
require("dotenv").config(); // eslint-disable-line
import { ValidationPipe, VersioningType } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import {
ExpressAdapter,
NestExpressApplication,
} from "@nestjs/platform-express";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import * as compression from "compression";
import * as morgan from "morgan";
import { middleware as expressCtx } from "express-ctx";
import * as requestIp from "request-ip";
import { AppModule } from "./app.module";
import { setupSwagger } from "./setup-swagger";
import { TransformationInterceptor } from "./interceptors/transform.interceptor";
import { ExceptionInterceptor } from "./interceptors/exception.interceptor";
import { NotFoundInterceptor } from "./interceptors/not-found.interceptor";
import { useContainer } from "class-validator";
import * as bodyParser from 'body-parser';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
new ExpressAdapter(),
{
cors: {
origin: "*", // TODO: need specific domains
},
}
);
// Microservice
// app.connectMicroservice(grpcUserOptions);
// await app.startAllMicroservices();
// security and config
app.use(helmet());
app.use(helmet.crossOriginResourcePolicy({ policy: "cross-origin" }));
app.use(
rateLimit({
windowMs: Number(process.env.RATE_LIMIT_WINDOW_MS),
max: Number(process.env.RATE_LIMIT_MAX),
})
);
app.use(compression()); // gzip
app.use(morgan("combined")); // format logs
app.use(requestIp.mw());
// prefix: /api/v1
app.setGlobalPrefix("/api");
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: "1",
});
// whitelist — removes any property of query, body, and a parameter that is not part of our DTO
// transform — enables the transformation of our incoming request
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.useGlobalInterceptors(new TransformationInterceptor()); // JSON response standarization
if (process.env.ENABLE_DOCUMENTATION == "1") {
setupSwagger(app);
}
app.use(bodyParser.json({limit: '50mb'}));
app.use(bodyParser.urlencoded({limit: '50mb', extended: true}));
// express-ctx: can use setValue/getValue
app.use(expressCtx);
await app.listen(process.env.PORT || 3000);
console.log(`Application is running on: ${await app.getUrl()}`);
// enable custom validator
useContainer(app.select(AppModule), { fallbackOnErrors: true });
}
bootstrap();
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { LoggerService } from 'src/common/logger/logger.service';
import { v4 as uuidv4 } from 'uuid';
import { setValue } from 'express-ctx';
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
constructor(private readonly loggerService: LoggerService) {}
use(req: Request, res: Response, next: NextFunction) {
setValue('session', uuidv4());
this.loggerService.requestIn(req, res, next)
}
}
\ No newline at end of file
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Logger,
NotFoundException,
Post,
Req,
UseGuards,
} from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { Request } from "express";
import { AuthService } from "./auth.service";
import { LoginAdminDto } from "./dto/login-admin.dto";
import { PasswordCheckingDto } from "./dto/password-checking.dto";
import { RegisterDto } from "./dto/register.dto";
import { ResendEmailDto } from "./dto/resend-email.dto";
import { ResetPasswordDto } from "./dto/reset-password.dto";
import { TokenDto } from "./dto/token.dto";
import { VerifyUserAccountDto } from "./dto/verify-user-account.dto";
import { JwtAuthGuard } from "./guards/jwt-auth.guard";
import { LoginStaffDto } from "./dto/login-staff.dto";
import { PinDto } from "./dto/pin.dto";
import { RolesGuard } from "src/share/guards/roles.guard";
import { Roles } from "src/share/decorators/roles.decorator";
import { RequestResetPasswordDto } from "./dto/request-reset-password.dto";
import { PinResetPasswordDto } from "./dto/pin-reset-password.dto";
import { ChangePasswordDto } from "./dto/change-password.dto";
import { ChangePinDto } from "./dto/change-pin.dto";
import { UsersService } from "../users/users.service";
@Controller("auth")
@ApiTags("auth")
export class AuthController {
constructor(
private userService: UsersService,
private authService: AuthService,
) {}
@Post("login")
@HttpCode(HttpStatus.OK)
async login(@Body() payload: LoginAdminDto) {
const user = await this.authService.validateUser(
payload
);
const token = await this.authService.createAccessToken({
user: user.id,
role: user.roleId,
});
const data_user = await this.userService.findOne(user.id);
return {
token: token,
user: data_user
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get("me")
@HttpCode(HttpStatus.OK)
async my(@Req() request: Request): Promise<any> {
return request.user;
}
@Post("request-reset-password")
@HttpCode(HttpStatus.OK)
async requestResetPassword(@Body() payload: RequestResetPasswordDto): Promise<any> {
return this.authService.requestResetPassword(payload.username);
}
@Post("reset-password")
@HttpCode(HttpStatus.OK)
async resetPassword(@Body() payload: ResetPasswordDto): Promise<any> {
return this.authService.resetPassword(payload);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('staff')
@Post("change-password")
@HttpCode(HttpStatus.OK)
async changePassword(@Req() request: Request, @Body() payload: ChangePasswordDto): Promise<any> {
return this.authService.changePassword(request.user, payload);
}
}
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { SequelizeModule } from "@nestjs/sequelize";
import { PasswordReset } from "src/database/models/password_reset.model";
import { UserVerification } from "src/database/models/user_verification.model";
import { AppConfigService } from "../../share/app-config.service";
import { UsersModule } from "../users/users.module";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtStrategy } from "./strategies/jwt.strategy";
@Module({
imports: [
PassportModule.register({ defaultStrategy: "jwt" }),
JwtModule.registerAsync({
useFactory: (appConfigService: AppConfigService) => ({
privateKey: appConfigService.jwt().privateKey,
publicKey: appConfigService.jwt().publicKey,
signOptions: {
algorithm: "RS256",
expiresIn: appConfigService.jwt().expirationTime,
},
verifyOptions: {
algorithms: ["RS256"],
},
}),
inject: [AppConfigService],
}),
SequelizeModule.forFeature([
UserVerification,
PasswordReset,
]),
UsersModule,
],
controllers: [AuthController],
providers: [
AuthService,
JwtStrategy
],
exports: [JwtModule, AuthService],
})
export class AuthModule {}
import { BadGatewayException, BadRequestException, Injectable, NotFoundException, ServiceUnavailableException, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { AppConfigService } from "../../share/app-config.service";
import { generateHash, validateHash } from "src/common/hash";
import { User } from "../../database/models/user.model";
import { UsersService } from "../users/users.service";
import { LoginAdminDto } from "./dto/login-admin.dto";
import { TokenDto } from "./dto/token.dto";
import { Op } from "sequelize";
import { RegisterDto } from "./dto/register.dto";
import { VerifyUserAccountDto } from "./dto/verify-user-account.dto";
import { UserVerification } from "src/database/models/user_verification.model";
import { InjectModel } from "@nestjs/sequelize";
import * as moment from "moment";
import { PasswordReset } from "src/database/models/password_reset.model";
import { HttpService } from "../../share/http.service";
import { ResetPasswordDto } from "./dto/reset-password.dto";
import { Client, ClientGrpc } from "@nestjs/microservices";
import { lastValueFrom } from "rxjs";
import { toUrl } from "./../../common/cast";
import { LoginStaffDto } from "./dto/login-staff.dto";
import { error_message } from "src/common/error_message";
import { join as pathJoin } from "path";
import _ from "lodash";
import { TemplateService } from "src/share/template.service";
import { MailService } from "src/share/mail.service";
import { LoggerService } from "src/common/logger/logger.service";
import { randomNumberString } from "src/common/string";
import { ACTIVE, CANCELLED, DEACTIVATE, EXPIRED } from "src/common/constant";
import { ChangePasswordDto } from "./dto/change-password.dto";
import { ChangePinDto } from "./dto/change-pin.dto";
const short = require('short-uuid');
@Injectable()
export class AuthService {
constructor(
@InjectModel(UserVerification)
private readonly userVerificationModel: typeof UserVerification,
@InjectModel(PasswordReset)
private readonly passwordResetModel: typeof PasswordReset,
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
private readonly httpService: HttpService,
private readonly templateService: TemplateService,
private readonly mailService: MailService,
private appConfigService: AppConfigService,
private loggerService: LoggerService
) {}
async isPasswordValid(password, hash): Promise<boolean> {
return await validateHash(password, hash);
}
async validateUser(payload: LoginAdminDto, errorMessage?: string): Promise<User> {
const user = await this.usersService.findNoScope({
where: {
[Op.or]: [
{ username: payload.username},
{ email: payload.username},
{ phoneNumber: payload.username}
]
},
});
if (!user || !user.length) throw new UnauthorizedException(error_message.invalid_account);
const isPasswordValid = await this.isPasswordValid(
payload.password,
user[0].password
);
if (!isPasswordValid) throw new UnauthorizedException(error_message.invalid_account);
if (!user[0].active) throw new UnauthorizedException(error_message.account_not_active);
return user[0];
}
async createAccessToken(data: {
role: number;
user: number;
}): Promise<TokenDto> {
let expiresIn = this.appConfigService.jwt().expirationTime;
return new TokenDto({
expiresIn: expiresIn,
accessToken: await this.jwtService.signAsync({
type: this.appConfigService.jwt().accessTokenType,
user: data.user,
role: data.role,
}, {
expiresIn: expiresIn,
}),
});
}
async requestResetPassword(username) {
const user = await this.usersService.findBy({
[Op.or]: [
{ username: username },
{ email: username },
{ phoneNumber: username },
]
});
if (!user) throw new NotFoundException(error_message.invalid_account);
const password_resets = await this.passwordResetModel.findAll({
where: {
userId: user.id,
createdAt: {
[Op.gte]: moment().startOf('days')
}
},
order: [
['createdAt', 'DESC']
]
});
if (password_resets.length > 0) {
const last_createdat = password_resets[0].createdAt;
if (moment().diff(moment(last_createdat), 'minutes') < 1) {
throw new BadRequestException(error_message.once_a_minute)
}
if (password_resets.length > 2) {
const statuses = [ACTIVE, EXPIRED, CANCELLED]
if (statuses.includes(password_resets[0].status) && statuses.includes(password_resets[1].status) && statuses.includes(password_resets[2].status)) {
throw new BadRequestException(error_message.once_3_times_a_minute)
}
}
}
await this.passwordResetModel.update({
status: CANCELLED,
}, {
where: {
userId: user.id,
status: ACTIVE
}
})
const token = randomNumberString(4);
const passwordReset = await this.passwordResetModel.create({
userId: user.id,
email: user.email,
token: token,
expiredAt: moment().add(3, 'minutes').toString(),
status: ACTIVE
});
try {
const filePath = pathJoin(
"src/templates/email/forgot-password.hbs"
);
const template = await this.templateService.render(filePath, {
pin: token,
});
let name = user.email;
await this.mailService.sendMail({
to: [
{
name: name,
email: user.email,
},
],
subject: "Reset Password",
htmlContent: template,
});
return {
message: "Email has been sent",
data: {
email: passwordReset.email,
expiredAt: passwordReset.expiredAt
}
};
} catch (error) {
this.loggerService.error(error);
throw new BadGatewayException(error_message.failed_send_email);
}
}
async resetPassword(payload: ResetPasswordDto): Promise<any> {
const { pin, username, newPassword } = payload;
const user = await this.usersService.findBy({
[Op.or]: [
{ username: username },
{ email: username },
{ phoneNumber: username },
]
});
if (!user) throw new NotFoundException(error_message.invalid_account);
const password_reset = await this.passwordResetModel.findOne({
where: {
userId: user.id,
createdAt: {
[Op.gte]: moment().startOf('days')
}
},
order: [
['createdAt', 'DESC']
]
});
if (!password_reset) throw new NotFoundException(error_message.invalid_pin);
if (moment() > moment(password_reset.expiredAt)) throw new BadRequestException(error_message.expired_pin);
if (password_reset.token != pin) throw new NotFoundException(error_message.invalid_pin);
(await user).password = newPassword;
(await user).save();
(await password_reset).status = DEACTIVATE;
(await password_reset).save();
return "Password has been changed";
}
async findUserByToken(token: string) {
const tokenDecode = this.jwtService.decode(token, {
json: false
});
if (!tokenDecode) return null;
const userId = tokenDecode['user'] ?? null;
if (moment().unix() > tokenDecode['exp']) {
return null;
}
return this.usersService.findBy({id: userId});
}
async changePassword(user, payload: ChangePasswordDto) {
user = await this.usersService.findOneNoScope({
where: {
id: user.id
}
})
const isPasswordValid = await this.isPasswordValid(
payload.oldPassword,
user.password
);
if (!isPasswordValid) throw new BadRequestException(error_message.invalid_credentials);
(await user).password = payload.newPassword;
(await user).save();
return "Password has been changed";
}
}
import { IsDefined, IsString, IsNotEmpty } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class ChangePasswordDto {
@ApiProperty({ example: "6~$*N+{g}gPH?q2D" })
@IsDefined()
@IsNotEmpty()
@IsString()
oldPassword: string;
@ApiProperty({ example: "Password123?" })
@IsDefined()
@IsNotEmpty()
@IsString()
newPassword: string;
}
import { IsDefined, IsString, IsNotEmpty } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class ChangePinDto {
@ApiProperty({ example: "1111", maxLength: 4, minLength: 4 })
@IsDefined()
@IsNotEmpty()
@IsString()
newPin: string;
}
import { IsDefined, IsString, IsNotEmpty, IsEmail } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class LoginAdminDto {
@ApiProperty({ example: "admin@example.com" })
@IsDefined()
@IsNotEmpty()
@IsString()
username: string;
@ApiProperty({ example: "6~$*N+{g}gPH?q2D" })
@IsDefined()
@IsNotEmpty()
@IsString()
password: string;
}
import { IsDefined, IsString, IsNotEmpty, IsEmail, IsOptional } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class LoginStaffDto {
@ApiProperty({ example: "john.doe@example.com" })
@IsDefined()
@IsNotEmpty()
@IsString()
username: string;
@ApiProperty({ example: "6~$*N+{g}gPH?q2D" })
@IsDefined()
@IsNotEmpty()
@IsString()
password: string;
@ApiProperty({ example: "20c0b586-126c-5203-9c3a-626a6870269b" })
@IsDefined()
@IsOptional()
@IsString()
imei: string;
}
import { IsDefined, IsString, IsNotEmpty, IsEmail } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class PasswordCheckingDto {
@ApiProperty({ example: "password" })
@IsDefined()
@IsNotEmpty()
@IsString()
password: string;
}
import { IsDefined, IsString, IsNotEmpty } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class PinResetPasswordDto {
@ApiProperty({ example: "john@example.com" })
@IsDefined()
@IsNotEmpty()
@IsString()
username: string;
@ApiProperty({ example: "0000" })
@IsDefined()
@IsNotEmpty()
@IsString()
pin: string;
}
import { IsDefined, IsString, IsNotEmpty } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class PinDto {
@ApiProperty({ example: "0000" })
@IsDefined()
@IsNotEmpty()
@IsString()
pin: string;
}
import { IsDefined, IsString, IsNotEmpty, IsEmail, IsNumber, IsPhoneNumber } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
import { IsUserEmailUnique } from "src/modules/users/validators/is_user_email_unique";
import { IsUserPhoneUnique } from "src/modules/users/validators/is_user_phone_unique";
import { IsPhoneOnlyNumber } from "src/share/validators/is_phone_only_number";
import { IsName } from "src/share/validators/is_name";
export class RegisterDto {
@ApiProperty({ example: "John Doe" })
@IsDefined()
@IsNotEmpty()
@IsString()
@IsName()
name: string;
@ApiProperty({ example: "john@example.com" })
@IsDefined()
@IsNotEmpty()
@IsString()
@IsEmail()
@IsUserEmailUnique()
email: string;
@ApiProperty({ example: "083333333333" })
@IsDefined()
@IsNotEmpty()
@IsString()
@IsUserPhoneUnique()
@IsPhoneOnlyNumber()
phoneMobile: string;
@ApiProperty({ example: "password" })
@IsDefined()
@IsNotEmpty()
@IsString()
password: string;
}
import { IsDefined, IsString, IsNotEmpty } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class RequestResetPasswordDto {
@ApiProperty({ example: "john@example.com" })
@IsDefined()
@IsNotEmpty()
@IsString()
username: string;
}
import { IsDefined, IsString, IsNotEmpty, IsEmail } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class ResendEmailDto {
@ApiProperty({ example: "john@example.com" })
@IsDefined()
@IsNotEmpty()
@IsString()
email: string;
}
import { IsDefined, IsString, IsNotEmpty } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class ResetPasswordDto {
@ApiProperty({ example: "" })
@IsDefined()
@IsNotEmpty()
@IsString()
pin: string;
@ApiProperty({ example: "john@example.com" })
@IsDefined()
@IsNotEmpty()
@IsString()
username: string;
@ApiProperty({ example: "Password123?" })
@IsDefined()
@IsNotEmpty()
@IsString()
newPassword: string;
}
import { ApiProperty } from "@nestjs/swagger";
export class TokenDto {
@ApiProperty()
expiresIn: number;
@ApiProperty()
accessToken: string;
constructor(data: { expiresIn: number; accessToken: string }) {
this.expiresIn = data.expiresIn;
this.accessToken = data.accessToken;
}
}
import { IsDefined, IsString, IsNotEmpty, IsOptional, IsEnum } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
enum type {
'phone' = 'phone',
'email' = 'email'
}
export class VerifyUserAccountDto {
@ApiProperty({
example: "phone",
description: "Group By (phone / email)",
required: true
})
@IsDefined()
@IsNotEmpty()
@IsEnum(type)
type?: type;
@ApiProperty({ example: "" })
@IsDefined()
@IsOptional()
@IsString()
email: string;
@ApiProperty({ example: "083333333333" })
@IsDefined()
@IsOptional()
@IsString()
phone: string;
@ApiProperty({ example: "" })
@IsDefined()
@IsNotEmpty()
@IsString()
code: string;
}
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {}
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { AppConfigService } from "../../../share/app-config.service";
import { User } from "../../../database/models/user.model";
import { UsersService } from "../../users/users.service";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private appConfigService: AppConfigService,
private usersService: UsersService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: appConfigService.jwt().publicKey,
});
}
async validate(args: {
user: number;
role: number;
type: string;
}): Promise<User> {
if (args.type !== this.appConfigService.jwt().accessTokenType) {
throw new UnauthorizedException();
}
const user = await this.usersService.findBy({
id: args.user,
roleId: args.role,
active: true,
});
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
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 {
IsDefined,
IsString,
IsBoolean,
IsOptional,
IsNotEmpty,
IsEmail,
} from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { toBoolean, toNumber } from "src/common/cast";
export class CreateUserDto {
@ApiProperty({ example: "083333333333" })
@IsDefined()
@IsNotEmpty()
@IsString()
phoneNumber: string;
@ApiProperty({ example: "admin@example.com" })
@IsDefined()
@IsNotEmpty()
@IsString()
@IsEmail()
email: string;
@ApiProperty({ example: "password" })
@IsString()
@IsOptional()
password: string;
@ApiProperty({
example: true,
description: "Set true if you want to activate this user",
required: false,
})
@Transform(({ value }) => toBoolean(value))
@IsOptional()
@IsBoolean()
active?: boolean;
@ApiProperty({ example: 1, required: false })
@IsOptional()
@Transform(({ value }) => toNumber(value))
roleId?: number;
}
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 { IsDefined, IsString, IsNotEmpty } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class ResetUserPasswordDto {
@ApiProperty({ example: "pasien@example.com" })
@IsDefined()
@IsNotEmpty()
@IsString()
emailOrPhone: string;
@ApiProperty({ example: "password" })
@IsDefined()
@IsNotEmpty()
@IsString()
password: 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 { IsString, IsOptional, IsNotEmpty, IsEmail } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { toBoolean, toNumber } from "src/common/cast";
export class UpdateUserDto {
@ApiProperty({ example: "admin" })
@IsOptional()
@IsNotEmpty()
@IsString()
name?: string;
@ApiProperty({ example: "admin@example.com" })
@IsOptional()
@IsNotEmpty()
@IsString()
@IsEmail()
email?: string;
@ApiProperty({
example: true,
description: "Set true if you want to activate this user",
})
@IsOptional()
@Transform(({ value }) => toBoolean(value))
active?: boolean;
@ApiProperty({ example: 1 })
@IsOptional()
@Transform(({ value }) => toNumber(value))
roleId?: number;
}
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 UserQueryDto extends QueryDto {
@ApiProperty({ required: false })
@IsOptional()
@Transform(({ value }) => ({ [Op.iLike]: `%${value}%` }))
email?: string;
@ApiProperty({ required: false })
@Transform(({ value }) => toBoolean(value))
@IsOptional()
active?: boolean;
@ApiProperty({ required: false })
@IsOptional()
roleId?: number;
}
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 {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Put,
Query,
Res,
UseGuards,
} from "@nestjs/common";
import { ApiTags } from "@nestjs/swagger";
import { UsersService } from "./users.service";
import { AppConfigService } from "../../share/app-config.service";
@ApiTags("users")
@Controller("users")
export class UsersController {
constructor(
private readonly usersService: UsersService,
private readonly appConfigService: AppConfigService,
) {}
}
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";
import { IsUserPhoneUniqueRule } from "./validators/is_user_phone_unique";
@Module({
imports: [
SequelizeModule.forFeature([
User,
Role,
Permission,
])
],
providers: [
UsersService,
RolesService,
// Validator
IsUserEmailUniqueRule,
IsUserPhoneUniqueRule
],
controllers: [
UsersController,
RolesController,
],
exports: [UsersService],
})
export class UsersModule {}
import { Injectable, NotFoundException } from "@nestjs/common";
import { InjectModel } from "@nestjs/sequelize";
import { Op } from "sequelize";
import * as _ from "lodash";
import { CreateUserDto } from "./dto/create-user.dto";
import { UserQueryDto } from "./dto/user.query.dto";
import { User } from "../../database/models/user.model";
import { UpdateUserDto } from "./dto/update-user.dto";
@Injectable()
export class UsersService {
constructor(
@InjectModel(User)
private readonly userModel: typeof User,
) {}
async create(payload: CreateUserDto): Promise<User> {
const newUser = await this.userModel.create({
phoneNumber: payload.phoneNumber,
email: payload.email,
active: payload.active,
roleId: payload.roleId,
password: payload.password,
emailVerificationSentAt: new Date()
});
return this.findOne(newUser.id);
}
async count(query?: UserQueryDto): Promise<number> {
return this.userModel.count({ where: this.createWhere(query) });
}
findAll(query?: UserQueryDto): Promise<User[]> {
const { page, limit, order, orderBy } = query;
return this.userModel.findAll({
where: this.createWhere(query),
limit,
offset: (page - 1) * limit,
order: [[orderBy, order]],
});
}
findAllRaw(opts?: any): Promise<User[]> {
return this.userModel.findAll(opts);
}
findOne(id: number): Promise<User> {
return this.userModel.findOne({
where: {
id,
},
});
}
findBy(payload: { [x: string]: any }): Promise<User> {
return this.userModel.findOne({
where: payload,
});
}
findByEmailOrPhone(payload: string): Promise<User> {
return this.userModel.findOne({
where: {
[Op.or]: [
{email: payload},
{phoneNumber: payload},
]
}
});
}
findNoScope(option?: any): Promise<User[]> {
return this.userModel.unscoped().findAll(option);
}
findOneNoScope(option?: any): Promise<User> {
return this.userModel.unscoped().findOne(option);
}
async update(id: number, payload: UpdateUserDto): Promise<User> {
const user = await this.findOne(id);
if (!user) throw new NotFoundException("User not found");
await user.update(payload);
return user;
}
async delete(id: number): Promise<void> {
const user = await this.findOne(id);
if (!user) throw new NotFoundException("User not found");
await user.destroy();
}
async verification(id: number, type: string): Promise<User> {
const user = await this.findOne(id);
if (!user) throw new NotFoundException("Data pengguna tidak ditemukan");
if (type === "phone") {
await user.update({
active: true,
phoneNumberVerificationAt: new Date()
});
} else {
await user.update({
active: true,
emailVerificationAt: new Date()
});
}
return user;
}
private createWhere(conditions) {
const { page, limit, order, orderBy, active, ...rest } = conditions; // eslint-disable-line
const where = _.isEmpty(rest) ? {} : { [Op.or]: { ...rest } };
where["active"] = _.isUndefined(active) ? [true, false] : active;
return where;
}
}
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
import { Injectable } from "@nestjs/common";
import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
import { UsersService } from "src/modules/users/users.service";
@ValidatorConstraint({ name: 'IsUserEmailUnique', async: true })
@Injectable()
export class IsUserEmailUniqueRule implements ValidatorConstraintInterface {
constructor(private readonly usersService: UsersService) {}
async validate(email: string): Promise<boolean> {
return await this.usersService.findBy({
email: email,
active: true
}) === null
}
defaultMessage() {
return `Email telah digunakan`;
}
}
export function IsUserEmailUnique(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsUserEmailUniqueRule,
})
}
}
\ No newline at end of file
import { Injectable } from "@nestjs/common";
import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
import { UsersService } from "src/modules/users/users.service";
@ValidatorConstraint({ name: 'IsUserPhoneUnique', async: true })
@Injectable()
export class IsUserPhoneUniqueRule implements ValidatorConstraintInterface {
constructor(private readonly usersService: UsersService) {}
async validate(phoneNumber: string): Promise<boolean> {
return await this.usersService.findBy({
phoneNumber: phoneNumber,
active: true
}) === null
}
defaultMessage() {
return `Nomor HP telah digunakan`;
}
}
export function IsUserPhoneUnique(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsUserPhoneUniqueRule,
})
}
}
\ No newline at end of file
import type { INestApplication } from "@nestjs/common";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
export function setupSwagger(app: INestApplication): void {
const documentBuilder = new DocumentBuilder()
.setTitle("Service User - API Documentation")
.setDescription("")
.setVersion("1.0")
.addBearerAuth();
const document = SwaggerModule.createDocument(app, documentBuilder.build());
SwaggerModule.setup("api-documentation", app, document, {
swaggerOptions: {
persistAuthorization: true,
},
customSiteTitle: 'User - API Documentation'
});
console.info(
`Documentation: http://localhost:${process.env.PORT}/api-documentation`
);
}
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { join as pathJoin } from "path";
import { clearNewLine } from "src/common/string";
@Injectable()
export class AppConfigService {
constructor(private configService: ConfigService) {}
app() {
return {
apiHost: this.configService.get("API_HOST"),
backOfficeHost: this.configService.get("BO_HOST"),
appWebHost: this.configService.get("APP_WEB_HOST"),
appProductHost: this.configService.get("APP_PRODUCT_HOST"),
appUserHost: this.configService.get("APP_USER_HOST"),
appNotificationHost: this.configService.get("APP_NOTIFICATION_HOST"),
appMiscHost: this.configService.get("APP_MISC_HOST"),
appPaymentHost: this.configService.get("APP_PAYMENT_HOST"),
appOrderHost: this.configService.get("APP_ORDER_HOST"),
appCryptoHost: this.configService.get("APP_CRYPTO_HOST"),
rootDir: pathJoin(__dirname, "../../../"),
};
}
db() {
return {
dialect: this.configService.get("DATABASE_DIALECT"),
host: this.configService.get("DATABASE_HOST"),
port: this.configService.get("DATABASE_PORT"),
username: this.configService.get("DATABASE_USERNAME"),
password: this.configService.get("DATABASE_PASSWORD"),
database: this.configService.get("DATABASE_NAME"),
timezone: this.configService.get("DATABASE_TIMEZONE"),
logging: this.configService.get("DATABASE_LOGGING"),
};
}
jwt() {
return {
privateKey: clearNewLine(this.configService.get("JWT_PRIVATE_KEY")),
publicKey: clearNewLine(this.configService.get("JWT_PUBLIC_KEY")),
expirationTime: this.configService.get("JWT_EXPIRATION_TIME"),
accessTokenType: this.configService.get("ACCESS_TOKEN_TYPE"),
};
}
mail() {
return {
domain: this.configService.get("MAIL_DOMAIN"),
api_key: this.configService.get("MAIL_API_KEY"),
from_name: this.configService.get("MAIL_FROM_NAME"),
from_email: this.configService.get("MAIL_FROM_ADDRESS"),
};
}
}
import { SetMetadata } from "@nestjs/common";
export const ROLES_KEY = "roles";
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { ROLES_KEY } from "../decorators/roles.decorator";
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()]
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (user?.role?.name) {
if (requiredRoles.includes(user.role.name)) {
return true;
} else {
return false;
}
} else {
return false;
}
}
}
import { BadGatewayException, HttpException, Injectable } from '@nestjs/common';
import { HttpService as AxiosService } from '@nestjs/axios';
import { lastValueFrom } from 'rxjs';
import { LoggerService } from 'src/common/logger/logger.service';
import { error_message } from 'src/common/error_message';
import * as moment from 'moment-timezone';
import _ from 'lodash';
const short = require('short-uuid');
@Injectable()
export class HttpService {
constructor(
private http: AxiosService,
private loggerService: LoggerService
) {}
async sendRequest(method = "get", url, headers = {}, data = {}, showError = true) {
let response;
const session = short.generate(), start = moment();
this.loggerService.requestOut({
method,
url,
headers,
data
}, session)
try {
switch (method) {
case "get":
response = await this.get(url, headers);
this.loggerService.responseIn('success', response, session, moment().diff(start, 'millisecond'))
return response
break;
case "post":
response = await this.post(url, headers, data);
this.loggerService.responseIn('success', response, session, moment().diff(start, 'millisecond'))
return response
break;
case "put":
response = await this.put(url, headers, data);
this.loggerService.responseIn('success', response, session, moment().diff(start, 'millisecond'))
return response
break;
case "patch":
response = await this.patch(url, headers, data);
this.loggerService.responseIn('success', response, session, moment().diff(start, 'millisecond'))
return response
break;
case "delete":
response = await this.delete(url, headers);
this.loggerService.responseIn('success', response, session, moment().diff(start, 'millisecond'))
return response
break;
}
} catch (error) {
if (error?.response?.data) {
this.loggerService.responseIn('error', error.response.data, session, moment().diff(start, 'millisecond'))
if (showError) {
throw new BadGatewayException(error.response.data);
} else {
return false
}
} else {
this.loggerService.responseIn('error', error, session, moment().diff(start, 'millisecond'))
if (showError) {
throw new BadGatewayException(error_message.connection_refused);
} else {
return false
}
}
}
}
private async get(url, headers) {
const request = await this.http.get(url, headers);
return await (await lastValueFrom(request)).data;
}
private async post(url, headers, payload) {
const request = await this.http.post(url, payload, headers);
return await (await lastValueFrom(request)).data;
}
private async put(url, headers, payload) {
const request = await this.http.put(url, payload, headers);
return await (await lastValueFrom(request)).data;
}
private async patch(url, headers, payload) {
const request = await this.http.patch(url, payload, headers);
return await (await lastValueFrom(request)).data;
}
private async delete(url, headers) {
const request = await this.http.delete(url, headers);
return await (await lastValueFrom(request)).data;
}
}
import { Injectable } from "@nestjs/common";
import { AppConfigService } from "./app-config.service";
import { LoggerService } from "src/common/logger/logger.service";
const mg = require("nodemailer-mailgun-transport")
const nodemailer = require("nodemailer");
@Injectable()
export class MailService {
constructor(
private appConfigService: AppConfigService,
private loggerService: LoggerService
) {}
async sendMail({ to, subject, htmlContent }: any): Promise<void> {
const transporter = nodemailer.createTransport(mg({
auth: {
domain: this.appConfigService.mail().domain,
api_key: this.appConfigService.mail().api_key,
}
}));
const sent = await transporter.sendMail({
from: `"${this.appConfigService.mail().from_name}" <${this.appConfigService.mail().from_email}>`, // sender address
to: to.map(item => `"${item.name}" <${item.email}>`).join(","), // list of receivers
subject, // Subject line
html: htmlContent, // html body
});
this.loggerService.log('info', JSON.stringify(sent))
}
}
import { Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class RedisService {
private client: Redis.Redis;
private redisOption: Redis.RedisOptions;
constructor(private configService: ConfigService) {
this.redisOption = {
host: this.configService.get('REDIS_HOST'),
port: this.configService.get('REDIS_PORT'),
db: 1,
};
this.client = new Redis.Redis(this.redisOption); // Create a Redis client instance
}
async set(
key: string,
value: any,
expirationSeconds = 60 * 60 * 24
): Promise<void> {
await this.client.set(
key,
JSON.stringify(value),
'EX',
expirationSeconds
);
}
async get(key: string): Promise<any> {
const value = await this.client.get(key);
if (value) {
return JSON.parse(value);
} else {
return null;
}
}
async incr(key: string): Promise<any> {
const value = await this.client.incr(key);
return value;
}
}
import { HttpModule } from "@nestjs/axios";
import { Global, Module } from "@nestjs/common";
import { AppConfigService } from "./app-config.service";
import { HttpService } from "./http.service";
import { LoggerService } from "src/common/logger/logger.service";
import { SequelizeModule } from "@nestjs/sequelize";
import { MailService } from "./mail.service";
import { TemplateService } from "./template.service";
import { RedisService } from "./redis.service";
import { ShortenService } from "./shorten.service";
const providers = [
AppConfigService,
HttpService,
MailService,
TemplateService,
LoggerService,
RedisService,
ShortenService
];
@Global()
@Module({
imports: [
HttpModule,
SequelizeModule.forFeature([])
],
providers,
exports: [...providers],
})
export class ShareModule {}
import { BadRequestException, Injectable } from '@nestjs/common';
import { HttpService } from './http.service';
import { RedisService } from './redis.service';
import { ConfigService } from '@nestjs/config';
const configService = new ConfigService();
@Injectable()
export class ShortenService {
constructor(
private readonly httpService: HttpService,
private readonly redisService: RedisService
) {}
async getTokenShortUrl() {
let request_token = await this.redisService.get(`shorten_url:token`);
if (!request_token) {
const request = await this.httpService.sendRequest(
'post',
configService.get('URL_SHORTENER_URL') + `/api/login`,
{},
{
username: configService.get('URL_SHORTENER_USERNAME'),
password: configService.get('URL_SHORTENER_PASSWORD'),
},
false
);
request_token = request?.data;
await this.redisService.set(`shorten_url:token`, request?.data, 60 * 60 * 24);
}
return request_token.token;
}
async getShortUrl(title, full) {
let cacheToken = await this.getTokenShortUrl();
const request = await this.httpService.sendRequest(
'post',
configService.get('URL_SHORTENER_URL') + `/api/short_url`,
{
headers: {
Authorization: `Bearer ${cacheToken}`,
},
},
{
title,
full,
}
);
return request.data.url;
}
}
import { Test, TestingModule } from '@nestjs/testing';
import { TemplateService } from './template.service';
describe('TemplateService', () => {
let service: TemplateService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TemplateService],
}).compile();
service = module.get<TemplateService>(TemplateService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable } from '@nestjs/common';
import { readFileSync } from 'fs';
import { handlebars } from 'hbs';
@Injectable()
export class TemplateService {
render(path: string, payload: any): string {
const file = readFileSync(path).toString()
const template = handlebars.compile(file);
return template(payload)
}
}
import { Injectable } from "@nestjs/common";
import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
import { UsersService } from "src/modules/users/users.service";
@ValidatorConstraint({ name: 'IsUserEmailUnique', async: true })
@Injectable()
export class IsNameRule implements ValidatorConstraintInterface {
constructor(private readonly usersService: UsersService) {}
async validate(payload: string): Promise<boolean> {
return /^[a-zA-Z0-9 ]*$/.test( payload )
}
defaultMessage() {
return `Nama hanya diijinkan huruf dan angka.`;
}
}
export function IsName(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsNameRule,
})
}
}
\ No newline at end of file
import { Injectable } from "@nestjs/common";
import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
import { UsersService } from "src/modules/users/users.service";
@ValidatorConstraint({ name: 'IsUserEmailUnique', async: true })
@Injectable()
export class IsPhoneOnlyNumberRule implements ValidatorConstraintInterface {
constructor(private readonly usersService: UsersService) {}
async validate(payload: string): Promise<boolean> {
return /^\d+$/.test(payload);
}
defaultMessage() {
return `Nomor telepon hanya diijinkan angka.`;
}
}
export function IsPhoneOnlyNumber(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsPhoneOnlyNumberRule,
})
}
}
\ No newline at end of file
<html>
<head>
<title>Message HTML Title</title>
</head>
<body>
<div>
<span style="font-weight: bold;">PIN: </span>
<span>{{pin}}</span>
</div>
</body>
</html>
\ No newline at end of file
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
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