Front: Pull Migration to Native Base

This commit is contained in:
Arthi-chaud
2022-10-07 09:47:30 +01:00
76 changed files with 1569 additions and 363 deletions

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = tab
indent_size = tab

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_NAME=
POSTGRES_HOST=
DATABASE_URL=
JWT_SECRET=

5
.gitignore vendored
View File

@@ -5,4 +5,7 @@ pyvenv.cfg
include
.env
prisma/migrations/*
.vscode
.vscode
output.xml
report.html
log.html

10
back/.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
node_modules
Dockerfile
Dockerfile.dev
dist
test
.dockerignore
.gitignore
.eslintrc.json
.pretiierrc
README.MD

7
back/.gitignore vendored
View File

@@ -32,4 +32,9 @@ lerna-debug.log*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/extensions.json
# Robots tests
log.html
output.xml
report.html

View File

@@ -1,7 +1,8 @@
FROM node:17
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY ./package.json ./package-lock.json ./
RUN npm install --frozen-lockfile
COPY . .
RUN npx prisma generate
RUN npm run build
CMD npx prisma generate ; npx prisma migrate dev ; npm run start:prod
CMD npx prisma migrate dev; npm run start:prod

View File

@@ -1,6 +1,3 @@
FROM node:17
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
CMD npx prisma generate ; npx prisma migrate dev ; npm run start:dev

View File

@@ -1,5 +1,14 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src"
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": [{
"name": "@nestjs/swagger",
"options": {
"introspectComments": true,
"dtoFileNameSuffix": []
}
}]
}
}

35
back/package-lock.json generated
View File

@@ -27,7 +27,8 @@
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"
"rxjs": "^7.2.0",
"swagger-ui-express": "^4.5.0"
},
"devDependencies": {
"@nestjs/cli": "^8.0.0",
@@ -8194,6 +8195,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swagger-ui-dist": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.14.0.tgz",
"integrity": "sha512-TBzhheU15s+o54Cgk9qxuYcZMiqSm/SkvKnapoGHOF66kz0Y5aGjpzj5BT/vpBbn6rTPJ9tUYXQxuDWfsjiGMw=="
},
"node_modules/swagger-ui-express": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.5.0.tgz",
"integrity": "sha512-DHk3zFvsxrkcnurGvQlAcLuTDacAVN1JHKDgcba/gr2NFRE4HGwP1YeHIXMiGznkWR4AeS7X5vEblNn4QljuNA==",
"dependencies": {
"swagger-ui-dist": ">=4.11.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0"
}
},
"node_modules/symbol-observable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
@@ -15400,6 +15420,19 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
"swagger-ui-dist": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.14.0.tgz",
"integrity": "sha512-TBzhheU15s+o54Cgk9qxuYcZMiqSm/SkvKnapoGHOF66kz0Y5aGjpzj5BT/vpBbn6rTPJ9tUYXQxuDWfsjiGMw=="
},
"swagger-ui-express": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.5.0.tgz",
"integrity": "sha512-DHk3zFvsxrkcnurGvQlAcLuTDacAVN1JHKDgcba/gr2NFRE4HGwP1YeHIXMiGznkWR4AeS7X5vEblNn4QljuNA==",
"requires": {
"swagger-ui-dist": ">=4.11.0"
}
},
"symbol-observable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",

View File

@@ -39,7 +39,8 @@
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"
"rxjs": "^7.2.0",
"swagger-ui-express": "^4.5.0"
},
"devDependencies": {
"@nestjs/cli": "^8.0.0",

View File

@@ -0,0 +1,88 @@
-- CreateEnum
CREATE TYPE "DifficultyPoint" AS ENUM ('TwoHands', 'Rhythm', 'NoteCombo', 'Arpeggio', 'Distance', 'LeftHand', 'RightHand', 'LeadHandChange', 'ChordComplexity', 'ChordTiming', 'Length', 'PedalPoint', 'Precision');
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"username" TEXT NOT NULL,
"password" TEXT NOT NULL,
"email" TEXT NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Song" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"artistId" INTEGER,
"albumId" INTEGER,
"genreId" INTEGER,
"difficulties" JSONB NOT NULL,
CONSTRAINT "Song_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Genre" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "Genre_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Artist" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "Artist_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Album" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "Album_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Lesson" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL,
"requiredLevel" INTEGER NOT NULL,
"mainSkill" "DifficultyPoint" NOT NULL,
CONSTRAINT "Lesson_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LessonHistory" (
"lessonID" INTEGER NOT NULL,
"userID" INTEGER NOT NULL,
CONSTRAINT "LessonHistory_pkey" PRIMARY KEY ("lessonID","userID")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- CreateIndex
CREATE UNIQUE INDEX "Song_name_key" ON "Song"("name");
-- AddForeignKey
ALTER TABLE "Song" ADD CONSTRAINT "Song_genreId_fkey" FOREIGN KEY ("genreId") REFERENCES "Genre"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Song" ADD CONSTRAINT "Song_artistId_fkey" FOREIGN KEY ("artistId") REFERENCES "Artist"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Song" ADD CONSTRAINT "Song_albumId_fkey" FOREIGN KEY ("albumId") REFERENCES "Album"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LessonHistory" ADD CONSTRAINT "LessonHistory_userID_fkey" FOREIGN KEY ("userID") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LessonHistory" ADD CONSTRAINT "LessonHistory_lessonID_fkey" FOREIGN KEY ("lessonID") REFERENCES "Lesson"("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

@@ -10,8 +10,78 @@ datasource db {
}
model User {
id Int @default(autoincrement()) @id
username String @unique
password String
email String
id Int @id @default(autoincrement())
username String @unique
password String
email String
LessonHistory LessonHistory[]
}
model Song {
id Int @id @default(autoincrement())
name String @unique
artistId Int?
artist Artist? @relation(fields: [artistId], references: [id])
albumId Int?
album Album? @relation(fields: [albumId], references: [id])
genreId Int?
genre Genre? @relation(fields: [genreId], references: [id])
difficulties Json
}
model Genre {
id Int @id @default(autoincrement())
name String
Song Song[]
}
model Artist {
id Int @id @default(autoincrement())
name String
Song Song[]
}
model Album {
id Int @id @default(autoincrement())
name String
Song Song[]
}
model Lesson {
id Int @id @default(autoincrement())
name String
description String
requiredLevel Int
mainSkill Skill
LessonHistory LessonHistory[]
}
model LessonHistory {
lesson Lesson @relation(fields: [lessonID], references: [id])
lessonID Int
user User @relation(fields: [userID], references: [id])
userID Int
@@id([lessonID, userID])
}
enum Skill {
TwoHands
Rhythm
NoteCombo
Arpeggio
Distance
LeftHand
RightHand
LeadHandChange
ChordComplexity
ChordTiming
Length
PedalPoint
Precision
@@map("DifficultyPoint")
}

View File

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

View File

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

View File

@@ -5,10 +5,12 @@ import { PrismaService } from './prisma/prisma.service';
import { UsersModule } from './users/users.module';
import { PrismaModule } from './prisma/prisma.module';
import { AuthModule } from './auth/auth.module';
import { SongModule } from './song/song.module';
import { LessonModule } from './lesson/lesson.module';
@Module({
imports: [UsersModule, PrismaModule, AuthModule],
controllers: [AppController],
providers: [AppService, PrismaService],
imports: [UsersModule, PrismaModule, AuthModule, SongModule, LessonModule],
controllers: [AppController],
providers: [AppService, PrismaService],
})
export class AppModule {}

View File

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

View File

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

View File

@@ -1,43 +1,71 @@
import { Controller, Request, Post, Get, UseGuards, Res, Body } from '@nestjs/common';
import {
Controller,
Request,
Post,
Get,
UseGuards,
Body,
Delete,
BadRequestException,
HttpCode,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';
import { LocalAuthGuard } from './local-auth.guard';
import { RegisterDto } from './dto/register.dto';
import { Response } from 'express';
import { UsersService } from 'src/users/users.service';
import { ApiBearerAuth, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
import {
ApiBearerAuth,
ApiBody,
ApiOkResponse,
ApiParam,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { User } from '../models/user';
import { JwtToken } from './models/jwt';
import { LoginDto } from './dto/login.dto';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(
private authService: AuthService,
private usersService: UsersService,
private configService: ConfigService
) {}
@Post('register')
async register(@Body() registerDto: RegisterDto, @Res() res: Response) {
async register(@Body() registerDto: RegisterDto): Promise<void> {
try {
await this.usersService.createUser(registerDto);
return res.status(200).json({"status": "user created"});
} catch {
return res.status(400).json({"status": "user not created"});
throw new BadRequestException();
}
}
@ApiBody({ type: LoginDto })
@HttpCode(200)
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) {
async login(@Request() req: any): Promise<JwtToken> {
return this.authService.login(req.user);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'Successfully logged in' })
@ApiOkResponse({ description: 'Successfully logged in', type: User })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Get('me')
getProfile(@Request() req) {
return req.user;
}
getProfile(@Request() req: any): User {
return req.user;
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'Successfully deleted', type: User })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Delete('me')
deleteSelf(@Request() req: any): Promise<User> {
return this.usersService.deleteUser({ id: req.user.id });
}
}

View File

@@ -10,16 +10,20 @@ import { ConfigService } from '@nestjs/config';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [ConfigModule, UsersModule, PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
inject: [ConfigService],
})],
providers: [AuthService, LocalStrategy, JwtStrategy],
controllers: [AuthController]
imports: [
ConfigModule,
UsersModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
inject: [ConfigService],
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
controllers: [AuthController],
})
export class AuthModule {}

View File

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

View File

@@ -1,22 +1,24 @@
import { Injectable } from '@nestjs/common';
import { BadRequestException, Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';
import PayloadInterface from './interface/payload.interface';
@Injectable()
export class AuthService {
constructor(
private userService: UsersService,
private jwtService: JwtService
private jwtService: JwtService,
) {}
async validateUser(username: string, password: string): Promise<PayloadInterface> {
const user = await this.userService.user({username});
async validateUser(
username: string,
password: string,
): Promise<PayloadInterface | null> {
const user = await this.userService.user({ username });
if (user && bcrypt.compareSync(password, user.password)) {
return {
username: user.username,
id: user.id
id: user.id,
};
}
return null;
@@ -26,7 +28,7 @@ export class AuthService {
const payload = { username: user.username, id: user.id };
const access_token = this.jwtService.sign(payload);
return {
access_token
access_token,
};
}
}

View File

@@ -1,12 +1,11 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class Constants {
constructor(private configService: ConfigService) {}
getSecret = () => {
return this.configService.get("JWT_SECRET");
}
}
return this.configService.get('JWT_SECRET');
};
}

View File

@@ -1,4 +1,4 @@
import { IsNotEmpty } from "class-validator";
import { IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class LoginDto {

View File

@@ -1,4 +1,4 @@
import { IsNotEmpty } from "class-validator";
import { IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class RegisterDto {

View File

@@ -1,4 +1,4 @@
export default interface PayloadInterface {
username: string;
id: number;
}
}

View File

@@ -2,4 +2,4 @@ import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
export class JwtAuthGuard extends AuthGuard('jwt') {}

View File

@@ -1,4 +1,3 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@@ -6,15 +5,15 @@ import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('JWT_SECRET'),
});
}
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('JWT_SECRET'),
});
}
async validate(payload: any) {
return { id: payload.id, username: payload.username };
}
}
async validate(payload: any) {
return { id: payload.id, username: payload.username };
}
}

View File

@@ -1,4 +1,3 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

View File

@@ -1,4 +1,3 @@
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
@@ -7,15 +6,18 @@ import PayloadInterface from './interface/payload.interface';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<PayloadInterface> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
async validate(
username: string,
password: string,
): Promise<PayloadInterface> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,103 @@
import {
Controller,
Get,
Res,
Query,
Req,
Request,
Param,
ParseIntPipe,
DefaultValuePipe,
BadRequestException,
Post,
Body,
Delete,
NotFoundException,
} from '@nestjs/common';
import { Plage } from 'src/models/plage';
import { LessonService } from './lesson.service';
import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
import { Prisma, Skill } from '@prisma/client';
export class Lesson {
@ApiProperty()
id: number;
@ApiProperty()
name: string;
@ApiProperty()
description: string;
@ApiProperty()
requiredLevel: number;
@ApiProperty()
mainSkill: Skill;
}
@ApiTags('lessons')
@Controller('lesson')
export class LessonController {
constructor(private lessonService: LessonService) {}
@ApiOperation({
summary: 'Get all lessons',
})
@Get()
async getAll(
@Req() request: Request,
@Query() filter: Prisma.LessonWhereInput,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Lesson>> {
try {
const ret = await this.lessonService.getAll({
skip,
take,
where: {
...filter,
requiredLevel: filter.requiredLevel
? +filter.requiredLevel
: undefined,
},
});
return new Plage(ret, request);
} catch (e) {
console.log(e);
throw new BadRequestException(null, e?.toString());
}
}
@ApiOperation({
summary: 'Get a particular lessons',
})
@Get(':id')
async get(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
const ret = await this.lessonService.get(id);
if (!ret) throw new NotFoundException();
return ret;
}
@ApiOperation({
summary: 'Create a lessons',
})
@Post()
async post(@Body() lesson: Lesson): Promise<Lesson> {
try {
return await this.lessonService.create(lesson);
} catch (e) {
console.log(e);
throw new BadRequestException(null, e.toString());
}
}
@ApiOperation({
summary: 'Delete a lessons',
})
@Delete(':id')
async delete(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
try {
return await this.lessonService.delete(id);
} catch (e) {
console.log(e);
throw new BadRequestException(null, e.toString());
}
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/prisma/prisma.module';
import { LessonController } from './lesson.controller';
import { LessonService } from './lesson.service';
@Module({
imports: [PrismaModule],
controllers: [LessonController],
providers: [LessonService],
})
export class LessonModule {}

View File

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

View File

@@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { Lesson, Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
@Injectable()
export class LessonService {
constructor(private prisma: PrismaService) {}
async getAll(params: {
skip?: number;
take?: number;
cursor?: Prisma.LessonWhereUniqueInput;
where?: Prisma.LessonWhereInput;
orderBy?: Prisma.LessonOrderByWithRelationInput;
}): Promise<Lesson[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.lesson.findMany({
skip,
take,
cursor,
where,
orderBy,
});
}
async get(id: number): Promise<Lesson | null> {
return this.prisma.lesson.findFirst({
where: {
id: id,
},
});
}
async create(lesson: Lesson): Promise<Lesson> {
return this.prisma.lesson.create({ data: lesson });
}
async delete(id: number): Promise<Lesson> {
return this.prisma.lesson.delete({ where: { id: id } });
}
}

View File

@@ -1,11 +1,21 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { PrismaService } from './prisma/prisma.service';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const prismaService = app.get(PrismaService);
await prismaService.enableShutdownHooks(app)
await app.listen(3000);
const app = await NestFactory.create(AppModule);
const prismaService = app.get(PrismaService);
await prismaService.enableShutdownHooks(app);
const config = new DocumentBuilder()
.setTitle('Chromacase')
.setDescription('The chromacase API')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();

51
back/src/models/plage.ts Normal file
View File

@@ -0,0 +1,51 @@
/*
* Thanks to https://github.com/Arthi-chaud/Meelo/blob/master/src/pagination/models/paginated-response.ts
*/
import { ApiProperty } from '@nestjs/swagger';
export class Plage<T> {
@ApiProperty()
metadata: {
this: string;
next: string | null;
previous: string | null;
};
@ApiProperty()
data: T[];
constructor(data: T[], request: Request | any) {
this.data = data;
let take = Number(request.query['take'] ?? 20).valueOf();
if (take == 0) take = 20;
let skipped: number = Number(request.query['skip'] ?? 0).valueOf();
if (skipped % take) {
skipped += take - (skipped % take);
}
this.metadata = {
this: this.buildUrl(request.path, request.query),
next:
data.length >= take
? this.buildUrl(request.path, {
...request.query,
skip: skipped + take,
})
: null,
previous: skipped
? this.buildUrl(request.path, {
...request.query,
skip: Math.max(0, skipped - take),
})
: null,
};
}
private buildUrl(route: string, queryParameters: any) {
if (queryParameters.skip == 0) delete queryParameters.skip;
const builtQueryParameters = new URLSearchParams(
queryParameters,
).toString();
if (builtQueryParameters.length) return `${route}?${builtQueryParameters}`;
return route;
}
}

