diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b696824 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7f48820 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_NAME= +POSTGRES_HOST= +DATABASE_URL= +JWT_SECRET= diff --git a/.gitignore b/.gitignore index c7564a0..0af2a7f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ pyvenv.cfg include .env prisma/migrations/* -.vscode \ No newline at end of file +.vscode +output.xml +report.html +log.html diff --git a/back/.dockerignore b/back/.dockerignore new file mode 100644 index 0000000..ff2cba8 --- /dev/null +++ b/back/.dockerignore @@ -0,0 +1,10 @@ +node_modules +Dockerfile +Dockerfile.dev +dist +test +.dockerignore +.gitignore +.eslintrc.json +.pretiierrc +README.MD diff --git a/back/.gitignore b/back/.gitignore index 22f55ad..69122cb 100644 --- a/back/.gitignore +++ b/back/.gitignore @@ -32,4 +32,9 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +# Robots tests +log.html +output.xml +report.html diff --git a/back/Dockerfile b/back/Dockerfile index 33225cc..65bba41 100644 --- a/back/Dockerfile +++ b/back/Dockerfile @@ -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 diff --git a/back/Dockerfile.dev b/back/Dockerfile.dev index d86196a..1d7d470 100644 --- a/back/Dockerfile.dev +++ b/back/Dockerfile.dev @@ -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 diff --git a/back/nest-cli.json b/back/nest-cli.json index 2566481..a427513 100644 --- a/back/nest-cli.json +++ b/back/nest-cli.json @@ -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": [] + } + }] + } } diff --git a/back/package-lock.json b/back/package-lock.json index c9fb02a..c4714a8 100644 --- a/back/package-lock.json +++ b/back/package-lock.json @@ -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", diff --git a/back/package.json b/back/package.json index ac6a010..773af23 100644 --- a/back/package.json +++ b/back/package.json @@ -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", diff --git a/back/prisma/migrations/20220926143147_/migration.sql b/back/prisma/migrations/20220926143147_/migration.sql new file mode 100644 index 0000000..2e548f1 --- /dev/null +++ b/back/prisma/migrations/20220926143147_/migration.sql @@ -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; diff --git a/back/prisma/migrations/migration_lock.toml b/back/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/back/prisma/migrations/migration_lock.toml @@ -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" \ No newline at end of file diff --git a/back/prisma/schema.prisma b/back/prisma/schema.prisma index 3f22507..f18c7b7 100644 --- a/back/prisma/schema.prisma +++ b/back/prisma/schema.prisma @@ -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") } diff --git a/back/src/app.controller.spec.ts b/back/src/app.controller.spec.ts index d22f389..e2c0215 100644 --- a/back/src/app.controller.spec.ts +++ b/back/src/app.controller.spec.ts @@ -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 = app.get(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!'); + }); + }); }); diff --git a/back/src/app.controller.ts b/back/src/app.controller.ts index cce879e..6509517 100644 --- a/back/src/app.controller.ts +++ b/back/src/app.controller.ts @@ -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(); + } } diff --git a/back/src/app.module.ts b/back/src/app.module.ts index 6b93fd1..23e15a7 100644 --- a/back/src/app.module.ts +++ b/back/src/app.module.ts @@ -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 {} diff --git a/back/src/app.service.ts b/back/src/app.service.ts index 927d7cc..7e99928 100644 --- a/back/src/app.service.ts +++ b/back/src/app.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { - getHello(): string { - return 'Hello World!'; - } + getHello(): string { + return 'Hello World!'; + } } diff --git a/back/src/auth/auth.controller.spec.ts b/back/src/auth/auth.controller.spec.ts index 27a31e6..fa86936 100644 --- a/back/src/auth/auth.controller.spec.ts +++ b/back/src/auth/auth.controller.spec.ts @@ -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); - }); + controller = module.get(AuthController); + }); - it('should be defined', () => { - expect(controller).toBeDefined(); - }); + it('should be defined', () => { + expect(controller).toBeDefined(); + }); }); diff --git a/back/src/auth/auth.controller.ts b/back/src/auth/auth.controller.ts index ea7e3a0..446ee6b 100644 --- a/back/src/auth/auth.controller.ts +++ b/back/src/auth/auth.controller.ts @@ -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 { 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 { 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 { + return this.usersService.deleteUser({ id: req.user.id }); + } } diff --git a/back/src/auth/auth.module.ts b/back/src/auth/auth.module.ts index 4c721b4..3f09593 100644 --- a/back/src/auth/auth.module.ts +++ b/back/src/auth/auth.module.ts @@ -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 {} diff --git a/back/src/auth/auth.service.spec.ts b/back/src/auth/auth.service.spec.ts index 800ab66..a80b4b6 100644 --- a/back/src/auth/auth.service.spec.ts +++ b/back/src/auth/auth.service.spec.ts @@ -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); - }); + service = module.get(AuthService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/back/src/auth/auth.service.ts b/back/src/auth/auth.service.ts index 44831f1..1568cd8 100644 --- a/back/src/auth/auth.service.ts +++ b/back/src/auth/auth.service.ts @@ -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 { - const user = await this.userService.user({username}); + async validateUser( + username: string, + password: string, + ): Promise { + 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, }; } } diff --git a/back/src/auth/constants.ts b/back/src/auth/constants.ts index 6f4f3ba..38163cb 100644 --- a/back/src/auth/constants.ts +++ b/back/src/auth/constants.ts @@ -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"); - } -} \ No newline at end of file + return this.configService.get('JWT_SECRET'); + }; +} diff --git a/back/src/auth/dto/login.dto.ts b/back/src/auth/dto/login.dto.ts index 78d3526..2282784 100644 --- a/back/src/auth/dto/login.dto.ts +++ b/back/src/auth/dto/login.dto.ts @@ -1,4 +1,4 @@ -import { IsNotEmpty } from "class-validator"; +import { IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class LoginDto { diff --git a/back/src/auth/dto/register.dto.ts b/back/src/auth/dto/register.dto.ts index d8dd9a8..58529e4 100644 --- a/back/src/auth/dto/register.dto.ts +++ b/back/src/auth/dto/register.dto.ts @@ -1,4 +1,4 @@ -import { IsNotEmpty } from "class-validator"; +import { IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class RegisterDto { diff --git a/back/src/auth/interface/payload.interface.ts b/back/src/auth/interface/payload.interface.ts index e414f74..2fbce8a 100644 --- a/back/src/auth/interface/payload.interface.ts +++ b/back/src/auth/interface/payload.interface.ts @@ -1,4 +1,4 @@ export default interface PayloadInterface { username: string; id: number; -} \ No newline at end of file +} diff --git a/back/src/auth/jwt-auth.guard.ts b/back/src/auth/jwt-auth.guard.ts index 18588a5..2155290 100644 --- a/back/src/auth/jwt-auth.guard.ts +++ b/back/src/auth/jwt-auth.guard.ts @@ -2,4 +2,4 @@ import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() -export class JwtAuthGuard extends AuthGuard('jwt') {} \ No newline at end of file +export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/back/src/auth/jwt.strategy.ts b/back/src/auth/jwt.strategy.ts index a03932e..844062c 100644 --- a/back/src/auth/jwt.strategy.ts +++ b/back/src/auth/jwt.strategy.ts @@ -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 }; - } -} \ No newline at end of file + async validate(payload: any) { + return { id: payload.id, username: payload.username }; + } +} diff --git a/back/src/auth/local-auth.guard.ts b/back/src/auth/local-auth.guard.ts index a3a4bf3..ccf962b 100644 --- a/back/src/auth/local-auth.guard.ts +++ b/back/src/auth/local-auth.guard.ts @@ -1,4 +1,3 @@ - import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; diff --git a/back/src/auth/local.strategy.ts b/back/src/auth/local.strategy.ts index efa05fb..c9d1cce 100644 --- a/back/src/auth/local.strategy.ts +++ b/back/src/auth/local.strategy.ts @@ -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 { - const user = await this.authService.validateUser(username, password); - if (!user) { - throw new UnauthorizedException(); - } - return user; - } -} \ No newline at end of file + async validate( + username: string, + password: string, + ): Promise { + const user = await this.authService.validateUser(username, password); + if (!user) { + throw new UnauthorizedException(); + } + return user; + } +} diff --git a/back/src/auth/models/jwt.ts b/back/src/auth/models/jwt.ts new file mode 100644 index 0000000..bc06aeb --- /dev/null +++ b/back/src/auth/models/jwt.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class JwtToken { + @ApiProperty() + access_token: string; +} diff --git a/back/src/lesson/lesson.controller.spec.ts b/back/src/lesson/lesson.controller.spec.ts new file mode 100644 index 0000000..9350d52 --- /dev/null +++ b/back/src/lesson/lesson.controller.spec.ts @@ -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); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/back/src/lesson/lesson.controller.ts b/back/src/lesson/lesson.controller.ts new file mode 100644 index 0000000..1b54bff --- /dev/null +++ b/back/src/lesson/lesson.controller.ts @@ -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> { + 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 { + 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 { + 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 { + try { + return await this.lessonService.delete(id); + } catch (e) { + console.log(e); + throw new BadRequestException(null, e.toString()); + } + } +} diff --git a/back/src/lesson/lesson.module.ts b/back/src/lesson/lesson.module.ts new file mode 100644 index 0000000..66799da --- /dev/null +++ b/back/src/lesson/lesson.module.ts @@ -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 {} diff --git a/back/src/lesson/lesson.service.spec.ts b/back/src/lesson/lesson.service.spec.ts new file mode 100644 index 0000000..3713a08 --- /dev/null +++ b/back/src/lesson/lesson.service.spec.ts @@ -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); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/back/src/lesson/lesson.service.ts b/back/src/lesson/lesson.service.ts new file mode 100644 index 0000000..aed4a1e --- /dev/null +++ b/back/src/lesson/lesson.service.ts @@ -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 { + const { skip, take, cursor, where, orderBy } = params; + return this.prisma.lesson.findMany({ + skip, + take, + cursor, + where, + orderBy, + }); + } + + async get(id: number): Promise { + return this.prisma.lesson.findFirst({ + where: { + id: id, + }, + }); + } + + async create(lesson: Lesson): Promise { + return this.prisma.lesson.create({ data: lesson }); + } + + async delete(id: number): Promise { + return this.prisma.lesson.delete({ where: { id: id } }); + } +} diff --git a/back/src/main.ts b/back/src/main.ts index 0f304a2..f82b632 100644 --- a/back/src/main.ts +++ b/back/src/main.ts @@ -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(); diff --git a/back/src/models/plage.ts b/back/src/models/plage.ts new file mode 100644 index 0000000..1da0f13 --- /dev/null +++ b/back/src/models/plage.ts @@ -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 { + @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; + } +} diff --git a/back/src/models/user.ts b/back/src/models/user.ts new file mode 100644 index 0000000..d650ae0 --- /dev/null +++ b/back/src/models/user.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class User { + @ApiProperty() + id: number; + @ApiProperty() + username: string; + @ApiProperty() + password: string; + @ApiProperty() + email: string; +} diff --git a/back/src/prisma/prisma.module.ts b/back/src/prisma/prisma.module.ts index e569e2d..3e6d1b6 100644 --- a/back/src/prisma/prisma.module.ts +++ b/back/src/prisma/prisma.module.ts @@ -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 {} diff --git a/back/src/prisma/prisma.service.spec.ts b/back/src/prisma/prisma.service.spec.ts index a68cb9e..d694c29 100644 --- a/back/src/prisma/prisma.service.spec.ts +++ b/back/src/prisma/prisma.service.spec.ts @@ -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); - }); + service = module.get(PrismaService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/back/src/prisma/prisma.service.ts b/back/src/prisma/prisma.service.ts index edf6532..6feee25 100644 --- a/back/src/prisma/prisma.service.ts +++ b/back/src/prisma/prisma.service.ts @@ -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(); + }); + } } diff --git a/back/src/song/dto/create-song.dto.ts b/back/src/song/dto/create-song.dto.ts new file mode 100644 index 0000000..02b4a2d --- /dev/null +++ b/back/src/song/dto/create-song.dto.ts @@ -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; +} diff --git a/back/src/song/song.controller.spec.ts b/back/src/song/song.controller.spec.ts new file mode 100644 index 0000000..bbe129b --- /dev/null +++ b/back/src/song/song.controller.spec.ts @@ -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); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/back/src/song/song.controller.ts b/back/src/song/song.controller.ts new file mode 100644 index 0000000..cb06efe --- /dev/null +++ b/back/src/song/song.controller.ts @@ -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> { + 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; + } +} diff --git a/back/src/song/song.module.ts b/back/src/song/song.module.ts new file mode 100644 index 0000000..1cf9780 --- /dev/null +++ b/back/src/song/song.module.ts @@ -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 {} diff --git a/back/src/song/song.service.spec.ts b/back/src/song/song.service.spec.ts new file mode 100644 index 0000000..dd86410 --- /dev/null +++ b/back/src/song/song.service.spec.ts @@ -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); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/back/src/song/song.service.ts b/back/src/song/song.service.ts new file mode 100644 index 0000000..4a7a285 --- /dev/null +++ b/back/src/song/song.service.ts @@ -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 { + return this.prisma.song.create({ + data, + }); + } + + async song( + songWhereUniqueInput: Prisma.SongWhereUniqueInput, + ): Promise { + return this.prisma.song.findUnique({ + where: songWhereUniqueInput, + }); + } + + async songs(params: { + skip?: number; + take?: number; + cursor?: Prisma.SongWhereUniqueInput; + where?: Prisma.SongWhereInput; + orderBy?: Prisma.SongOrderByWithRelationInput; + }): Promise { + const { skip, take, cursor, where, orderBy } = params; + return this.prisma.song.findMany({ + skip, + take, + cursor, + where, + orderBy, + }); + } + + async deleteSong(where: Prisma.SongWhereUniqueInput): Promise { + return this.prisma.song.delete({ + where, + }); + } +} diff --git a/back/src/users/dto/create-user.dto.ts b/back/src/users/dto/create-user.dto.ts index b8eef8f..2466a61 100644 --- a/back/src/users/dto/create-user.dto.ts +++ b/back/src/users/dto/create-user.dto.ts @@ -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; } diff --git a/back/src/users/entities/user.entity.ts b/back/src/users/entities/user.entity.ts deleted file mode 100644 index 4f82c14..0000000 --- a/back/src/users/entities/user.entity.ts +++ /dev/null @@ -1 +0,0 @@ -export class User {} diff --git a/back/src/users/users.controller.spec.ts b/back/src/users/users.controller.spec.ts index a76d310..7721e48 100644 --- a/back/src/users/users.controller.spec.ts +++ b/back/src/users/users.controller.spec.ts @@ -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); - }); + controller = module.get(UsersController); + }); - it('should be defined', () => { - expect(controller).toBeDefined(); - }); + it('should be defined', () => { + expect(controller).toBeDefined(); + }); }); diff --git a/back/src/users/users.controller.ts b/back/src/users/users.controller.ts index ed2c15d..ca5b5f2 100644 --- a/back/src/users/users.controller.ts +++ b/back/src/users/users.controller.ts @@ -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 { + return this.usersService.createUser(createUserDto); + } - @Get() - findAll() { - return this.usersService.users({}); - } + @Get() + findAll(): Promise { + 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 { + 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 { + 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 { + return this.usersService.deleteUser({ id: +id }); + } } diff --git a/back/src/users/users.module.ts b/back/src/users/users.module.ts index 89332aa..d1e76e7 100644 --- a/back/src/users/users.module.ts +++ b/back/src/users/users.module.ts @@ -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 {} diff --git a/back/src/users/users.service.spec.ts b/back/src/users/users.service.spec.ts index 62815ba..38b96e7 100644 --- a/back/src/users/users.service.spec.ts +++ b/back/src/users/users.service.spec.ts @@ -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); - }); + service = module.get(UsersService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/back/src/users/users.service.ts b/back/src/users/users.service.ts index c167c87..df3a75d 100644 --- a/back/src/users/users.service.ts +++ b/back/src/users/users.service.ts @@ -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 { - return this.prisma.user.findUnique({ - where: userWhereUniqueInput, - }); - } + async user( + userWhereUniqueInput: Prisma.UserWhereUniqueInput, + ): Promise { + return this.prisma.user.findUnique({ + where: userWhereUniqueInput, + }); + } - async users(params: { - skip?: number; - take?: number; - cursor?: Prisma.UserWhereUniqueInput; - where?: Prisma.UserWhereInput; - orderBy?: Prisma.UserOrderByWithRelationInput; - }): Promise { - 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 { + const { skip, take, cursor, where, orderBy } = params; + return this.prisma.user.findMany({ + skip, + take, + cursor, + where, + orderBy, + }); + } - async createUser(data: Prisma.UserCreateInput): Promise { - data.password = await bcrypt.hash(data.password, 8) - return this.prisma.user.create({ - data, - }); - } + async createUser(data: Prisma.UserCreateInput): Promise { + data.password = await bcrypt.hash(data.password, 8); + return this.prisma.user.create({ + data, + }); + } - async updateUser(params: { - where: Prisma.UserWhereUniqueInput; - data: Prisma.UserUpdateInput; - }): Promise { - const { where, data } = params; - return this.prisma.user.update({ - data, - where, - }); - } + async updateUser(params: { + where: Prisma.UserWhereUniqueInput; + data: Prisma.UserUpdateInput; + }): Promise { + const { where, data } = params; + return this.prisma.user.update({ + data, + where, + }); + } - async deleteUser(where: Prisma.UserWhereUniqueInput): Promise { - return this.prisma.user.delete({ - where, - }); - } + async deleteUser(where: Prisma.UserWhereUniqueInput): Promise { + return this.prisma.user.delete({ + where, + }); + } } diff --git a/back/test/app.e2e-spec.ts b/back/test/app.e2e-spec.ts index 50cda62..5c49beb 100644 --- a/back/test/app.e2e-spec.ts +++ b/back/test/app.e2e-spec.ts @@ -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!'); + }); }); diff --git a/back/test/robot/.gitignore b/back/test/robot/.gitignore new file mode 100644 index 0000000..73a37df --- /dev/null +++ b/back/test/robot/.gitignore @@ -0,0 +1,4 @@ +log.html +output.xml +report.html +env diff --git a/back/test/robot/auth/auth.robot b/back/test/robot/auth/auth.robot new file mode 100644 index 0000000..15d8176 --- /dev/null +++ b/back/test/robot/auth/auth.robot @@ -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 diff --git a/back/test/robot/env/lib64 b/back/test/robot/env/lib64 new file mode 120000 index 0000000..7951405 --- /dev/null +++ b/back/test/robot/env/lib64 @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/back/test/robot/lesson/lesson.robot b/back/test/robot/lesson/lesson.robot new file mode 100644 index 0000000..9c72ff7 --- /dev/null +++ b/back/test/robot/lesson/lesson.robot @@ -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} diff --git a/back/test/robot/rest.resource b/back/test/robot/rest.resource index 41db3c9..42db9ed 100644 --- a/back/test/robot/rest.resource +++ b/back/test/robot/rest.resource @@ -1,4 +1,4 @@ *** Settings *** Documentation Common things to handle rest requests -Library REST http://localhost:3000/api +Library REST http://localhost:3000 diff --git a/back/test/robot/songs/songs.robot b/back/test/robot/songs/songs.robot new file mode 100644 index 0000000..73379c6 --- /dev/null +++ b/back/test/robot/songs/songs.robot @@ -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} diff --git a/back/test/robot/users/users.robot b/back/test/robot/users/users.robot new file mode 100644 index 0000000..034d07d --- /dev/null +++ b/back/test/robot/users/users.robot @@ -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} diff --git a/back/tsconfig.json b/back/tsconfig.json index adb614c..cf106ab 100644 --- a/back/tsconfig.json +++ b/back/tsconfig.json @@ -12,10 +12,10 @@ "baseUrl": "./", "incremental": true, "skipLibCheck": true, - "strictNullChecks": false, + "strictNullChecks": true, "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": true } } diff --git a/docker-compose.yml b/docker-compose.yml index ea0af46..a80b7bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: back: - build: ./back + build: ./back ports: - "3000:3000" depends_on: diff --git a/front/App.tsx b/front/App.tsx index 2639d50..46a8bd2 100644 --- a/front/App.tsx +++ b/front/App.tsx @@ -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 ( - - - - - - - - ); + return ( + + + + + + + + ); } diff --git a/front/Navigation.tsx b/front/Navigation.tsx index 38daade..555f244 100644 --- a/front/Navigation.tsx +++ b/front/Navigation.tsx @@ -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'; diff --git a/front/Theme.tsx b/front/Theme.tsx index fa016c4..62725e9 100644 --- a/front/Theme.tsx +++ b/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; \ No newline at end of file diff --git a/front/components/Loading.tsx b/front/components/Loading.tsx index dcf1f5c..775479c 100644 --- a/front/components/Loading.tsx +++ b/front/components/Loading.tsx @@ -1,6 +1,8 @@ +import { useTheme } from "native-base"; import { ActivityIndicator } from "react-native-paper"; const LoadingComponent = () => { - return + const theme = useTheme(); + return } export default LoadingComponent; \ No newline at end of file diff --git a/front/package.json b/front/package.json index 49a1782..afd0291 100644 --- a/front/package.json +++ b/front/package.json @@ -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" diff --git a/front/state/Store.ts b/front/state/Store.ts index 253f578..9a2d390 100644 --- a/front/state/Store.ts +++ b/front/state/Store.ts @@ -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 }, -}) \ No newline at end of file +}) + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType +// 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 = reduxSelector + +export default store \ No newline at end of file diff --git a/front/views/AuthenticationView.tsx b/front/views/AuthenticationView.tsx index abd012c..416c8ff 100644 --- a/front/views/AuthenticationView.tsx +++ b/front/views/AuthenticationView.tsx @@ -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 ( - - { translate('welcome') } - - - ); + const dispatch = useDispatch(); + return ( +
+ {translate('welcome')} + +
+ ); } diff --git a/front/views/SongLobbyView.tsx b/front/views/SongLobbyView.tsx index a6de683..c8fbe57 100644 --- a/front/views/SongLobbyView.tsx +++ b/front/views/SongLobbyView.tsx @@ -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 + return
- +
return ( - - - + + + - - - - - {songQuery.data!.title} + + + + + {songQuery.data!.title} {'3:20'} - {translate('level')} { chaptersQuery.data!.reduce((a, b) => a + b.difficulty, 0) / chaptersQuery.data!.length } - - - - - - - {translate('bestScore') } + + + + + + + {translate('bestScore') } {scoresQuery.data!.sort()[0]?.score} - - - {translate('lastScore') } + + + {translate('lastScore') } {scoresQuery.data!.slice(-1)[0]!.score} - - + + {songQuery.data!.description} - setChaptersOpen(!chaptersOpen)}> - { chaptersQuery.isLoading && } - { !chaptersQuery.isLoading && chaptersQuery.data!.map((chapter) => - <> - - - - )} - - + + + + + { chaptersQuery.isLoading && } + { !chaptersQuery.isLoading && + }> + { chaptersQuery.data!.map((chapter) => + + {chapter.name} + + {`${translate('level')} ${chapter.difficulty} - ${formatDuration((chapter.end - chapter.start) * 1000)}`} + + + )} + + } + + ) } diff --git a/front/yarn.lock b/front/yarn.lock index e06eadd..f4df595 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -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" diff --git a/node_modules/.yarn-integrity b/node_modules/.yarn-integrity deleted file mode 100644 index 19ce570..0000000 --- a/node_modules/.yarn-integrity +++ /dev/null @@ -1,12 +0,0 @@ -{ - "systemParams": "linux-x64-108", - "modulesFolders": [ - "node_modules" - ], - "flags": [], - "linkedModules": [], - "topLevelPatterns": [], - "lockfileEntries": {}, - "files": [], - "artifacts": {} -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..64e05ed --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Chromacase", + "lockfileVersion": 2, + "requires": true, + "packages": {} +}