Compare commits
	
		
			2 Commits
		
	
	
		
			1ae6b6634c
			...
			138ff1df65
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 138ff1df65 | |||
| 86427bb41b | 
							
								
								
									
										11
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -17,6 +17,7 @@ | ||||
|         "@nestjs/swagger": "^8.1.0", | ||||
|         "@prisma/client": "^6.0.1", | ||||
|         "bcrypt": "^5.1.1", | ||||
|         "class-transformer": "^0.5.1", | ||||
|         "class-validator": "^0.14.1", | ||||
|         "passport": "^0.7.0", | ||||
|         "passport-jwt": "^4.0.1", | ||||
| @@ -3674,12 +3675,10 @@ | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/class-transformer": { | ||||
|       "version": "0.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.4.0.tgz", | ||||
|       "integrity": "sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA==", | ||||
|       "license": "MIT", | ||||
|       "optional": true, | ||||
|       "peer": true | ||||
|       "version": "0.5.1", | ||||
|       "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", | ||||
|       "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/class-validator": { | ||||
|       "version": "0.14.1", | ||||
|   | ||||
| @@ -28,6 +28,7 @@ | ||||
|     "@nestjs/swagger": "^8.1.0", | ||||
|     "@prisma/client": "^6.0.1", | ||||
|     "bcrypt": "^5.1.1", | ||||
|     "class-transformer": "^0.5.1", | ||||
|     "class-validator": "^0.14.1", | ||||
|     "passport": "^0.7.0", | ||||
|     "passport-jwt": "^4.0.1", | ||||
|   | ||||
| @@ -0,0 +1,14 @@ | ||||
| /* | ||||
|   Warnings: | ||||
|  | ||||
|   - Added the required column `accuracy` to the `Geolocation` table without a default value. This is not possible if the table is not empty. | ||||
|   - Added the required column `altitude` to the `Geolocation` table without a default value. This is not possible if the table is not empty. | ||||
|   - Added the required column `altitudeAccuracy` to the `Geolocation` table without a default value. This is not possible if the table is not empty. | ||||
|   - Added the required column `speed` to the `Geolocation` table without a default value. This is not possible if the table is not empty. | ||||
|  | ||||
| */ | ||||
| -- AlterTable | ||||
| ALTER TABLE "Geolocation" ADD COLUMN     "accuracy" DOUBLE PRECISION NOT NULL, | ||||
| ADD COLUMN     "altitude" DOUBLE PRECISION NOT NULL, | ||||
| ADD COLUMN     "altitudeAccuracy" DOUBLE PRECISION NOT NULL, | ||||
| ADD COLUMN     "speed" DOUBLE PRECISION NOT NULL; | ||||
| @@ -20,12 +20,16 @@ model User { | ||||
| } | ||||
|  | ||||
| model Geolocation { | ||||
|   id        Int      @id @default(autoincrement()) | ||||
|   userId    Int | ||||
|   longitude Float | ||||
|   latitude  Float | ||||
|   timestamp DateTime | ||||
|   user      User     @relation(fields: [userId], references: [id]) | ||||
|   id               Int      @id @default(autoincrement()) | ||||
|   user             User     @relation(fields: [userId], references: [id]) | ||||
|   userId           Int | ||||
|   longitude        Float | ||||
|   latitude         Float | ||||
|   speed            Float | ||||
|   accuracy         Float | ||||
|   altitude         Float | ||||
|   altitudeAccuracy Float | ||||
|   timestamp        DateTime | ||||
| } | ||||
|  | ||||
| model Challenge { | ||||
| @@ -38,17 +42,18 @@ model Challenge { | ||||
|  | ||||
| model ChallengeAction { | ||||
|   id          Int          @id @default(autoincrement()) | ||||
|   user        User         @relation(fields: [userId], references: [id]) | ||||
|   userId      Int | ||||
|   challenge   Challenge    @relation(fields: [challengeId], references: [id]) | ||||
|   challengeId Int          @unique | ||||
|   active      Boolean      @default(false) | ||||
|   success     Boolean      @default(false) | ||||
|   challenge   Challenge    @relation(fields: [challengeId], references: [id]) | ||||
|   user        User         @relation(fields: [userId], references: [id]) | ||||
|   moneyUpdate MoneyUpdate? | ||||
| } | ||||
|  | ||||
| model TrainTrip { | ||||
|   id            String       @id | ||||
|   user          User         @relation(fields: [userId], references: [id]) | ||||
|   userId        Int | ||||
|   distance      Float | ||||
|   from          String | ||||
| @@ -58,20 +63,19 @@ model TrainTrip { | ||||
|   infoJson      Json | ||||
|   geometry      String | ||||
|   moneyUpdate   MoneyUpdate? | ||||
|   user          User         @relation(fields: [userId], references: [id]) | ||||
| } | ||||
|  | ||||
| model MoneyUpdate { | ||||
|   id       Int              @id @default(autoincrement()) | ||||
|   user     User             @relation(fields: [userId], references: [id]) | ||||
|   userId   Int | ||||
|   before   Int | ||||
|   after    Int | ||||
|   reason   MoneyUpdateType | ||||
|   actionId Int?             @unique | ||||
|   tripId   String?          @unique | ||||
|   action   ChallengeAction? @relation(fields: [actionId], references: [id]) | ||||
|   actionId Int?             @unique | ||||
|   trip     TrainTrip?       @relation(fields: [tripId], references: [id]) | ||||
|   user     User             @relation(fields: [userId], references: [id]) | ||||
|   tripId   String?          @unique | ||||
| } | ||||
|  | ||||
| enum MoneyUpdateType { | ||||
|   | ||||
| @@ -3,9 +3,10 @@ import { PrismaService } from './prisma/prisma.service' | ||||
| import { PrismaModule } from './prisma/prisma.module' | ||||
| import { UsersModule } from './users/users.module' | ||||
| import { AuthModule } from './auth/auth.module' | ||||
| import { GeolocationsModule } from './geolocations/geolocations.module' | ||||
|  | ||||
| @Module({ | ||||
|   imports: [PrismaModule, UsersModule, AuthModule], | ||||
|   imports: [PrismaModule, UsersModule, AuthModule, GeolocationsModule], | ||||
|   providers: [PrismaService], | ||||
| }) | ||||
| export class AppModule {} | ||||
|   | ||||
| @@ -1,5 +1,9 @@ | ||||
| import { Injectable } from '@nestjs/common' | ||||
| import { AuthGuard } from '@nestjs/passport' | ||||
| import { User } from '@prisma/client' | ||||
| import { Request } from 'express' | ||||
|  | ||||
| @Injectable() | ||||
| export class JwtAuthGuard extends AuthGuard('jwt') {} | ||||
|  | ||||
| export type AuthenticatedRequest = Request & { user: User } | ||||
|   | ||||
							
								
								
									
										42
									
								
								server/src/common/dto/pagination-output.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								server/src/common/dto/pagination-output.dto.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import { ApiProperty } from "@nestjs/swagger" | ||||
| import { IsNumber, IsOptional } from "class-validator" | ||||
|  | ||||
| export const DEFAULT_PAGE_NUMBER = 1 | ||||
| export const DEFAULT_PAGE_SIZE = 20 | ||||
|  | ||||
| export class MetaPaginateOutputDto { | ||||
|     @IsNumber() | ||||
|     @ApiProperty() | ||||
|     total: number | ||||
|    | ||||
|     @IsNumber() | ||||
|     @ApiProperty() | ||||
|     lastPage: number | ||||
|    | ||||
|     @IsNumber() | ||||
|     @ApiProperty({ default: DEFAULT_PAGE_NUMBER }) | ||||
|     currentPage: number = DEFAULT_PAGE_NUMBER | ||||
|    | ||||
|     @IsNumber() | ||||
|     @ApiProperty({ default: DEFAULT_PAGE_SIZE }) | ||||
|     totalPerPage: number = DEFAULT_PAGE_SIZE | ||||
|    | ||||
|     @IsOptional() | ||||
|     @IsNumber() | ||||
|     @ApiProperty({ required: false, nullable: true, default: null }) | ||||
|     prevPage?: number | null | ||||
|    | ||||
|     @IsOptional() | ||||
|     @IsNumber() | ||||
|     @ApiProperty({ required: false, nullable: true, default: null }) | ||||
|     nextPage?: number | null | ||||
|   } | ||||
|    | ||||
|   export class PaginateOutputDto<T> { | ||||
|     @ApiProperty({ isArray: true }) | ||||
|     data: T[] | ||||
|    | ||||
|     @ApiProperty() | ||||
|     meta: MetaPaginateOutputDto | ||||
|   } | ||||
|    | ||||
							
								
								
									
										17
									
								
								server/src/common/dto/pagination-query.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								server/src/common/dto/pagination-query.dto.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger' | ||||
| import { Type } from 'class-transformer' | ||||
| import { IsNumber, IsOptional } from 'class-validator' | ||||
|  | ||||
| export class QueryPaginationDto { | ||||
|   @IsOptional() | ||||
|   @IsNumber() | ||||
|   @Type(() => Number) | ||||
|   @ApiProperty({default: 1, required: false, description: "Numéro de page à charger"}) | ||||
|   page?: number = 1 | ||||
|  | ||||
|   @IsOptional() | ||||
|   @IsNumber() | ||||
|   @Type(() => Number) | ||||
|   @ApiProperty({default: 20, required: false, description: "Nombre d'éléments à charger par page"}) | ||||
|   size?: number = 20 | ||||
| } | ||||
							
								
								
									
										86
									
								
								server/src/common/utils/pagination.utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								server/src/common/utils/pagination.utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| import { applyDecorators, NotFoundException, Type } from '@nestjs/common' | ||||
| import { QueryPaginationDto } from '../dto/pagination-query.dto' | ||||
| import { ApiExtraModels, ApiOkResponse, ApiResponseNoStatusOptions, getSchemaPath } from '@nestjs/swagger' | ||||
| import { DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_SIZE, PaginateOutputDto } from '../dto/pagination-output.dto' | ||||
|  | ||||
| export interface PrismaPaginationParams { | ||||
|   skip: number | ||||
|   take: number | ||||
| } | ||||
|  | ||||
| export const paginate = ( | ||||
|   query: QueryPaginationDto, | ||||
| ): PrismaPaginationParams => { | ||||
|   const size = query.size || DEFAULT_PAGE_SIZE | ||||
|   const page = query.page || DEFAULT_PAGE_NUMBER | ||||
|   return { | ||||
|     skip: size * (page - 1), | ||||
|     take: size, | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const paginateOutput = <T>( | ||||
|   data: T[], | ||||
|   total: number, | ||||
|   query: QueryPaginationDto, | ||||
| ): PaginateOutputDto<T> => { | ||||
|   const page = query.page || DEFAULT_PAGE_NUMBER | ||||
|   const size = query.size || DEFAULT_PAGE_SIZE | ||||
|  | ||||
|   const lastPage = Math.ceil(total / size) | ||||
|  | ||||
|   // if data is empty, return empty array | ||||
|   if (!data.length) { | ||||
|     return { | ||||
|       data, | ||||
|       meta: { | ||||
|         total, | ||||
|         lastPage, | ||||
|         currentPage: page, | ||||
|         totalPerPage: size, | ||||
|         prevPage: null, | ||||
|         nextPage: null, | ||||
|       }, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // if page is greater than last page, throw an error | ||||
|   if (page > lastPage) { | ||||
|     throw new NotFoundException( | ||||
|       `Page ${page} not found. Last page is ${lastPage}`, | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     data, | ||||
|     meta: { | ||||
|       total, | ||||
|       lastPage, | ||||
|       currentPage: page, | ||||
|       totalPerPage: size, | ||||
|       prevPage: page > 1 ? page - 1 : null, | ||||
|       nextPage: page < lastPage ? page + 1 : null, | ||||
|     }, | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const ApiOkResponsePaginated = <DataDto extends Type<unknown>>(dataDto: DataDto, options?: ApiResponseNoStatusOptions) => | ||||
|   applyDecorators( | ||||
|     ApiExtraModels(PaginateOutputDto, dataDto), | ||||
|     ApiOkResponse({ | ||||
|       ...options, | ||||
|       schema: { | ||||
|         allOf: [ | ||||
|           { $ref: getSchemaPath(PaginateOutputDto) }, | ||||
|           { | ||||
|             properties: { | ||||
|               data: { | ||||
|                 type: 'array', | ||||
|                 items: { $ref: getSchemaPath(dataDto) }, | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     }), | ||||
|   ) | ||||
							
								
								
									
										24
									
								
								server/src/geolocations/dto/create-geolocation.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								server/src/geolocations/dto/create-geolocation.dto.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import { ApiProperty } from "@nestjs/swagger" | ||||
|  | ||||
| export class CreateGeolocationDto { | ||||
|     @ApiProperty({description: "Longitude en degrés"}) | ||||
|     longitude: number | ||||
|    | ||||
|     @ApiProperty({description: "Latitude en degrés"}) | ||||
|     latitude: number | ||||
|    | ||||
|     @ApiProperty({description: "Vitesse en mètres par seconde"}) | ||||
|     speed: number | ||||
|    | ||||
|     @ApiProperty({description: "Précision en mètres de la position obtenue"}) | ||||
|     accuracy: number | ||||
|    | ||||
|     @ApiProperty({description: "Altitude en mètres"}) | ||||
|     altitude: number | ||||
|    | ||||
|     @ApiProperty({description: "Précision de l'altitude en mètres"}) | ||||
|     altitudeAccuracy: number | ||||
|    | ||||
|     @ApiProperty({description: "Date et heure de capture de la géolocalisation"}) | ||||
|     timestamp: Date | ||||
| } | ||||
							
								
								
									
										35
									
								
								server/src/geolocations/entities/geolocation.entity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								server/src/geolocations/entities/geolocation.entity.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import { ApiProperty } from "@nestjs/swagger" | ||||
| import { Geolocation } from "@prisma/client" | ||||
|  | ||||
| export class GeolocationEntity implements Geolocation { | ||||
|   constructor(partial: Partial<GeolocationEntity>) { | ||||
|     Object.assign(this, partial) | ||||
|   } | ||||
|  | ||||
|   @ApiProperty({description: "Identifiant unique"}) | ||||
|   id: number | ||||
|  | ||||
|   @ApiProperty({description: "Utilisateur⋅rice ayant émis la géolocalisation"}) | ||||
|   userId: number | ||||
|  | ||||
|   @ApiProperty({description: "Longitude en degrés"}) | ||||
|   longitude: number | ||||
|  | ||||
|   @ApiProperty({description: "Latitude en degrés"}) | ||||
|   latitude: number | ||||
|  | ||||
|   @ApiProperty({description: "Vitesse en mètres par seconde"}) | ||||
|   speed: number | ||||
|  | ||||
|   @ApiProperty({description: "Précision en mètres de la position obtenue"}) | ||||
|   accuracy: number | ||||
|  | ||||
|   @ApiProperty({description: "Altitude en mètres"}) | ||||
|   altitude: number | ||||
|  | ||||
|   @ApiProperty({description: "Précision de l'altitude en mètres"}) | ||||
|   altitudeAccuracy: number | ||||
|  | ||||
|   @ApiProperty({description: "Date et heure de capture de la géolocalisation"}) | ||||
|   timestamp: Date | ||||
| } | ||||
							
								
								
									
										20
									
								
								server/src/geolocations/geolocations.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								server/src/geolocations/geolocations.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| import { Test, TestingModule } from '@nestjs/testing' | ||||
| import { GeolocationsController } from './geolocations.controller' | ||||
| import { GeolocationsService } from './geolocations.service' | ||||
|  | ||||
| describe('GeolocationsController', () => { | ||||
|   let controller: GeolocationsController | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     const module: TestingModule = await Test.createTestingModule({ | ||||
|       controllers: [GeolocationsController], | ||||
|       providers: [GeolocationsService], | ||||
|     }).compile() | ||||
|  | ||||
|     controller = module.get<GeolocationsController>(GeolocationsController) | ||||
|   }) | ||||
|  | ||||
|   it('should be defined', () => { | ||||
|     expect(controller).toBeDefined() | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										66
									
								
								server/src/geolocations/geolocations.controller.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								server/src/geolocations/geolocations.controller.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, UseGuards, HttpCode, Req, NotFoundException, Query } from '@nestjs/common' | ||||
| import { GeolocationsService } from './geolocations.service' | ||||
| import { CreateGeolocationDto } from './dto/create-geolocation.dto' | ||||
| import { AuthenticatedRequest, JwtAuthGuard } from 'src/auth/jwt-auth.guard' | ||||
| import { ApiBearerAuth, ApiCreatedResponse, ApiForbiddenResponse, ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger' | ||||
| import { GeolocationEntity } from './entities/geolocation.entity' | ||||
| import { ApiOkResponsePaginated, paginateOutput } from 'src/common/utils/pagination.utils' | ||||
| import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto' | ||||
| import { PaginateOutputDto } from 'src/common/dto/pagination-output.dto' | ||||
|  | ||||
| @Controller('geolocations') | ||||
| export class GeolocationsController { | ||||
|   constructor(private readonly geolocationsService: GeolocationsService) {} | ||||
|  | ||||
|   @Post() | ||||
|   @HttpCode(201) | ||||
|   @UseGuards(JwtAuthGuard) | ||||
|   @ApiBearerAuth() | ||||
|   @ApiCreatedResponse({ type: GeolocationEntity, description: "Objet créé avec succès" }) | ||||
|   @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) | ||||
|   @ApiForbiddenResponse({ description: "Permission refusée" }) | ||||
|   @ApiNotFoundResponse({ description: "Object non trouvé" }) | ||||
|   async create(@Req() request: AuthenticatedRequest, @Body() createGeolocationDto: CreateGeolocationDto): Promise<GeolocationEntity> { | ||||
|     const user = request.user | ||||
|     const geolocation = await this.geolocationsService.create(user, createGeolocationDto) | ||||
|     return new GeolocationEntity(geolocation) | ||||
|   } | ||||
|  | ||||
|   @Get() | ||||
|   @UseGuards(JwtAuthGuard) | ||||
|   @ApiBearerAuth() | ||||
|   @ApiOkResponsePaginated(GeolocationEntity) | ||||
|   @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) | ||||
|   @ApiForbiddenResponse({ description: "Permission refusée" }) | ||||
|   @ApiNotFoundResponse({ description: "Objet non trouvé" }) | ||||
|   async findAll(@Query() queryPagination?: QueryPaginationDto): Promise<PaginateOutputDto<GeolocationEntity>> { | ||||
|     const [geolocations, total] = await this.geolocationsService.findAll(queryPagination) | ||||
|     return paginateOutput<GeolocationEntity>(geolocations.map(geolocation => new GeolocationEntity(geolocation)), total, queryPagination) | ||||
|   } | ||||
|  | ||||
|   @Get(':id') | ||||
|   @UseGuards(JwtAuthGuard) | ||||
|   @ApiBearerAuth() | ||||
|   @ApiOkResponse({ type: GeolocationEntity }) | ||||
|   @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) | ||||
|   @ApiForbiddenResponse({ description: "Permission refusée" }) | ||||
|   @ApiNotFoundResponse({ description: "Objet non trouvé" }) | ||||
|   async findOne(@Param('id', ParseIntPipe) id: number): Promise<GeolocationEntity> { | ||||
|     const geolocation = await this.geolocationsService.findOne(+id) | ||||
|     if (!geolocation) | ||||
|       throw new NotFoundException(`Géolocalisation inexistante avec l'identifiant ${id}`) | ||||
|     return new GeolocationEntity(geolocation) | ||||
|   } | ||||
|  | ||||
|   @Delete(':id') | ||||
|   @HttpCode(204) | ||||
|   @UseGuards(JwtAuthGuard) | ||||
|   @ApiBearerAuth() | ||||
|   @ApiNoContentResponse({ description: "Objet supprimé avec succès" }) | ||||
|   @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) | ||||
|   @ApiForbiddenResponse({ description: "Permission refusée" }) | ||||
|   @ApiNotFoundResponse({ description: "Objet non trouvé" }) | ||||
|   async remove(@Param('id', ParseIntPipe) id: number): Promise<void> { | ||||
|     await this.geolocationsService.remove(+id) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										11
									
								
								server/src/geolocations/geolocations.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								server/src/geolocations/geolocations.module.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import { Module } from '@nestjs/common' | ||||
| import { GeolocationsService } from './geolocations.service' | ||||
| import { GeolocationsController } from './geolocations.controller' | ||||
| import { PrismaModule } from 'src/prisma/prisma.module' | ||||
|  | ||||
| @Module({ | ||||
|   controllers: [GeolocationsController], | ||||
|   providers: [GeolocationsService], | ||||
|   imports: [PrismaModule], | ||||
| }) | ||||
| export class GeolocationsModule {} | ||||
							
								
								
									
										18
									
								
								server/src/geolocations/geolocations.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								server/src/geolocations/geolocations.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { Test, TestingModule } from '@nestjs/testing' | ||||
| import { GeolocationsService } from './geolocations.service' | ||||
|  | ||||
| describe('GeolocationsService', () => { | ||||
|   let service: GeolocationsService | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     const module: TestingModule = await Test.createTestingModule({ | ||||
|       providers: [GeolocationsService], | ||||
|     }).compile() | ||||
|  | ||||
|     service = module.get<GeolocationsService>(GeolocationsService) | ||||
|   }) | ||||
|  | ||||
|   it('should be defined', () => { | ||||
|     expect(service).toBeDefined() | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										34
									
								
								server/src/geolocations/geolocations.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								server/src/geolocations/geolocations.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import { Injectable } from '@nestjs/common' | ||||
| import { CreateGeolocationDto } from './dto/create-geolocation.dto' | ||||
| import { PrismaService } from 'src/prisma/prisma.service' | ||||
| import { Geolocation, User } from '@prisma/client' | ||||
| import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto' | ||||
| import { paginate } from 'src/common/utils/pagination.utils' | ||||
|  | ||||
| @Injectable() | ||||
| export class GeolocationsService { | ||||
|   constructor(private prisma: PrismaService) { } | ||||
|  | ||||
|   async create(authenticatedUser: User, createGeolocationDto: CreateGeolocationDto): Promise<Geolocation> { | ||||
|     const data = { ...createGeolocationDto, userId: authenticatedUser.id } | ||||
|     return await this.prisma.geolocation.create({ data: data }) | ||||
|   } | ||||
|  | ||||
|   async findAll(queryPagination?: QueryPaginationDto): Promise<[Geolocation[], number]> { | ||||
|     return [ | ||||
|       await this.prisma.geolocation.findMany({ | ||||
|         ...paginate(queryPagination), | ||||
|       }), | ||||
|       await this.prisma.geolocation.count(), | ||||
|     ] | ||||
|   } | ||||
|  | ||||
|   async findOne(id: number): Promise<Geolocation> { | ||||
|     return await this.prisma.geolocation.findUnique({ where: { id } }) | ||||
|   } | ||||
|  | ||||
|  | ||||
|   async remove(id: number): Promise<Geolocation> { | ||||
|     return await this.prisma.geolocation.delete({ where: { id } }) | ||||
|   } | ||||
| } | ||||
| @@ -6,7 +6,7 @@ import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common' | ||||
| async function bootstrap() { | ||||
|   const app = await NestFactory.create(AppModule) | ||||
|  | ||||
|   app.useGlobalPipes(new ValidationPipe()) | ||||
|   app.useGlobalPipes(new ValidationPipe({ transform: true })) | ||||
|   app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))) | ||||
|  | ||||
|   const config = new DocumentBuilder() | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { Body, Controller, Get, HttpCode, NotFoundException, Param, ParseIntPipe, Patch, Post, Req, UseGuards } from '@nestjs/common' | ||||
| import { Body, Controller, Get, HttpCode, NotFoundException, Param, ParseIntPipe, Patch, Query, Req, UseGuards } from '@nestjs/common' | ||||
| import { UsersService } from './users.service' | ||||
| import { ApiBadRequestResponse, ApiBearerAuth, ApiForbiddenResponse, ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger' | ||||
| import { UserEntity } from './entities/user.entity' | ||||
| import { JwtAuthGuard } from 'src/auth/jwt-auth.guard' | ||||
| import { User } from '@prisma/client' | ||||
| import { AuthenticatedRequest, JwtAuthGuard } from 'src/auth/jwt-auth.guard' | ||||
| import { UpdatePasswordDto } from './dto/user_password.dto' | ||||
|  | ||||
| export type AuthenticatedRequest = Request & { user: User } | ||||
| import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto' | ||||
| import { ApiOkResponsePaginated, paginateOutput } from 'src/common/utils/pagination.utils' | ||||
| import { PaginateOutputDto } from 'src/common/dto/pagination-output.dto' | ||||
|  | ||||
| @Controller('users') | ||||
| export class UsersController { | ||||
| @@ -15,12 +15,12 @@ export class UsersController { | ||||
|   @Get() | ||||
|   @UseGuards(JwtAuthGuard) | ||||
|   @ApiBearerAuth() | ||||
|   @ApiOkResponse({ type: UserEntity, isArray: true }) | ||||
|   @ApiOkResponsePaginated(UserEntity) | ||||
|   @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) | ||||
|   @ApiForbiddenResponse({ description: "Permission refusée" }) | ||||
|   async findAll() { | ||||
|     const users = await this.usersService.findAll() | ||||
|     return users.map(user => new UserEntity(user)) | ||||
|   async findAll(@Query() queryPagination?: QueryPaginationDto): Promise<PaginateOutputDto<UserEntity>> { | ||||
|     const [users, total] = await this.usersService.findAll(queryPagination) | ||||
|     return paginateOutput<UserEntity>(users.map(user => new UserEntity(user)), total, queryPagination) | ||||
|   } | ||||
|  | ||||
|   @Get(':id') | ||||
| @@ -45,8 +45,8 @@ export class UsersController { | ||||
|   @ApiBadRequestResponse({description: "Erreur dans la saisie du nouveau mot de passe."}) | ||||
|   @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) | ||||
|   @ApiForbiddenResponse({ description: "Permission refusée" }) | ||||
|   async updatePassword(@Req() request: AuthenticatedRequest, @Body() { password }: UpdatePasswordDto) { | ||||
|   async updatePassword(@Req() request: AuthenticatedRequest, @Body() body: UpdatePasswordDto) { | ||||
|     const user = request.user | ||||
|     await this.usersService.updatePassword(user, password) | ||||
|     await this.usersService.updatePassword(user, body) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -2,20 +2,26 @@ import { Injectable } from '@nestjs/common' | ||||
| import { User } from '@prisma/client' | ||||
| import { PrismaService } from 'src/prisma/prisma.service' | ||||
| import * as bcrypt from 'bcrypt' | ||||
| import { UpdatePasswordDto } from './dto/user_password.dto' | ||||
| import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto' | ||||
| import { paginate } from 'src/common/utils/pagination.utils' | ||||
|  | ||||
| @Injectable() | ||||
| export class UsersService { | ||||
|   constructor(private prisma: PrismaService) {} | ||||
|  | ||||
|   async findAll() { | ||||
|     return await this.prisma.user.findMany() | ||||
|   async findAll(queryPagination?: QueryPaginationDto): Promise<[User[], number]> { | ||||
|     return [ | ||||
|       await this.prisma.user.findMany({ ...paginate(queryPagination) }), | ||||
|       await this.prisma.user.count() | ||||
|     ] | ||||
|   } | ||||
|  | ||||
|   async findOne(id: number) { | ||||
|   async findOne(id: number): Promise<User> { | ||||
|     return await this.prisma.user.findUnique({ where: { id } }) | ||||
|   } | ||||
|  | ||||
|   async updatePassword(user: User, password: string) { | ||||
|   async updatePassword(user: User, { password }: UpdatePasswordDto): Promise<void> { | ||||
|     const hashedPassword = await bcrypt.hash(password, 10) | ||||
|     await this.prisma.user.update({ | ||||
|       where: { id: user.id }, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user