12
back/src/models/user.ts Normal file
View File

@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
export class User {
@ApiProperty()
id: number;
@ApiProperty()
username: string;
@ApiProperty()
password: string;
@ApiProperty()
email: string;
}

View File

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

View File

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

View File

@@ -3,13 +3,13 @@ import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}

View File

@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export class CreateSongDto {
@IsNotEmpty()
@ApiProperty()
name: string;
@IsNotEmpty()
@ApiProperty()
difficulties: object;
}

View File

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

View File

@@ -0,0 +1,65 @@
import {
BadRequestException,
Body,
Controller,
DefaultValuePipe,
Delete,
Get,
NotFoundException,
Param,
ParseIntPipe,
Post,
Query,
Req,
} from '@nestjs/common';
import { Plage } from 'src/models/plage';
import { CreateSongDto } from './dto/create-song.dto';
import { SongService } from './song.service';
import { Request } from 'express';
import { Prisma, Song } from '@prisma/client';
@Controller('song')
export class SongController {
constructor(private readonly songService: SongService) {}
@Post()
async create(@Body() createSongDto: CreateSongDto) {
return await this.songService.createSong(createSongDto);
}
@Delete(':id')
async remove(@Param('id', ParseIntPipe) id: number) {
return await this.songService.deleteSong({ id });
}
@Get()
async findAll(
@Req() req: Request,
@Query() filter: Prisma.SongWhereInput,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Song>> {
try {
const ret = await this.songService.songs({
skip,
take,
where: {
...filter,
id: filter.id ? +filter.id : undefined,
},
});
return new Plage(ret, req);
} catch (e) {
console.log(e);
throw new BadRequestException(null, e?.toString());
}
}
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
let res = await this.songService.song({ id });
if (res === null) throw new NotFoundException('Song not found');
return res;
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { SongService } from './song.service';
import { SongController } from './song.controller';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [PrismaModule],
providers: [SongService],
controllers: [SongController],
})
export class SongModule {}

