Compare commits

...

7 Commits

46 changed files with 1465 additions and 166 deletions

15
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
"sqltools.connections": [
{
"previewLimit": 50,
"server": "localhost",
"port": 5432,
"driver": "PostgreSQL",
"name": "Traintrape-moi",
"database": "traintrape-moi",
"username": "traintrapemoi",
"socketPath": "/run/postgresql/.s.PGSQL.5432"
}
],
"sqltools.useNodeRuntime": true
}

View File

@ -1,7 +1,7 @@
import { ScrollView } from 'react-native';
import { ScrollView } from 'react-native'
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { ThemedText } from '@/components/ThemedText'
import { ThemedView } from '@/components/ThemedView'
export default function ChallengesScreen() {
return (
@ -10,5 +10,5 @@ export default function ChallengesScreen() {
<ThemedText>Ici on aura la gestion des challenges</ThemedText>
</ThemedView>
</ScrollView>
);
)
}

View File

@ -1,7 +1,6 @@
import { ScrollView } from 'react-native';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { ScrollView } from 'react-native'
import { ThemedText } from '@/components/ThemedText'
import { ThemedView } from '@/components/ThemedView'
export default function HistoryScreen() {
return (
@ -10,5 +9,5 @@ export default function HistoryScreen() {
<ThemedText>Ici on aura la gestion de l'historique des trains empruntés et des challenges effectués</ThemedText>
</ThemedView>
</ScrollView>
);
)
}

View File

@ -27,4 +27,4 @@ const styles = StyleSheet.create({
flex: 1,
alignSelf: 'stretch',
},
});
})

View File

@ -1,7 +1,7 @@
import { ScrollView } from 'react-native';
import { ScrollView } from 'react-native'
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { ThemedText } from '@/components/ThemedText'
import { ThemedView } from '@/components/ThemedView'
export default function TrainScreen() {
return (
@ -10,5 +10,5 @@ export default function TrainScreen() {
<ThemedText>Ici on aura la page pour ajouter un trajet en train depuis Rail Planner</ThemedText>
</ThemedView>
</ScrollView>
);
)
}

View File

@ -1,12 +1,11 @@
import { Text, type TextProps, StyleSheet } from 'react-native';
import { useThemeColor } from '@/hooks/useThemeColor';
import { Text, type TextProps, StyleSheet } from 'react-native'
import { useThemeColor } from '@/hooks/useThemeColor'
export type ThemedTextProps = TextProps & {
lightColor?: string;
darkColor?: string;
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
};
lightColor?: string
darkColor?: string
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'
}
export function ThemedText({
style,
@ -15,7 +14,7 @@ export function ThemedText({
type = 'default',
...rest
}: ThemedTextProps) {
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text')
return (
<Text
@ -30,7 +29,7 @@ export function ThemedText({
]}
{...rest}
/>
);
)
}
const styles = StyleSheet.create({
@ -57,4 +56,4 @@ const styles = StyleSheet.create({
fontSize: 16,
color: '#0a7ea4',
},
});
})

View File

@ -1,14 +1,13 @@
import { View, type ViewProps } from 'react-native';
import { useThemeColor } from '@/hooks/useThemeColor';
import { View, type ViewProps } from 'react-native'
import { useThemeColor } from '@/hooks/useThemeColor'
export type ThemedViewProps = ViewProps & {
lightColor?: string;
darkColor?: string;
};
lightColor?: string
darkColor?: string
}
export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background')
return <View style={[{ backgroundColor }, style]} {...otherProps} />;
return <View style={[{ backgroundColor }, style]} {...otherProps} />
}

View File

@ -1,10 +1,10 @@
import * as React from 'react';
import renderer from 'react-test-renderer';
import * as React from 'react'
import renderer from 'react-test-renderer'
import { ThemedText } from '../ThemedText';
import { ThemedText } from '../ThemedText'
it(`renders correctly`, () => {
const tree = renderer.create(<ThemedText>Snapshot test!</ThemedText>).toJSON();
const tree = renderer.create(<ThemedText>Snapshot test!</ThemedText>).toJSON()
expect(tree).toMatchSnapshot();
});
expect(tree).toMatchSnapshot()
})

