├── .prettierrc ├── tsconfig.build.json ├── src ├── reports │ ├── dto │ │ ├── approve-report.dto.ts │ │ ├── create-report.dto.ts │ │ ├── report.dto.ts │ │ └── get-estimate.dto.ts │ ├── reports.module.ts │ ├── reports.entity.ts │ ├── reports.controller.ts │ └── reports.service.ts ├── app.service.ts ├── users │ ├── dto │ │ ├── create-user.dto.ts │ │ ├── update-user.dto.ts │ │ └── user.dto.ts │ ├── users.entity.ts │ ├── decorators │ │ └── current-user.decorator.ts │ ├── users.module.ts │ ├── middlewares │ │ └── current-user.middleware.ts │ ├── auth.service.ts │ ├── users.service.ts │ └── users.controller.ts ├── app.controller.ts ├── guards │ ├── admin.guard.ts │ └── auth.guard.ts ├── main.ts ├── interceptors │ └── serialize.interceptor.ts └── app.module.ts ├── nest-cli.json ├── test ├── jest-e2e.json └── app.e2e-spec.ts ├── .gitignore ├── tsconfig.json ├── .eslintrc.js ├── package.json └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/reports/dto/approve-report.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean } from 'class-validator'; 2 | 3 | export class ApproveReportDto { 4 | @IsBoolean() 5 | approved: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/users/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString } from 'class-validator'; 2 | 3 | export class CreateUserDto { 4 | @IsEmail() 5 | email: string; 6 | 7 | @IsString() 8 | password: string; 9 | } 10 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/users/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsOptional, IsString } from 'class-validator'; 2 | 3 | export class UpdateUserDto { 4 | @IsEmail() 5 | @IsOptional() 6 | email: string; 7 | 8 | @IsString() 9 | @IsOptional() 10 | password: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/users/dto/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { Expose } from 'class-transformer'; 2 | 3 | export class UserDto { 4 | // specify this property will be exposed to responses 5 | @Expose() 6 | id: number; 7 | 8 | // sepcify this peropty will be exposed to responses 9 | @Expose() 10 | email: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/guards/admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext } from "@nestjs/common"; 2 | import { Observable } from "rxjs"; 3 | 4 | export class AdminGuard implements CanActivate { 5 | canActivate(context: ExecutionContext) { 6 | const request = context.switchToHttp().getRequest() 7 | if (!request.currentUser) { 8 | return false; 9 | } 10 | 11 | return request.currentUser.admin; 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/reports/reports.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ReportsController } from './reports.controller'; 3 | import { ReportsService } from './reports.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Report } from './reports.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Report])], 9 | controllers: [ReportsController], 10 | providers: [ReportsService], 11 | }) 12 | export class ReportsModule {} 13 | -------------------------------------------------------------------------------- /src/users/users.entity.ts: -------------------------------------------------------------------------------- 1 | import { Report } from 'src/reports/reports.entity'; 2 | import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; 3 | 4 | @Entity() 5 | export class User { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column() 10 | email: string; 11 | 12 | @Column() 13 | password: string; 14 | 15 | @Column({ default: true }) 16 | admin:boolean; 17 | 18 | @OneToMany(() => Report, (report) => report.user) 19 | reports: Report[] 20 | } 21 | -------------------------------------------------------------------------------- /src/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext } from "@nestjs/common"; 2 | import { Observable } from "rxjs"; 3 | 4 | // this AuthGuard to preventing access with authentication guards 5 | // in case if no value to return like null or defined it'll be considered falsy and will prevent the access to the rest 6 | 7 | export class AuthGuard implements CanActivate { 8 | canActivate(context: ExecutionContext) { 9 | const request = context.switchToHttp().getRequest(); 10 | 11 | return request.session.userId; 12 | } 13 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | const cookieSession = require('cookie-session'); 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | app.use( 9 | cookieSession({ 10 | keys: ["cookie secret"] 11 | }) 12 | ); 13 | app.useGlobalPipes( 14 | new ValidationPipe({ 15 | whitelist: true, 16 | }), 17 | ); 18 | await app.listen(3000); 19 | } 20 | bootstrap(); 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | db.sqlite 38 | 39 | .env.* -------------------------------------------------------------------------------- /src/users/decorators/current-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from "@nestjs/common"; 2 | 3 | export const CurrentUser = createParamDecorator( 4 | // data of type never to specify it won't receive anthing 5 | // context of type ExuctionContenct to be suitable for any type of connection http or websocket ... etc 6 | (data: never, context: ExecutionContext) => { 7 | const request = context.switchToHttp().getRequest(); // convert the context of incoming request to http to can extract the session id 8 | return request.currentUser; 9 | } 10 | ) -------------------------------------------------------------------------------- /src/reports/dto/create-report.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsLatitude, IsLongitude, IsNumber, IsString, Max, Min } from "class-validator"; 2 | 3 | export class CreateReportDto { 4 | @IsString() 5 | make: string; 6 | 7 | @IsString() 8 | model: string; 9 | 10 | @IsNumber() 11 | @Min(1930) 12 | @Max(2050) 13 | year: number; 14 | 15 | @IsNumber() 16 | @Min(0) 17 | @Max(1_000_000) 18 | mileage: number; 19 | 20 | @IsLongitude() 21 | lng: number; 22 | 23 | @IsLatitude() 24 | lat: number; 25 | 26 | @IsNumber() 27 | @Min(0) 28 | @Max(1000000) 29 | price: number; 30 | } -------------------------------------------------------------------------------- /src/reports/dto/report.dto.ts: -------------------------------------------------------------------------------- 1 | import { Expose, Transform } from "class-transformer"; 2 | 3 | export class ReportDto { 4 | @Expose() // expose specify this property will be exposed 5 | id: number; 6 | @Expose() 7 | price: number; 8 | @Expose() 9 | year: number; 10 | @Expose() 11 | lng: number; 12 | @Expose() 13 | lat: number; 14 | @Expose() 15 | make: string; 16 | @Expose() 17 | model: string; 18 | @Expose() 19 | mileage: string 20 | @Transform(({ obj }) =>obj.user.id) //transform is decorator to trnasofrm plain object to a new shape 21 | @Expose() 22 | userId: number; 23 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/reports/reports.entity.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'src/users/users.entity'; 2 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 3 | 4 | @Entity() 5 | export class Report { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ default: false }) 10 | approved: boolean; 11 | 12 | @Column() 13 | price: number; 14 | 15 | @Column() 16 | make: string; 17 | 18 | @Column() 19 | model: string; 20 | 21 | @Column() 22 | year: number; 23 | 24 | @Column() 25 | lng: number; 26 | 27 | @Column() 28 | lat: number; 29 | 30 | @Column() 31 | mileage: number; 32 | 33 | @ManyToOne(() => User, (user) => user.reports) 34 | user: User; 35 | } 36 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareConsumer, Module } from '@nestjs/common'; 2 | import { UsersController } from './users.controller'; 3 | import { UsersService } from './users.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { User } from './users.entity'; 6 | import { AuthService } from './auth.service'; 7 | import { APP_INTERCEPTOR } from '@nestjs/core'; 8 | import { CurrentUserMiddleWare } from './middlewares/current-user.middleware'; 9 | 10 | @Module({ 11 | imports: [TypeOrmModule.forFeature([User])], 12 | controllers: [UsersController], 13 | providers: [UsersService, AuthService], 14 | }) 15 | export class UsersModule { 16 | configure(consumer: MiddlewareConsumer) { 17 | consumer.apply(CurrentUserMiddleWare).forRoutes('*'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/reports/dto/get-estimate.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from "class-transformer"; 2 | import { IsLatitude, IsLongitude, IsNumber, IsString, Max, Min } from "class-validator"; 3 | 4 | 5 | export class GetEstimateDto { 6 | @IsString() 7 | make: string; 8 | 9 | @IsString() 10 | model: string; 11 | 12 | @Transform(({ value }) => parseInt(value)) 13 | @IsNumber() 14 | @Min(1930) 15 | @Max(2050) 16 | year: number; 17 | 18 | @Transform(({ value }) => parseInt(value)) 19 | @IsNumber() 20 | @Min(0) 21 | @Max(1_000_000) 22 | mileage: number; 23 | 24 | @Transform(({ value }) => parseInt(value)) 25 | @IsLongitude() 26 | lng: number; 27 | 28 | @Transform(({ value }) => parseInt(value)) 29 | @IsLatitude() 30 | lat: number; 31 | } -------------------------------------------------------------------------------- /src/users/middlewares/current-user.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware } from "@nestjs/common"; 2 | import { UsersService } from "../users.service"; 3 | import { NextFunction, Request, Response } from "express"; 4 | import { User } from "../users.entity"; 5 | 6 | declare global { 7 | namespace Express { 8 | interface Request { 9 | currentUser?: User; 10 | } 11 | } 12 | } 13 | 14 | @Injectable() 15 | export class CurrentUserMiddleWare implements NestMiddleware { 16 | constructor(private userService: UsersService) { } 17 | 18 | async use(req: Request, res: Response, next: NextFunction) { 19 | const { userId } = req.session || {} 20 | 21 | if (userId) { 22 | const user = await this.userService.findOne(userId) 23 | req.currentUser = user; 24 | } 25 | 26 | next() 27 | } 28 | } -------------------------------------------------------------------------------- /src/interceptors/serialize.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, NestInterceptor, UseInterceptors } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { plainToInstance } from 'class-transformer'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | interface ClassConstructor { 7 | // sepcify it will be an class 8 | new (...args: any[]): {}; 9 | } 10 | 11 | export function Serialize(dto: ClassConstructor) { // implement a decorator for serialize to simplify the Interceptors call 12 | return UseInterceptors(new SerializeInterceptor(dto)); 13 | } 14 | 15 | export class SerializeInterceptor implements NestInterceptor { 16 | constructor(private dto: any) {} 17 | 18 | intercept( 19 | context: ExecutionContext, 20 | next: CallHandler, 21 | ): Observable { 22 | // Run code for incoming request before handler excuted 23 | 24 | return next.handle().pipe( 25 | map((data: any) => { 26 | // Run code for responses after sending the handler 27 | return plainToInstance(this.dto, data, { 28 | excludeExtraneousValues: true, 29 | }); 30 | }), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/users/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; 2 | import { UsersService } from "./users.service"; 3 | import * as bcrypt from "bcrypt" 4 | import { User } from "./users.entity"; 5 | @Injectable() 6 | export class AuthService { 7 | constructor(private userService: UsersService) { } 8 | 9 | async signup(email: string, password: string) { 10 | const users = await this.userService.find(email) 11 | 12 | if (users.length) { 13 | throw new BadRequestException(`this email:${email} is already exists`) 14 | } 15 | 16 | const salt = await bcrypt.genSalt(); 17 | password = await bcrypt.hash(password, salt); 18 | 19 | const user = await this.userService.create(email, password); 20 | 21 | return user; 22 | } 23 | 24 | async signin(email: string, password: string) { 25 | const [user] = await this.userService.find(email) 26 | 27 | if (!user) { 28 | throw new NotFoundException("user not found"); 29 | } 30 | 31 | const verifiedUser = await bcrypt.compare(password, user.password) 32 | if (!verifiedUser) { 33 | throw new BadRequestException('incorrect password') 34 | } 35 | 36 | return user; 37 | } 38 | } -------------------------------------------------------------------------------- /src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { User } from './users.entity'; 5 | 6 | @Injectable() 7 | export class UsersService { 8 | constructor(@InjectRepository(User) private userRepo: Repository) { } 9 | 10 | create(email: string, password: string) { 11 | const user = this.userRepo.create({ email, password }); 12 | 13 | return this.userRepo.save(user); 14 | } 15 | 16 | findOne(id: number) { 17 | if (!id) { 18 | return null; 19 | } 20 | return this.userRepo.findOne({ where: { id } }); 21 | } 22 | 23 | find(email: string) { 24 | return this.userRepo.find({ where: { email } }); 25 | } 26 | 27 | async update(id: number, attrs: Partial) { 28 | const user = await this.findOne(id); 29 | if (!user) { 30 | throw new NotFoundException('user not found'); 31 | } 32 | 33 | Object.assign(user, attrs); 34 | return this.userRepo.save(user); 35 | } 36 | 37 | async remove(id: number) { 38 | const user = await this.findOne(id); 39 | if (!user) { 40 | throw new NotFoundException('user not found'); 41 | } 42 | 43 | return this.userRepo.remove(user); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { ConfigModule, ConfigService } from "@nestjs/config" 6 | import { ReportsModule } from './reports/reports.module'; 7 | import { UsersModule } from './users/users.module'; 8 | import { Report } from './reports/reports.entity'; 9 | import { User } from './users/users.entity'; 10 | 11 | @Module({ 12 | imports: [ 13 | ConfigModule.forRoot({ 14 | isGlobal: true, 15 | envFilePath: `.env.${process.env.NODE_ENV}` 16 | }), 17 | TypeOrmModule.forRootAsync({ 18 | inject: [ConfigService], 19 | useFactory: (config: ConfigService) => { 20 | return { 21 | type: 'sqlite', 22 | database: config.get('DB_NAME'), 23 | synchronize: true, 24 | entities: [Report, User], 25 | } 26 | } 27 | }), 28 | // TypeOrmModule.forRoot({ 29 | // type: 'sqlite', 30 | // database: 'db.sqlite', 31 | // entities: [Report, User], 32 | // synchronize: true, 33 | // }), 34 | ReportsModule, 35 | UsersModule, 36 | ], 37 | controllers: [AppController], 38 | providers: [AppService], 39 | }) 40 | export class AppModule { } 41 | -------------------------------------------------------------------------------- /src/reports/reports.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; 2 | import { CreateReportDto } from './dto/create-report.dto'; 3 | import { CurrentUser } from 'src/users/decorators/current-user.decorator'; 4 | import { User } from 'src/users/users.entity'; 5 | import { ReportsService } from './reports.service'; 6 | import { AuthGuard } from 'src/guards/auth.guard'; 7 | import { Serialize } from 'src/interceptors/serialize.interceptor'; 8 | import { ReportDto } from './dto/report.dto'; 9 | import { ApproveReportDto } from './dto/approve-report.dto'; 10 | import { AdminGuard } from 'src/guards/admin.guard'; 11 | import { GetEstimateDto } from './dto/get-estimate.dto'; 12 | 13 | @Controller('reports') 14 | export class ReportsController { 15 | constructor(private reportService: ReportsService) { } 16 | @Get() 17 | getEstimate(@Query() query: GetEstimateDto) { 18 | return this.reportService.createEstimate(query); 19 | } 20 | 21 | @Post() 22 | @UseGuards(AuthGuard) 23 | @Serialize(ReportDto) 24 | createReport(@Body() body: CreateReportDto, @CurrentUser() user: User) { 25 | return this.reportService.create(body, user) 26 | } 27 | 28 | @Patch("/:id") 29 | @UseGuards(AdminGuard) 30 | approveReport(@Param("id") id: string, @Body() body: ApproveReportDto) { 31 | return this.reportService.changeApproval(id, body.approved) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | NotFoundException, 7 | Param, 8 | Patch, 9 | Post, 10 | Query, 11 | Session, 12 | UseGuards, 13 | } from '@nestjs/common'; 14 | import { CreateUserDto } from './dto/create-user.dto'; 15 | import { UsersService } from './users.service'; 16 | import { UpdateUserDto } from './dto/update-user.dto'; 17 | import { Serialize } from 'src/interceptors/serialize.interceptor'; 18 | import { UserDto } from './dto/user.dto'; 19 | import { AuthService } from './auth.service'; 20 | import { CurrentUser } from './decorators/current-user.decorator'; 21 | import { User } from './users.entity'; 22 | import { AuthGuard } from 'src/guards/auth.guard'; 23 | 24 | @Serialize(UserDto) 25 | @Controller('users') 26 | export class UsersController { 27 | constructor(private userService: UsersService, private authService: AuthService) { } 28 | 29 | @Post('/signup') 30 | async createUser(@Body() body: CreateUserDto, @Session() session: any) { 31 | const user = await this.authService.signup(body.email, body.password); 32 | session.userId = user.id; 33 | 34 | return user; 35 | } 36 | 37 | @Post('/signin') 38 | async signin(@Body() body: CreateUserDto, @Session() session: any) { 39 | const user = await this.authService.signin(body.email, body.password); 40 | session.userId = user.id; 41 | 42 | return user; 43 | } 44 | 45 | @Post('/signout') 46 | signOut(@Session() session: any) { 47 | session.userId = null; 48 | } 49 | 50 | @Get('/whoami') 51 | @UseGuards(AuthGuard) 52 | async whoAmI(@CurrentUser() user: User) { 53 | return user; 54 | } 55 | 56 | @Get('/:id') 57 | async findUser(@Param('id') id: string) { 58 | const user = await this.userService.findOne(parseInt(id)); 59 | if (!user) { 60 | throw new NotFoundException('user not found'); 61 | } 62 | 63 | return user; 64 | } 65 | 66 | @Get() 67 | async findAllUsers(@Query('email') email: string) { 68 | return this.userService.find(email); 69 | } 70 | 71 | @Delete('/:id') 72 | removeUser(@Param('id') id: string) { 73 | return this.userService.remove(parseInt(id)); 74 | } 75 | 76 | @Patch('/:id') 77 | updateUser(@Param('id') id: string, @Body() body: UpdateUserDto) { 78 | return this.userService.update(parseInt(id), body); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/reports/reports.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Report } from './reports.entity'; 4 | import { Repository } from 'typeorm'; 5 | import { CreateReportDto } from './dto/create-report.dto'; 6 | import { User } from 'src/users/users.entity'; 7 | import { GetEstimateDto } from './dto/get-estimate.dto'; 8 | 9 | @Injectable() 10 | export class ReportsService { 11 | constructor(@InjectRepository(Report) private reports: Repository) { } 12 | 13 | create(reportDto: CreateReportDto, user: User) { 14 | const report = this.reports.create(reportDto) 15 | report.user = user; 16 | return this.reports.save(report); 17 | } 18 | 19 | async changeApproval(id: string, approved: boolean) { 20 | const report = await this.reports.findOne({ where: { id: parseInt(id) } }); 21 | 22 | if (!report) { 23 | throw new NotFoundException("report not found") 24 | } 25 | 26 | report.approved = approved 27 | return this.reports.save(report); 28 | } 29 | 30 | createEstimate({ make, model, lng, lat, year, mileage }: GetEstimateDto) { 31 | return this.reports 32 | .createQueryBuilder() // Create a query builder for the reports table. 33 | .select("AVG(price)", 'price') // Select the average price of cars as the price column. 34 | .where("make = :make", { make }) // Where the make of the car is equal to the make parameter. 35 | .andWhere("model = :model", { model }) // And the model of the car is equal to the model parameter. 36 | .andWhere("lng - :lng BETWEEN -5 AND 5", { lng }) // And the longitude of the car's location is between 5 degrees east and west of the lng parameter. 37 | .andWhere("lat - :lat BETWEEN -5 AND 5", { lat }) // And the latitude of the car's location is between 5 degrees north and south of the lat parameter. 38 | .andWhere("year - :year BETWEEN -3 AND 3", { year }) // And the year of the car is between 3 years before and after the year parameter. 39 | .andWhere("approved IS TRUE") // And the car is approved for sale. 40 | .orderBy("ABS(mileage - :mileage)", 'DESC') // Order by the absolute difference between the car's mileage and the mileage parameter in descending order. 41 | .setParameters({ mileage }) // Set the parameters for the query. 42 | .limit(3) // Limit the results to the top 3. 43 | .getRawOne() // Get the first raw result from the query. 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "car", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "cross-env NODE_ENV=development nest start", 12 | "start:dev": "cross-env NODE_ENV=development nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^10.0.0", 24 | "@nestjs/config": "^3.1.1", 25 | "@nestjs/core": "^10.0.0", 26 | "@nestjs/platform-express": "^10.0.0", 27 | "@nestjs/typeorm": "^10.0.0", 28 | "@types/cookie-session": "^2.0.45", 29 | "bcrypt": "^5.1.1", 30 | "class-transformer": "^0.5.1", 31 | "class-validator": "^0.14.0", 32 | "cookie-session": "^2.0.0", 33 | "cross-env": "^7.0.3", 34 | "reflect-metadata": "^0.1.13", 35 | "rxjs": "^7.8.1", 36 | "sqlite3": "^5.1.6", 37 | "typeorm": "^0.3.17" 38 | }, 39 | "devDependencies": { 40 | "@nestjs/cli": "^10.0.0", 41 | "@nestjs/schematics": "^10.0.0", 42 | "@nestjs/testing": "^10.0.0", 43 | "@types/bcrypt": "^5.0.0", 44 | "@types/express": "^4.17.17", 45 | "@types/jest": "^29.5.2", 46 | "@types/node": "^20.3.1", 47 | "@types/supertest": "^2.0.12", 48 | "@typescript-eslint/eslint-plugin": "^6.0.0", 49 | "@typescript-eslint/parser": "^6.0.0", 50 | "eslint": "^8.42.0", 51 | "eslint-config-prettier": "^9.0.0", 52 | "eslint-plugin-prettier": "^5.0.0", 53 | "jest": "^29.5.0", 54 | "prettier": "^3.0.0", 55 | "source-map-support": "^0.5.21", 56 | "supertest": "^6.3.3", 57 | "ts-jest": "^29.1.0", 58 | "ts-loader": "^9.4.3", 59 | "ts-node": "^10.9.1", 60 | "tsconfig-paths": "^4.2.0", 61 | "typescript": "^5.1.3" 62 | }, 63 | "jest": { 64 | "moduleFileExtensions": [ 65 | "js", 66 | "json", 67 | "ts" 68 | ], 69 | "rootDir": "src", 70 | "testRegex": ".*\\.spec\\.ts$", 71 | "transform": { 72 | "^.+\\.(t|j)s$": "ts-jest" 73 | }, 74 | "collectCoverageFrom": [ 75 | "**/*.(t|j)s" 76 | ], 77 | "coverageDirectory": "../coverage", 78 | "testEnvironment": "node" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Nest Logo 5 |

6 | 7 |

Car

8 | 9 | ### 📑 Table of Contents 10 | - [📘 Introduction](#introduction) 11 | - [🚀 Getting Started](#getting-started) 12 | - [Prerequisites ❗](#prerequisites) 13 | - [Environment Variables :key:](#environment-variables) 14 | - [Setup ⬇️](#setup) 15 | - [Install :heavy_check_mark: ](#install) 16 | - [Usage 🤿 🏃‍♂️](#usage) 17 | - [🔍🏗️ API Reference](#api-reference) 18 | - [❓ FAQ ](#-faq-) 19 | - [👥 Authors](#authors) 20 | - [🤝 Contributing](#contributing) 21 | - [⭐️ Show Your Support](#show-your-support) 22 | - [💎 Lessons Learned](#lessons-learned) 23 | - [🙏 Acknowledgements](#acknowledgements) 24 | 25 | ## 📘 Introduction 26 |

27 | Welcome to Car, a Nest.js project that aims to provide accurate pricing information for used cars. This application leverages the power of Nest.js, a progressive Node.js framework, to build efficient and scalable server-side applications. 28 |

29 | 30 |

31 | The main goal of this project is to help users determine the fair market value of used cars by considering various factors such as make, model, mileage, condition, and location. By using Admin approval for the reports and estimate report querie, the application provides reliable pricing estimates that can assist car buyers, sellers, and enthusiasts in making informed decisions. 32 |

33 | 34 |

(back to top)

35 | 36 | ## 🚀 Getting Started 37 | 38 | To get a local copy up and running, follow these steps. 39 | 40 | ### Prerequisites ❗ 41 | 42 | Before running this project, make sure you have the following prerequisites installed: 43 | 44 |

45 | 46 | 47 | 48 | 49 |

50 | 51 | ### Environment Variables :key: 52 | To run this project, you will need to add the following environment variables to a new file named `.env.development`: 53 | - `DB_NAME`: The name of the database (eg: db.sqlite). 54 | 55 | ### Setup ⬇️ 56 | 1. Clone the repository: 57 | ```shell 58 | git clone https://github.com/ahmedeid6842/car.git 59 | ``` 60 | 2. Change to the project directory: 61 | ```shell 62 | cd ./car 63 | ``` 64 | 65 | ### Install :heavy_check_mark: 66 | 67 | Install the project dependencies using NPM: 68 | 69 | ```shell 70 | npm install 71 | ``` 72 | 73 | ### Usage 🤿 🏃‍♂️ 74 | To start the application in development mode, run the following command: 75 | 76 | ```shell 77 | npm run start:dev 78 | ``` 79 | 80 | The application will be accessible at http://localhost:3000. 81 | 82 | - Alright, it's showtime! 🔥 Hit `http://localhost:3000` and BOOM! 💥 You should see the "Hello world" message and the Car APIs working flawlessly. ✨🧙‍♂️ 83 | 84 |

(back to top)

85 | 86 | ## 🔍🏗️ API Refernce 87 | 88 | > The API reference provides detailed documentation on the available endpoints, request/response formats, and authentication requirements. 89 | 90 | ![10-10 - routes drawio (1)](https://github.com/ahmedeid6842/Car/assets/57197702/c4b976b2-fd38-4bf4-ba51-440d7188bc80) 91 | 92 |

(back to top)

93 | 94 | ## ❓ FAQ 95 | 96 | ### How Nest.js enhances this application 97 | 98 | Nest.js brings several benefits to this project, including: 99 | 100 | - **Modularity**: The application is structured using modules, allowing for better organization and maintainability of code. 101 | - **Dependency Injection**: Nest.js utilizes dependency injection, making it easy to manage and test different components of the application separately. 102 | - **Decorators**: Decorators in Nest.js simplify the implementation of features such as validation, authorization, and logging. 103 | - **Middleware**: Nest.js middleware enables handling of cross-cutting concerns, such as authentication and error handling, in a centralized manner. 104 | - **Scalability**: Nest.js is designed to support scalability, allowing the application to handle high traffic and large datasets efficiently. 105 | 106 |

(back to top)

107 | 108 | ## 👤 Author 109 | **Ahmed Eid 🙋‍♂️** 110 | - Github: [@ahmedeid6842](https://github.com/ahmedeid6842/) 111 | - LinkedIn : [Ahmed Eid](https://www.linkedin.com/in/ahmed-eid-0018571b1/) 112 | - Twitter: [@ahmedeid2684](https://twitter.com/ahmedeid2684) 113 | 114 |

(back to top)

115 | 116 | ## 🤝 Contributing 117 | 118 | We're always looking to improve this project! 🔍 If you notice any issues or have ideas for new features, please don't hesitate to submit a [pull request](https://github.com/ahmedeid6842/car/pulls) 🙌 or create a [new issue](https://github.com/ahmedeid6842/car/issues/new) 💡. Your contribution will help make this project even better! ❤️ 💪 119 | 120 | ## ⭐️ Show your support 121 | 122 | If you find this project helpful, I would greatly appreciate it if you could leave a star! 🌟 💟 123 | 124 | ## 💎 Lessons Learned 125 | 126 | 1. **Nest Architecture: Services and Repositories** 127 | - Learn about the Nest.js architecture and how to organize code using services and repositories. 128 | - Understand the benefits of modular structure and separation of concerns. 129 | 130 | 2. **Inversion of Control in NestJS** 131 | - Explore the concept of inversion of control and how Nest.js follows this principle. 132 | - Understand the importance of dependency injection and how it helps in managing and testing components. 133 | 134 | 3. **Persisting Data with TypeORM** 135 | - Learn how to persist data in the car project using TypeORM, an Object-Relational Mapping (ORM) library for TypeScript and JavaScript. 136 | - Understand how to define entities and relationships, perform CRUD operations, and handle database migrations. 137 | 138 | 4. **Interceptors and Middlewares** 139 | - Intercept incoming requests and customize the serialization of user data using interceptors. 140 | - Transform outgoing responses and add common functionality to requests using middlewares. 141 | 142 | 5. **Authentication From Scratch** 143 | - Implement authentication from scratch in the car project using Nest.js and the cookie-session package. 144 | - Understand the concepts of sessions, cookies, and secure authentication strategies. 145 | 146 | 6. **Setting up Database Relations with TypeORM** 147 | - Learn how to define and manage database relations using TypeORM. 148 | - Understand different types of relationships such as one-to-one, one-to-many, and many-to-many, and how to establish them in your application. 149 | 150 |

(back to top)

151 | 152 | ## 🙏 Acknowledgments 153 | 154 | Special thanks to @StephenGrider for his invaluable course on Nest.js, which greatly contributed to the successful completion of the car project. 💟 155 | 156 | 157 | --------------------------------------------------------------------------------