View File

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

View File

@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { Prisma, Song } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
@Injectable()
export class SongService {
constructor(private prisma: PrismaService) {}
async createSong(data: Prisma.SongCreateInput): Promise<Song> {
return this.prisma.song.create({
data,
});
}
async song(
songWhereUniqueInput: Prisma.SongWhereUniqueInput,
): Promise<Song | null> {
return this.prisma.song.findUnique({
where: songWhereUniqueInput,
});
}
async songs(params: {
skip?: number;
take?: number;
cursor?: Prisma.SongWhereUniqueInput;
where?: Prisma.SongWhereInput;
orderBy?: Prisma.SongOrderByWithRelationInput;
}): Promise<Song[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.song.findMany({
skip,
take,
cursor,
where,
orderBy,
});
}
async deleteSong(where: Prisma.SongWhereUniqueInput): Promise<Song> {
return this.prisma.song.delete({
where,
});
}
}

View File

@@ -1,5 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
email: string;
username: string;
password: string;
@ApiProperty()
email: string;
@ApiProperty()
username: string;
@ApiProperty()
password: string;
}

View File

@@ -1 +0,0 @@
export class User {}

View File

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

View File

@@ -1,34 +1,55 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Put } from '@nestjs/common';
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
NotFoundException,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger';
import { User } from 'src/models/user';
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.createUser(createUserDto);
}
@Post()
create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.createUser(createUserDto);
}
@Get()
findAll() {
return this.usersService.users({});
}
@Get()
findAll(): Promise<User[]> {
return this.usersService.users({});
}
@Get(':id')
findOne(@Param('id') id: number) {
return this.usersService.user({"id": +id});
}
@Get(':id')
@ApiNotFoundResponse()
async findOne(@Param('id') id: number): Promise<User> {
const ret = await this.usersService.user({ id: +id });
if (!ret) throw new NotFoundException();
return ret;
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.updateUser({where: {"id": +id}, data: updateUserDto});
}
@Patch(':id')
update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto,
): Promise<User> {
return this.usersService.updateUser({
where: { id: +id },
data: updateUserDto,
});
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.deleteUser({"id": +id});
}
@Delete(':id')
remove(@Param('id') id: string): Promise<User> {
return this.usersService.deleteUser({ id: +id });
}
}

