Front: Pull Migration to Native Base
This commit is contained in:
9
.editorconfig
Normal file
9
.editorconfig
Normal 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
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
POSTGRES_USER=
|
||||
POSTGRES_PASSWORD=
|
||||
POSTGRES_NAME=
|
||||
POSTGRES_HOST=
|
||||
DATABASE_URL=
|
||||
JWT_SECRET=
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,4 +5,7 @@ pyvenv.cfg
|
||||
include
|
||||
.env
|
||||
prisma/migrations/*
|
||||
.vscode
|
||||
.vscode
|
||||
output.xml
|
||||
report.html
|
||||
log.html
|
||||
|
||||
10
back/.dockerignore
Normal file
10
back/.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
Dockerfile
|
||||
Dockerfile.dev
|
||||
dist
|
||||
test
|
||||
.dockerignore
|
||||
.gitignore
|
||||
.eslintrc.json
|
||||
.pretiierrc
|
||||
README.MD
|
||||
7
back/.gitignore
vendored
7
back/.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
35
back/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
88
back/prisma/migrations/20220926143147_/migration.sql
Normal file
88
back/prisma/migrations/20220926143147_/migration.sql
Normal 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;
|
||||
3
back/prisma/migrations/migration_lock.toml
Normal file
3
back/prisma/migrations/migration_lock.toml
Normal 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"
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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!');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LoginDto {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class RegisterDto {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default interface PayloadInterface {
|
||||
username: string;
|
||||
id: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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') {}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
6
back/src/auth/models/jwt.ts
Normal file
6
back/src/auth/models/jwt.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class JwtToken {
|
||||
@ApiProperty()
|
||||
access_token: string;
|
||||
}
|
||||
18
back/src/lesson/lesson.controller.spec.ts
Normal file
18
back/src/lesson/lesson.controller.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
103
back/src/lesson/lesson.controller.ts
Normal file
103
back/src/lesson/lesson.controller.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
back/src/lesson/lesson.module.ts
Normal file
11
back/src/lesson/lesson.module.ts
Normal 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 {}
|
||||
18
back/src/lesson/lesson.service.spec.ts
Normal file
18
back/src/lesson/lesson.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
41
back/src/lesson/lesson.service.ts
Normal file
41
back/src/lesson/lesson.service.ts
Normal 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 } });
|
||||
}
|
||||
}
|
||||
@@ -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
51
back/src/models/plage.ts
Normal 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
12
back/src/models/user.ts
Normal 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;
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
12
back/src/song/dto/create-song.dto.ts
Normal file
12
back/src/song/dto/create-song.dto.ts
Normal 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;
|
||||
}
|
||||
18
back/src/song/song.controller.spec.ts
Normal file
18
back/src/song/song.controller.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
65
back/src/song/song.controller.ts
Normal file
65
back/src/song/song.controller.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
11
back/src/song/song.module.ts
Normal file
11
back/src/song/song.module.ts
Normal 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 {}
|
||||
18
back/src/song/song.service.spec.ts
Normal file
18
back/src/song/song.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
45
back/src/song/song.service.ts
Normal file
45
back/src/song/song.service.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export class User {}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
4
back/test/robot/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
log.html
|
||||
output.xml
|
||||
report.html
|
||||
env
|
||||
83
back/test/robot/auth/auth.robot
Normal file
83
back/test/robot/auth/auth.robot
Normal 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
1
back/test/robot/env/lib64
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
lib
|
||||
80
back/test/robot/lesson/lesson.robot
Normal file
80
back/test/robot/lesson/lesson.robot
Normal 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}
|
||||
@@ -1,4 +1,4 @@
|
||||
*** Settings ***
|
||||
Documentation Common things to handle rest requests
|
||||
|
||||
Library REST http://localhost:3000/api
|
||||
Library REST http://localhost:3000
|
||||
|
||||
84
back/test/robot/songs/songs.robot
Normal file
84
back/test/robot/songs/songs.robot
Normal 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}
|
||||
14
back/test/robot/users/users.robot
Normal file
14
back/test/robot/users/users.robot
Normal 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}
|
||||
@@ -12,10 +12,10 @@
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
"noFallthroughCasesInSwitch": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
back:
|
||||
build: ./back
|
||||
build: ./back
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
136
front/Theme.tsx
136
front/Theme.tsx
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
12
node_modules/.yarn-integrity
generated
vendored
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"systemParams": "linux-x64-108",
|
||||
"modulesFolders": [
|
||||
"node_modules"
|
||||
],
|
||||
"flags": [],
|
||||
"linkedModules": [],
|
||||
"topLevelPatterns": [],
|
||||
"lockfileEntries": {},
|
||||
"files": [],
|
||||
"artifacts": {}
|
||||
}
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Chromacase",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Reference in New Issue
Block a user