├── .env ├── .eslintrc.yaml ├── .gitignore ├── Dockerfile ├── docker-compose.yml ├── nodemon.json ├── ormconfig.example.json ├── package-lock.json ├── package.json ├── public └── uploads │ └── .gitignore ├── readme.md ├── src ├── Domain │ ├── Core │ │ ├── AccessDeniedError.ts │ │ ├── EntityNotFound.ts │ │ ├── IAuthService.ts │ │ ├── IUploadService.ts │ │ └── Pagination.ts │ ├── FriendRequest │ │ ├── FriendRequest.ts │ │ ├── FriendRequestNotFound.ts │ │ ├── FriendRequestRepository.ts │ │ └── IFriendRequestService.ts │ ├── Image │ │ ├── IImageService.ts │ │ ├── Image.ts │ │ ├── ImageNotFound.ts │ │ └── ImageRepository.ts │ └── User │ │ ├── IUserService.ts │ │ ├── User.ts │ │ ├── UserNotFound.ts │ │ └── UserRepository.ts ├── Http │ ├── Controllers │ │ ├── Auth │ │ │ ├── SignInController.ts │ │ │ └── SignUpController.ts │ │ ├── FriendRequestController.ts │ │ ├── HomeController.ts │ │ ├── ImageController.ts │ │ ├── ProfileController.ts │ │ ├── UploadController.ts │ │ └── UserController.ts │ └── Middleware │ │ └── CustomMiddleware.ts ├── Infrastructure │ ├── DTO │ │ ├── Auth │ │ │ ├── SignInDTO.ts │ │ │ └── SignUpDTO.ts │ │ ├── FriendRequest │ │ │ └── FriendRequestDTO.ts │ │ ├── Image │ │ │ └── ImageDTO.ts │ │ └── Profile │ │ │ └── ProfileDTO.ts │ ├── Domain │ │ └── TypeOrm │ │ │ ├── TypeOrmFriendRequestRepository.ts │ │ │ ├── TypeOrmImageRepository.ts │ │ │ ├── TypeOrmRepository.ts │ │ │ └── TypeOrmUserRepository.ts │ ├── Services │ │ ├── AuthService.ts │ │ ├── FriendRequestService.ts │ │ ├── ImageService.ts │ │ ├── MultipartUploadService.ts │ │ └── UserService.ts │ └── Validators │ │ ├── Auth │ │ ├── SignInValidator.ts │ │ └── SignUpValidator.ts │ │ ├── FriendRequest │ │ └── FriendRequestValidator.ts │ │ ├── Image │ │ └── ImageValidator.ts │ │ └── Profile │ │ └── ProfileValidator.ts ├── Tests │ ├── Acceptance │ │ ├── AuthTest.spec.ts │ │ ├── HomeTest.spec.ts │ │ └── UserTest.spec.ts │ └── TestCase.ts ├── Utils │ └── Request │ │ └── custom.ts ├── app.ts ├── config │ ├── database.ts │ ├── inversify.config.ts │ └── types.ts └── database │ ├── fixtures │ └── UserFactory.ts │ └── migrations │ └── 1522414949149-InitMigration.ts ├── tsconfig.json ├── tslint.json └── typings.json /.env: -------------------------------------------------------------------------------- 1 | APP_ENV=local 2 | PORT=3123 3 | JWT_SECRET=WXYzrcmF5EZ87JjYovzQRdsyp3pl1xwB 4 | 5 | DB_CONNECTION=postgres 6 | DB_HOST=localhost 7 | DB_PORT=5411 8 | DB_DATABASE=starter_db 9 | DB_USERNAME=starter_user 10 | DB_PASSWORD=Eh7gLagHHHzK2h7j -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: airbnb-base 3 | env: 4 | node: true 5 | mocha: true 6 | es6: true 7 | parser: typescript-eslint-parser 8 | parserOptions: 9 | sourceType: module 10 | ecmaFeatures: 11 | modules: true 12 | rules: 13 | generator-star-spacing: 14 | - 2 15 | - before: true 16 | after: true 17 | no-shadow: 0 18 | import/no-unresolved: 0 19 | import/extensions: 0 20 | require-yield: 0 21 | no-param-reassign: 0 22 | comma-dangle: 0 23 | no-underscore-dangle: 0 24 | no-control-regex: 0 25 | import/no-extraneous-dependencies: 26 | - 2 27 | - devDependencies: true 28 | func-names: 0 29 | no-unused-expressions: 0 30 | prefer-arrow-callback: 1 31 | no-use-before-define: 32 | - 2 33 | - functions: false 34 | space-before-function-paren: 35 | - 2 36 | - always 37 | max-len: 38 | - 2 39 | - 120 40 | - 2 41 | semi: 42 | - 2 43 | - never 44 | strict: 45 | - 2 46 | - global 47 | arrow-parens: 48 | - 2 49 | - always -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .nyc_output 4 | coverage 5 | bin 6 | ormconfig.json 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.9-alpine 2 | 3 | RUN apk add --no-cache make gcc g++ python git bash 4 | 5 | RUN npm install -g typescript 6 | RUN npm install -g typeorm 7 | 8 | EXPOSE 3000 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | 4 | # node: 5 | # build: . 6 | # ports: 7 | # - 3123:3123 8 | # depends_on: 9 | # - database 10 | # links: 11 | # - database 12 | # working_dir: /usr/local/src/app 13 | # volumes: 14 | # - .:/usr/local/src/app 15 | # command: bash -c "npm install && npm run dev" 16 | 17 | database: 18 | image: postgres:9.6 19 | ports: 20 | - 5411:5432 21 | volumes: 22 | - data:/var/lib/postgresql/data 23 | environment: 24 | POSTGRES_PASSWORD: Eh7gLagHHHzK2h7j 25 | POSTGRES_USER: starter_user 26 | POSTGRES_DB: starter_db 27 | 28 | volumes: 29 | data: {} 30 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node ./src/app.ts" 6 | } 7 | -------------------------------------------------------------------------------- /ormconfig.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "default", 3 | "type": "postgres", 4 | "host": "127.0.0.1", 5 | "port": 5411, 6 | "username": "starter_user", 7 | "password": "Eh7gLagHHHzK2h7j", 8 | "database": "starter_db", 9 | "synchronize": "all", 10 | "logging": false, 11 | "entities": [ 12 | "bin/Domain/User/User.js", 13 | "bin/Domain/Image/Image.js", 14 | "bin/Domain/FriendRequest/FriendRequest.js" 15 | ], 16 | "migrations": [ 17 | "bin/database/migrations/*.js" 18 | ], 19 | "cli": { 20 | "entitiesDir": "src/Domain", 21 | "migrationsDir": "src/database/migrations" 22 | } 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dmiseev/ddd-node-starter", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "compile": "tsc", 8 | "start": "tsc && nodemon bin/app", 9 | "dev": "nodemon -e ts -w ./src --ignore './src/Tests/' --exec \"npm run start\"", 10 | "test": "nyc --clean --require ts-node/register --require reflect-metadata/Reflect --extension .ts -- mocha --exit --timeout 5000", 11 | "test:all": "npm test **/*.spec.ts", 12 | "dev:no-compile": "nodemon" 13 | }, 14 | "keywords": [], 15 | "author": "Dima Aseev", 16 | "license": "ISC", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/dmiseev/ddd-node-starter.git" 20 | }, 21 | "homepage": "https://github.com/dmiseev/ddd-node-starter#readme", 22 | "dependencies": { 23 | "bcrypt": "1.0.3", 24 | "body-parser": "1.18.2", 25 | "class-transformer": "0.1.9", 26 | "express": "4.16.2", 27 | "express-validation": "^1.0.2", 28 | "helmet": "3.12.0", 29 | "inversify": "4.11.0", 30 | "inversify-express-utils": "5.2.1", 31 | "inversify-inject-decorators": "3.1.0", 32 | "inversify-logger-middleware": "^3.1.0", 33 | "joi": "^13.2.0", 34 | "jsonwebtoken": "8.1.1", 35 | "multer": "^1.3.0", 36 | "nodemon": "^1.17.3", 37 | "path": "^0.12.7", 38 | "pg": "7.4.1", 39 | "reflect-metadata": "0.1.12", 40 | "typeorm": "^0.1.20", 41 | "uuid": "^3.2.1" 42 | }, 43 | "devDependencies": { 44 | "@types/bluebird": "3.5.20", 45 | "@types/body-parser": "1.16.8", 46 | "@types/chai": "4.1.2", 47 | "@types/chai-http": "^3.0.4", 48 | "@types/express": "4.11.1", 49 | "@types/helmet": "0.0.37", 50 | "@types/mocha": "2.2.48", 51 | "chai": "4.1.2", 52 | "chai-http": "^4.0.0", 53 | "mocha": "5.0.5", 54 | "nyc": "11.6.0", 55 | "ts-node": "5.0.1", 56 | "typescript": "2.7.2" 57 | }, 58 | "nyc": { 59 | "reporter": [ 60 | "lcov", 61 | "text" 62 | ], 63 | "include": [ 64 | "src/**/*.ts", 65 | "src/**/*.tsx" 66 | ], 67 | "exclude": [ 68 | "src/**/*.d.ts", 69 | "src/**/*.spec.ts", 70 | "src/**/*.spec.tsx" 71 | ], 72 | "extension": [ 73 | ".ts", 74 | ".tsx" 75 | ], 76 | "require": [ 77 | "ts-node/register" 78 | ] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /public/uploads/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Skeleton for Node.js applications written in TypeScript 2 | 3 | ### First deploy 4 | 5 | 1. Start database container; 6 | 7 | ```bash 8 | docker-compose up -d 9 | ``` 10 | 11 | 2. Copy ormconfig.example.json to ormconfig.json with your DB connection; 12 | 3. Install dependencies; 13 | ```bash 14 | npm i 15 | ``` 16 | 17 | 4. Install globally typeorm; 18 | ```bash 19 | sudo npm i typeorm -g 20 | ``` 21 | 22 | 5. Compile .ts to .js files to ./bin/ folder; 23 | ```bash 24 | npm run compile 25 | ``` 26 | 27 | 6. Up migrations; 28 | ```bash 29 | typeorm migrations:run 30 | ``` 31 | 32 | 7. Run Application in dev mode. 33 | ```bash 34 | npm run dev 35 | ``` 36 | 37 | ### Testing 38 | 39 | 1. Execute command in cli; 40 | ```bash 41 | npm run test:all 42 | ``` -------------------------------------------------------------------------------- /src/Domain/Core/AccessDeniedError.ts: -------------------------------------------------------------------------------- 1 | 2 | export class AccessDeniedError extends Error{ 3 | 4 | constructor(message = 'Access denied.') { 5 | super(message); 6 | } 7 | } -------------------------------------------------------------------------------- /src/Domain/Core/EntityNotFound.ts: -------------------------------------------------------------------------------- 1 | 2 | export class EntityNotFound extends Error { 3 | 4 | constructor(message) { 5 | super(message); 6 | } 7 | } -------------------------------------------------------------------------------- /src/Domain/Core/IAuthService.ts: -------------------------------------------------------------------------------- 1 | import { SignInDTO } from '../../Infrastructure/DTO/Auth/SignInDTO'; 2 | import { SignUpDTO } from '../../Infrastructure/DTO/Auth/SignUpDTO'; 3 | import { User } from '../User/User'; 4 | 5 | export interface IAuthService { 6 | 7 | /** 8 | * @param {SignInDTO} DTO 9 | * @returns {Promise} 10 | */ 11 | signIn(DTO: SignInDTO): Promise; 12 | 13 | /** 14 | * @param {SignUpDTO} DTO 15 | * @returns {Promise} 16 | */ 17 | signUp(DTO: SignUpDTO): Promise; 18 | } -------------------------------------------------------------------------------- /src/Domain/Core/IUploadService.ts: -------------------------------------------------------------------------------- 1 | import { IRequest } from '../../Utils/Request/custom'; 2 | 3 | export interface IUploadService { 4 | 5 | /** 6 | * @param {IRequest} request 7 | * @returns {Object} 8 | */ 9 | fromRequest(request: IRequest): Object; 10 | } -------------------------------------------------------------------------------- /src/Domain/Core/Pagination.ts: -------------------------------------------------------------------------------- 1 | import { IRequest } from '../../Utils/Request/custom'; 2 | 3 | export class Pagination { 4 | 5 | private _page: number; 6 | private _perPage: number; 7 | 8 | constructor(page: number, perPage: number) 9 | { 10 | this._page = page; 11 | this._perPage = perPage; 12 | } 13 | 14 | /** 15 | * @param {IRequest} request 16 | * @returns {Pagination} 17 | */ 18 | public static fromRequest(request: IRequest): Pagination 19 | { 20 | return new Pagination( 21 | request.query.page ? Number(request.query.page) : 1, 22 | request.query.perPage ? Number(request.query.perPage) : 10 23 | ); 24 | } 25 | 26 | public page(): number { 27 | return this._page; 28 | } 29 | 30 | public perPage(): number { 31 | return this._perPage; 32 | } 33 | 34 | /** 35 | * @returns {number} 36 | */ 37 | public offset(): number { 38 | return (this.page() - 1) * this.perPage(); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Domain/FriendRequest/FriendRequest.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, JoinColumn, ManyToOne } from 'typeorm'; 2 | import { User } from '../User/User'; 3 | 4 | @Entity('friend_requests') 5 | export class FriendRequest { 6 | 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @ManyToOne(type => User) 11 | @JoinColumn({name: 'sender_id', referencedColumnName: 'id'}) 12 | sender: User; 13 | 14 | @ManyToOne(type => User) 15 | @JoinColumn({name: 'receiver_id', referencedColumnName: 'id'}) 16 | receiver: User; 17 | 18 | @Column({ 19 | name: 'created_at', 20 | type: 'timestamp', 21 | nullable: false 22 | }) 23 | createdAt: Date; 24 | 25 | @Column({ 26 | name: 'deleted_at', 27 | type: 'timestamp', 28 | nullable: true 29 | }) 30 | deletedAt: Date; 31 | 32 | constructor(sender: User, receiver: User, createdAt: Date) { 33 | this.sender = sender; 34 | this.receiver = receiver; 35 | this.createdAt = createdAt; 36 | } 37 | 38 | /** 39 | * @param {User} sender 40 | * @param {User} receiver 41 | * 42 | * @returns {FriendRequest} 43 | */ 44 | static register(sender: User, receiver: User): FriendRequest { 45 | return new FriendRequest(sender, receiver, new Date()); 46 | } 47 | 48 | public remove(): void { 49 | this.deletedAt = new Date(); 50 | } 51 | } -------------------------------------------------------------------------------- /src/Domain/FriendRequest/FriendRequestNotFound.ts: -------------------------------------------------------------------------------- 1 | import { UserNotFound } from '../User/UserNotFound'; 2 | import { AccessDeniedError } from '../Core/AccessDeniedError'; 3 | 4 | export class FriendRequestNotFound extends Error{ 5 | 6 | /** 7 | * @param {number} id 8 | * @returns {FriendRequestNotFound} 9 | */ 10 | static fromId(id: number): FriendRequestNotFound 11 | { 12 | return new FriendRequestNotFound('Friend request with ID #' + id + ' not found.') 13 | } 14 | 15 | /** 16 | * @returns {UserNotFound} 17 | */ 18 | static forbidden(): AccessDeniedError 19 | { 20 | return new AccessDeniedError('Accept friend request can only recipient.') 21 | } 22 | 23 | /** 24 | * @returns {UserNotFound} 25 | */ 26 | static friendExist(): FriendRequestNotFound 27 | { 28 | return new FriendRequestNotFound('You already have this user in your friend list.') 29 | } 30 | 31 | /** 32 | * @returns {UserNotFound} 33 | */ 34 | static waitingAcceptFromReceiver(): FriendRequestNotFound 35 | { 36 | return new FriendRequestNotFound('This friend request is waiting for accept.') 37 | } 38 | } -------------------------------------------------------------------------------- /src/Domain/FriendRequest/FriendRequestRepository.ts: -------------------------------------------------------------------------------- 1 | import { FriendRequest } from './FriendRequest'; 2 | import { Pagination } from '../Core/Pagination'; 3 | 4 | export interface FriendRequestRepository { 5 | 6 | /** 7 | * @param {number} senderId 8 | * @param {Pagination} pagination 9 | * 10 | * @returns {Promise<[FriendRequest[] , number]>} 11 | */ 12 | bySenderId(senderId: number, pagination: Pagination): Promise<[FriendRequest[], number]>; 13 | 14 | /** 15 | * @param {number} receiverId 16 | * @param {Pagination} pagination 17 | * 18 | * @returns {Promise<[FriendRequest[] , number]>} 19 | */ 20 | byReceiverId(receiverId: number, pagination: Pagination): Promise<[FriendRequest[], number]>; 21 | 22 | /** 23 | * @param {number} id 24 | * @returns {Promise} 25 | */ 26 | byId(id: number): Promise; 27 | 28 | /** 29 | * @param {number} senderId 30 | * @param {number} receiverId 31 | * 32 | * @return {Promise} 33 | */ 34 | find(senderId: number, receiverId: number): Promise; 35 | 36 | /** 37 | * @param {FriendRequest} user 38 | */ 39 | store(user: FriendRequest): Promise; 40 | } -------------------------------------------------------------------------------- /src/Domain/FriendRequest/IFriendRequestService.ts: -------------------------------------------------------------------------------- 1 | import { FriendRequest } from './FriendRequest'; 2 | import { Pagination } from '../Core/Pagination'; 3 | import { FriendRequestDTO } from '../../Infrastructure/DTO/FriendRequest/FriendRequestDTO'; 4 | import { User } from '../User/User'; 5 | 6 | export interface IFriendRequestService { 7 | 8 | /** 9 | * @param {number} userId 10 | * @param {Pagination} pagination 11 | * 12 | * @returns {Promise} 13 | */ 14 | bySenderId(userId: number, pagination: Pagination): Promise<[FriendRequest[], number]>; 15 | 16 | /** 17 | * @param {number} userId 18 | * @param {Pagination} pagination 19 | * 20 | * @returns {Promise} 21 | */ 22 | byReceiverId(userId: number, pagination: Pagination): Promise<[FriendRequest[], number]>; 23 | 24 | /** 25 | * @param {number} id 26 | * @returns {Promise} 27 | */ 28 | byId(id: number): Promise; 29 | 30 | /** 31 | * @param {User} sender 32 | * @param {FriendRequestDTO} DTO 33 | * 34 | * @returns {Promise} 35 | */ 36 | store(sender: User, DTO: FriendRequestDTO): Promise; 37 | 38 | /** 39 | * @param {User} user 40 | * @param {number} id 41 | * 42 | * @return {Promise} 43 | */ 44 | accept(user: User, id: number): Promise; 45 | 46 | /** 47 | * @param {number} id 48 | * @returns {Promise} 49 | */ 50 | remove(id: number): Promise; 51 | } -------------------------------------------------------------------------------- /src/Domain/Image/IImageService.ts: -------------------------------------------------------------------------------- 1 | import { Image } from './Image'; 2 | import { ImageDTO } from '../../Infrastructure/DTO/Image/ImageDTO'; 3 | import { User } from '../User/User'; 4 | 5 | export interface IImageService { 6 | 7 | /** 8 | * @returns {Promise} 9 | */ 10 | all(): Promise; 11 | 12 | /** 13 | * @param {ImageDTO} DTO 14 | * @param {User} user 15 | * 16 | * @returns {Promise} 17 | */ 18 | store(DTO: ImageDTO, user: User): Promise; 19 | } -------------------------------------------------------------------------------- /src/Domain/Image/Image.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; 2 | import { User } from '../User/User'; 3 | 4 | @Entity('images') 5 | export class Image { 6 | 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @Column({name: 'name', type: 'varchar', length: 255, nullable: false}) 11 | name: string; 12 | 13 | @Column({name: 'path', type: 'varchar', length: 255, nullable: false}) 14 | path: string; 15 | 16 | @Column({name: 'created_at', type: 'timestamp', nullable: false}) 17 | createdAt: Date; 18 | 19 | @ManyToOne(type => User, user => user.images) 20 | @JoinColumn({name: 'user_id', referencedColumnName: 'id'}) 21 | user: User; 22 | 23 | constructor(user: User, name: string, path: string, createdAt: Date) { 24 | this.user = user; 25 | this.name = name; 26 | this.path = path; 27 | this.createdAt = createdAt; 28 | } 29 | 30 | /** 31 | * @param {User} user 32 | * @param {string} name 33 | * @param {string} path 34 | * 35 | * @returns {Image} 36 | */ 37 | static register(user: User, name: string, path: string): Image { 38 | return new Image(user, name, path, new Date()); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Domain/Image/ImageNotFound.ts: -------------------------------------------------------------------------------- 1 | import { EntityNotFound } from '../Core/EntityNotFound'; 2 | 3 | export class ImageNotFound extends EntityNotFound { 4 | 5 | /** 6 | * @param {number} id 7 | * @returns {ImageNotFound} 8 | */ 9 | static fromId(id: number): ImageNotFound { 10 | return new ImageNotFound('Image with ID #' + id + ' not found.'); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Domain/Image/ImageRepository.ts: -------------------------------------------------------------------------------- 1 | import { Image } from './Image'; 2 | 3 | export interface ImageRepository { 4 | 5 | /** 6 | * @returns {Promise} 7 | */ 8 | all(): Promise; 9 | 10 | /** 11 | * @param {number} id 12 | * @returns {Promise} 13 | */ 14 | byId(id: number): Promise; 15 | 16 | /** 17 | * @param {number} userId 18 | * @returns {Promise} 19 | */ 20 | byUserId(userId: number): Promise; 21 | 22 | /** 23 | * @param {Image} user 24 | */ 25 | store(user: Image): Promise; 26 | } -------------------------------------------------------------------------------- /src/Domain/User/IUserService.ts: -------------------------------------------------------------------------------- 1 | import { User } from './User'; 2 | import { Pagination } from '../Core/Pagination'; 3 | import { ProfileDTO } from '../../Infrastructure/DTO/Profile/ProfileDTO'; 4 | 5 | export interface IUserService { 6 | 7 | /** 8 | * @param {Pagination} pagination 9 | * @returns {Promise} 10 | */ 11 | all(pagination: Pagination): Promise<[User[], number]>; 12 | 13 | /** 14 | * @param {number} id 15 | * @returns {Promise} 16 | */ 17 | byId(id: number): Promise; 18 | 19 | /** 20 | * @param {number} id 21 | * @param {ProfileDTO} DTO 22 | * @returns {Promise} 23 | */ 24 | update(id: number, DTO: ProfileDTO): Promise; 25 | 26 | /** 27 | * @param {number} id 28 | * @returns {Promise} 29 | */ 30 | remove(id: number): Promise; 31 | } -------------------------------------------------------------------------------- /src/Domain/User/User.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, Index, OneToMany, ManyToMany, JoinTable } from 'typeorm'; 2 | import { Exclude, Expose } from 'class-transformer'; 3 | import { Image } from '../Image/Image'; 4 | 5 | @Entity('users') 6 | @Index('users_email_deleted_sequence', ['email', 'deletedAt'], {unique: true}) 7 | export class User { 8 | 9 | @PrimaryGeneratedColumn() 10 | id: number; 11 | 12 | @Column({ 13 | name: 'email', 14 | type: 'varchar', 15 | length: 255, 16 | nullable: false 17 | }) 18 | email: string; 19 | 20 | @Exclude() 21 | @Column({ 22 | name: 'password', 23 | type: 'varchar', 24 | length: 60, 25 | nullable: false 26 | }) 27 | password: string; 28 | 29 | @Column({ 30 | name: 'first_name', 31 | type: 'varchar', 32 | length: 255, 33 | nullable: false 34 | }) 35 | firstName: string; 36 | 37 | @Column({ 38 | name: 'last_name', 39 | type: 'varchar', 40 | length: 255, 41 | nullable: false 42 | }) 43 | lastName: string; 44 | 45 | @Column({ 46 | name: 'is_active', 47 | type: 'boolean', 48 | nullable: false 49 | }) 50 | isActive: boolean; 51 | 52 | @Expose({ groups: ['detail'] }) 53 | @Column({ 54 | name: 'created_at', 55 | type: 'timestamp', 56 | nullable: false 57 | }) 58 | createdAt: Date; 59 | 60 | @Exclude() 61 | @Column({ 62 | name: 'deleted_at', 63 | type: 'timestamp', 64 | nullable: true 65 | }) 66 | deletedAt: Date; 67 | 68 | @OneToMany(type => Image, image => image.user) 69 | images: Image[]; 70 | 71 | @ManyToMany(type => User) 72 | @JoinTable({ 73 | name: 'users_friends', 74 | joinColumn: { 75 | name: 'user_id', 76 | referencedColumnName: 'id' 77 | }, 78 | inverseJoinColumn: { 79 | name: 'friend_id', 80 | referencedColumnName: 'id' 81 | } 82 | }) 83 | friends: User[]; 84 | 85 | constructor(email: string, password: string, firstName: string, lastName: string, createdAt: Date) { 86 | this.email = email; 87 | this.password = password; 88 | this.firstName = firstName; 89 | this.lastName = lastName; 90 | this.isActive = false; 91 | this.createdAt = createdAt; 92 | } 93 | 94 | /** 95 | * @param {string} email 96 | * @param {string} password 97 | * @param {string} firstName 98 | * @param {string} lastName 99 | * 100 | * @returns {User} 101 | */ 102 | static register(email: string, password: string, firstName: string, lastName: string): User { 103 | return new User(email, password, firstName, lastName, new Date()); 104 | } 105 | 106 | public remove(): void { 107 | this.deletedAt = new Date(); 108 | } 109 | 110 | /** 111 | * @param {User} user 112 | * @return boolean 113 | */ 114 | public isFriend(user: User): boolean { 115 | 116 | if (!this.friends) { 117 | return false; 118 | } 119 | 120 | let friends = this.friends.filter((friend: User) => { 121 | return friend.id === user.id; 122 | }); 123 | 124 | return !!friends; 125 | } 126 | 127 | /** 128 | * @param {User} friend 129 | */ 130 | public addFriend(friend: User): void { 131 | 132 | if (this.isFriend(friend)) { 133 | return; 134 | } 135 | 136 | if (!this.friends) { 137 | this.friends = []; 138 | } 139 | 140 | this.friends.push(friend); 141 | } 142 | 143 | @Expose() 144 | get fullName(): string { 145 | return this.firstName + ' ' + this.lastName; 146 | } 147 | } -------------------------------------------------------------------------------- /src/Domain/User/UserNotFound.ts: -------------------------------------------------------------------------------- 1 | import { EntityNotFound } from '../Core/EntityNotFound'; 2 | 3 | export class UserNotFound extends EntityNotFound { 4 | 5 | /** 6 | * @returns {UserNotFound} 7 | */ 8 | static authorized(): UserNotFound { 9 | return new UserNotFound('The email or password is incorrect. Try again, please.'); 10 | } 11 | 12 | /** 13 | * @param {number} id 14 | * @returns {UserNotFound} 15 | */ 16 | static fromId(id: number): UserNotFound { 17 | return new UserNotFound('User with ID #' + id + ' not found.'); 18 | } 19 | 20 | /** 21 | * @param {string} email 22 | * @returns {UserNotFound} 23 | */ 24 | static fromEmail(email: string): UserNotFound { 25 | return new UserNotFound('User with email ' + email + ' not found.'); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Domain/User/UserRepository.ts: -------------------------------------------------------------------------------- 1 | import { User } from './User'; 2 | import { Pagination } from '../Core/Pagination'; 3 | 4 | export interface UserRepository { 5 | 6 | /** 7 | * @param {Pagination} pagination 8 | * @returns {Promise<[User[] , number]>} 9 | */ 10 | all(pagination: Pagination): Promise<[User[], number]>; 11 | 12 | /** 13 | * @param {number} id 14 | * @returns {Promise} 15 | */ 16 | byId(id: number): Promise; 17 | 18 | /** 19 | * @param {string} email 20 | * @returns {Promise} 21 | */ 22 | byEmail(email: string): Promise; 23 | 24 | /** 25 | * @param {User} user 26 | */ 27 | store(user: User): Promise; 28 | } -------------------------------------------------------------------------------- /src/Http/Controllers/Auth/SignInController.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { controller, httpPost } from 'inversify-express-utils'; 3 | import { inject } from 'inversify'; 4 | import { SignInDTO } from "../../../Infrastructure/DTO/Auth/SignInDTO"; 5 | import { IAuthService } from '../../../Domain/Core/IAuthService'; 6 | import * as validate from 'express-validation'; 7 | import * as signInValidator from '../../../Infrastructure/Validators/Auth/SignInValidator'; 8 | 9 | @controller('/auth/sign-in') 10 | export class SignInController { 11 | 12 | constructor(@inject('IAuthService') private authService: IAuthService) {} 13 | 14 | /** 15 | * @param {Request} request 16 | * @returns {Promise} 17 | */ 18 | @httpPost('/', validate(signInValidator)) 19 | public async signIn(request: Request) { 20 | 21 | return await this.authService.signIn( 22 | SignInDTO.fromRequest(request) 23 | ); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Http/Controllers/Auth/SignUpController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { controller, httpPost } from 'inversify-express-utils'; 3 | import { inject } from 'inversify'; 4 | import { SignUpDTO } from "../../../Infrastructure/DTO/Auth/SignUpDTO"; 5 | import { IAuthService } from '../../../Domain/Core/IAuthService'; 6 | import * as validate from 'express-validation'; 7 | import * as signUpValidator from '../../../Infrastructure/Validators/Auth/SignUpValidator'; 8 | import { User } from '../../../Domain/User/User'; 9 | import { serialize } from 'class-transformer'; 10 | 11 | @controller('/auth/sign-up') 12 | export class SignUpController { 13 | 14 | constructor(@inject('IAuthService') private authService: IAuthService) {} 15 | 16 | /** 17 | * @param {Request} request 18 | * @param {Response} response 19 | * 20 | * @returns {Promise} 21 | */ 22 | @httpPost('/', validate(signUpValidator)) 23 | public async signUp(request: Request, response: Response) { 24 | 25 | return await this.authService.signUp(SignUpDTO.fromRequest(request)) 26 | .then((user: User) => { 27 | response.status(201); 28 | return serialize(user); 29 | }); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Http/Controllers/FriendRequestController.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | import { controller, httpDelete, httpGet, httpPatch, httpPost } from 'inversify-express-utils'; 3 | import { inject } from 'inversify'; 4 | import { FriendRequest } from '../../Domain/FriendRequest/FriendRequest'; 5 | import { authMiddleware } from '../Middleware/CustomMiddleware'; 6 | import { IFriendRequestService } from '../../Domain/FriendRequest/IFriendRequestService'; 7 | import { IRequest } from '../../Utils/Request/custom'; 8 | import { serialize } from 'class-transformer'; 9 | import { FriendRequestDTO } from '../../Infrastructure/DTO/FriendRequest/FriendRequestDTO'; 10 | import * as validate from 'express-validation'; 11 | import * as friendRequestValidator from '../../Infrastructure/Validators/FriendRequest/FriendRequestValidator'; 12 | 13 | @controller('/friend-requests', authMiddleware) 14 | export class FriendRequestController { 15 | 16 | constructor(@inject('IFriendRequestService') private friendRequestService: IFriendRequestService) { 17 | } 18 | 19 | /** 20 | * @param {IRequest} request 21 | * @param {Response} response 22 | * 23 | * @returns {Promise} 24 | */ 25 | @httpPost('/', validate(friendRequestValidator)) 26 | public async store(request: IRequest, response: Response) { 27 | 28 | return await this.friendRequestService.store( 29 | request.user, 30 | FriendRequestDTO.fromRequest(request) 31 | ); 32 | } 33 | 34 | /** 35 | * @param {IRequest} request 36 | * @param {Response} response 37 | * 38 | * @returns {Promise} 39 | */ 40 | @httpPatch('/:id') 41 | public async accept(request: IRequest, response: Response) { 42 | 43 | return await this.friendRequestService.accept(request.user, request.params.id) 44 | .then(() => { 45 | response.status(202); 46 | return {message: 'Request successfully accepted.'}; 47 | }); 48 | } 49 | 50 | /** 51 | * @param {Request} request 52 | * @returns {Promise} 53 | */ 54 | @httpGet('/:id') 55 | public async byId(request: IRequest,) { 56 | 57 | return await this.friendRequestService.byId(parseInt(request.params.id)) 58 | .then((friendRequest: FriendRequest) => { 59 | return serialize(friendRequest); 60 | }); 61 | } 62 | 63 | /** 64 | * @param {Request} request 65 | * @param {Response} response 66 | */ 67 | @httpDelete('/:id') 68 | public async remove(request: IRequest, response: Response) { 69 | 70 | return await this.friendRequestService.remove(parseInt(request.params.id)) 71 | .then(() => { 72 | response.set('X-Items-Count', '0'); 73 | response.status(204); 74 | }); 75 | } 76 | } -------------------------------------------------------------------------------- /src/Http/Controllers/HomeController.ts: -------------------------------------------------------------------------------- 1 | import { controller, httpGet } from 'inversify-express-utils'; 2 | 3 | @controller('/') 4 | export class HomeController { 5 | 6 | @httpGet('/') 7 | public home() { 8 | 9 | return {message: 'Home page.'}; 10 | } 11 | } -------------------------------------------------------------------------------- /src/Http/Controllers/ImageController.ts: -------------------------------------------------------------------------------- 1 | import { controller, httpPost } from 'inversify-express-utils'; 2 | import { inject } from 'inversify'; 3 | import { authMiddleware } from '../Middleware/CustomMiddleware'; 4 | import { IRequest } from '../../Utils/Request/custom'; 5 | import { Image } from '../../Domain/Image/Image'; 6 | import { IImageService } from '../../Domain/Image/IImageService'; 7 | import { ImageDTO } from '../../Infrastructure/DTO/Image/ImageDTO'; 8 | import * as validate from 'express-validation'; 9 | import * as imageValidator from '../../Infrastructure/Validators/Image/ImageValidator'; 10 | 11 | @controller('/images', authMiddleware) 12 | export class ImageController { 13 | 14 | constructor(@inject('IImageService') private imageService: IImageService) { 15 | } 16 | 17 | /** 18 | * @returns {Promise} 19 | */ 20 | @httpPost('/', validate(imageValidator)) 21 | public async store(request: IRequest): Promise { 22 | 23 | return await this.imageService.store( 24 | ImageDTO.fromRequest(request), 25 | request.user 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Http/Controllers/ProfileController.ts: -------------------------------------------------------------------------------- 1 | import { controller, httpGet, httpPut } from 'inversify-express-utils'; 2 | import { User } from '../../Domain/User/User'; 3 | import { authMiddleware } from '../Middleware/CustomMiddleware'; 4 | import { IRequest } from '../../Utils/Request/custom'; 5 | import { inject } from 'inversify'; 6 | import { IUserService } from '../../Domain/User/IUserService'; 7 | import { ProfileDTO } from '../../Infrastructure/DTO/Profile/ProfileDTO'; 8 | import { Response } from 'express'; 9 | import { serialize } from 'class-transformer'; 10 | import * as validate from 'express-validation'; 11 | import * as profileValidator from '../../Infrastructure/Validators/Profile/ProfileValidator'; 12 | 13 | @controller('/users/me', authMiddleware) 14 | export class ProfileController { 15 | 16 | constructor(@inject('IUserService') private userService: IUserService) { 17 | } 18 | 19 | /** 20 | * @param {IRequest} request 21 | */ 22 | @httpGet('/') 23 | public me(request: IRequest) { 24 | 25 | return serialize(request.user, { groups: ['detail'] }); 26 | } 27 | 28 | /** 29 | * @param {IRequest} request 30 | * @param {Response} response 31 | */ 32 | @httpPut('/', validate(profileValidator)) 33 | public async update(request: IRequest, response: Response) { 34 | 35 | return await this.userService.update(request.user.id, ProfileDTO.fromRequest(request)) 36 | .then((user: User) => { 37 | response.status(202); 38 | return serialize(user); 39 | }); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Http/Controllers/UploadController.ts: -------------------------------------------------------------------------------- 1 | import { controller, httpPost } from 'inversify-express-utils'; 2 | import { inject } from 'inversify'; 3 | import { IUploadService } from '../../Domain/Core/IUploadService'; 4 | import * as multer from 'multer'; 5 | import * as path from 'path'; 6 | import * as fs from 'fs'; 7 | import * as uuidv4 from 'uuid/v4'; 8 | import { authMiddleware } from '../Middleware/CustomMiddleware'; 9 | import { IRequest } from '../../Utils/Request/custom'; 10 | 11 | @controller('/uploads', authMiddleware) 12 | export class UploadController { 13 | 14 | constructor(@inject('IUploadService') private uploadService: IUploadService) { 15 | } 16 | 17 | /** 18 | * TODO: need think about refactoring :) 19 | * @param {IRequest} request 20 | */ 21 | @httpPost('/', multer({ 22 | 23 | storage: multer.diskStorage({ 24 | 25 | destination: (req: IRequest, file, callback) => { 26 | let folderPath = path.resolve('./public/uploads/' + req.user.id + '/'); 27 | 28 | if (!fs.existsSync(folderPath)) { 29 | fs.mkdirSync(folderPath); 30 | } 31 | 32 | callback(null, folderPath); 33 | }, 34 | 35 | filename: function (req, file, callback) { 36 | callback(null, uuidv4() + '.' + file.mimetype.split('/')[1]); 37 | } 38 | }), 39 | 40 | fileFilter: (req, file, callback) => { 41 | 42 | if (!file) { 43 | return callback(new Error('Could not upload image.'), false); 44 | } 45 | 46 | if (!file.mimetype.startsWith('image/') || !file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) { 47 | return callback(new Error('Could not upload image. The file does not match the type: jpeg, png, gif.'), false); 48 | } 49 | 50 | callback(null, true); 51 | } 52 | }).single('image')) 53 | public upload(request: IRequest) { 54 | 55 | return this.uploadService.fromRequest(request); 56 | } 57 | } -------------------------------------------------------------------------------- /src/Http/Controllers/UserController.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | import { controller, httpDelete, httpGet } from 'inversify-express-utils'; 3 | import { inject } from 'inversify'; 4 | import { User } from '../../Domain/User/User'; 5 | import { authMiddleware } from '../Middleware/CustomMiddleware'; 6 | import { IUserService } from '../../Domain/User/IUserService'; 7 | import { IRequest } from '../../Utils/Request/custom'; 8 | import { Pagination } from '../../Domain/Core/Pagination'; 9 | import { serialize } from 'class-transformer'; 10 | 11 | @controller('/users', authMiddleware) 12 | export class UserController { 13 | 14 | constructor(@inject('IUserService') private userService: IUserService) { 15 | } 16 | 17 | /** 18 | * @param {IRequest} request 19 | * @param {Response} response 20 | * 21 | * @returns {User[]} 22 | */ 23 | @httpGet('/') 24 | public async all(request: IRequest, response: Response) { 25 | 26 | return await this.userService.all(Pagination.fromRequest(request)) 27 | .then((data: [User[], number]) => { 28 | response.set('X-Items-Count', data[1].toString()); 29 | return serialize(data[0]); 30 | }); 31 | } 32 | 33 | /** 34 | * @param {Request} request 35 | * @returns {Promise} 36 | */ 37 | @httpGet('/:id') 38 | public async byId(request: IRequest,) { 39 | 40 | return await this.userService.byId(parseInt(request.params.id)) 41 | .then((user: User) => { 42 | return serialize(user); 43 | }); 44 | } 45 | 46 | /** 47 | * @param {Request} request 48 | * @param {Response} response 49 | */ 50 | @httpDelete('/:id') 51 | public async remove(request: IRequest, response: Response) { 52 | 53 | return await this.userService.remove(parseInt(request.params.id)) 54 | .then(() => { 55 | response.set('X-Items-Count', '0'); 56 | response.status(204); 57 | }); 58 | } 59 | } -------------------------------------------------------------------------------- /src/Http/Middleware/CustomMiddleware.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as jwt from 'jsonwebtoken'; 3 | import { getManager } from "typeorm"; 4 | import { User } from "../../Domain/User/User"; 5 | import { IRequest } from '../../Utils/Request/custom'; 6 | 7 | /** 8 | * Show REST info in logs 9 | */ 10 | export function loggerMiddleware(req: express.Request, res: any, next: any) { 11 | 12 | let fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl; 13 | console.log('[' + req.method + ']: ' + fullUrl); 14 | 15 | next(); 16 | } 17 | 18 | export function jsonMiddleware(req: express.Request, res: any, next: any) { 19 | res.set('Content-Type', 'application/json'); 20 | res.set('X-Items-Count', '1'); 21 | next(); 22 | } 23 | 24 | export function authMiddleware(req: IRequest, res: any, next: any) { 25 | 26 | let token = req.body.token || req.query.token || req.headers['x-access-token']; 27 | 28 | if (!token) { 29 | res.status(401).send({errorMessage: 'not authorized'}); return; 30 | } 31 | 32 | let userJson = jwt.decode(token); 33 | 34 | if (!userJson || !userJson.id) { 35 | res.status(401).send({errorMessage: 'not authorized'}); return; 36 | } 37 | 38 | let entityManager = getManager(); 39 | 40 | entityManager.createQueryBuilder(User, 'u') 41 | .where('u.id = :id') 42 | .setParameter('id', userJson.id) 43 | .getOne() 44 | .then((user: User) => { 45 | req.user = user; 46 | 47 | next(); 48 | }) 49 | .catch((err) => { 50 | console.log(err); 51 | res.status(401).send({errorMessage: 'not authorized'}); return; 52 | }); 53 | } -------------------------------------------------------------------------------- /src/Infrastructure/DTO/Auth/SignInDTO.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | export class SignInDTO{ 4 | 5 | private _email: string; 6 | private _password: string; 7 | 8 | constructor(email: string, password: string) 9 | { 10 | this._email = email; 11 | this._password = password; 12 | } 13 | 14 | /** 15 | * @param {Request} request 16 | * @returns {SignInDTO} 17 | */ 18 | static fromRequest(request: Request) 19 | { 20 | return new SignInDTO( 21 | request.body.email, 22 | request.body.password 23 | ); 24 | } 25 | 26 | get email(): string { 27 | return this._email; 28 | } 29 | 30 | get password(): string { 31 | return this._password; 32 | } 33 | } -------------------------------------------------------------------------------- /src/Infrastructure/DTO/Auth/SignUpDTO.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | export class SignUpDTO{ 4 | 5 | private _email: string; 6 | private _password: string; 7 | private _firstName: string; 8 | private _lastName: string; 9 | 10 | constructor(email: string, password: string, firstName: string, lastName: string) 11 | { 12 | this._email = email; 13 | this._password = password; 14 | this._firstName = firstName; 15 | this._lastName = lastName; 16 | } 17 | 18 | /** 19 | * @param {} request 20 | * @returns {SignUpDTO} 21 | */ 22 | static fromRequest(request: Request) 23 | { 24 | return new SignUpDTO( 25 | request.body.email, 26 | request.body.password, 27 | request.body.firstName, 28 | request.body.lastName 29 | ); 30 | } 31 | 32 | get email(): string { 33 | return this._email; 34 | } 35 | 36 | get password(): string { 37 | return this._password; 38 | } 39 | 40 | get firstName(): string { 41 | return this._firstName; 42 | } 43 | 44 | get lastName(): string { 45 | return this._lastName; 46 | } 47 | } -------------------------------------------------------------------------------- /src/Infrastructure/DTO/FriendRequest/FriendRequestDTO.ts: -------------------------------------------------------------------------------- 1 | import { IRequest } from '../../../Utils/Request/custom'; 2 | 3 | export class FriendRequestDTO { 4 | 5 | private _userId: number; 6 | 7 | constructor(userId: number) { 8 | this._userId = userId; 9 | } 10 | 11 | /** 12 | * @param {Request} request 13 | * @returns {FriendRequestDTO} 14 | */ 15 | static fromRequest(request: IRequest) { 16 | 17 | return new FriendRequestDTO( 18 | request.body.userId 19 | ); 20 | } 21 | 22 | get userId(): number { 23 | return this._userId; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Infrastructure/DTO/Image/ImageDTO.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | export class ImageDTO { 4 | 5 | private _name: string; 6 | private _path: string; 7 | 8 | constructor(name: string, path: string) 9 | { 10 | this._name = name; 11 | this._path = path; 12 | } 13 | 14 | /** 15 | * @param {Request} request 16 | * @returns {SignUpDTO} 17 | */ 18 | static fromRequest(request: Request) 19 | { 20 | return new ImageDTO(request.body.name, request.body.path); 21 | } 22 | 23 | get name(): string { 24 | return this._name; 25 | } 26 | 27 | get path(): string { 28 | return this._path; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Infrastructure/DTO/Profile/ProfileDTO.ts: -------------------------------------------------------------------------------- 1 | import { IRequest } from '../../../Utils/Request/custom'; 2 | 3 | export class ProfileDTO { 4 | 5 | private _email: string; 6 | private _firstName: string; 7 | private _lastName: string; 8 | 9 | constructor(email: string, firstName: string, lastName: string) { 10 | this._email = email; 11 | this._firstName = firstName; 12 | this._lastName = lastName; 13 | } 14 | 15 | /** 16 | * @param {Request} request 17 | * @returns {ProfileDTO} 18 | */ 19 | static fromRequest(request: IRequest) { 20 | return new ProfileDTO( 21 | request.body.email, 22 | request.body.firstName, 23 | request.body.lastName 24 | ); 25 | } 26 | 27 | get email(): string { 28 | return this._email; 29 | } 30 | 31 | get firstName(): string { 32 | return this._firstName; 33 | } 34 | 35 | get lastName(): string { 36 | return this._lastName; 37 | } 38 | } -------------------------------------------------------------------------------- /src/Infrastructure/Domain/TypeOrm/TypeOrmFriendRequestRepository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, SelectQueryBuilder } from 'typeorm'; 2 | import { FriendRequestRepository } from '../../../Domain/FriendRequest/FriendRequestRepository'; 3 | import { FriendRequest } from '../../../Domain/FriendRequest/FriendRequest'; 4 | import { injectable } from 'inversify'; 5 | import { FriendRequestNotFound } from '../../../Domain/FriendRequest/FriendRequestNotFound'; 6 | import { Pagination } from '../../../Domain/Core/Pagination'; 7 | import { TypeOrmRepository } from './TypeOrmRepository'; 8 | import { ObjectType } from 'typeorm/common/ObjectType'; 9 | 10 | @injectable() 11 | @EntityRepository() 12 | export class TypeOrmFriendRequestRepository extends TypeOrmRepository implements FriendRequestRepository { 13 | 14 | /** 15 | * @param {number} senderId 16 | * @param {Pagination} pagination 17 | * 18 | * @returns {Promise<[FriendRequest[] , number]>} 19 | */ 20 | public bySenderId(senderId: number, pagination: Pagination): Promise<[FriendRequest[], number]> { 21 | 22 | return this.createQueryBuilder() 23 | .andWhere('fr.sender = :senderId') 24 | .setParameters({senderId}) 25 | .orderBy('fr.id', 'DESC') 26 | .skip(pagination.offset()) 27 | .take(pagination.perPage()) 28 | .getManyAndCount(); 29 | } 30 | 31 | /** 32 | * @param {number} receiverId 33 | * @param {Pagination} pagination 34 | * 35 | * @returns {Promise<[FriendRequest[] , number]>} 36 | */ 37 | public byReceiverId(receiverId: number, pagination: Pagination): Promise<[FriendRequest[], number]> { 38 | 39 | return this.createQueryBuilder() 40 | .andWhere('fr.receiver = :receiverId') 41 | .setParameters({receiverId}) 42 | .orderBy('fr.id', 'DESC') 43 | .skip(pagination.offset()) 44 | .take(pagination.perPage()) 45 | .getManyAndCount(); 46 | } 47 | 48 | /** 49 | * @param {number} id 50 | * @returns {Promise} 51 | */ 52 | public byId(id: number): Promise { 53 | 54 | return this.createQueryBuilder() 55 | .andWhere('fr.id = :id') 56 | .setParameters({id}) 57 | .getOne() 58 | .then((friendRequest: FriendRequest) => { 59 | if (!friendRequest) throw FriendRequestNotFound.fromId(id); 60 | return friendRequest; 61 | }); 62 | } 63 | 64 | /** 65 | * @param {number} senderId 66 | * @param {number} receiverId 67 | * 68 | * @return {Promise} 69 | */ 70 | public find(senderId: number, receiverId: number): Promise { 71 | 72 | return this.createQueryBuilder() 73 | .andWhere('fr.sender = :senderId') 74 | .setParameters({senderId}) 75 | .andWhere('fr.receiver = :receiverId') 76 | .setParameters({receiverId}) 77 | .getOne(); 78 | } 79 | 80 | /** 81 | * @param {FriendRequest} friendRequest 82 | * @returns {Promise} 83 | */ 84 | public store(friendRequest: FriendRequest): Promise { 85 | 86 | return this.entityManager.save(friendRequest); 87 | } 88 | 89 | /** 90 | * @param {ObjectType} entityClass 91 | * @param {string} alias 92 | * 93 | * @returns {SelectQueryBuilder} 94 | */ 95 | protected createQueryBuilder(entityClass: ObjectType = FriendRequest, alias: string = 'fr'): SelectQueryBuilder { 96 | 97 | return this.entityManager.createQueryBuilder(entityClass, alias) 98 | .select(alias) 99 | .where(alias + '.deletedAt IS NULL') 100 | .leftJoinAndSelect('fr.sender', 'us') 101 | .leftJoinAndSelect('fr.receiver', 'ur'); 102 | } 103 | } -------------------------------------------------------------------------------- /src/Infrastructure/Domain/TypeOrm/TypeOrmImageRepository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, SelectQueryBuilder } from 'typeorm'; 2 | import { ImageRepository } from '../../../Domain/Image/ImageRepository'; 3 | import { Image } from '../../../Domain/Image/Image'; 4 | import { injectable } from 'inversify'; 5 | import { TypeOrmRepository } from './TypeOrmRepository'; 6 | import { ObjectType } from 'typeorm/common/ObjectType'; 7 | import { ImageNotFound } from '../../../Domain/Image/ImageNotFound'; 8 | 9 | @injectable() 10 | @EntityRepository() 11 | export class TypeOrmImageRepository extends TypeOrmRepository implements ImageRepository { 12 | 13 | /** 14 | * @returns {Promise} 15 | */ 16 | public all(): Promise { 17 | 18 | return this.createQueryBuilder() 19 | .getMany(); 20 | } 21 | 22 | /** 23 | * @param {number} id 24 | * @returns {Promise} 25 | */ 26 | public byId(id: number): Promise { 27 | 28 | return this.createQueryBuilder() 29 | .andWhere('i.id = :id') 30 | .setParameters({id}) 31 | .getOne() 32 | .then((image: Image) => { 33 | if (!image) throw ImageNotFound.fromId(id); 34 | return image; 35 | }); 36 | } 37 | 38 | /** 39 | * @param {number} userId 40 | * @returns {Promise} 41 | */ 42 | public byUserId(userId: number): Promise { 43 | 44 | return this.createQueryBuilder() 45 | .andWhere('i.user = :userId') 46 | .setParameters({userId}) 47 | .getMany(); 48 | } 49 | 50 | /** 51 | * @param {Image} image 52 | * @returns {Promise} 53 | */ 54 | public store(image: Image): Promise { 55 | 56 | return this.entityManager.save(image); 57 | } 58 | 59 | protected createQueryBuilder(entityClass: ObjectType = Image, alias: string = 'i'): SelectQueryBuilder { 60 | 61 | return this.entityManager.createQueryBuilder(entityClass, alias) 62 | .select(alias) 63 | .where(alias + '.deletedAt IS NULL'); 64 | } 65 | } -------------------------------------------------------------------------------- /src/Infrastructure/Domain/TypeOrm/TypeOrmRepository.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager, getManager, SelectQueryBuilder } from 'typeorm'; 2 | import { ObjectType } from 'typeorm/common/ObjectType'; 3 | import { injectable } from 'inversify'; 4 | 5 | @injectable() 6 | export abstract class TypeOrmRepository { 7 | 8 | public entityManager: EntityManager; 9 | 10 | constructor() { 11 | this.entityManager = getManager(); 12 | } 13 | 14 | /** 15 | * @param {ObjectType} entityClass 16 | * @param {string} alias 17 | * 18 | * @returns {SelectQueryBuilder} 19 | */ 20 | protected abstract createQueryBuilder(entityClass: ObjectType, alias: string): SelectQueryBuilder; 21 | } -------------------------------------------------------------------------------- /src/Infrastructure/Domain/TypeOrm/TypeOrmUserRepository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, SelectQueryBuilder } from 'typeorm'; 2 | import { UserRepository } from '../../../Domain/User/UserRepository'; 3 | import { User } from '../../../Domain/User/User'; 4 | import { injectable } from 'inversify'; 5 | import { UserNotFound } from '../../../Domain/User/UserNotFound'; 6 | import { Pagination } from '../../../Domain/Core/Pagination'; 7 | import { TypeOrmRepository } from './TypeOrmRepository'; 8 | import { ObjectType } from 'typeorm/common/ObjectType'; 9 | 10 | @injectable() 11 | @EntityRepository() 12 | export class TypeOrmUserRepository extends TypeOrmRepository implements UserRepository { 13 | 14 | /** 15 | * @param {Pagination} pagination 16 | * @returns {Promise<[User[] , number]>} 17 | */ 18 | public all(pagination: Pagination): Promise<[User[], number]> { 19 | 20 | return this.createQueryBuilder() 21 | .leftJoinAndSelect('u.images', 'i') 22 | .orderBy('u.id', 'DESC') 23 | .skip(pagination.offset()) 24 | .take(pagination.perPage()) 25 | .getManyAndCount(); 26 | } 27 | 28 | /** 29 | * @param {number} id 30 | * @returns {Promise} 31 | */ 32 | public byId(id: number): Promise { 33 | 34 | return this.createQueryBuilder() 35 | .leftJoinAndSelect('u.friends', 'uf') 36 | .andWhere('u.id = :id') 37 | .setParameters({id}) 38 | .getOne() 39 | .then((user: User) => { 40 | if (!user) throw UserNotFound.fromId(id); 41 | return user; 42 | }); 43 | } 44 | 45 | /** 46 | * @param {string} email 47 | * @returns {Promise} 48 | */ 49 | public byEmail(email: string): Promise { 50 | 51 | return this.createQueryBuilder() 52 | .andWhere('u.email = :email') 53 | .setParameters({email}) 54 | .getOne() 55 | .then((user: User) => { 56 | if (!user) throw UserNotFound.fromEmail(email); 57 | return user; 58 | }); 59 | } 60 | 61 | /** 62 | * @param {User} user 63 | * @returns {Promise} 64 | */ 65 | public store(user: User): Promise { 66 | 67 | return this.entityManager.save(user); 68 | } 69 | 70 | /** 71 | * @param {ObjectType} entityClass 72 | * @param {string} alias 73 | * 74 | * @returns {SelectQueryBuilder} 75 | */ 76 | protected createQueryBuilder(entityClass: ObjectType = User, alias: string = 'u'): SelectQueryBuilder { 77 | 78 | return this.entityManager.createQueryBuilder(entityClass, alias) 79 | .select(alias) 80 | .where(alias + '.deletedAt IS NULL'); 81 | } 82 | } -------------------------------------------------------------------------------- /src/Infrastructure/Services/AuthService.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'inversify'; 2 | import { UserRepository } from '../../Domain/User/UserRepository'; 3 | import * as bcrypt from 'bcrypt'; 4 | import * as jwt from 'jsonwebtoken'; 5 | import { User } from '../../Domain/User/User'; 6 | import { UserNotFound } from "../../Domain/User/UserNotFound"; 7 | import { SignInDTO } from "../DTO/Auth/SignInDTO"; 8 | import { SignUpDTO } from "../DTO/Auth/SignUpDTO"; 9 | import { IAuthService } from '../../Domain/Core/IAuthService'; 10 | 11 | @injectable() 12 | export class AuthService implements IAuthService{ 13 | 14 | constructor(@inject('UserRepository') private userRepository: UserRepository) { 15 | this.userRepository = userRepository; 16 | } 17 | 18 | /** 19 | * @param {SignInDTO} DTO 20 | * @returns {Promise} 21 | */ 22 | public signIn(DTO: SignInDTO): Promise { 23 | 24 | return this.userRepository.byEmail(DTO.email).then((user: User) => { 25 | 26 | if (user && bcrypt.compareSync(DTO.password, user.password)) { 27 | return {token: jwt.sign({id: user.id}, process.env.JWT_SECRET)}; 28 | } 29 | 30 | throw UserNotFound.authorized(); 31 | }); 32 | } 33 | 34 | /** 35 | * @param {SignUpDTO} DTO 36 | * @returns {Promise} 37 | */ 38 | public signUp(DTO: SignUpDTO) { 39 | 40 | let user = User.register( 41 | DTO.email, 42 | bcrypt.hashSync(DTO.password, bcrypt.genSaltSync(10)), 43 | DTO.firstName, 44 | DTO.lastName 45 | ); 46 | 47 | return this.userRepository.store(user); 48 | } 49 | } -------------------------------------------------------------------------------- /src/Infrastructure/Services/FriendRequestService.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'inversify'; 2 | import { FriendRequest } from '../../Domain/FriendRequest/FriendRequest'; 3 | import { FriendRequestRepository } from '../../Domain/FriendRequest/FriendRequestRepository'; 4 | import { IFriendRequestService } from '../../Domain/FriendRequest/IFriendRequestService'; 5 | import { Pagination } from '../../Domain/Core/Pagination'; 6 | import { FriendRequestDTO } from '../DTO/FriendRequest/FriendRequestDTO'; 7 | import { User } from '../../Domain/User/User'; 8 | import { UserRepository } from '../../Domain/User/UserRepository'; 9 | import { UserNotFound } from '../../Domain/User/UserNotFound'; 10 | import { FriendRequestNotFound } from '../../Domain/FriendRequest/FriendRequestNotFound'; 11 | 12 | @injectable() 13 | export class FriendRequestService implements IFriendRequestService { 14 | 15 | constructor(@inject('FriendRequestRepository') private friendRequestRepository: FriendRequestRepository, 16 | @inject('UserRepository') private userRepository: UserRepository) 17 | { 18 | this.friendRequestRepository = friendRequestRepository; 19 | this.userRepository = userRepository; 20 | } 21 | 22 | /** 23 | * @param {number} userId 24 | * @param {Pagination} pagination 25 | * 26 | * @returns {Promise} 27 | */ 28 | public bySenderId(userId: number, pagination: Pagination): Promise<[FriendRequest[], number]> { 29 | 30 | return this.friendRequestRepository.bySenderId(userId, pagination); 31 | } 32 | 33 | /** 34 | * @param {number} userId 35 | * @param {Pagination} pagination 36 | * 37 | * @returns {Promise} 38 | */ 39 | public byReceiverId(userId: number, pagination: Pagination): Promise<[FriendRequest[], number]> { 40 | 41 | return this.friendRequestRepository.byReceiverId(userId, pagination); 42 | } 43 | 44 | /** 45 | * @param {number} id 46 | * @returns {Promise} 47 | */ 48 | public byId(id: number): Promise { 49 | 50 | return this.friendRequestRepository.byId(id); 51 | } 52 | 53 | /** 54 | * @param {User} sender 55 | * @param {FriendRequestDTO} DTO 56 | * 57 | * @returns {Promise} 58 | */ 59 | store(sender: User, DTO: FriendRequestDTO): Promise { 60 | 61 | return this.friendRequestRepository.find(sender.id, DTO.userId) 62 | .then((friendRequest: FriendRequest) => { 63 | 64 | if (friendRequest) { 65 | throw FriendRequestNotFound.waitingAcceptFromReceiver(); 66 | } 67 | 68 | return this.userRepository.byId(DTO.userId) 69 | .then((receiver: User) => { 70 | 71 | if (!receiver) { 72 | throw UserNotFound.fromId(DTO.userId); 73 | } 74 | 75 | if (receiver.isFriend(sender)) { 76 | throw FriendRequestNotFound.friendExist(); 77 | } 78 | 79 | return this.friendRequestRepository.store( 80 | FriendRequest.register(sender, receiver) 81 | ); 82 | }); 83 | }); 84 | } 85 | 86 | /** 87 | * @param {User} user 88 | * @param {number} id 89 | * 90 | * @return {Promise} 91 | */ 92 | accept(user: User, id: number): Promise { 93 | 94 | return this.friendRequestRepository.byId(id) 95 | .then((friendRequest: FriendRequest) => { 96 | 97 | if (!friendRequest) { 98 | throw FriendRequestNotFound.fromId(id); 99 | } 100 | 101 | if (user.id !== friendRequest.receiver.id) { 102 | throw FriendRequestNotFound.forbidden(); 103 | } 104 | 105 | friendRequest.sender.addFriend(friendRequest.receiver); 106 | friendRequest.receiver.addFriend(friendRequest.sender); 107 | friendRequest.remove(); 108 | 109 | this.friendRequestRepository.store(friendRequest); 110 | }); 111 | } 112 | 113 | /** 114 | * @param {number} id 115 | * @returns {Promise} 116 | */ 117 | public remove(id: number): Promise { 118 | 119 | return this.friendRequestRepository.byId(id) 120 | .then((friendRequest: FriendRequest) => { 121 | friendRequest.remove(); 122 | this.friendRequestRepository.store(friendRequest); 123 | }); 124 | } 125 | } -------------------------------------------------------------------------------- /src/Infrastructure/Services/ImageService.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'inversify'; 2 | import { IImageService } from '../../Domain/Image/IImageService'; 3 | import { ImageRepository } from '../../Domain/Image/ImageRepository'; 4 | import { Image } from '../../Domain/Image/Image'; 5 | import { User } from '../../Domain/User/User'; 6 | import { ImageDTO } from '../DTO/Image/ImageDTO'; 7 | 8 | @injectable() 9 | export class ImageService implements IImageService { 10 | 11 | constructor(@inject('ImageRepository') private imageRepository: ImageRepository) { 12 | this.imageRepository = imageRepository; 13 | } 14 | 15 | /** 16 | * @returns {Promise} 17 | */ 18 | public all(): Promise { 19 | 20 | return this.imageRepository.all(); 21 | } 22 | 23 | /** 24 | * @param {ImageDTO} DTO 25 | * @param {User} user 26 | * 27 | * @returns {Promise} 28 | */ 29 | public store(DTO: ImageDTO, user: User): Promise { 30 | 31 | let image = Image.register(user, DTO.name, DTO.path); 32 | return this.imageRepository.store(image); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Infrastructure/Services/MultipartUploadService.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { IUploadService } from '../../Domain/Core/IUploadService'; 3 | import { IRequest } from '../../Utils/Request/custom'; 4 | 5 | @injectable() 6 | export class MultipartUploadService implements IUploadService{ 7 | 8 | /** 9 | * @param {IRequest} request 10 | * @returns {Object} 11 | */ 12 | fromRequest(request: IRequest): Object { 13 | 14 | const imagePath = 'uploads/' + request.user.id + '/' + request.file.filename; 15 | 16 | return { 17 | message: 'Image has been uploaded successfully.', 18 | imagePath: imagePath, 19 | imageFullPath: request.protocol + '://' + request.get('host') + '/' + imagePath 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Infrastructure/Services/UserService.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'inversify'; 2 | import { User } from '../../Domain/User/User'; 3 | import { UserRepository } from '../../Domain/User/UserRepository'; 4 | import { IUserService } from '../../Domain/User/IUserService'; 5 | import { Pagination } from '../../Domain/Core/Pagination'; 6 | import { ProfileDTO } from '../DTO/Profile/ProfileDTO'; 7 | import { UserNotFound } from '../../Domain/User/UserNotFound'; 8 | 9 | @injectable() 10 | export class UserService implements IUserService { 11 | 12 | constructor(@inject('UserRepository') private userRepository: UserRepository) { 13 | this.userRepository = userRepository; 14 | } 15 | 16 | /** 17 | * @param {Pagination} pagination 18 | * @returns {Promise} 19 | */ 20 | public all(pagination: Pagination): Promise<[User[], number]> { 21 | 22 | return this.userRepository.all(pagination); 23 | } 24 | 25 | /** 26 | * @param {number} id 27 | * @returns {Promise} 28 | */ 29 | public byId(id: number): Promise { 30 | 31 | return this.userRepository.byId(id); 32 | } 33 | 34 | /** 35 | * @param {number} id 36 | * @param {ProfileDTO} DTO 37 | * @returns {Promise} 38 | */ 39 | update(id: number, DTO: ProfileDTO): Promise { 40 | 41 | return this.userRepository.byId(id) 42 | .then((user: User) => { 43 | 44 | if (!user) throw UserNotFound.fromId(id); 45 | 46 | user.email = DTO.email; 47 | user.firstName = DTO.firstName; 48 | user.lastName = DTO.lastName; 49 | 50 | return this.userRepository.store(user); 51 | }); 52 | } 53 | 54 | /** 55 | * @param {number} id 56 | * @returns {Promise} 57 | */ 58 | public remove(id: number): Promise { 59 | 60 | return this.userRepository.byId(id) 61 | .then((user: User) => { 62 | user.remove(); 63 | this.userRepository.store(user); 64 | }); 65 | } 66 | } -------------------------------------------------------------------------------- /src/Infrastructure/Validators/Auth/SignInValidator.ts: -------------------------------------------------------------------------------- 1 | import * as joi from 'joi'; 2 | 3 | module.exports = { 4 | body: { 5 | email: joi.string().max(255).email().required(), 6 | password: joi.string().min(6).max(30).regex(/[a-zA-Z0-9]{6,30}/).required() 7 | } 8 | }; -------------------------------------------------------------------------------- /src/Infrastructure/Validators/Auth/SignUpValidator.ts: -------------------------------------------------------------------------------- 1 | import * as joi from 'joi'; 2 | 3 | module.exports = { 4 | body: { 5 | email: joi.string().max(255).email().required(), 6 | password: joi.string().min(6).max(30).regex(/[a-zA-Z0-9]{6,30}/).required(), 7 | firstName: joi.string().max(255).required(), 8 | lastName: joi.string().max(255).required(), 9 | } 10 | }; -------------------------------------------------------------------------------- /src/Infrastructure/Validators/FriendRequest/FriendRequestValidator.ts: -------------------------------------------------------------------------------- 1 | import * as joi from 'joi'; 2 | 3 | module.exports = { 4 | body: { 5 | userId: joi.number().integer().required(), 6 | } 7 | }; -------------------------------------------------------------------------------- /src/Infrastructure/Validators/Image/ImageValidator.ts: -------------------------------------------------------------------------------- 1 | import * as joi from 'joi'; 2 | 3 | module.exports = { 4 | body: { 5 | name: joi.string().max(255).required(), 6 | path: joi.string().max(255).regex(/([a-z\-_0-9\/\:\.]*\.(jpg|jpeg|png|gif)$)/).required() 7 | } 8 | }; -------------------------------------------------------------------------------- /src/Infrastructure/Validators/Profile/ProfileValidator.ts: -------------------------------------------------------------------------------- 1 | import * as joi from 'joi'; 2 | 3 | module.exports = { 4 | body: { 5 | email: joi.string().max(255).email().required(), 6 | firstName: joi.string().max(255).required(), 7 | lastName: joi.string().max(255).required(), 8 | } 9 | }; -------------------------------------------------------------------------------- /src/Tests/Acceptance/AuthTest.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | 3 | import chai = require('chai'); 4 | import chaiHttp = require('chai-http'); 5 | import { environment, rollbackMigrations } from '../TestCase'; 6 | 7 | let should = chai.should(); 8 | 9 | chai.use(chaiHttp); 10 | 11 | describe('Auth', () => { 12 | 13 | before((done) => { 14 | rollbackMigrations(done); 15 | }); 16 | 17 | describe('/POST /auth/sign-up', () => { 18 | 19 | it('should register new user', (done) => { 20 | 21 | chai.request(environment.baseUrl + environment.apiVersion) 22 | .post('/auth/sign-up') 23 | .type('form') 24 | .send({ 25 | 'email': 'test@test.com', 26 | 'password': 'testpass', 27 | 'firstName': 'Ivan', 28 | 'lastName': 'Ivanov' 29 | }) 30 | .end((err, res) => { 31 | res.should.have.status(201); 32 | 33 | res.body.should.be.a('object'); 34 | res.body.should.have.property('id'); 35 | res.body.should.have.property('email').eql('test@test.com'); 36 | res.body.should.have.property('firstName').eql('Ivan'); 37 | res.body.should.have.property('lastName').eql('Ivanov'); 38 | res.body.should.have.property('isActive').eql(false); 39 | 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should return validation error while registration', (done) => { 45 | 46 | chai.request(environment.baseUrl + environment.apiVersion) 47 | .post('/auth/sign-up') 48 | .type('form') 49 | .send({ 50 | 'email': 'test', 51 | 'password': 'testpass', 52 | 'firstName': 'Ivan', 53 | 'lastName': 'Ivanov' 54 | }) 55 | .end((err, res) => { 56 | res.should.have.status(422); 57 | 58 | res.body.should.be.a('object'); 59 | res.body.should.have.property('errorMessage').eql('Validation error.'); 60 | res.body.should.have.property('error'); 61 | 62 | done(); 63 | }); 64 | }); 65 | }); 66 | 67 | describe('/POST /auth/sign-in', () => { 68 | 69 | it('should login user and return jwt token', (done) => { 70 | 71 | chai.request(environment.baseUrl + environment.apiVersion) 72 | .post('/auth/sign-in') 73 | .type('form') 74 | .send({ 75 | 'email': 'alex.clare@test.com', 76 | 'password': 'testpass' 77 | }) 78 | .end((err, res) => { 79 | res.should.have.status(200); 80 | 81 | res.body.should.be.a('object'); 82 | res.body.should.have.property('token'); 83 | 84 | done(); 85 | }); 86 | }); 87 | 88 | it('should return validation error while login', (done) => { 89 | 90 | chai.request(environment.baseUrl + environment.apiVersion) 91 | .post('/auth/sign-up') 92 | .type('form') 93 | .send({ 94 | 'email': 'alex.clare@test.com' 95 | }) 96 | .end((err, res) => { 97 | res.should.have.status(422); 98 | 99 | res.body.should.be.a('object'); 100 | res.body.should.have.property('errorMessage').eql('Validation error.'); 101 | res.body.should.have.property('error'); 102 | 103 | done(); 104 | }); 105 | }); 106 | }); 107 | }); -------------------------------------------------------------------------------- /src/Tests/Acceptance/HomeTest.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | 3 | import chai = require('chai'); 4 | import chaiHttp = require('chai-http'); 5 | import { environment } from '../TestCase'; 6 | 7 | chai.use(chaiHttp); 8 | 9 | describe('Home', () => { 10 | 11 | describe('/GET /', () => { 12 | 13 | it('should return message', (done) => { 14 | 15 | chai.request(environment.baseUrl + environment.apiVersion) 16 | .get('') 17 | .end((err, res) => { 18 | chai.assert.equal(res.status, 200); 19 | chai.assert.deepEqual(res.body, {message: 'Home page.'}); 20 | done(); 21 | }); 22 | }); 23 | }); 24 | }); -------------------------------------------------------------------------------- /src/Tests/Acceptance/UserTest.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | 3 | import chai = require('chai'); 4 | import chaiHttp = require('chai-http'); 5 | import { environment, rollbackMigrations } from '../TestCase'; 6 | 7 | let should = chai.should(); 8 | 9 | chai.use(chaiHttp); 10 | 11 | describe('User', () => { 12 | 13 | before((done) => { 14 | rollbackMigrations(done); 15 | }); 16 | 17 | describe('/Get /users', () => { 18 | 19 | it('try to get users without x-access-token', (done) => { 20 | 21 | chai.request(environment.baseUrl + environment.apiVersion) 22 | .get('/users') 23 | .end((err, res) => { 24 | res.should.have.status(401); 25 | res.body.should.have.property('errorMessage').eql('not authorized'); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('try to get users with wrong x-access-token', (done) => { 31 | 32 | chai.request(environment.baseUrl + environment.apiVersion) 33 | .get('/users') 34 | .set('x-access-token', 'dawda2dasf12rsdf.dawda2dasf12rsdf.dawda2dasf12rsdf') 35 | .end((err, res) => { 36 | res.should.have.status(401); 37 | res.body.should.have.property('errorMessage').eql('not authorized'); 38 | done(); 39 | }); 40 | }); 41 | 42 | it('should return all users', (done) => { 43 | 44 | chai.request(environment.baseUrl + environment.apiVersion) 45 | .post('/auth/sign-in').type('form').send({ 46 | 'email': environment.email, 47 | 'password': environment.password 48 | }).then((res) => { 49 | chai.request(environment.baseUrl + environment.apiVersion) 50 | .get('/users') 51 | .set('x-access-token', res.body.token) 52 | .end((err, res) => { 53 | res.should.have.status(200); 54 | res.should.have.header('x-items-count', '2'); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | 60 | it('should return user by ID', (done) => { 61 | 62 | chai.request(environment.baseUrl + environment.apiVersion) 63 | .post('/auth/sign-in').type('form').send({ 64 | 'email': environment.email, 65 | 'password': environment.password 66 | }).then((res) => { 67 | chai.request(environment.baseUrl + environment.apiVersion) 68 | .get('/users/1') 69 | .set('x-access-token', res.body.token) 70 | .end((err, res) => { 71 | res.should.have.status(200); 72 | res.body.should.have.property('id'); 73 | res.body.should.have.property('email').eql('alex.clare@test.com'); 74 | res.body.should.have.property('firstName').eql('Alex'); 75 | res.body.should.have.property('lastName').eql('Clare'); 76 | res.body.should.have.property('fullName').eql('Alex Clare'); 77 | res.body.should.have.property('isActive').eql(false); 78 | done(); 79 | }); 80 | }); 81 | }); 82 | 83 | it('try to get user with wrong ID', (done) => { 84 | 85 | chai.request(environment.baseUrl + environment.apiVersion) 86 | .post('/auth/sign-in').type('form').send({ 87 | 'email': environment.email, 88 | 'password': environment.password 89 | }).then((res) => { 90 | chai.request(environment.baseUrl + environment.apiVersion) 91 | .get('/users/228') 92 | .set('x-access-token', res.body.token) 93 | .end((err, res) => { 94 | res.should.have.status(404); 95 | res.body.should.have.property('errorMessage').eql('User with ID #228 not found.'); 96 | done(); 97 | }); 98 | }); 99 | }); 100 | }); 101 | }); -------------------------------------------------------------------------------- /src/Tests/TestCase.ts: -------------------------------------------------------------------------------- 1 | import { createConnection, MigrationInterface } from 'typeorm'; 2 | import { User } from '../Domain/User/User'; 3 | import { UserFactory } from '../database/fixtures/UserFactory'; 4 | import { Connection } from 'typeorm/connection/Connection'; 5 | import { createConnectionOptions } from '../config/database'; 6 | 7 | export const environment = { 8 | baseUrl: 'http://localhost:3123', 9 | apiVersion: '/api/v1', 10 | email: 'alex.clare@test.com', 11 | password: 'testpass' 12 | }; 13 | 14 | const getConnection: Promise = createConnection(createConnectionOptions()); 15 | 16 | // Down and Up migrations with fixtures 17 | export function rollbackMigrations(done) { 18 | 19 | getConnection.then(connection => { 20 | connection.migrations.forEach((migration: MigrationInterface) => { 21 | migration.down(connection.createQueryRunner()).then(() => { 22 | migration.up(connection.createQueryRunner()).then(() => { 23 | connection.manager.save(UserFactory.fakeUsers()).then((users: User[]) => { 24 | done(); 25 | }); 26 | }); 27 | }); 28 | }); 29 | }); 30 | } -------------------------------------------------------------------------------- /src/Utils/Request/custom.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../../Domain/User/User'; 2 | import * as express from 'express'; 3 | 4 | export interface IRequest extends express.Request { 5 | user?: User 6 | file?: any 7 | } -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { createConnection } from 'typeorm'; 3 | import container from './config/inversify.config'; 4 | import { InversifyExpressServer } from 'inversify-express-utils'; 5 | import * as bodyParser from 'body-parser'; 6 | import * as helmet from 'helmet'; 7 | import * as express from 'express'; 8 | import * as validate from 'express-validation'; 9 | import * as path from "path"; 10 | import { jsonMiddleware, loggerMiddleware } from './Http/Middleware/CustomMiddleware'; 11 | import { AccessDeniedError } from './Domain/Core/AccessDeniedError'; 12 | import { createConnectionOptions } from './config/database'; 13 | import { EntityNotFound } from './Domain/Core/EntityNotFound'; 14 | 15 | createConnection(createConnectionOptions()).then(async connection => { 16 | 17 | const port: number = parseInt(process.env.PORT); 18 | 19 | let server = new InversifyExpressServer(container, null, {rootPath: '/api/v1'}); 20 | 21 | server.setConfig((app) => { 22 | app.use(bodyParser.urlencoded({extended: true})); 23 | app.use(bodyParser.json()); 24 | app.use('/uploads', express.static(path.resolve('./public/uploads'))); 25 | app.use(jsonMiddleware, loggerMiddleware); 26 | app.use(helmet()); 27 | }); 28 | 29 | server.setErrorConfig((app) => { 30 | 31 | app.use((err, req, res, next) => { 32 | 33 | if (err instanceof validate.ValidationError) { 34 | err.status = 422; 35 | res.status(err.status).json(Object.assign({errorMessage: 'Validation error.'}, {error: err})); 36 | return; 37 | } 38 | 39 | if (err instanceof EntityNotFound) { 40 | res.status(404).send({errorMessage: err.message}); 41 | return; 42 | } 43 | 44 | if (err instanceof AccessDeniedError) { 45 | res.status(403).send({errorMessage: err.message}); 46 | return; 47 | } 48 | 49 | if (err instanceof Error) { 50 | console.error(err.stack); 51 | res.status(400).send({errorMessage: err.message}); 52 | return; 53 | } 54 | 55 | res.status(500).send(err.stack); 56 | 57 | // TODO: uncomment when production 58 | // res.status(500).send('Something broke!'); 59 | }); 60 | }); 61 | 62 | server.build().listen(port, '0.0.0.0', function () { 63 | console.log('listening on http://localhost:' + port); 64 | }); 65 | 66 | }).catch(error => console.log('TypeORM connection error: ', error)); 67 | 68 | process.on('unhandledRejection', error => { 69 | console.log('unhandledRejection', error.message); 70 | }); 71 | -------------------------------------------------------------------------------- /src/config/database.ts: -------------------------------------------------------------------------------- 1 | import { FriendRequest } from '../Domain/FriendRequest/FriendRequest'; 2 | import { Image } from '../Domain/Image/Image'; 3 | import { User } from '../Domain/User/User'; 4 | import { InitMigration1522414949149 } from '../database/migrations/1522414949149-InitMigration'; 5 | import { ConnectionOptions } from 'typeorm'; 6 | import * as dotenv from 'dotenv'; 7 | 8 | dotenv.load(); 9 | 10 | export function createConnectionOptions(): ConnectionOptions { 11 | 12 | return { 13 | type: 'postgres', 14 | host: process.env.DB_HOST, 15 | port: parseInt(process.env.DB_PORT), 16 | username: process.env.DB_USERNAME, 17 | password: process.env.DB_PASSWORD, 18 | database: process.env.DB_DATABASE, 19 | entities: [ 20 | User, Image, FriendRequest 21 | ], 22 | synchronize: true, 23 | logging: false, 24 | migrations: [ 25 | InitMigration1522414949149 26 | ], 27 | }; 28 | } -------------------------------------------------------------------------------- /src/config/inversify.config.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | 3 | import { Container } from 'inversify'; 4 | 5 | import TYPES from './types'; 6 | import { UserController } from '../Http/Controllers/UserController'; 7 | import { UserService } from '../Infrastructure/Services/UserService'; 8 | import { TypeOrmUserRepository } from '../Infrastructure/Domain/TypeOrm/TypeOrmUserRepository'; 9 | import { UserRepository } from '../Domain/User/UserRepository'; 10 | import { AuthService } from '../Infrastructure/Services/AuthService'; 11 | import { ImageRepository } from '../Domain/Image/ImageRepository'; 12 | import { TypeOrmImageRepository } from '../Infrastructure/Domain/TypeOrm/TypeOrmImageRepository'; 13 | import { HomeController } from '../Http/Controllers/HomeController'; 14 | import { SignInController } from '../Http/Controllers/Auth/SignInController'; 15 | import { SignUpController } from '../Http/Controllers/Auth/SignUpController'; 16 | import { IUserService } from '../Domain/User/IUserService'; 17 | import { IAuthService } from '../Domain/Core/IAuthService'; 18 | import { UploadController } from '../Http/Controllers/UploadController'; 19 | import { MultipartUploadService } from '../Infrastructure/Services/MultipartUploadService'; 20 | import { IUploadService } from '../Domain/Core/IUploadService'; 21 | import { ProfileController } from '../Http/Controllers/ProfileController'; 22 | import { ImageController } from '../Http/Controllers/ImageController'; 23 | import { IImageService } from '../Domain/Image/IImageService'; 24 | import { ImageService } from '../Infrastructure/Services/ImageService'; 25 | import { FriendRequestRepository } from '../Domain/FriendRequest/FriendRequestRepository'; 26 | import { TypeOrmFriendRequestRepository } from '../Infrastructure/Domain/TypeOrm/TypeOrmFriendRequestRepository'; 27 | import { IFriendRequestService } from '../Domain/FriendRequest/IFriendRequestService'; 28 | import { FriendRequestService } from '../Infrastructure/Services/FriendRequestService'; 29 | import { FriendRequestController } from '../Http/Controllers/FriendRequestController'; 30 | 31 | let container: Container = new Container(); 32 | 33 | container.bind('UserRepository').to(TypeOrmUserRepository); 34 | container.bind('ImageRepository').to(TypeOrmImageRepository); 35 | container.bind('FriendRequestRepository').to(TypeOrmFriendRequestRepository); 36 | 37 | container.bind('IUserService').to(UserService); 38 | container.bind('IAuthService').to(AuthService); 39 | container.bind('IUploadService').to(MultipartUploadService); 40 | container.bind('IImageService').to(ImageService); 41 | container.bind('IFriendRequestService').to(FriendRequestService); 42 | 43 | container.bind(TYPES.Controller).to(HomeController); 44 | container.bind(TYPES.Controller).to(UserController); 45 | container.bind(TYPES.Controller).to(FriendRequestController); 46 | container.bind(TYPES.Controller).to(ProfileController); 47 | container.bind(TYPES.Controller).to(SignInController); 48 | container.bind(TYPES.Controller).to(SignUpController); 49 | container.bind(TYPES.Controller).to(UploadController); 50 | container.bind(TYPES.Controller).to(ImageController); 51 | 52 | export default container; -------------------------------------------------------------------------------- /src/config/types.ts: -------------------------------------------------------------------------------- 1 | const TYPES = { 2 | Controller: Symbol('Controller'), 3 | Repository: Symbol('Repository'), 4 | Service: Symbol('Service') 5 | }; 6 | 7 | export default TYPES; 8 | -------------------------------------------------------------------------------- /src/database/fixtures/UserFactory.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../../Domain/User/User'; 2 | import * as bcrypt from 'bcrypt'; 3 | 4 | export class UserFactory { 5 | 6 | /** 7 | * @return {User[]} 8 | */ 9 | public static fakeUsers(): User[] { 10 | 11 | let users = []; 12 | 13 | let user1 = User.register( 14 | 'alex.clare@test.com', 15 | bcrypt.hashSync('testpass', bcrypt.genSaltSync(10)), 16 | 'Alex', 17 | 'Clare' 18 | ); 19 | 20 | let user2 = User.register( 21 | 'jack.green@test.com', 22 | bcrypt.hashSync('testpass', bcrypt.genSaltSync(10)), 23 | 'Jack', 24 | 'Green' 25 | ); 26 | 27 | users.push(user1); 28 | users.push(user2); 29 | 30 | return users; 31 | } 32 | } -------------------------------------------------------------------------------- /src/database/migrations/1522414949149-InitMigration.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class InitMigration1522414949149 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.query(`CREATE TABLE "images" ("id" SERIAL NOT NULL, "name" character varying(255) NOT NULL, "path" character varying(255) NOT NULL, "created_at" TIMESTAMP NOT NULL, "user_id" integer, PRIMARY KEY("id"))`); 7 | await queryRunner.query(`CREATE TABLE "users" ("id" SERIAL NOT NULL, "email" character varying(255) NOT NULL, "password" character varying(60) NOT NULL, "first_name" character varying(255) NOT NULL, "last_name" character varying(255) NOT NULL, "is_active" boolean NOT NULL, "created_at" TIMESTAMP NOT NULL, "deleted_at" TIMESTAMP, PRIMARY KEY("id"))`); 8 | await queryRunner.query(`CREATE TABLE "friend_requests" ("id" SERIAL NOT NULL, "created_at" TIMESTAMP NOT NULL, "deleted_at" TIMESTAMP, "sender_id" integer, "receiver_id" integer, PRIMARY KEY("id"))`); 9 | await queryRunner.query(`CREATE TABLE "users_friends" ("user_id" integer NOT NULL, "friend_id" integer NOT NULL, PRIMARY KEY("user_id", "friend_id"))`); 10 | await queryRunner.query(`CREATE UNIQUE INDEX "users_email_deleted_sequence" ON "users"("email","deleted_at")`); 11 | } 12 | 13 | public async down(queryRunner: QueryRunner): Promise { 14 | await queryRunner.query(`DROP TABLE "users_friends"`); 15 | await queryRunner.query(`DROP TABLE "friend_requests"`); 16 | await queryRunner.query(`DROP TABLE "images"`); 17 | await queryRunner.query(`DROP TABLE "users"`); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "sourceMap": true, 5 | "outDir": "bin", 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "listFiles": false, 9 | "diagnostics": false, 10 | "module": "commonjs", 11 | "noImplicitAny": false, 12 | "moduleResolution": "node" 13 | }, 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "compileOnSave": true, 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | false 5 | ], 6 | "ban": false, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space", 11 | "check-lowercase" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": true, 16 | "indent": [ 17 | true, 18 | "spaces" 19 | ], 20 | "interface-name": true, 21 | "jsdoc-format": true, 22 | "label-position": true, 23 | "label-undefined": true, 24 | "max-line-length": [ 25 | true, 26 | 140 27 | ], 28 | "member-access": true, 29 | "member-ordering": [ 30 | true, 31 | "public-before-private", 32 | "static-before-instance", 33 | "variables-before-functions" 34 | ], 35 | "no-arg": true, 36 | "no-bitwise": true, 37 | "no-conditional-assignment": true, 38 | "no-consecutive-blank-lines": false, 39 | "no-console": [ 40 | true, 41 | "debug", 42 | "info", 43 | "time", 44 | "timeEnd", 45 | "trace" 46 | ], 47 | "no-construct": true, 48 | "no-debugger": true, 49 | "no-duplicate-key": true, 50 | "no-duplicate-variable": true, 51 | "no-empty": true, 52 | "no-eval": true, 53 | "no-inferrable-types": false, 54 | "no-internal-module": true, 55 | "no-shadowed-variable": true, 56 | "no-string-literal": true, 57 | "no-switch-case-fall-through": true, 58 | "no-trailing-whitespace": true, 59 | "no-unreachable": true, 60 | "no-unused-expression": true, 61 | "no-unused-variable": true, 62 | "no-use-before-declare": true, 63 | "no-var-keyword": true, 64 | "no-var-requires": true, 65 | "object-literal-sort-keys": true, 66 | "one-line": [ 67 | true, 68 | "check-open-brace", 69 | "check-catch", 70 | "check-else", 71 | "check-whitespace" 72 | ], 73 | "quotemark": [ 74 | true, 75 | "single", 76 | "avoid-escape" 77 | ], 78 | "radix": true, 79 | "semicolon": true, 80 | "trailing-comma": [ 81 | false 82 | ], 83 | "triple-equals": [ 84 | true, 85 | "allow-null-check" 86 | ], 87 | "typedef-whitespace": [ 88 | true, 89 | { 90 | "call-signature": "nospace", 91 | "index-signature": "nospace", 92 | "parameter": "nospace", 93 | "property-declaration": "nospace", 94 | "variable-declaration": "nospace" 95 | } 96 | ], 97 | "use-strict": [ 98 | true, 99 | "check-module", 100 | "check-function" 101 | ], 102 | "variable-name": [ 103 | true, 104 | "check-format", 105 | "allow-leading-underscore", 106 | "ban-keywords" 107 | ], 108 | "whitespace": [ 109 | true, 110 | "check-branch", 111 | "check-decl", 112 | "check-operator", 113 | "check-separator", 114 | "check-type" 115 | ] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-ioc", 3 | "version": false, 4 | "dependencies": {}, 5 | "globalDependencies": { 6 | "express": "registry:dt/express#4.0.0+20160317120654", 7 | "express-serve-static-core": "registry:dt/express-serve-static-core#0.0.0+20160322035842", 8 | "mime": "registry:dt/mime#0.0.0+20160316155526", 9 | "node": "registry:dt/node#6.0.0+20160514165920", 10 | "serve-static": "registry:dt/serve-static#0.0.0+20160501131543" 11 | }, 12 | "globalDevDependencies": { 13 | "chai": "registry:dt/chai#3.4.0+20160317120654", 14 | "mocha": "registry:dt/mocha#2.2.5+20160317120654", 15 | "sinon": "registry:dt/sinon#1.16.0+20160517064723", 16 | "superagent": "registry:dt/superagent#1.4.0+20160317120654", 17 | "supertest": "registry:dt/supertest#1.1.0+20160317120654" 18 | } 19 | } --------------------------------------------------------------------------------