View File

@@ -4,9 +4,9 @@ import { UsersController } from './users.controller';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService]
imports: [PrismaModule],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}

View File

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

View File

@@ -7,55 +7,54 @@ import * as bcrypt from 'bcryptjs';
@Injectable()
export class UsersService {
[x: string]: any;
constructor(private prisma: PrismaService) {}
constructor(private prisma: PrismaService) {}
async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
): Promise<User | null> {
return this.prisma.user.findUnique({
where: userWhereUniqueInput,
});
}
async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
): Promise<User | null> {
return this.prisma.user.findUnique({
where: userWhereUniqueInput,
});
}
async users(params: {
skip?: number;
take?: number;
cursor?: Prisma.UserWhereUniqueInput;
where?: Prisma.UserWhereInput;
orderBy?: Prisma.UserOrderByWithRelationInput;
}): Promise<User[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.user.findMany({
skip,
take,
cursor,
where,
orderBy,
});
}
async users(params: {
skip?: number;
take?: number;
cursor?: Prisma.UserWhereUniqueInput;
where?: Prisma.UserWhereInput;
orderBy?: Prisma.UserOrderByWithRelationInput;
}): Promise<User[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.user.findMany({
skip,
take,
cursor,
where,
orderBy,
});
}
async createUser(data: Prisma.UserCreateInput): Promise<User> {
data.password = await bcrypt.hash(data.password, 8)
return this.prisma.user.create({
data,
});
}
async createUser(data: Prisma.UserCreateInput): Promise<User> {
data.password = await bcrypt.hash(data.password, 8);
return this.prisma.user.create({
data,
});
}
async updateUser(params: {
where: Prisma.UserWhereUniqueInput;
data: Prisma.UserUpdateInput;
}): Promise<User> {
const { where, data } = params;
return this.prisma.user.update({
data,
where,
});
}
async updateUser(params: {
where: Prisma.UserWhereUniqueInput;
data: Prisma.UserUpdateInput;
}): Promise<User> {
const { where, data } = params;
return this.prisma.user.update({
data,
where,
});
}
async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
return this.prisma.user.delete({
where,
});
}
async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
return this.prisma.user.delete({
where,
});
}
}

View File

@@ -4,21 +4,21 @@ 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();
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).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!');
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

4
back/test/robot/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
log.html
output.xml
report.html
env

View File