View File

@ -21,4 +21,4 @@ exports[`renders correctly 1`] = `
>
Snapshot test!
</Text>
`;
`

View File

@ -3,8 +3,8 @@
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
*/
const tintColorLight = '#0a7ea4';
const tintColorDark = '#fff';
const tintColorLight = '#0a7ea4'
const tintColorDark = '#fff'
export const Colors = {
light: {
@ -23,4 +23,4 @@ export const Colors = {
tabIconDefault: '#9BA1A6',
tabIconSelected: tintColorDark,
},
};
}

View File

@ -1 +1 @@
export { useColorScheme } from 'react-native';
export { useColorScheme } from 'react-native'

View File

@ -1,21 +1,21 @@
import { useEffect, useState } from 'react';
import { useColorScheme as useRNColorScheme } from 'react-native';
import { useEffect, useState } from 'react'
import { useColorScheme as useRNColorScheme } from 'react-native'
/**
* To support static rendering, this value needs to be re-calculated on the client side for web
*/
export function useColorScheme() {
const [hasHydrated, setHasHydrated] = useState(false);
const [hasHydrated, setHasHydrated] = useState(false)
useEffect(() => {
setHasHydrated(true);
}, []);
setHasHydrated(true)
}, [])
const colorScheme = useRNColorScheme();
const colorScheme = useRNColorScheme()
if (hasHydrated) {
return colorScheme;
return colorScheme
}
return 'light';
return 'light'
}

View File

@ -3,19 +3,19 @@
* https://docs.expo.dev/guides/color-schemes/
*/
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { Colors } from '@/constants/Colors'
import { useColorScheme } from '@/hooks/useColorScheme'
export function useThemeColor(
props: { light?: string; dark?: string },
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
) {
const theme = useColorScheme() ?? 'light';
const colorFromProps = props[theme];
const theme = useColorScheme() ?? 'light'
const colorFromProps = props[theme]
if (colorFromProps) {
return colorFromProps;
return colorFromProps
} else {
return Colors[theme][colorName];
return Colors[theme][colorName]
}
}

2
server/.env.example Normal file
View File

@ -0,0 +1,2 @@
DATABASE_URL="postgres://username:password@localhost:5432/traintrape-moi"
JWT_SECRET="CHANGE_ME"

View File

@ -21,5 +21,6 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/semi': 'never',
},
};
}

9
server/.gitignore vendored
View File

@ -3,6 +3,10 @@
/node_modules
/build
# Databases
*.sqlite3
*.sqlite3-journal
# Logs
logs
*.log
@ -37,10 +41,7 @@ lerna-debug.log*
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
.env*.local
# temp directory
.temp

836
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,18 +21,30 @@
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/core": "^10.4.13",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^8.1.0",
"@prisma/client": "^6.0.1",
"bcrypt": "^5.1.1",
"class-validator": "^0.14.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"prisma": "^6.0.1",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.1"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@nestjs/testing": "^10.4.13",
"@types/bcrypt": "^5.0.2",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
@ -65,5 +77,8 @@
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}

View File

@ -0,0 +1,109 @@
-- CreateEnum
CREATE TYPE "MoneyUpdateType" AS ENUM ('START', 'WIN_CHALLENGE', 'BUY_TRAIN');
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"password" TEXT NOT NULL,
"money" INTEGER NOT NULL DEFAULT 0,
"currentRunner" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Geolocation" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"longitude" DOUBLE PRECISION NOT NULL,
"latitude" DOUBLE PRECISION NOT NULL,
"timestamp" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Geolocation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Challenge" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"reward" INTEGER NOT NULL,
CONSTRAINT "Challenge_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ChallengeAction" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"challengeId" INTEGER NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT false,
"success" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "ChallengeAction_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TrainTrip" (
"id" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"distance" DOUBLE PRECISION NOT NULL,
"from" TEXT NOT NULL,
"to" TEXT NOT NULL,
"departureTime" TIMESTAMP(3) NOT NULL,
"arrivalTime" TIMESTAMP(3) NOT NULL,
"infoJson" JSONB NOT NULL,
"geometry" TEXT NOT NULL,
CONSTRAINT "TrainTrip_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MoneyUpdate" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"before" INTEGER NOT NULL,
"after" INTEGER NOT NULL,
"reason" "MoneyUpdateType" NOT NULL,
"actionId" INTEGER,
"tripId" TEXT,
CONSTRAINT "MoneyUpdate_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_name_key" ON "User"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Challenge_title_key" ON "Challenge"("title");
-- CreateIndex
CREATE UNIQUE INDEX "ChallengeAction_challengeId_key" ON "ChallengeAction"("challengeId");
-- CreateIndex
CREATE UNIQUE INDEX "MoneyUpdate_actionId_key" ON "MoneyUpdate"("actionId");
-- CreateIndex
CREATE UNIQUE INDEX "MoneyUpdate_tripId_key" ON "MoneyUpdate"("tripId");
-- AddForeignKey
ALTER TABLE "Geolocation" ADD CONSTRAINT "Geolocation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ChallengeAction" ADD CONSTRAINT "ChallengeAction_challengeId_fkey" FOREIGN KEY ("challengeId") REFERENCES "Challenge"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ChallengeAction" ADD CONSTRAINT "ChallengeAction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TrainTrip" ADD CONSTRAINT "TrainTrip_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MoneyUpdate" ADD CONSTRAINT "MoneyUpdate_actionId_fkey" FOREIGN KEY ("actionId") REFERENCES "ChallengeAction"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MoneyUpdate" ADD CONSTRAINT "MoneyUpdate_tripId_fkey" FOREIGN KEY ("tripId") REFERENCES "TrainTrip"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MoneyUpdate" ADD CONSTRAINT "MoneyUpdate_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@ -0,0 +1,81 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String @unique
password String
money Int @default(0)
currentRunner Boolean @default(false)
actions ChallengeAction[]
geolocations Geolocation[]
moneyUpdates MoneyUpdate[]
trips TrainTrip[]
}
model Geolocation {
id Int @id @default(autoincrement())
userId Int
longitude Float
latitude Float
timestamp DateTime
user User @relation(fields: [userId], references: [id])
}
model Challenge {
id Int @id @default(autoincrement())
title String @unique
description String
reward Int
action ChallengeAction?
}
model ChallengeAction {
id Int @id @default(autoincrement())
userId Int
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
userId Int
distance Float
from String
to String
departureTime DateTime
arrivalTime DateTime
infoJson Json
geometry String
moneyUpdate MoneyUpdate?
user User @relation(fields: [userId], references: [id])
}
model MoneyUpdate {
id Int @id @default(autoincrement())
userId Int
before Int
after Int
reason MoneyUpdateType
actionId Int? @unique
tripId String? @unique
action ChallengeAction? @relation(fields: [actionId], references: [id])
trip TrainTrip? @relation(fields: [tripId], references: [id])
user User @relation(fields: [userId], references: [id])
}
enum MoneyUpdateType {
START
WIN_CHALLENGE
BUY_TRAIN
}

30
server/prisma/seed.ts Normal file
View File

@ -0,0 +1,30 @@
import { PrismaClient } from '@prisma/client'
import * as bcrypt from 'bcrypt'
const prisma = new PrismaClient()
async function main() {
const emmyPassword = await bcrypt.hash("Emmy", 10)
const emmy = await prisma.user.upsert({
where: { id: 1 },
update: { name: 'Emmy' },
create: { name: 'Emmy', password: emmyPassword },
})
const taminaPassword = await bcrypt.hash("Tamina", 10)
const tamina = await prisma.user.upsert({
where: { id: 2 },
update: { name: 'Tamina' },
create: { name: 'Tamina', password: taminaPassword },
})
console.log({ emmy, tamina })
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})

View File

@ -1,22 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Module } from '@nestjs/common'
import { PrismaService } from './prisma/prisma.service'
import { PrismaModule } from './prisma/prisma.module'
import { UsersModule } from './users/users.module'
import { AuthModule } from './auth/auth.module'
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
imports: [PrismaModule, UsersModule, AuthModule],
providers: [PrismaService],
})
export class AppModule {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
describe('AuthController', () => {
let controller: AuthController
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [AuthService],
}).compile()
controller = module.get<AuthController>(AuthController)
})
it('should be defined', () => {
expect(controller).toBeDefined()
})
})

View File

@ -0,0 +1,17 @@
import { Body, Controller, Post } from '@nestjs/common'
import { AuthService } from './auth.service'
import { ApiOkResponse, ApiTags } from '@nestjs/swagger'
import { AuthEntity } from './entity/auth.entity'
import { LoginDto } from './dto/login.dto'
@Controller('auth')
@ApiTags('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@ApiOkResponse({ type: AuthEntity })
login(@Body() { name, password }: LoginDto) {
return this.authService.login(name, password)
}
}

View File

@ -0,0 +1,26 @@
import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { AuthController } from './auth.controller'
import { PrismaModule } from 'src/prisma/prisma.module'
import { PassportModule } from '@nestjs/passport'
import { JwtModule } from '@nestjs/jwt'
import { env } from 'process'
import { UsersModule } from 'src/users/users.module'
import { JwtStrategy } from './jwt.strategy'
export const JWT_SECRET = env.JWT_SECRET
@Module({
imports: [
PrismaModule,
PassportModule,
JwtModule.register({
secret: JWT_SECRET,
signOptions: { expiresIn: '5m' },
}),
UsersModule,
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
})
export class AuthModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { AuthService } from './auth.service'
describe('AuthService', () => {
let service: AuthService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile()
service = module.get<AuthService>(AuthService)
})
it('should be defined', () => {
expect(service).toBeDefined()
})
})

View File

@ -0,0 +1,27 @@
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'
import { PrismaService } from './../prisma/prisma.service'
import { JwtService } from '@nestjs/jwt'
import { AuthEntity } from './entity/auth.entity'
import * as bcrypt from 'bcrypt'
@Injectable()
export class AuthService {
constructor(private prisma: PrismaService, private jwtService: JwtService) {}
async login(name: string, password: string): Promise<AuthEntity> {
const user = await this.prisma.user.findUnique({ where: { name: name } })
if (!user) {
throw new NotFoundException(`Aucun⋅e utilisateur⋅rice avec pour nom ${name}`)
}
const isPasswordValid = await bcrypt.compare(password, user.password)
if (!isPasswordValid) {
throw new UnauthorizedException('Mot de passe incorrect')
}
return {
accessToken: this.jwtService.sign({ userId: user.id }),
}
}
}

View File

@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsNotEmpty, IsString, MinLength } from 'class-validator'
export class LoginDto {
@IsString()
@IsNotEmpty()
@ApiProperty()
name: string
@IsString()
@IsNotEmpty()
@ApiProperty()
password: string
}

View File

@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'
export class AuthEntity {
@ApiProperty()
accessToken: string
}

View File

@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

View File

@ -0,0 +1,23 @@
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { JWT_SECRET } from './auth.module'
import { UsersService } from 'src/users/users.service'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private usersService: UsersService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: JWT_SECRET,
})
}
async validate(payload: { userId: number }) {
const user = await this.usersService.findOne(payload.userId)
if (!user) {
throw new UnauthorizedException()
}
return user
}
}

View File

@ -1,8 +1,23 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestFactory, Reflector } from '@nestjs/core'
import { AppModule } from './app.module'
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common'
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
const app = await NestFactory.create(AppModule)
app.useGlobalPipes(new ValidationPipe())
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)))
const config = new DocumentBuilder()
.setTitle('Traintrape-moi')
.setDescription('API permettant de stocker les données de Traintrape-moi')
.setVersion('1.0')
.addBearerAuth()
.build()
const document = SwaggerModule.createDocument(app, config)
SwaggerModule.setup('', app, document)
await app.listen(process.env.PORT ?? 3000)
}
bootstrap();
bootstrap()

View File

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common'
import { PrismaService } from './prisma.service'
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { PrismaService } from './prisma.service'
describe('PrismaService', () => {
let service: PrismaService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PrismaService],
}).compile()
service = module.get<PrismaService>(PrismaService)
})
it('should be defined', () => {
expect(service).toBeDefined()
})
})

View File

@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'
@Injectable()
export class PrismaService extends PrismaClient {}

View File

@ -0,0 +1,24 @@
import { ApiProperty } from "@nestjs/swagger"
import { User } from "@prisma/client"
import { Exclude } from 'class-transformer'
export class UserEntity implements User {
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial)
}
@ApiProperty({description: "Identifiant unique"})
id: number
@ApiProperty({description: "Nom de læ joueur⋅se"})
name: string
@Exclude()
password: string
@ApiProperty({description: "Nombre de jetons dont dispose actuellement læ joueur⋅se"})
money: number
@ApiProperty({description: "Est-ce que cet⋅te joueur⋅se est cellui actuellement en course"})
currentRunner: boolean
}

View File

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
describe('UsersController', () => {
let controller: UsersController
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [UsersService],
}).compile()
controller = module.get<UsersController>(UsersController)
})
it('should be defined', () => {
expect(controller).toBeDefined()
})
})

View File

@ -0,0 +1,31 @@
import { Controller, Get, NotFoundException, Param, ParseIntPipe, UseGuards } from '@nestjs/common'
import { UsersService } from './users.service'
import { ApiBearerAuth, ApiNotFoundResponse, ApiOkResponse } from '@nestjs/swagger'
import { UserEntity } from './entities/user.entity'
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: UserEntity, isArray: true })
async findAll() {
const users = await this.usersService.findAll()
return users.map(user => new UserEntity(user))
}
@Get(':id')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: UserEntity })
@ApiNotFoundResponse({ description: "Utilisateur⋅rice non trouvé⋅e" })
async findOne(@Param('id', ParseIntPipe) id: number) {
const user = await this.usersService.findOne(id)
if (!user)
throw new NotFoundException(`L'utilisateur⋅rice avec l'identifiant ${id} n'existe pas`)
return new UserEntity(user)
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common'
import { UsersService } from './users.service'
import { UsersController } from './users.controller'
import { PrismaModule } from 'src/prisma/prisma.module'
@Module({
controllers: [UsersController],
providers: [UsersService],
imports: [PrismaModule],
exports: [UsersService],
})
export class UsersModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { UsersService } from './users.service'
describe('UsersService', () => {
let service: UsersService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile()
service = module.get<UsersService>(UsersService)
})
it('should be defined', () => {
expect(service).toBeDefined()
})
})

View File

@ -0,0 +1,15 @@
import { Injectable } from '@nestjs/common'
import { PrismaService } from 'src/prisma/prisma.service'
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async findAll() {
return await this.prisma.user.findMany()
}
async findOne(id: number) {
return await this.prisma.user.findUnique({ where: { id } })
}
}

View File

@ -1,24 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication } from '@nestjs/common'
import * as request from 'supertest'
import { AppModule } from './../src/app.module'
describe('AppController (e2e)', () => {
let app: INestApplication;
let app: INestApplication
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
}).compile()
app = moduleFixture.createNestApplication();
await app.init();
});
app = moduleFixture.createNestApplication()
await app.init()
})
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});
.expect('Hello World!')
})
})