Merge branch 'main' into feature/adc/#224-genre-view

This commit is contained in:
danis
2023-09-05 09:41:20 +02:00
56 changed files with 5782 additions and 6097 deletions

11092
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,51 +21,55 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^2.1.0",
"@nestjs/core": "^8.0.0",
"@nestjs/jwt": "^8.0.1",
"@nestjs/common": "^10.1.0",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.1.0",
"@nestjs/jwt": "^10.1.0",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^8.2.2",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/swagger": "^5.2.1",
"@prisma/client": "^4.4.0",
"@nestjs/passport": "^10.0.0",
"@nestjs/platform-express": "^10.1.0",
"@nestjs/swagger": "^7.1.2",
"@prisma/client": "^5.0.0",
"@types/bcrypt": "^5.0.0",
"@types/bcryptjs": "^2.4.2",
"@types/passport": "^1.0.9",
"@types/passport": "^1.0.12",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"passport-jwt": "^4.0.0",
"class-validator": "^0.14.0",
"node-fetch": "^2.6.12",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"swagger-ui-express": "^4.5.0"
"rimraf": "^5.0.1",
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.0"
},
"devDependencies": {
"@nestjs/cli": "^8.0.0",
"@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0",
"@types/express": "^4.17.13",
"@types/jest": "27.4.1",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^27.2.5",
"prettier": "^2.3.2",
"prisma": "^4.4.0",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.10.1",
"typescript": "^4.3.5"
"@nestjs/cli": "^10.1.10",
"@nestjs/schematics": "^10.0.1",
"@nestjs/testing": "^10.1.0",
"@types/express": "^4.17.17",
"@types/jest": "29.5.3",
"@types/multer": "^1.4.7",
"@types/node": "^20.4.4",
"@types/passport-google-oauth20": "^2.0.11",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.6.1",
"prettier": "^3.0.0",
"prisma": "^5.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.6"
},
"jest": {
"moduleFileExtensions": [

View File

@@ -0,0 +1,12 @@
/*
Warnings:
- A unique constraint covering the columns `[googleID]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "googleID" TEXT,
ALTER COLUMN "password" DROP NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "User_googleID_key" ON "User"("googleID");

View File

@@ -12,8 +12,9 @@ datasource db {
model User {
id Int @id @default(autoincrement())
username String @unique
password String
password String?
email String
googleID String? @unique
isGuest Boolean @default(false)
partyPlayed Int @default(0)
LessonHistory LessonHistory[]

View File

@@ -12,6 +12,12 @@ import {
InternalServerErrorException,
Patch,
NotFoundException,
Req,
UseInterceptors,
UploadedFile,
HttpStatus,
ParseFilePipeBuilder,
Response,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';
@@ -32,6 +38,9 @@ import { Profile } from './dto/profile.dto';
import { Setting } from 'src/models/setting';
import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto';
import { SettingsService } from 'src/settings/settings.service';
import { AuthGuard } from '@nestjs/passport';
import { FileInterceptor } from '@nestjs/platform-express';
import { writeFile } from 'fs';
@ApiTags('auth')
@Controller('auth')
@@ -42,12 +51,27 @@ export class AuthController {
private settingsService: SettingsService,
) {}
@Get('login/google')
@UseGuards(AuthGuard('google'))
googleLogin() {}
@Get('logged/google')
@UseGuards(AuthGuard('google'))
async googleLoginCallbakc(@Req() req: any) {
let user = await this.usersService.user({ googleID: req.user.googleID });
if (!user) {
user = await this.usersService.createUser(req.user);
await this.settingsService.createUserSetting(user.id);
}
return this.authService.login(user);
}
@Post('register')
async register(@Body() registerDto: RegisterDto): Promise<void> {
try {
const user = await this.usersService.createUser(registerDto)
const user = await this.usersService.createUser(registerDto);
await this.settingsService.createUserSetting(user.id);
} catch(e) {
} catch (e) {
console.error(e);
throw new BadRequestException();
}
@@ -69,6 +93,40 @@ export class AuthController {
return this.authService.login(user);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'The user profile picture' })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Get('me/picture')
async getProfilePicture(@Request() req: any, @Response() res: any) {
return await this.usersService.getProfilePicture(req.user.id, res);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'The user profile picture' })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Post('me/picture')
@UseInterceptors(FileInterceptor('file'))
async postProfilePicture(
@Request() req: any,
@UploadedFile(
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: 'jpeg',
})
.build({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
}),
)
file: Express.Multer.File,
) {
const path = `/data/${req.user.id}.jpg`
writeFile(path, file.buffer, (err) => {
if (err) throw err;
});
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'Successfully logged in', type: User })
@@ -116,25 +174,28 @@ export class AuthController {
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({description: 'Successfully edited settings', type: Setting})
@ApiUnauthorizedResponse({description: 'Invalid token'})
@ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Patch('me/settings')
udpateSettings(
@Request() req: any,
@Body() settingUserDto: UpdateSettingDto): Promise<Setting> {
@Body() settingUserDto: UpdateSettingDto,
): Promise<Setting> {
return this.settingsService.updateUserSettings({
where: { userId: +req.user.id},
where: { userId: +req.user.id },
data: settingUserDto,
});
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({description: 'Successfully edited settings', type: Setting})
@ApiUnauthorizedResponse({description: 'Invalid token'})
@ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Get('me/settings')
async getSettings(@Request() req: any): Promise<Setting> {
const result = await this.settingsService.getUserSetting({ userId: +req.user.id });
const result = await this.settingsService.getUserSetting({
userId: +req.user.id,
});
if (!result) throw new NotFoundException();
return result;
}

View File

@@ -9,6 +9,7 @@ import { ConfigModule } from '@nestjs/config';
import { ConfigService } from '@nestjs/config';
import { JwtStrategy } from './jwt.strategy';
import { SettingsModule } from 'src/settings/settings.module';
import { GoogleStrategy } from './google.strategy';
@Module({
imports: [
@@ -25,7 +26,7 @@ import { SettingsModule } from 'src/settings/settings.module';
inject: [ConfigService],
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
providers: [AuthService, LocalStrategy, JwtStrategy, GoogleStrategy],
controllers: [AuthController],
})
export class AuthModule {}

View File

@@ -15,7 +15,7 @@ export class AuthService {
password: string,
): Promise<PayloadInterface | null> {
const user = await this.userService.user({ username });
if (user && bcrypt.compareSync(password, user.password)) {
if (user && user.password && bcrypt.compareSync(password, user.password)) {
return {
username: user.username,
id: user.id,

View File

@@ -0,0 +1,35 @@
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
scope: ['email', 'profile'],
});
}
async validate(
_accessToken: string,
_refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const user = {
email: profile.emails[0].value,
username: profile.displayName,
password: null,
googleID: profile.id,
// firstName: name.givenName,
// lastName: name.familyName,
// picture: photos[0].value,
};
done(null, user);
return user;
}
}

View File

@@ -1,13 +1,11 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { PrismaService } from './prisma/prisma.service';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const prismaService = app.get(PrismaService);
await prismaService.enableShutdownHooks(app);
app.enableShutdownHooks();
const config = new DocumentBuilder()
.setTitle('Chromacase')

View File

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

View File

@@ -1,4 +1,4 @@
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
import { Controller, Get, Param, NotFoundException, Response } from '@nestjs/common';
import { UsersService } from './users.service';
import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger';
import { User } from 'src/models/user';
@@ -20,4 +20,9 @@ export class UsersController {
if (!ret) throw new NotFoundException();
return ret;
}
@Get(':id/picture')
async getPicture(@Response() res: any, @Param('id') id: number) {
return await this.usersService.getProfilePicture(+id, res);
}
}

View File

@@ -1,8 +1,15 @@
import { Injectable } from '@nestjs/common';
import {
Injectable,
InternalServerErrorException,
NotFoundException,
StreamableFile,
} from '@nestjs/common';
import { User, Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import * as bcrypt from 'bcryptjs';
import { randomUUID } from 'crypto';
import { createHash, randomUUID } from 'crypto';
import { createReadStream, existsSync } from 'fs';
import fetch from 'node-fetch';
@Injectable()
export class UsersService {
@@ -34,7 +41,7 @@ export class UsersService {
}
async createUser(data: Prisma.UserCreateInput): Promise<User> {
data.password = await bcrypt.hash(data.password, 8);
if (data.password) data.password = await bcrypt.hash(data.password, 8);
return this.prisma.user.create({
data,
});
@@ -72,4 +79,24 @@ export class UsersService {
where,
});
}
async getProfilePicture(userId: number, res: any) {
const path = `/data/${userId}.jpg`;
if (existsSync(path)) {
const file = createReadStream(path);
return file.pipe(res);
}
// We could not find a profile icon locally, using gravatar instead.
const user = await this.user({ id: userId });
if (!user) throw new InternalServerErrorException();
const hash = createHash('md5')
.update(user.email.trim().toLowerCase())
.digest('hex');
const resp = await fetch(
`https://www.gravatar.com/avatar/${hash}.jpg?d=404&s=200`,
);
for (const [k, v] of resp.headers)
resp.headers.set(k, v);
resp.body!.pipe(res);
}
}