@@ -0,0 +1,83 @@
*** Settings ***
Documentation Tests of the /auth route.
... Ensures that the user can authenticate on kyoo.
Resource ../rest.resource
*** Keywords ***
Login
[Documentation] Shortcut to login with the given username for future requests
[Arguments] ${username}
&{res}= POST /auth/login {"username": "${username}", "password": "password-${username}"}
Output
Integer response status 200
String response body access_token
Set Headers {"Authorization": "Bearer ${res.body.access_token}"}
Register
[Documentation] Shortcut to register with the given username for future requests
[Arguments] ${username}
&{res}= POST
... /auth/register
... {"username": "${username}", "password": "password-${username}", "email": "${username}@chromacase.moe"}
Output
Integer response status 201
Logout
[Documentation] Logout the current user, only the local client is affected.
Set Headers {"Authorization": ""}
*** Test Cases ***
Me cant be accessed without an account
Get /auth/me
Output
Integer response status 401
Bad Account
[Documentation] Login fails if user does not exist
POST /auth/login {"username": "i-don-t-exist", "password": "pass"}
Output
Integer response status 401
RegisterAndLogin
[Documentation] Create a new user and login in it
Register user-1
Login user-1
[Teardown] DELETE /auth/me
Register Duplicates
[Documentation] If two users tries to register with the same username, it fails
Register user-duplicate
# We can't use the `Register` keyword because it assert for success
POST /auth/register {"username": "user-duplicate", "password": "pass", "email": "mail@kyoo.moe"}
Output
Integer response status 400
Login user-duplicate
[Teardown] DELETE /auth/me
Delete Account
[Documentation] Check if a user can delete it's account
Register I-should-be-deleted
Login I-should-be-deleted
DELETE /auth/me
Output
Integer response status 200
Login
[Documentation] Create a new user and login in it
Register login-user
Login login-user
${res}= GET /auth/me
Output
Integer response status 200
String response body username login-user
Logout
Login login-user
${me}= Get /auth/me
Output
Output ${me}
Should Be Equal As Strings ${res["body"]} ${me["body"]}
[Teardown] DELETE /auth/me

1
back/test/robot/env/lib64 vendored Symbolic link
View File

@@ -0,0 +1 @@
lib

View File

@@ -0,0 +1,80 @@
*** Settings ***
Documentation Tests of the /lesson route.
... Ensures that the lesson CRUD works corectly.
Resource ../rest.resource
*** Test Cases ***
Post a lesson
[Documentation] Get a lesson
&{res}= POST
... /lesson
... {"name": "toto", "requiredLevel": 3, "mainSkill": "TwoHands", "description": "What am i doing"}
Output
Integer response status 201
[Teardown] DELETE /lesson/${res.body.id}
Get a lesson
[Documentation] Get a lesson
&{res}= POST
... /lesson
... {"name": "toto", "requiredLevel": 3, "mainSkill": "TwoHands", "description": "What am i doing"}
Output
Integer response status 201
&{get}= GET /lesson/${res.body.id}
Output
Should Be Equal ${res.body} ${get.body}
[Teardown] DELETE /lesson/${res.body.id}
Get a non-lesson
[Documentation] Get a lesson
&{get}= GET /lesson/toto
Output
Integer response status 400
Get a not-existing-lesson
[Documentation] Get a lesson
&{get}= GET /lesson/99999999
Output
Integer response status 404
Get all lessons
[Documentation] Get a lesson
&{res}= POST
... /lesson
... {"name": "toto", "requiredLevel": 3, "mainSkill": "TwoHands", "description": "What am i doing"}
Output
Integer response status 201
&{res2}= POST
... /lesson
... {"name": "tata", "requiredLevel": 3, "mainSkill": "TwoHands", "description": "What am i doing"}
Output
Integer response status 201
&{get}= GET /lesson
Output
Should Contain ${get.body.data} ${res.body}
Should Contain ${get.body.data} ${res2.body}
[Teardown] Run Keywords DELETE /lesson/${res.body.id}
... AND DELETE /lesson/${res2.body.id}
Get all lessons filtered
[Documentation] Get a lesson
&{res}= POST
... /lesson
... {"name": "toto", "requiredLevel": 3, "mainSkill": "TwoHands", "description": "What am i doing"}
Output
Integer response status 201
&{res2}= POST
... /lesson
... {"name": "tata", "requiredLevel": 3, "mainSkill": "Distance", "description": "What am i doing"}
Output
Integer response status 201
&{get}= GET /lesson?mainSkill=Distance
Output
Should Not Contain ${get.body.data} ${res.body}
Should Contain ${get.body.data} ${res2.body}
[Teardown] Run Keywords DELETE /lesson/${res.body.id}
... AND DELETE /lesson/${res2.body.id}

View File

@@ -1,4 +1,4 @@
*** Settings ***
Documentation Common things to handle rest requests
Library REST http://localhost:3000/api
Library REST http://localhost:3000

View File

@@ -0,0 +1,84 @@
*** Settings ***
Documentation Tests of the /song route.
... Ensures that the songs CRUD works corectly.
Resource ../rest.resource
*** Keywords ***
*** Test Cases ***
Create a song
[Documentation] Create a song
&{res}= POST /song {"name": "Mama mia", "difficulties": {}}
Output
Integer response status 201
[Teardown] DELETE /song/${res.body.id}
Find a song
[Documentation] Create a song and find it
&{res}= POST /song {"name": "Mama mia", "difficulties": {}}
Output
Integer response status 201
&{get}= GET /song/${res.body.id}
Output
Integer response status 200
Should Be Equal ${res.body} ${get.body}
[Teardown] DELETE /song/${res.body.id}
Find a song non existant
[Documentation] Find non existant song
&{get}= GET /song/9999
Integer response status 404
Find multiples songs
[Documentation] Create two songs and find them
&{res}= POST /song {"name": "Mama mia", "difficulties": {}}
Output
Integer response status 201
&{res2}= POST /song {"name": "Here we go again", "difficulties": {}}
Output
Integer response status 201
&{get}= GET /song
Output
Integer response status 200
Should Contain ${get.body.data} ${res.body}
Should Contain ${get.body.data} ${res2.body}
[Teardown] Run Keywords DELETE /song/${res.body.id}
... AND DELETE /song/${res2.body.id}
Find multiples songs filtered
[Documentation] Create two songs and find them
&{res}= POST /song {"name": "Mamamia", "difficulties": {}}
Output
Integer response status 201
&{res2}= POST /song {"name": "Here we go again", "difficulties": {}}
Output
Integer response status 201
&{get}= GET /song?name=Mamamia
Output
Integer response status 200
Should Contain ${get.body.data} ${res.body}
Should Not Contain ${get.body.data} ${res2.body}
[Teardown] Run Keywords DELETE /song/${res.body.id}
... AND DELETE /song/${res2.body.id}
Find multiples songs filtered by type
[Documentation] Create two songs and find them
&{res}= POST /song {"name": "Mamamia", "difficulties": {}}
Output
Integer response status 201
&{res2}= POST /song {"name": "Here we go again", "difficulties": {}}
Output
Integer response status 201
&{get}= GET /song?id=${res.body.id}
Output
Integer response status 200
Should Contain ${get.body.data} ${res.body}
Should Not Contain ${get.body.data} ${res2.body}
[Teardown] Run Keywords DELETE /song/${res.body.id}
... AND DELETE /song/${res2.body.id}

