├── .gitignore ├── database └── Dockerfile ├── docker-compose.yml ├── readme.md └── source ├── .env.example ├── Dockerfile ├── nodemon.json ├── package.json ├── src ├── apis │ └── upload.api.ts ├── app.ts ├── cache │ └── redis.client.ts ├── configs │ ├── db │ │ └── connector.db.ts │ └── middlewares │ │ ├── body-parser.config-middleware.ts │ │ ├── compression.config-middleware.ts │ │ ├── index.ts │ │ └── lusca.config-middleware.ts ├── constants │ └── auth.constant.ts ├── decorators │ ├── controller.decorator.ts │ ├── http-method.decorator.ts │ ├── injectable.decorator.ts │ └── middleware.decorator.ts ├── di │ ├── container.ts │ └── index.ts ├── helpers │ ├── file.helper.ts │ └── jwt.helper.ts ├── interfaces │ └── repository.interface.ts ├── middlewares │ ├── admin.authenticate.middleware.ts │ └── authentication.middleware.ts ├── models │ ├── db-query │ │ └── output.db-query.ts │ ├── decorators │ │ └── route.definition.ts │ └── request │ │ └── request.model.ts ├── providers │ └── api.provider.ts ├── repositories │ ├── base.repository.ts │ └── media.repository.ts ├── server.ts ├── services │ └── upload-cloudinary.service.ts └── utils │ ├── constants │ ├── di.constant.ts │ └── file.constant.ts │ ├── exceptions │ └── raise.exception.ts │ ├── helpers │ └── object.helper.ts │ ├── slugable │ └── slug.function.ts │ ├── transform │ └── image.transform.ts │ ├── upload │ └── upload-function.util.ts │ └── validators │ └── request.validate.ts ├── start.sh └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /source/node_modules/ 2 | /.idea/ 3 | /source/.env 4 | /source/package-lock.json 5 | -------------------------------------------------------------------------------- /database/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql:5.7.27 2 | 3 | ENV MYSQL_ROOT_PASSWORD root 4 | ENV MYSQL_DATABASE shop160_db 5 | ENV MYSQL_USER root 6 | ENV MYSQL_PASSWORD root -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | redis: 4 | image: redis 5 | ports: 6 | - "63792:6379" 7 | mysql: 8 | image: mysql:5.7.27 9 | ports: 10 | - "33065:3306" 11 | environment: 12 | - MYSQL_DATABASE=shop160_db 13 | - MYSQL_ROOT_PASSWORD=root 14 | security: 15 | build: ./source 16 | links: 17 | - redis 18 | ports: 19 | - "5001:3500" 20 | depends_on: 21 | - mysql 22 | environment: 23 | - DATABASE_HOST=db 24 | - redis_server_addr=redis 25 | volumes: 26 | - ./source:/app 27 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | **Edit a file, create a new file, and clone from Bitbucket in under 2 minutes** 2 | 3 | When you're done, you can delete the content in this README and update the file with details for others getting started with your repository. 4 | 5 | *We recommend that you open this README in another tab as you perform the tasks below. You can [watch our video](https://youtu.be/0ocf7u76WSo) for a full demo of all the steps in this tutorial. Open the video in a new tab to avoid leaving Bitbucket.* 6 | 7 | --- 8 | 9 | ## Edit a file 10 | 11 | You’ll start by editing this README file to learn how to edit a file in Bitbucket. 12 | 13 | 1. Click **Source** on the left side. 14 | 2. Click the README.md link from the list of files. 15 | 3. Click the **Edit** button. 16 | 4. Delete the following text: *Delete this line to make a change to the README from Bitbucket.* 17 | 5. After making your change, click **Commit** and then **Commit** again in the dialog. The commit page will open and you’ll see the change you just made. 18 | 6. Go back to the **Source** page. 19 | 20 | --- 21 | 22 | ## Create a file 23 | 24 | Next, you’ll add a new file to this repository. 25 | 26 | 1. Click the **New file** button at the top of the **Source** page. 27 | 2. Give the file a filename of **contributors.txt**. 28 | 3. Enter your name in the empty file space. 29 | 4. Click **Commit** and then **Commit** again in the dialog. 30 | 5. Go back to the **Source** page. 31 | 32 | Before you move on, go ahead and explore the repository. You've already seen the **Source** page, but check out the **Commits**, **Branches**, and **Settings** pages. 33 | 34 | --- 35 | 36 | ## Clone a repository 37 | 38 | Use these steps to clone from SourceTree, our client for using the repository command-line free. Cloning allows you to work on your files locally. If you don't yet have SourceTree, [download and install first](https://www.sourcetreeapp.com/). If you prefer to clone from the command line, see [Clone a repository](https://confluence.atlassian.com/x/4whODQ). 39 | 40 | 1. You’ll see the clone button under the **Source** heading. Click that button. 41 | 2. Now click **Check out in SourceTree**. You may need to create a SourceTree account or log in. 42 | 3. When you see the **Clone New** dialog in SourceTree, update the destination path and name if you’d like to and then click **Clone**. 43 | 4. Open the directory you just created to see your repository’s files. 44 | 45 | Now that you're more familiar with your Bitbucket repository, go ahead and add a new file locally. You can [push your change back to Bitbucket with SourceTree](https://confluence.atlassian.com/x/iqyBMg), or you can [add, commit,](https://confluence.atlassian.com/x/8QhODQ) and [push from the command line](https://confluence.atlassian.com/x/NQ0zDQ). -------------------------------------------------------------------------------- /source/.env.example: -------------------------------------------------------------------------------- 1 | CLOUD_NAME= 2 | CLOUD_API_KEY= 3 | CLOUD_API_SECRET= 4 | API_PREFIX= 5 | CRYPTO_SECRET=]P9Bf]-)7$?oNzcwwOd)zXE$$7}s2EeUm?l|1R U1:6HvuP,!fnHFMqwy = await this.uploadImageToCloudinary(files); 24 | const dataToSave: Array = result.map((item: any) => { 25 | return [ 26 | item.public_id,'image/upload', 27 | `${ item.public_id }.${ item.format }`, 28 | 3, 29 | item.version, 30 | item.signature, 31 | item.resource_type, 32 | request.folderId 33 | ]; 34 | }); 35 | const data = await this.mediaRepository.createMediaRow(dataToSave); 36 | return responseServer(request, response, 201, 'Upload successfully', data); 37 | } catch (e) { 38 | return raiseException(request, response, 500, e.message); 39 | } 40 | } 41 | 42 | private async uploadImageToCloudinary(files: { [key: string]: Array }): Promise> { 43 | try { 44 | return await this.uploadCloudService.uploadToCloudinary(files.file); 45 | } catch (e) { 46 | throw new Error("Error when upload to cloud " + e); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /source/src/app.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | require('dotenv').config(); 3 | import express from 'express'; 4 | import { Request, Response, Application } from 'express'; 5 | import { 6 | initLuscaConfigMiddleware, 7 | initBodyParser, 8 | initCompressionConfigMiddleware 9 | } from './configs/middlewares'; 10 | import ApiProviders from './providers/api.provider'; 11 | import { IRouteDefinition } from './models/decorators/route.definition'; 12 | import { DiContainer } from './di' 13 | import { Container } from "./di/container"; 14 | import { DiConstant } from "./utils/constants/di.constant"; 15 | import { IncomingForm } from 'formidable'; 16 | import { RequestHandler } from './models/request/request.model'; 17 | import cors from 'cors'; 18 | 19 | const mediaParser: Array = [ 20 | '/api/v1/media/upload/image/' 21 | ]; 22 | 23 | export default class App { 24 | private readonly port: number; 25 | private readonly app: Application; 26 | private container: Container; 27 | 28 | constructor(port: number) { 29 | this.port = port; 30 | this.app = express(); 31 | } 32 | 33 | public bootstrapApplication(): void { 34 | try { 35 | this.initConfigures(); 36 | this.initMiddlewares(); 37 | this.startContainer(); 38 | this.startProviders(); 39 | this.startServer(); 40 | 41 | } catch(error) { 42 | throw new Error(error); 43 | } 44 | } 45 | 46 | private startContainer(): void { 47 | this.container = DiContainer.getInstance(); 48 | } 49 | 50 | private startServer(): void { 51 | this.app.listen(this.port, () => { 52 | console.log("App start at port " + this.port); 53 | }); 54 | } 55 | 56 | private initConfigures(): void { 57 | 58 | } 59 | 60 | private startProviders(): void { 61 | const apiPrefix: string = process.env.API_PREFIX || '/api/v1'; 62 | const apiProviders = new ApiProviders(); 63 | [ 64 | ...apiProviders.providers 65 | ].map((controller: any) => { 66 | type instanceType = ReturnType; 67 | const paramsConstructors: Array = Reflect.getMetadata('design:paramtypes', controller); 68 | let instanceDependencies: Array = []; 69 | if (paramsConstructors) { 70 | instanceDependencies = paramsConstructors.map((param: any) => { 71 | return this.container.resolve(param.name); 72 | }); 73 | } 74 | const instance: InstanceType = new controller(...instanceDependencies); 75 | const prefix: any = Reflect.getMetadata('prefix', controller); 76 | const routes: IRouteDefinition[] = Reflect.getMetadata('routes', controller); 77 | routes.forEach((route: IRouteDefinition) => { 78 | let middlewares: Array = []; 79 | if (route && route.middleware && route.middleware.length > 0) { 80 | middlewares = route.middleware.map((middleware: any) => { 81 | const instanceMiddleware: ReturnType = this.container.resolve(middleware.name); 82 | return instanceMiddleware.handle(); 83 | }); 84 | } 85 | console.log(`Method ${ route.requestMethod } - ${ apiPrefix + prefix + route.path }`); 86 | this.app[route.requestMethod](apiPrefix + prefix + route.path, [...middlewares], (request: Request, response: Response) => { 87 | (instance as any)[route.methodName](request, response); 88 | }) 89 | }); 90 | }); 91 | } 92 | 93 | private initMiddlewares(): void { 94 | const corsOptions = { 95 | origin: '*', 96 | "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", 97 | "preflightContinue": false, 98 | "optionsSuccessStatus": 204 99 | }; 100 | this.app.use(cors(corsOptions)); 101 | process.setMaxListeners(0); 102 | initLuscaConfigMiddleware(this.app); 103 | initBodyParser(this.app); 104 | initCompressionConfigMiddleware(this.app); 105 | this.app.use(function (error: Error, request: Request, response: Response, next: Function) { 106 | return response.status(500).json({ message: error.message, code: 500, success: false }); 107 | }); 108 | this.app.use((request: RequestHandler, response: Response, next) => { 109 | if (mediaParser.includes(request.path)) { 110 | const form = new IncomingForm(); 111 | form.multiples = true; 112 | response.setHeader('Content-Type', 'application/json'); 113 | form.parse(request, function(err, fields: any, files) { 114 | request.files = files as any; 115 | request.folderId = fields.folderId; 116 | next(); 117 | }); 118 | } else { 119 | next(); 120 | } 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /source/src/cache/redis.client.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "../decorators/injectable.decorator"; 2 | import {ClientOpts, createClient, RedisClient} from 'redis'; 3 | 4 | @Injectable() 5 | export class RedisClientFacade { 6 | public redisClient: RedisClient; 7 | private clientOptions: ClientOpts = { 8 | host: 'redis', 9 | port: 6379 10 | }; 11 | 12 | constructor() { 13 | this.createRedisClient(); 14 | } 15 | 16 | public getKey(key: string): Promise { 17 | return new Promise((resolve: any, reject: any) => { 18 | this.redisClient.get(key, (error: Error, result: any) => { 19 | if (error) { 20 | return reject(error); 21 | } 22 | 23 | return resolve(result); 24 | }); 25 | }); 26 | } 27 | 28 | public setKey(key: string, data: any): Promise { 29 | return new Promise((resolve: any, reject: any) => { 30 | this.redisClient.set(key, data, (error: Error, result: any) => { 31 | if (error) { 32 | return reject(error); 33 | } 34 | return resolve(result); 35 | }); 36 | }); 37 | } 38 | 39 | private createRedisClient(): void { 40 | this.redisClient = createClient(this.clientOptions); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/src/configs/db/connector.db.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from './../../decorators/injectable.decorator'; 2 | import { createPool, Pool, PoolOptions, PoolConnection } from 'mysql2/promise'; 3 | 4 | @Injectable() 5 | export class Connector { 6 | private connection: Pool; 7 | private poolConfig: PoolOptions = { 8 | host : 'mysql', 9 | database : 'shop160_db', 10 | user : 'root', 11 | password : 'root', 12 | port : 3306, 13 | connectionLimit : 10 14 | }; 15 | 16 | constructor() { 17 | this.initPollConnection(); 18 | } 19 | 20 | private initPollConnection(): void { 21 | console.log("Init database"); 22 | this.connection = this.createConnection(); 23 | } 24 | 25 | private createConnection(): Pool { 26 | return createPool(this.poolConfig); 27 | } 28 | 29 | public async getConnection(): Promise { 30 | return this.connection.getConnection(); 31 | } 32 | } -------------------------------------------------------------------------------- /source/src/configs/middlewares/body-parser.config-middleware.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import { Application } from 'express'; 3 | 4 | export const initBodyParser = (app: Application): void => { 5 | app.use(bodyParser.json()); 6 | app.use(bodyParser.urlencoded({ extended: false })); 7 | }; -------------------------------------------------------------------------------- /source/src/configs/middlewares/compression.config-middleware.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'express'; 2 | import compression from 'compression'; 3 | 4 | export const initCompressionConfigMiddleware = (app: Application) => { 5 | app.use(compression()); 6 | }; 7 | -------------------------------------------------------------------------------- /source/src/configs/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './body-parser.config-middleware'; 2 | export * from './lusca.config-middleware'; 3 | export * from './compression.config-middleware'; -------------------------------------------------------------------------------- /source/src/configs/middlewares/lusca.config-middleware.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'express'; 2 | import lusca from 'lusca'; 3 | import session from 'express-session'; 4 | 5 | const luscaOptions: Partial = { 6 | csrf: false, 7 | xframe: 'SAMEORIGIN', 8 | p3p: 'ABCDEF', 9 | hsts: {maxAge: 31536000, includeSubDomains: true, preload: true}, 10 | xssProtection: true, 11 | nosniff: true, 12 | referrerPolicy: 'same-origin' 13 | }; 14 | 15 | export const initLuscaConfigMiddleware = (app: Application): void => { 16 | app.use(session({ 17 | secret: '12321312321321321312', 18 | resave: true, 19 | saveUninitialized: true 20 | })); 21 | app.use(lusca(luscaOptions)); 22 | }; 23 | -------------------------------------------------------------------------------- /source/src/constants/auth.constant.ts: -------------------------------------------------------------------------------- 1 | export const AUTH_CONSTANT = { 2 | ADMIN_ROLE: 3 3 | }; 4 | -------------------------------------------------------------------------------- /source/src/decorators/controller.decorator.ts: -------------------------------------------------------------------------------- 1 | export const Controller = (prefix: string): ClassDecorator => { 2 | return (target) => { 3 | Reflect.defineMetadata('prefix', prefix, target); 4 | // if controller not have any Router,, we will define its router is an empty array 5 | if (!Reflect.hasMetadata('routes', target)) { 6 | Reflect.defineMetadata('routes', [], target); 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /source/src/decorators/http-method.decorator.ts: -------------------------------------------------------------------------------- 1 | import { IRouteDefinition, RequestMethod } from "../models/decorators/route.definition"; 2 | 3 | export const Get = (path: string, middleware: Array = []): MethodDecorator => { 4 | return (target: Object, propertyKey: string | symbol) => { 5 | checkMetaData(target, path, middleware, 'get', propertyKey, []); 6 | } 7 | }; 8 | 9 | export const Post = (path: string, middleware: Array = []): MethodDecorator => { 10 | return (target: Object, propertyKey: string | symbol) => { 11 | checkMetaData(target, path, middleware, 'post', propertyKey, []); 12 | } 13 | }; 14 | 15 | export const Put = (path: string, middleware: Array = []): MethodDecorator => { 16 | return (target: Object, propertyKey: string | symbol) => { 17 | checkMetaData(target, path, middleware, 'put', propertyKey, []); 18 | } 19 | }; 20 | 21 | export const Patch = (path: string, middleware: Array = []): MethodDecorator => { 22 | return (target: Object, propertyKey: string | symbol) => { 23 | checkMetaData(target, path, middleware, 'patch', propertyKey, []); 24 | } 25 | }; 26 | 27 | export const Delete = (path: string, middleware: Array = []): MethodDecorator => { 28 | return (target: Object, propertyKey: string | symbol) => { 29 | checkMetaData(target, path, middleware, 'delete', propertyKey, []); 30 | } 31 | }; 32 | 33 | const checkMetaData = ( 34 | target: Object, 35 | path: string, 36 | middleware: Array, 37 | method: RequestMethod, 38 | propertyKey: string | symbol, 39 | defaultValue: IRouteDefinition[] 40 | ): void => 41 | { 42 | if(!Reflect.hasMetadata('routes', target.constructor)) { 43 | Reflect.defineMetadata('routes', defaultValue, target.constructor); 44 | } 45 | const routes: IRouteDefinition[] = Reflect 46 | .getMetadata( 47 | 'routes', 48 | target.constructor 49 | ); 50 | routes.push({ 51 | path, 52 | requestMethod: method, 53 | methodName: propertyKey, 54 | middleware: middleware 55 | }); 56 | Reflect.defineMetadata('routes', routes, target.constructor); 57 | }; 58 | -------------------------------------------------------------------------------- /source/src/decorators/injectable.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DiContainer } from '../di'; 2 | import { Container } from "../di/container"; 3 | import {DiConstant} from "../utils/constants/di.constant"; 4 | 5 | const container: Container = DiContainer.getInstance(); 6 | 7 | export const Injectable = (config?: { providerIn: 'root' }): ClassDecorator => { 8 | return (target) => { 9 | const params: Array = Reflect.getMetadata('design:paramtypes', target); 10 | injectParams(params, target); 11 | } 12 | }; 13 | 14 | const injectParams = (params: Array, target: any): any => { 15 | const instance: any = container.resolve(target.name); 16 | if (!!instance) { 17 | return instance; 18 | } 19 | if(params && params.length > 0) { 20 | const listDependencies = params.map((param: any) => { 21 | const listParams: Array = Reflect.getMetadata('design:paramtypes', param); 22 | return injectParams(listParams, param); 23 | }); 24 | bindToContainer(target.name, new target(...listDependencies)); 25 | } else { 26 | bindToContainer(target.name, new target()); 27 | } 28 | 29 | return container.resolve(target.name); 30 | }; 31 | 32 | const bindToContainer = (className: string, target: any): void => { 33 | container.injectDependency(className, target, DiConstant.DE); 34 | }; -------------------------------------------------------------------------------- /source/src/decorators/middleware.decorator.ts: -------------------------------------------------------------------------------- 1 | import {Container} from "../di/container"; 2 | import {DiContainer} from "../di"; 3 | import {DiConstant} from "../utils/constants/di.constant"; 4 | 5 | const container: Container = DiContainer.getInstance(); 6 | 7 | export const Middleware = (): ClassDecorator => { 8 | return (target) => { 9 | const params: Array = Reflect.getMetadata('design:paramtypes', target); 10 | injectMiddleware(params, target, DiConstant.MI); 11 | } 12 | }; 13 | 14 | const injectMiddleware = (params: Array, target: any, typeTarget: string) => { 15 | const instance: any = container.resolve(target.name, typeTarget); 16 | if (!!instance) { 17 | return instance; 18 | } 19 | 20 | if(params && params.length > 0) { 21 | const listDependencies = params.map((param: any) => { 22 | const listParams: Array = Reflect.getMetadata('design:paramtypes', param); 23 | return injectMiddleware(listParams, param, DiConstant.DE); 24 | }); 25 | bindToContainer(target.name, new target(...listDependencies), DiConstant.DE); 26 | } else { 27 | bindToContainer(target.name, new target(), typeTarget); 28 | } 29 | 30 | return container.resolve(target.name, typeTarget); 31 | }; 32 | 33 | const bindToContainer = (className: string, target: any, type: string): void => { 34 | container.injectDependency(className, target, type); 35 | }; -------------------------------------------------------------------------------- /source/src/di/container.ts: -------------------------------------------------------------------------------- 1 | import {DiConstant} from "../utils/constants/di.constant"; 2 | 3 | export class Container { 4 | private dependencies: any = {}; 5 | 6 | public resolve(classInjection: string, type: string = DiConstant.DE): any { 7 | if (!this.dependencies[type]) { 8 | this.dependencies[type] = {}; 9 | } 10 | return this.dependencies[type][classInjection] || undefined; 11 | } 12 | 13 | public injectDependency(className: string, instanceInjection: any, type: string): void { 14 | if (!this.dependencies[type]) { 15 | this.dependencies[type] = {}; 16 | } 17 | this.dependencies[type][className] = instanceInjection; 18 | } 19 | 20 | public createContainer(): void { 21 | this.dependencies = []; 22 | } 23 | } -------------------------------------------------------------------------------- /source/src/di/index.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "./container"; 2 | 3 | export class DiContainer { 4 | private static instance: Container; 5 | 6 | public static getInstance(): Container { 7 | if (!DiContainer.instance) { 8 | DiContainer.instance = new Container(); 9 | } 10 | 11 | return DiContainer.instance; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/src/helpers/file.helper.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "../decorators/injectable.decorator"; 2 | import { RequestHandler } from '../models/request/request.model'; 3 | 4 | @Injectable() 5 | export class FileHelper { 6 | public getFilesInRequest(request: RequestHandler): Array<{ [key: string]: Array }> { 7 | let listFiles: any = {}; 8 | const filesInRequest: any = request.files; 9 | if (filesInRequest) { 10 | Object.keys(filesInRequest).map((key: string) => { 11 | const files = filesInRequest[key].path ? [filesInRequest[key]] : filesInRequest[key]; 12 | listFiles[key] = files; 13 | }); 14 | } 15 | 16 | return listFiles; 17 | } 18 | } -------------------------------------------------------------------------------- /source/src/helpers/jwt.helper.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "../decorators/injectable.decorator"; 2 | import { sign, SignOptions } from 'jsonwebtoken'; 3 | 4 | 5 | var iG = 'Shop 160'; 6 | var sMail = 'tuananhit.oct@gmail.com'; 7 | var aDress = 'http://tuananh.info'; 8 | 9 | var signOptions: SignOptions = { 10 | issuer: iG, 11 | subject: sMail, 12 | audience: aDress, 13 | expiresIn: '12h', 14 | algorithm: 'HS512' 15 | }; 16 | 17 | @Injectable() 18 | export class JWTMHelper { 19 | createToken(id: number): any { 20 | if (process.env.JWT_SECRET_KEY) { 21 | return sign( 22 | { 23 | id: id, 24 | }, 25 | process.env.JWT_SECRET_KEY, 26 | signOptions 27 | ); 28 | } 29 | 30 | throw new Error("JWT Secret Key is not generate?"); 31 | } 32 | } -------------------------------------------------------------------------------- /source/src/interfaces/repository.interface.ts: -------------------------------------------------------------------------------- 1 | export interface RepositoryInterface { 2 | fillable: Array; 3 | } -------------------------------------------------------------------------------- /source/src/middlewares/admin.authenticate.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "../decorators/middleware.decorator"; 2 | import { HttpService } from "../services/http.service"; 3 | import passport from 'passport'; 4 | import { Strategy as JwtStrategy, ExtractJwt, StrategyOptions } from 'passport-jwt'; 5 | import { UserRepository } from "../repositories/user.repository"; 6 | import { AUTH_CONSTANT } from './../constants/auth.constant'; 7 | 8 | const iG = 'Shop 160'; 9 | const aDress = 'http://tuananh.info'; 10 | const optionsPassportStrategy: StrategyOptions = { 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | secretOrKey: process.env.JWT_SECRET_KEY, 13 | passReqToCallback: true, 14 | issuer: iG, 15 | audience: aDress 16 | }; 17 | 18 | @Middleware() 19 | export class AdminAuthenticationMiddleware { 20 | passport: any; 21 | constructor(private userRepository: UserRepository) { 22 | this.passport = passport; 23 | this.initMiddleware(); 24 | } 25 | 26 | public handle() { 27 | return this.passport.authenticate('jwt', { session: false }); 28 | } 29 | 30 | private initMiddleware(): void { 31 | const callback = async (parentClosure: any, payload: any, next: Function) => { 32 | try { 33 | const result: Array = await this.userRepository.findUserById(payload.id || 0); 34 | if (result && result[0] && result[0].role_id == AUTH_CONSTANT.ADMIN_ROLE) { 35 | next(null, result[0]); 36 | } else { 37 | next(null, null); 38 | } 39 | } catch { 40 | next(null, false); 41 | } 42 | }; 43 | 44 | const jwtStrategy: JwtStrategy = new JwtStrategy(optionsPassportStrategy, callback); 45 | this.passport.use(jwtStrategy); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /source/src/middlewares/authentication.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "../decorators/middleware.decorator"; 2 | import { HttpService } from "../services/http.service"; 3 | import passport from 'passport'; 4 | import { Strategy as JwtStrategy, ExtractJwt, StrategyOptions } from 'passport-jwt'; 5 | import { UserRepository } from "../repositories/user.repository"; 6 | 7 | const iG = 'Shop 160'; 8 | const aDress = 'http://tuananh.info'; 9 | const optionsPassportStrategy: StrategyOptions = { 10 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 11 | secretOrKey: process.env.JWT_SECRET_KEY, 12 | passReqToCallback: true, 13 | issuer: iG, 14 | audience: aDress 15 | }; 16 | 17 | @Middleware() 18 | export class AuthenticationMiddleware { 19 | passport: any; 20 | constructor(private userRepository: UserRepository) { 21 | this.passport = passport; 22 | this.initMiddleware(); 23 | } 24 | 25 | public handle() { 26 | return this.passport.authenticate('jwt', { session: false }); 27 | } 28 | 29 | private initMiddleware(): void { 30 | const callback = async (parentClosure: any, payload: any, next: Function) => { 31 | try { 32 | const result: Array = await this.userRepository.findUserById(payload.id || 0); 33 | if (result && result[0]) { 34 | next(null, result[0]); 35 | } else { 36 | next(null, null); 37 | } 38 | } catch { 39 | next(null, false); 40 | } 41 | }; 42 | 43 | const jwtStrategy: JwtStrategy = new JwtStrategy(optionsPassportStrategy, callback); 44 | this.passport.use(jwtStrategy); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/src/models/db-query/output.db-query.ts: -------------------------------------------------------------------------------- 1 | export interface OutputDbQuery { 2 | ['rows']: Array, 3 | ['fields']: any 4 | } -------------------------------------------------------------------------------- /source/src/models/decorators/route.definition.ts: -------------------------------------------------------------------------------- 1 | export type RequestMethod = 'get' 2 | | 'post' 3 | | 'put' 4 | | 'options' 5 | | 'patch' 6 | | 'delete'; 7 | 8 | export interface IRouteDefinition { 9 | path: string, 10 | requestMethod: RequestMethod, 11 | methodName: string | symbol, 12 | middleware: Array 13 | } 14 | -------------------------------------------------------------------------------- /source/src/models/request/request.model.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | export interface RequestHandler extends Request { 4 | files: { [key: string]: Array }[], 5 | user: { 6 | id: number; 7 | name: string; 8 | email: string; 9 | }, 10 | folderId?: number; 11 | } -------------------------------------------------------------------------------- /source/src/providers/api.provider.ts: -------------------------------------------------------------------------------- 1 | export default class ApiProvider { 2 | private provider_array: any = []; 3 | 4 | constructor() { } 5 | 6 | public get providers(): any { 7 | return this.provider_array; 8 | } 9 | } -------------------------------------------------------------------------------- /source/src/repositories/base.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "../decorators/injectable.decorator"; 2 | 3 | @Injectable() 4 | export class BaseRepository { 5 | protected fillable: Array = []; 6 | constructor() { } 7 | 8 | composeDataWithFillable(data: { [key: string]: any }): { [key: string]: any } { 9 | let result: any = {}; 10 | this.fillable.map((key: string) => { 11 | if (key in data) { 12 | result[key] = data[key]; 13 | } 14 | }); 15 | 16 | return result; 17 | } 18 | } -------------------------------------------------------------------------------- /source/src/repositories/media.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from './../decorators/injectable.decorator'; 2 | import { Connector } from './../configs/db/connector.db'; 3 | import { PoolConnection } from 'mysql2/promise'; 4 | import { BaseRepository } from './base.repository'; 5 | import { RepositoryInterface } from '../interfaces/repository.interface'; 6 | 7 | @Injectable() 8 | export class MediaRepository extends BaseRepository implements RepositoryInterface { 9 | fillable: Array = ['path', 'file_name', 'driver']; 10 | 11 | constructor(private dbConnector: Connector) { 12 | super(); 13 | } 14 | 15 | async createMediaRow(data: Array, driver: number = 3, connection?: PoolConnection): Promise { 16 | if (!connection) connection = await this.dbConnector.getConnection(); 17 | try { 18 | await connection.beginTransaction(); 19 | let sql: string = `INSERT INTO process_images (process_key, path, file_name, driver, version, signature, resource_type, folder_id) VALUES `; 20 | data.map((item: any, index: number) => { 21 | sql += `(?)`; 22 | if (index !== data.length - 1) { 23 | sql += ',' 24 | } 25 | }); 26 | const result: any = await connection.query( 27 | sql, 28 | data 29 | ); 30 | await connection.commit(); 31 | return await this.mapAndReturnListMediaByIds(result[0].insertId, result[0].affectedRows, connection); 32 | } catch (e) { 33 | await connection.rollback(); 34 | throw new Error(e); 35 | } finally { 36 | connection.release(); 37 | } 38 | } 39 | 40 | private async mapAndReturnListMediaByIds(id: number, range: number, connection?: PoolConnection) { 41 | let listIds: number[] = []; 42 | for (let i = id; i < (id + range); i++) { 43 | listIds.push(i); 44 | } 45 | if (listIds && listIds.length > 0) { 46 | const result: any = await this.getListMediaByIds(listIds, connection); 47 | return result.map((item: any) => { 48 | return { 49 | id: item.id_process_image, 50 | process_key: item.process_key, 51 | path: `${ process.env.CLOUD_IMAGE_PATH }/${ item.path }/v${ item.version }/${ item.file_name }`, 52 | resource_type: item.resource_type 53 | } 54 | }) 55 | } 56 | return []; 57 | } 58 | 59 | async getListMediaByIds(listIds: number[], connection?: PoolConnection): Promise { 60 | if (!connection) connection = await this.dbConnector.getConnection(); 61 | try { 62 | let query_string = ``; 63 | const params_sql: Array = []; 64 | listIds.map((item: number, index: number) => { 65 | query_string += `?`; 66 | if (index !== listIds.length - 1) { 67 | query_string += ` , `; 68 | } 69 | params_sql.push(item); 70 | }); 71 | const sql = await connection.format( 72 | `SELECT * FROM process_images WHERE id_process_image IN (${query_string});`, 73 | params_sql 74 | ); 75 | const result: any = await connection.query(sql); 76 | return result[0]; 77 | } catch (e) { 78 | throw new Error(e); 79 | } finally { 80 | connection.release(); 81 | } 82 | } 83 | 84 | public async getListMediaByParent(parentId: number, offset: number, limit: number, connection?: PoolConnection): Promise { 85 | try { 86 | if (!connection) connection = await this.dbConnector.getConnection(); 87 | let sql: string = ` 88 | SELECT 89 | id_process_image, 90 | process_key, path, 91 | file_name, driver, 92 | folder_id, 93 | resource_type, 94 | version 95 | FROM process_images 96 | WHERE is_archive IS FALSE AND folder_id = ? 97 | ORDER BY id_process_image DESC 98 | LIMIT ?,?`; 99 | return connection.query(sql, [parentId, offset, limit]); 100 | } catch (error) { 101 | throw new Error(error.message); 102 | } finally { 103 | if (connection) connection.release(); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /source/src/server.ts: -------------------------------------------------------------------------------- 1 | import App from './app'; 2 | 3 | const port: number = parseInt(process.env.PORT) || 3500; 4 | const app = new App(port); 5 | 6 | /** 7 | * Bootstrap app heree. 8 | */ 9 | app.bootstrapApplication(); 10 | -------------------------------------------------------------------------------- /source/src/services/upload-cloudinary.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "../decorators/injectable.decorator"; 2 | import * as cloudinary from 'cloudinary'; 3 | import { upload } from '../utils/upload/upload-function.util'; 4 | 5 | @Injectable({ 6 | providerIn: 'root' 7 | }) 8 | export class UploadCloudinaryService { 9 | private cloudinaryVersion: any = cloudinary.v2; 10 | 11 | uploadToCloudinary(resources: Array): Promise { 12 | try { 13 | let arrayFileUpload: Array = []; 14 | for (let i = 0; i < resources.length; i++) { 15 | let file: any = resources[i]; 16 | const filePath: string = file.path; 17 | arrayFileUpload.push(upload(filePath)); 18 | } 19 | 20 | return Promise.all(arrayFileUpload); 21 | } catch (error) { 22 | throw error.message; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /source/src/utils/constants/di.constant.ts: -------------------------------------------------------------------------------- 1 | export const DiConstant = { 2 | DE: 'dependencies', 3 | MI: 'middlewares', 4 | HP: 'helpers' 5 | }; -------------------------------------------------------------------------------- /source/src/utils/constants/file.constant.ts: -------------------------------------------------------------------------------- 1 | export const fileConstants = { 2 | PATH_FILE: { 3 | 3: process.env.CLOUD_IMAGE_PATH || null 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /source/src/utils/exceptions/raise.exception.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | 3 | export function raiseException(request: Request, response: Response, code: number, message: string, data: T = null): any { 4 | let responseData: { code: number, message: string, data?: T } = { 5 | code, 6 | message 7 | }; 8 | if (data) { 9 | responseData = { ...responseData, data }; 10 | } 11 | return response.status(code).json(responseData); 12 | } 13 | 14 | export function responseServer(request: Request, response: Response, code: number, message: string, data: T = null): any { 15 | let responseData: { code: number, message: string, data?: T } = { 16 | code, 17 | message 18 | }; 19 | if (data) { 20 | responseData = { ...responseData, data }; 21 | } 22 | return response.status(code).json(responseData); 23 | } 24 | -------------------------------------------------------------------------------- /source/src/utils/helpers/object.helper.ts: -------------------------------------------------------------------------------- 1 | export const isEmptyObject = (object: Object) => { 2 | return Object.keys(object).length === 0; 3 | }; 4 | -------------------------------------------------------------------------------- /source/src/utils/slugable/slug.function.ts: -------------------------------------------------------------------------------- 1 | export class Str { 2 | public static slug(str: string): string { 3 | let slug: string = str.toLowerCase(); 4 | //Đổi ký tự có dấu thành không dấu 5 | slug = slug.replace(/á|à|ả|ạ|ã|ă|ắ|ằ|ẳ|ẵ|ặ|â|ấ|ầ|ẩ|ẫ|ậ/gi, 'a'); 6 | slug = slug.replace(/é|è|ẻ|ẽ|ẹ|ê|ế|ề|ể|ễ|ệ/gi, 'e'); 7 | slug = slug.replace(/i|í|ì|ỉ|ĩ|ị/gi, 'i'); 8 | slug = slug.replace(/ó|ò|ỏ|õ|ọ|ô|ố|ồ|ổ|ỗ|ộ|ơ|ớ|ờ|ở|ỡ|ợ/gi, 'o'); 9 | slug = slug.replace(/ú|ù|ủ|ũ|ụ|ư|ứ|ừ|ử|ữ|ự/gi, 'u'); 10 | slug = slug.replace(/ý|ỳ|ỷ|ỹ|ỵ/gi, 'y'); 11 | slug = slug.replace(/đ/gi, 'd'); 12 | //Xóa các ký tự đặt biệt 13 | slug = slug.replace(/\`|\~|\!|\@|\#|\||\$|\%|\^|\&|\*|\(|\)|\+|\=|\,|\.|\/|\?|\>|\<|\'|\"|\:|\;|_/gi, ''); 14 | //Đổi khoảng trắng thành ký tự gạch ngang 15 | slug = slug.replace(/ /gi, "-"); 16 | //Đổi nhiều ký tự gạch ngang liên tiếp thành 1 ký tự gạch ngang 17 | //Phòng trường hợp người nhập vào quá nhiều ký tự trắng 18 | slug = slug.replace(/\-\-\-\-\-/gi, '-'); 19 | slug = slug.replace(/\-\-\-\-/gi, '-'); 20 | slug = slug.replace(/\-\-\-/gi, '-'); 21 | slug = slug.replace(/\-\-/gi, '-'); 22 | //Xóa các ký tự gạch ngang ở đầu và cuối 23 | slug = '@' + slug + '@'; 24 | slug = slug.replace(/\@\-|\-\@|\@/gi, ''); 25 | 26 | return slug 27 | } 28 | } -------------------------------------------------------------------------------- /source/src/utils/transform/image.transform.ts: -------------------------------------------------------------------------------- 1 | export const createImagePath = (data: { file_name: string, path:string , version: string }) => { 2 | return data && data.file_name ? `${ process.env.CLOUD_IMAGE_PATH }/${ data.path }/v${ data.version }/${ data.file_name }` : null; 3 | }; 4 | -------------------------------------------------------------------------------- /source/src/utils/upload/upload-function.util.ts: -------------------------------------------------------------------------------- 1 | import { v2, ConfigOptions, UploadApiOptions } from 'cloudinary'; 2 | import * as Q from 'q'; 3 | 4 | const configs: ConfigOptions = { 5 | cloud_name: process.env.CLOUD_NAME || null, 6 | api_key: process.env.CLOUD_API_KEY || null, 7 | api_secret: process.env.CLOUD_API_SECRET || null 8 | }; 9 | 10 | v2.config(configs); 11 | const uploader = v2.uploader; 12 | // @ts-ignore 13 | export const upload = async function (file: string, options: UploadApiOptions = {}) { 14 | return await uploader.upload(file, { ...options, async: false }).then((data: any) => { 15 | return data; 16 | }); 17 | }; -------------------------------------------------------------------------------- /source/src/utils/validators/request.validate.ts: -------------------------------------------------------------------------------- 1 | export class RequestValidate { 2 | constructor() {} 3 | 4 | public static handle(validation: any, data: any, partial: boolean = false): {[key: string]: Array | boolean} { 5 | try { 6 | const result: Array = []; 7 | Object.keys(validation).map((key:string) => { 8 | const vl: any = typeof data[key] == 'number' ? data[key] : (data[key] || null); 9 | let validateResults: any; 10 | if (partial) { 11 | if (key in data) { 12 | validateResults = this.validateWithKey(key, validation[key], vl, true).filter((item: string) => item); 13 | } 14 | } else { 15 | validateResults = this.validateWithKey(key, validation[key], vl).filter((item: string) => item); 16 | } 17 | if (validateResults && validateResults.length > 0) { 18 | result.push(validateResults); 19 | } 20 | }); 21 | return { 22 | errors: result, 23 | success: result.length == 0 24 | }; 25 | 26 | } catch (err) { 27 | throw new Error(err.message); 28 | } 29 | } 30 | 31 | public static validateWithKey(name: string, validations: Array, data: any, partial: boolean = false) { 32 | if (validations) { 33 | return validations.map((item: string) => { 34 | const keySplitted: Array = item.split(":"); 35 | const keyName: string = keySplitted.length > 1 ? keySplitted[0].toString() : item; 36 | const length: number = keySplitted.length > 1 ? parseInt(keySplitted[1].toString()) : 0; 37 | if (this.handleFunction[keyName]) { 38 | return this.handleFunction[keyName](name, data, length, partial); 39 | } else { 40 | throw new Error("Can not find validate with driver " + item) 41 | } 42 | }); 43 | } 44 | } 45 | 46 | public static message: {[key: string] : Function} = { 47 | required: (name: string, partial: boolean = false) => `Property ${ name } is ${ partial ? 'not null' : 'required' }`, 48 | minLength: (name: string, length: number, partial: boolean = false) => `Property ${ name } has min length ${ length }`, 49 | maxLength: (name: string, length: number, partial: boolean = false) => `Property ${ name } has max length ${ length }`, 50 | email: (value: string, partial: boolean = false) => `Email ${ value } is invalid`, 51 | isNum: (value: string, length: number) => `Params must type number` 52 | }; 53 | 54 | private static handleFunction: {[key: string]: Function} = { 55 | required: (key: string, value: any, length: number, partial: boolean = false) => { 56 | return (typeof value != 'number' && value) || (typeof value == "number" && value >= 0) ? null : RequestValidate.message.required(key, partial) 57 | }, 58 | minLength: (key: string, value: any, minLength: number, partial: boolean = false) => value != '' && value.length >= minLength ? null : RequestValidate.message.minLength(key, minLength, partial), 59 | maxLength: (key: string, value:any, length: number, partial: boolean = false) => value != '' && value.length <= length ? null : RequestValidate.message.minLength(key, length, partial), 60 | email: (key: string, value: string, length: number, partial: boolean = false) => value != '' && RequestValidate.validateEmail(value) ? null : RequestValidate.message.email(value, partial), 61 | isNum: (key: string, value: string, length: number, partial: boolean = false) => !isNaN(+value) ? null : RequestValidate.message.isNum(value, length) 62 | }; 63 | 64 | private static validateEmail(email: string): boolean { 65 | var reGex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 66 | return reGex.test(email); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /source/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd /app 3 | npm install --quite 4 | npm install --save-dev ts-node 5 | npm install -g nodemon 6 | npm run start 7 | -------------------------------------------------------------------------------- /source/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "target": "es6", 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "outDir": "dist", 13 | "baseUrl": ".", 14 | "paths": { 15 | "*": [ 16 | "node_modules/*", 17 | "src/types/*" 18 | ] 19 | }, 20 | "watch": true 21 | }, 22 | "include": [ 23 | "src/**/*" 24 | ] 25 | } --------------------------------------------------------------------------------