diff --git a/server/prisma/migrations/20241207130209_more_geoloc/migration.sql b/server/prisma/migrations/20241207130209_more_geoloc/migration.sql new file mode 100644 index 0000000..4958e1c --- /dev/null +++ b/server/prisma/migrations/20241207130209_more_geoloc/migration.sql @@ -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; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index c99ee7c..c5a5104 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -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 { diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 5c1fcae..0f6ac32 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -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 {} diff --git a/server/src/auth/jwt-auth.guard.ts b/server/src/auth/jwt-auth.guard.ts index aa859f7..e6e730b 100644 --- a/server/src/auth/jwt-auth.guard.ts +++ b/server/src/auth/jwt-auth.guard.ts @@ -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 } diff --git a/server/src/geolocations/dto/create-geolocation.dto.ts b/server/src/geolocations/dto/create-geolocation.dto.ts new file mode 100644 index 0000000..ed37331 --- /dev/null +++ b/server/src/geolocations/dto/create-geolocation.dto.ts @@ -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 +} diff --git a/server/src/geolocations/entities/geolocation.entity.ts b/server/src/geolocations/entities/geolocation.entity.ts new file mode 100644 index 0000000..a7e876a --- /dev/null +++ b/server/src/geolocations/entities/geolocation.entity.ts @@ -0,0 +1,35 @@ +import { ApiProperty } from "@nestjs/swagger" +import { Geolocation } from "@prisma/client" + +export class GeolocationEntity implements Geolocation { + constructor(partial: Partial) { + 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 +} diff --git a/server/src/geolocations/geolocations.controller.spec.ts b/server/src/geolocations/geolocations.controller.spec.ts new file mode 100644 index 0000000..35c8838 --- /dev/null +++ b/server/src/geolocations/geolocations.controller.spec.ts @@ -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) + }) + + it('should be defined', () => { + expect(controller).toBeDefined() + }) +}) diff --git a/server/src/geolocations/geolocations.controller.ts b/server/src/geolocations/geolocations.controller.ts new file mode 100644 index 0000000..e544f08 --- /dev/null +++ b/server/src/geolocations/geolocations.controller.ts @@ -0,0 +1,63 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, UseGuards, HttpCode, Req, NotFoundException } 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' + +@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 { + const user = request.user + const geolocation = await this.geolocationsService.create(user, createGeolocationDto) + return new GeolocationEntity(geolocation) + } + + @Get() + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse({ type: GeolocationEntity, isArray: true }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + @ApiNotFoundResponse({ description: "Objet non trouvé" }) + async findAll(): Promise { + const geolocations = await this.geolocationsService.findAll() + return geolocations.map(geolocation => new GeolocationEntity(geolocation)) + } + + @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 { + 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 { + await this.geolocationsService.remove(+id) + } +} diff --git a/server/src/geolocations/geolocations.module.ts b/server/src/geolocations/geolocations.module.ts new file mode 100644 index 0000000..ba8f227 --- /dev/null +++ b/server/src/geolocations/geolocations.module.ts @@ -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 {} diff --git a/server/src/geolocations/geolocations.service.spec.ts b/server/src/geolocations/geolocations.service.spec.ts new file mode 100644 index 0000000..efbfe68 --- /dev/null +++ b/server/src/geolocations/geolocations.service.spec.ts @@ -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) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/server/src/geolocations/geolocations.service.ts b/server/src/geolocations/geolocations.service.ts new file mode 100644 index 0000000..fb15a54 --- /dev/null +++ b/server/src/geolocations/geolocations.service.ts @@ -0,0 +1,27 @@ +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' + +@Injectable() +export class GeolocationsService { + constructor(private prisma: PrismaService) { } + + async create(authenticatedUser: User, createGeolocationDto: CreateGeolocationDto): Promise { + const data = { ...createGeolocationDto, userId: authenticatedUser.id } + return await this.prisma.geolocation.create({ data: data }) + } + + async findAll(): Promise { + return await this.prisma.geolocation.findMany() + } + + async findOne(id: number): Promise { + return await this.prisma.geolocation.findUnique({ where: { id } }) + } + + + async remove(id: number): Promise { + return await this.prisma.geolocation.delete({ where: { id } }) + } +} diff --git a/server/src/users/users.controller.ts b/server/src/users/users.controller.ts index 53e4981..2c64851 100644 --- a/server/src/users/users.controller.ts +++ b/server/src/users/users.controller.ts @@ -1,13 +1,10 @@ -import { Body, Controller, Get, HttpCode, NotFoundException, Param, ParseIntPipe, Patch, Post, Req, UseGuards } from '@nestjs/common' +import { Body, Controller, Get, HttpCode, NotFoundException, Param, ParseIntPipe, Patch, 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 } - @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @@ -45,8 +42,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) } } diff --git a/server/src/users/users.service.ts b/server/src/users/users.service.ts index a41895e..88bf22f 100644 --- a/server/src/users/users.service.ts +++ b/server/src/users/users.service.ts @@ -2,6 +2,7 @@ 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' @Injectable() export class UsersService { @@ -15,7 +16,7 @@ export class UsersService { return await this.prisma.user.findUnique({ where: { id } }) } - async updatePassword(user: User, password: string) { + async updatePassword(user: User, { password }: UpdatePasswordDto) { const hashedPassword = await bcrypt.hash(password, 10) await this.prisma.user.update({ where: { id: user.id },