View File

@@ -0,0 +1,14 @@
*** Settings ***
Documentation Tests of the /users route.
... Ensures that the users CRUD works corectly.
Resource ../rest.resource
*** Test Cases ***
Create a user
[Documentation] Create a user
&{res}= POST /users {"username": "louis-boufon", "password": "pass", "email": "wow@gmail.com"}
Output
Integer response status 201
[Teardown] DELETE /users/${res.body.id}

View File

@@ -12,10 +12,10 @@
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"strictNullChecks": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
"noFallthroughCasesInSwitch": true
}
}

View File

@@ -1,6 +1,6 @@
services:
back:
build: ./back
build: ./back
ports:
- "3000:3000"
depends_on:

View File

@@ -1,20 +1,22 @@
import { NativeBaseProvider } from "native-base";
import Theme from './Theme';
import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import store from './state/Store';
import { Router } from './Navigation';
import './i18n/i18n';
const queryClient = new QueryClient();
export default function App() {
return (
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<NativeBaseProvider>
<Router/>
</NativeBaseProvider>
</QueryClientProvider>
</Provider>
);
return (
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<NativeBaseProvider theme={Theme}>
<Router />
</NativeBaseProvider>
</QueryClientProvider>
</Provider>
);
}

View File

@@ -3,7 +3,7 @@ import React from 'react';
import AuthenticationView from './views/AuthenticationView';
import HomeView from './views/HomeView/HomeView';
import { NavigationContainer } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import { useSelector } from './state/Store';
import SongLobbyView from './views/SongLobbyView';
import { translate } from './i18n/i18n';

View File

@@ -1,25 +1,129 @@
import { extendTheme } from 'native-base';
/**
* Color theme to use thoughout the application
* Using the Material Color guidelines
*/
import { DefaultTheme } from 'react-native-paper';
const Theme = {
...DefaultTheme,
const Theme = extendTheme({
roundness: 10,
colors: {
...DefaultTheme.colors,
primary: '#5db075',
background: '#F0F0F0',
surface: '#F6F6F6',
accent: '#00bdbd',
error: '#B00020',
text: '#000000',
onSurface: '#000000',
placeholder: '#C9C9C9',
notification: '#FF0000'
primary:
{
50: '#e6faea',
100: '#c8e7d0',
200: '#a7d6b5',
300: '#86c498',
400: '#65b47c',
500: '#4b9a62',
600: '#3a784b',
700: '#275635',
800: '#14341f',
900: '#001405',
},
background:
{
50: '#f2f2f2',
100: '#d9d9d9',
200: '#bfbfbf',
300: '#a6a6a6',
400: '#8c8c8c',
500: '#737373',
600: '#595959',
700: '#404040',
800: '#262626',
900: '#0d0d0d',
},
surface:
{
50: '#f2f2f2',
100: '#d9d9d9',
200: '#bfbfbf',
300: '#a6a6a6',
400: '#8c8c8c',
500: '#737373',
600: '#595959',
700: '#404040',
800: '#262626',
900: '#0d0d0d',
},
accent:
{
50: '#d8ffff',
100: '#acffff',
200: '#7dffff',
300: '#4dffff',
400: '#28ffff',
500: '#18e5e6',
600: '#00b2b3',
700: '#007f80',
800: '#004d4e',
900: '#001b1d',
},
error:
{
50: '#ffe2e9',
100: '#ffb1bf',
200: '#ff7f97',
300: '#ff4d6d',
400: '#fe1d43',
500: '#e5062b',
600: '#b30020',
700: '#810017',
800: '#4f000c',
900: '#200004',
},
text:
{
50: '#f2f2f2',
100: '#d9d9d9',
200: '#bfbfbf',
300: '#a6a6a6',
400: '#8c8c8c',
500: '#737373',
600: '#595959',
700: '#404040',
800: '#262626',
900: '#0d0d0d',
},
onSurface:
{
50: '#f2f2f2',
100: '#d9d9d9',
200: '#bfbfbf',
300: '#a6a6a6',
400: '#8c8c8c',
500: '#737373',
600: '#595959',
700: '#404040',
800: '#262626',
900: '#0d0d0d',
},
placeholder:
{
50: '#fbf0f2',
100: '#dcd8d9',
200: '#bfbfbf',
300: '#a6a6a6',
400: '#8c8c8c',
500: '#737373',
600: '#595959',
700: '#404040',
800: '#282626',
900: '#150a0d',
},
notification:
{
50: '#ffe1e1',
100: '#ffb1b1',
200: '#ff7f7f',
300: '#ff4c4c',
400: '#ff1a1a',
500: '#e60000',
600: '#b40000',
700: '#810000',
800: '#500000',
900: '#210000',
}
}
};
});
export default Theme;

View File

@@ -1,6 +1,8 @@
import { useTheme } from "native-base";
import { ActivityIndicator } from "react-native-paper";
const LoadingComponent = () => {
return <ActivityIndicator />
const theme = useTheme();
return <ActivityIndicator color={theme.colors.primary[500]}/>
}
export default LoadingComponent;

View File

@@ -38,6 +38,7 @@
"react-native-safe-area-context": "4.2.4",
"react-native-screens": "~3.11.1",
"react-native-super-grid": "^4.6.1",
"react-native-svg": "12.3.0",
"react-native-testing-library": "^6.0.0",
"react-native-web": "0.17.7",
"react-redux": "^8.0.2"

View File

@@ -1,10 +1,21 @@
import userReducer from '../state/UserSlice';
import { configureStore } from '@reduxjs/toolkit';
import languageReducer from './LanguageSlice';
import { TypedUseSelectorHook, useDispatch as reduxDispatch, useSelector as reduxSelector } from 'react-redux'
export default configureStore({
const store = configureStore({
reducer: {
user: userReducer,
language: languageReducer
},
})
})
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useDispatch: () => AppDispatch = reduxDispatch
export const useSelector: TypedUseSelectorHook<RootState> = reduxSelector
export default store

View File

@@ -1,21 +1,19 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Text, View } from 'react-native';
import { Button } from "react-native-paper";
import { useDispatch } from "react-redux";
import { useDispatch } from '../state/Store';
import { translate } from "../i18n/i18n";
import { setUserToken } from "../state/UserSlice";
import { Center, Button, Text } from 'native-base';
const AuthenticationView = () => {
const dispatch = useDispatch();
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text style={{ textAlign: "center" }}>{ translate('welcome') }</Text>
<Button onPress={() => dispatch(setUserToken('kkkk'))}>
{ translate('signinBtn') }
</Button>
</View>
);
const dispatch = useDispatch();
return (
<Center style={{ flex: 1 }}>
<Text>{translate('welcome')}</Text>
<Button variant='ghost' onPress={() => dispatch(setUserToken('kkkk'))}>
{translate('signinBtn')}
</Button>
</Center>
);
}

View File

@@ -1,6 +1,5 @@
import { useRoute } from "@react-navigation/native";
import { Image, View } from "react-native"
import { Button, Divider, IconButton, List, Surface, Text } from "react-native-paper";
import { Button, Divider, Box, Center, Image, Text, VStack, PresenceTransition, Icon } from "native-base";
import API from "../API";
import { useQuery } from 'react-query';
import LoadingComponent from "../components/Loading";
@@ -8,6 +7,7 @@ import React, { useEffect, useState } from "react";
import logo from '../assets/cover.png';
import { translate } from "../i18n/i18n";
import formatDuration from "format-duration";
import { Ionicons } from '@expo/vector-icons';
interface SongLobbyProps {
// The unique identifier to find a song
@@ -27,54 +27,60 @@ const SongLobbyView = () => {
}, [chaptersOpen]);
useEffect(() => {}, [songQuery.isLoading]);
if (songQuery.isLoading || scoresQuery.isLoading)
return <View style={{ flexGrow: 1, justifyContent: 'center' }}>
return <Center style={{ flexGrow: 1 }}>
<LoadingComponent/>
</View>
</Center>
return (
<View style={{ padding: 30, flexDirection: 'column' }}>
<View style={{ flexDirection: 'row', height: '30%'}}>
<View style={{ flex: 3 }}>
<Box style={{ padding: 30, flexDirection: 'column' }}>
<Box style={{ flexDirection: 'row', height: '30%'}}>
<Box style={{ flex: 3 }}>
<Image source={logo} style={{ height: '100%', width: undefined, resizeMode: 'contain' }}/>
</View>
<View style={{ flex: 0.5 }}/>
<View style={{ flex: 3, padding: 10, flexDirection: 'column', justifyContent: 'space-between' }}>
<View>
<Text style={{ fontWeight: 'bold', fontSize: 25 }}>{songQuery.data!.title}</Text>
</Box>
<Box style={{ flex: 0.5 }}/>
<Box style={{ flex: 3, padding: 10, flexDirection: 'column', justifyContent: 'space-between' }}>
<Box flex={1}>
<Text bold fontSize='lg'>{songQuery.data!.title}</Text>
<Text>{'3:20'} - {translate('level')} { chaptersQuery.data!.reduce((a, b) => a + b.difficulty, 0) / chaptersQuery.data!.length }</Text>
</View>
<Button icon="play" mode="contained" labelStyle={{ color: 'white' }} contentStyle={{ flexDirection: 'row-reverse' }}>
{ translate('playBtn') }
</Button>
</View>
</View>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', padding: 30}}>
<View style={{ flexDirection: 'column', alignItems: 'center' }}>
<Text style={{ fontWeight: 'bold', fontSize: 15 }}>{translate('bestScore') }</Text>
<Button width='fit-content' rightIcon={<Icon as={Ionicons} name="play-outline"/>}>{ translate('playBtn') }</Button>
</Box>
</Box>
</Box>
<Box style={{ flexDirection: 'row', justifyContent: 'space-between', padding: 30}}>
<Box style={{ flexDirection: 'column', alignItems: 'center' }}>
<Text bold fontSize='lg'>{translate('bestScore') }</Text>
<Text>{scoresQuery.data!.sort()[0]?.score}</Text>
</View>
<View style={{ flexDirection: 'column', alignItems: 'center' }}>
<Text style={{ fontWeight: 'bold', fontSize: 15}}>{translate('lastScore') }</Text>
</Box>
<Box style={{ flexDirection: 'column', alignItems: 'center' }}>
<Text bold fontSize='lg'>{translate('lastScore') }</Text>
<Text>{scoresQuery.data!.slice(-1)[0]!.score}</Text>
</View>
</View>
</Box>
</Box>
<Text style={{ paddingBottom: 10 }}>{songQuery.data!.description}</Text>
<List.Accordion
title={translate('chapters')}
expanded={chaptersOpen}
onPress={() => setChaptersOpen(!chaptersOpen)}>
{ chaptersQuery.isLoading && <LoadingComponent/>}
{ !chaptersQuery.isLoading && chaptersQuery.data!.map((chapter) =>
<>
<List.Item
key={chapter.id}
title={chapter.name}
description={`${translate('level')} ${chapter.difficulty} - ${formatDuration((chapter.end - chapter.start) * 1000)}`}
/>
<Divider />
</>
)}
</List.Accordion>
</View>
<Box flexDirection='row'>
<Button
variant='ghost'
onPress={() => setChaptersOpen(!chaptersOpen)}
endIcon={<Icon as={Ionicons} name={chaptersOpen ? "chevron-up-outline" : "chevron-down-outline"}/>}
>
{translate('chapters')}
</Button>
</Box>
<PresenceTransition visible={chaptersOpen} initial={{ opacity: 0 }}>
{ chaptersQuery.isLoading && <LoadingComponent/>}
{ !chaptersQuery.isLoading &&
<VStack flex={1} space={4} padding="4" divider={<Divider />}>
{ chaptersQuery.data!.map((chapter) =>
<Box flexGrow={1} flexDirection='row' justifyContent="space-between">
<Text>{chapter.name}</Text>
<Text>
{`${translate('level')} ${chapter.difficulty} - ${formatDuration((chapter.end - chapter.start) * 1000)}`}
</Text>
</Box>
)}
</VStack>
}
</PresenceTransition>
</Box>
)
}

View File

@@ -3619,6 +3619,11 @@ body-parser@1.19.0:
raw-body "2.4.0"
type-is "~1.6.17"
boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
bplist-creator@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.1.0.tgz#018a2d1b587f769e379ef5519103730f8963ba1e"
@@ -4216,6 +4221,30 @@ css-in-js-utils@^2.0.0:
hyphenate-style-name "^1.0.2"
isobject "^3.0.1"
css-select@^4.2.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==
dependencies:
boolbase "^1.0.0"
css-what "^6.0.1"
domhandler "^4.3.1"
domutils "^2.8.0"
nth-check "^2.0.1"
css-tree@^1.0.0-alpha.39:
version "1.1.3"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
dependencies:
mdn-data "2.0.14"
source-map "^0.6.1"
css-what@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
cssom@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
@@ -4446,6 +4475,23 @@ dom-helpers@^5.0.0:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
<<<<<<< HEAD
=======
dom-serializer@^1.0.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==
dependencies:
domelementtype "^2.0.1"
domhandler "^4.2.0"
entities "^2.0.0"
domelementtype@^2.0.1, domelementtype@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
>>>>>>> 6a31062336e1345961b0767376155d3f81c4550a
domexception@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"
@@ -4453,6 +4499,22 @@ domexception@^2.0.1:
dependencies:
webidl-conversions "^5.0.0"
domhandler@^4.2.0, domhandler@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
dependencies:
domelementtype "^2.2.0"
domutils@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
dependencies:
dom-serializer "^1.0.1"
domelementtype "^2.2.0"
domhandler "^4.2.0"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -4485,6 +4547,11 @@ end-of-stream@^1.1.0:
dependencies:
once "^1.4.0"
entities@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
env-editor@^0.4.1:
version "0.4.2"
resolved "https://registry.yarnpkg.com/env-editor/-/env-editor-0.4.2.tgz#4e76568d0bd8f5c2b6d314a9412c8fe9aa3ae861"
@@ -6882,6 +6949,11 @@ md5hex@^1.0.0:
resolved "https://registry.yarnpkg.com/md5hex/-/md5hex-1.0.0.tgz#ed74b477a2ee9369f75efee2f08d5915e52a42e8"
integrity sha512-c2YOUbp33+6thdCUi34xIyOU/a7bvGKj/3DB1iaPMTuPHf/Q2d5s4sn1FaCOO43XkXggnb08y5W2PU8UNYNLKQ==
mdn-data@2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -7530,6 +7602,13 @@ npm-run-path@^4.0.0:
dependencies:
path-key "^3.0.0"
nth-check@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
dependencies:
boolbase "^1.0.0"
nullthrows@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
@@ -8163,12 +8242,22 @@ react-native-screens@~3.11.1:
react-freeze "^1.0.0"
warn-once "^0.1.0"
<<<<<<< HEAD
react-native-super-grid@^4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/react-native-super-grid/-/react-native-super-grid-4.6.1.tgz#620a59e98375dd5138d3e6618991d09e93cbe318"
integrity sha512-YEKN//TT3DZlbz+1m6YqnclYS+T/Qn2ELrZ0fjoXzB2U/AQoBflvtw0VsJkcPkf3RGWLbD1GKbKN6Hz9fPCVfg==
dependencies:
prop-types "^15.6.0"
=======
react-native-svg@12.3.0:
version "12.3.0"
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-12.3.0.tgz#40f657c5d1ee366df23f3ec8dae76fd276b86248"
integrity sha512-ESG1g1j7/WLD7X3XRFTQHVv0r6DpbHNNcdusngAODIxG88wpTWUZkhcM3A2HJTb+BbXTFDamHv7FwtRKWQ/ALg==
dependencies:
css-select "^4.2.1"
css-tree "^1.0.0-alpha.39"
>>>>>>> 6a31062336e1345961b0767376155d3f81c4550a
react-native-testing-library@^6.0.0:
version "6.0.0"

12
node_modules/.yarn-integrity generated vendored
View File

@@ -1,12 +0,0 @@
{
"systemParams": "linux-x64-108",
"modulesFolders": [
"node_modules"
],
"flags": [],
"linkedModules": [],
"topLevelPatterns": [],
"lockfileEntries": {},
"files": [],
"artifacts": {}
}

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "Chromacase",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}