nicolas-arnaud 2 years ago
parent
commit
58040f5e9d
  1. 3
      .env_sample
  2. 3
      back/entrypoint.sh
  3. 11784
      back/volume/package-lock.json
  4. 4
      back/volume/package.json
  5. 2
      back/volume/src/app.module.ts
  6. 2
      back/volume/src/auth/42.decorator.ts
  7. 1
      back/volume/src/auth/42.strategy.ts
  8. 41
      back/volume/src/auth/auth.controller.ts
  9. 46
      back/volume/src/auth/auth.module.ts
  10. 55
      back/volume/src/auth/auth.service.ts
  11. 15
      back/volume/src/auth/mails/confirm.hbs
  12. 12
      back/volume/src/auth/mails/confirmed.hbs
  13. 6
      back/volume/src/chat/chat.gateway.ts
  14. 2
      back/volume/src/chat/entity/channel.entity.ts
  15. 2
      back/volume/src/chat/entity/message.entity.ts
  16. 6
      back/volume/src/users/dto/user.dto.ts
  17. 12
      back/volume/src/users/entity/user.entity.ts
  18. 16
      back/volume/src/users/users.controller.ts
  19. 25
      back/volume/src/users/users.service.ts
  20. 8
      front/volume/src/App.svelte
  21. 11
      front/volume/src/Auth.ts
  22. 4
      front/volume/src/components/Friends.svelte
  23. 23
      front/volume/src/components/Profile.svelte

3
.env_sample

@ -7,6 +7,9 @@ POSTGRES_DB=transcendence
PGADMIN_DEFAULT_EMAIL=admin@pg.com PGADMIN_DEFAULT_EMAIL=admin@pg.com
PGADMIN_DEFAULT_PASSWORD=admin PGADMIN_DEFAULT_PASSWORD=admin
MAIL_USER=vaganiwast@gmail.com
MAIL_PASSWORD=
FRONT_FPS=144 FRONT_FPS=144
HOST=localhost HOST=localhost

3
back/entrypoint.sh

@ -6,6 +6,9 @@ POSTGRES_USER=$POSTGRES_USER
POSTGRES_PASSWORD=$POSTGRES_PASSWORD POSTGRES_PASSWORD=$POSTGRES_PASSWORD
POSTGRES_DB=$POSTGRES_DB POSTGRES_DB=$POSTGRES_DB
MAIL_USER=$MAIL_USER
MAIL_PASSWORD=$MAIL_PASSWORD
HOST=$HOST HOST=$HOST
FRONT_PORT=$FRONT_PORT FRONT_PORT=$FRONT_PORT
BACK_PORT=$BACK_PORT BACK_PORT=$BACK_PORT

11784
back/volume/package-lock.json

File diff suppressed because it is too large

4
back/volume/package.json

@ -21,10 +21,12 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@nestjs-modules/mailer": "^1.8.1",
"@nestjs/axios": "^2.0.0", "@nestjs/axios": "^2.0.0",
"@nestjs/common": "^9.0.0", "@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.1", "@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.0.0", "@nestjs/core": "^9.0.0",
"@nestjs/jwt": "^10.0.2",
"@nestjs/mapped-types": "^1.2.2", "@nestjs/mapped-types": "^1.2.2",
"@nestjs/passport": "^9.0.3", "@nestjs/passport": "^9.0.3",
"@nestjs/platform-express": "^9.0.0", "@nestjs/platform-express": "^9.0.0",
@ -43,6 +45,7 @@
"@types/express-session": "^1.17.6", "@types/express-session": "^1.17.6",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/node": "^16.18.14", "@types/node": "^16.18.14",
"@types/nodemailer": "^6.4.7",
"@types/passport": "^1.0.12", "@types/passport": "^1.0.12",
"@types/ws": "^8.5.3", "@types/ws": "^8.5.3",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
@ -51,6 +54,7 @@
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"joi": "^17.8.3", "joi": "^17.8.3",
"nodemailer": "^6.9.1",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-42": "^1.2.6", "passport-42": "^1.2.6",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",

2
back/volume/src/app.module.ts

@ -19,6 +19,8 @@ import { UsersModule } from './users/users.module'
POSTGRES_USER: Joi.string().required(), POSTGRES_USER: Joi.string().required(),
POSTGRES_PASSWORD: Joi.string().required(), POSTGRES_PASSWORD: Joi.string().required(),
POSTGRES_DB: Joi.string().required(), POSTGRES_DB: Joi.string().required(),
MAIL_USER: Joi.string().required(),
MAIL_PASSWORD: Joi.string().required(),
JWT_SECRET: Joi.string().required(), JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION_TIME: Joi.string().required(), JWT_EXPIRATION_TIME: Joi.string().required(),
HOST: Joi.string().required(), HOST: Joi.string().required(),

2
back/volume/src/auth/42.decorator.ts

@ -1,7 +1,7 @@
import { createParamDecorator, type ExecutionContext } from '@nestjs/common' import { createParamDecorator, type ExecutionContext } from '@nestjs/common'
import { type Profile } from 'passport-42' import { type Profile } from 'passport-42'
export const FtUser = createParamDecorator( export const Profile42 = createParamDecorator(
(data: unknown, ctx: ExecutionContext): Profile => { (data: unknown, ctx: ExecutionContext): Profile => {
const request = ctx.switchToHttp().getRequest() const request = ctx.switchToHttp().getRequest()
return request.user return request.user

1
back/volume/src/auth/42.strategy.ts

@ -37,6 +37,7 @@ export class FtStrategy extends PassportStrategy(Strategy, '42') {
newUser.ftId = profile.id as number newUser.ftId = profile.id as number
newUser.username = profile.username newUser.username = profile.username
newUser.avatar = `${ftId}.jpg` newUser.avatar = `${ftId}.jpg`
newUser.email = profile.emails[0].value
void this.usersService.create(newUser) void this.usersService.create(newUser)
const file = createWriteStream(`avatars/${ftId}.jpg`) const file = createWriteStream(`avatars/${ftId}.jpg`)
get(profile._json.image.versions.small, function (response) { get(profile._json.image.versions.small, function (response) {

41
back/volume/src/auth/auth.controller.ts

@ -1,9 +1,21 @@
import { Controller, Get, Redirect, UseGuards, Res, Req } from '@nestjs/common' import {
Controller,
Get,
Redirect,
UseGuards,
Res,
Req,
Post,
Body
} from '@nestjs/common'
import { Response, Request } from 'express' import { Response, Request } from 'express'
import { Profile } from 'passport-42'
import { FtOauthGuard, AuthenticatedGuard } from './42-auth.guard' import { FtOauthGuard, AuthenticatedGuard } from './42-auth.guard'
import { FtUser } from './42.decorator' import { Profile } from 'passport-42'
import { Profile42 } from './42.decorator'
import { AuthService } from './auth.service'
import { UsersService } from 'src/users/users.service'
const frontHost = const frontHost =
process.env.HOST !== undefined && process.env.HOST !== '' process.env.HOST !== undefined && process.env.HOST !== ''
@ -16,6 +28,11 @@ const frontPort =
@Controller('log') @Controller('log')
export class AuthController { export class AuthController {
constructor (
private readonly authService: AuthService,
private readonly usersService: UsersService
) {}
@Get('in') @Get('in')
@UseGuards(FtOauthGuard) @UseGuards(FtOauthGuard)
ftAuth (): void {} ftAuth (): void {}
@ -31,9 +48,25 @@ export class AuthController {
response.cookie('connect.sid', request.cookies['connect.sid']) response.cookie('connect.sid', request.cookies['connect.sid'])
} }
@Get('/verify')
@UseGuards(AuthenticatedGuard)
@Redirect(`http://${frontHost}:${frontPort}`)
async VerifyEmail (@Profile42() profile: Profile) {
const ftId: number = profile.id
const user = await this.usersService.findUser(ftId)
if (user == null) throw new Error('User not found')
this.authService.sendConfirmationEmail(user)
}
@Post('/verify')
@Redirect(`http://${frontHost}:${frontPort}`)
async Verify (@Body() body: any) {
await this.authService.verifyAccount(body.code)
}
@Get('profile') @Get('profile')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
profile (@FtUser() user: Profile): any { profile (@Profile42() user: Profile): any {
return { user } return { user }
} }

46
back/volume/src/auth/auth.module.ts

@ -1,14 +1,54 @@
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { UsersModule } from 'src/users/users.module' import { UsersModule } from 'src/users/users.module'
import { PassportModule } from '@nestjs/passport' import { PassportModule } from '@nestjs/passport'
import { ConfigService } from '@nestjs/config' import { ConfigModule, ConfigService } from '@nestjs/config'
import { AuthController } from './auth.controller' import { AuthController } from './auth.controller'
import { FtStrategy } from './42.strategy' import { FtStrategy } from './42.strategy'
import { SessionSerializer } from './session.serializer' import { SessionSerializer } from './session.serializer'
import { JwtModule } from '@nestjs/jwt'
import { MailerModule } from '@nestjs-modules/mailer'
import { AuthService } from './auth.service'
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'
const mail_user =
process.env.MAIL_USER && process.env.MAIL_USER !== ''
? process.env.MAIL_USER
: ''
const mail_pass =
process.env.MAIL_PASSWORD && process.env.MAIL_PASSWORD !== ''
? process.env.MAIL_PASSWORD
: ''
@Module({ @Module({
imports: [UsersModule, PassportModule], imports: [
providers: [ConfigService, FtStrategy, SessionSerializer], UsersModule,
PassportModule,
ConfigModule.forRoot(),
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '60s' }
}),
MailerModule.forRoot({
transport: {
service: 'gmail',
auth: {
user: mail_user,
pass: mail_pass
}
},
template: {
dir: 'src/auth/mails',
adapter: new HandlebarsAdapter(),
options: {
strict: true
}
},
defaults: {
from: '"No Reply" vaganiwast@gmail.com'
}
})
],
providers: [ConfigService, FtStrategy, SessionSerializer, AuthService],
controllers: [AuthController] controllers: [AuthController]
}) })
export class AuthModule {} export class AuthModule {}

55
back/volume/src/auth/auth.service.ts

@ -0,0 +1,55 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
import { type User } from 'src/users/entity/user.entity'
import { UsersService } from 'src/users/users.service'
import { MailerService } from '@nestjs-modules/mailer'
import { UserDto } from 'src/users/dto/user.dto'
@Injectable()
export class AuthService {
constructor (
private readonly usersService: UsersService,
private readonly mailerService: MailerService
) {}
async sendConfirmedEmail (user: User) {
const { email, username } = user
await this.mailerService.sendMail({
to: email,
subject: 'Welcome to ft_transcendence! Email Confirmed',
template: 'confirmed',
context: {
username,
email
}
})
}
async sendConfirmationEmail (user: User) {
user.authToken = Math.floor(10000 + Math.random() * 90000).toString()
this.usersService.save(user)
await this.mailerService.sendMail({
to: user.email,
subject: 'Welcome to ft_transcendence! Confirm Email',
template: 'confirm',
context: {
username: user.username,
code: user.authToken
}
})
}
async verifyAccount (code: string): Promise<boolean> {
const user = await this.usersService.findByCode(code)
if (!user) {
throw new HttpException(
'Verification code has expired or not found',
HttpStatus.UNAUTHORIZED
)
}
user.authToken = ''
user.isVerified = true
await this.usersService.save(user)
await this.sendConfirmedEmail(user)
return true
}
}

15
back/volume/src/auth/mails/confirm.hbs

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Transcendence confirmation mail</title>
</head>
<body>
<h2>Hello {{username}}! </h2>
<p> Once you clicked on the next verify button, you will have access to the app</p>
<form action="http://localhost:3001/log/verify" method="post">
<button type="submit" name="code" value={{code}}>Verify</button>
</form
</body>
</html>

12
back/volume/src/auth/mails/confirmed.hbs

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Welcome to transcendence</title>
</head>
<body>
<h2>Hello {{username}}! </h2>
<p>You well verified your account for this session.</p>
</body>
</html>

6
back/volume/src/chat/chat.gateway.ts

@ -61,9 +61,9 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
): Promise<Channel | null> { ): Promise<Channel | null> {
const channel = new Channel() const channel = new Channel()
channel.name = channeldto.name channel.name = channeldto.name
const owner = await this.userService.findUser(channeldto.owner) // const owner = await this.userService.findUser(channeldto.owner)
if (owner == null) return null // if (owner == null) return null
channel.owners.push(owner) // channel.owners.push(owner)
channel.password = channeldto.password channel.password = channeldto.password
/// .../// /// ...///
return await this.chatService.createChannel(channel, socket.data.user) return await this.chatService.createChannel(channel, socket.data.user)

2
back/volume/src/chat/entity/channel.entity.ts

@ -22,7 +22,7 @@ export class Channel {
@ManyToMany(() => User) @ManyToMany(() => User)
@JoinTable() @JoinTable()
owners: User[] owner: User
@ManyToMany(() => User) @ManyToMany(() => User)
@JoinTable() @JoinTable()

2
back/volume/src/chat/entity/message.entity.ts

@ -28,7 +28,7 @@ export class Message {
channel: Channel channel: Channel
@CreateDateColumn() @CreateDateColumn()
created_at: Date createdAt: Date
} }
export default Message export default Message

6
back/volume/src/users/dto/user.dto.ts

@ -17,6 +17,12 @@ export class UserDto {
@IsOptional() @IsOptional()
readonly avatar: string readonly avatar: string
@IsOptional()
readonly authToken: string
@IsOptional()
readonly isVerified: boolean
} }
export class AvatarUploadDto { export class AvatarUploadDto {

12
back/volume/src/users/entity/user.entity.ts

@ -22,6 +22,18 @@ export class User {
@Column({ unique: true }) @Column({ unique: true })
ftId: number ftId: number
@Column({ unique: true })
email: string
@Column({ select: false, nullable: true })
authToken: string
@Column({ default: false })
twoFA: boolean
@Column({ default: false, nullable: true })
isVerified: boolean
@Column({ unique: true }) @Column({ unique: true })
socketKey: string socketKey: string

16
back/volume/src/users/users.controller.ts

@ -23,7 +23,7 @@ import { UserDto, AvatarUploadDto } from './dto/user.dto'
import { PongService } from 'src/pong/pong.service' import { PongService } from 'src/pong/pong.service'
import { AuthenticatedGuard } from 'src/auth/42-auth.guard' import { AuthenticatedGuard } from 'src/auth/42-auth.guard'
import { FtUser } from 'src/auth/42.decorator' import { Profile42 } from 'src/auth/42.decorator'
import { Profile } from 'passport-42' import { Profile } from 'passport-42'
import { ApiBody, ApiConsumes } from '@nestjs/swagger' import { ApiBody, ApiConsumes } from '@nestjs/swagger'
@ -51,13 +51,13 @@ export class UsersController {
@Get('friends') @Get('friends')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async getFriends (@FtUser() profile: Profile): Promise<User[]> { async getFriends (@Profile42() profile: Profile): Promise<User[]> {
return await this.usersService.getFriends(profile.id) return await this.usersService.getFriends(profile.id)
} }
@Get('invits') @Get('invits')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async getInvits (@FtUser() profile: Profile): Promise<User[]> { async getInvits (@Profile42() profile: Profile): Promise<User[]> {
return await this.usersService.getInvits(profile.id) return await this.usersService.getInvits(profile.id)
} }
@ -110,7 +110,7 @@ export class UsersController {
type: AvatarUploadDto type: AvatarUploadDto
}) })
async changeAvatar ( async changeAvatar (
@FtUser() profile: Profile, @Profile42() profile: Profile,
@UploadedFile() file: Express.Multer.File @UploadedFile() file: Express.Multer.File
): Promise<void> { ): Promise<void> {
if (file === undefined) return if (file === undefined) return
@ -120,7 +120,7 @@ export class UsersController {
@Get('avatar') @Get('avatar')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async getAvatar ( async getAvatar (
@FtUser() profile: Profile, @Profile42() profile: Profile,
@Res({ passthrough: true }) response: Response @Res({ passthrough: true }) response: Response
): Promise<StreamableFile> { ): Promise<StreamableFile> {
return await this.getAvatarById(profile.id, response) return await this.getAvatarById(profile.id, response)
@ -136,7 +136,7 @@ export class UsersController {
@Get('invit/:username') @Get('invit/:username')
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async invitUser ( async invitUser (
@FtUser() profile: Profile, @Profile42() profile: Profile,
@Param('username') username: string @Param('username') username: string
): Promise<void> { ): Promise<void> {
const target: User | null = await this.usersService.findUserByName(username) const target: User | null = await this.usersService.findUserByName(username)
@ -187,7 +187,7 @@ export class UsersController {
@Get() @Get()
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async getUser (@FtUser() profile: Profile): Promise<User | null> { async getUser (@Profile42() profile: Profile): Promise<User | null> {
return await this.usersService.findUser(profile.id) return await this.usersService.findUser(profile.id)
} }
@ -195,7 +195,7 @@ export class UsersController {
@UseGuards(AuthenticatedGuard) @UseGuards(AuthenticatedGuard)
async updateUser ( async updateUser (
@Body() payload: UserDto, @Body() payload: UserDto,
@FtUser() profile: Profile @Profile42() profile: Profile
): Promise<BadRequestException | User | null> { ): Promise<BadRequestException | User | null> {
const user = await this.usersService.findUser(profile.id) const user = await this.usersService.findUser(profile.id)
if (user == null) throw new BadRequestException('User not found.') if (user == null) throw new BadRequestException('User not found.')

25
back/volume/src/users/users.service.ts

@ -41,6 +41,7 @@ export class UsersService {
const users = await this.usersRepository.find({}) const users = await this.usersRepository.find({})
users.forEach((usr) => { users.forEach((usr) => {
if (Date.now() - usr.lastAccess > 60000) { if (Date.now() - usr.lastAccess > 60000) {
usr.isVerified = false
usr.status = 'offline' usr.status = 'offline'
this.usersRepository.save(usr).catch((err) => { this.usersRepository.save(usr).catch((err) => {
console.log(err) console.log(err)
@ -161,21 +162,33 @@ export class UsersService {
friends: true friends: true
} }
}) })
if (target === null) return 'Target not found.' if (target == null) return 'Target not found.'
const id = user.followers.findIndex( const id = user.followers.findIndex(
(follower) => follower.ftId === targetFtId (follower) => follower.ftId === targetFtId
) )
if (target.followers.findIndex((follower) => follower.ftId === user.ftId) !== -1) { if (
target.followers.findIndex((follower) => follower.ftId === user.ftId) !== -1
) {
return 'Invitation already sent.' return 'Invitation already sent.'
} else if (user.followers.findIndex((follower) => follower.ftId === targetFtId) !== -1) { } else if (
user.followers.findIndex((follower) => follower.ftId === targetFtId) !== -1
) {
user.friends.push(target) user.friends.push(target)
target.friends.push(user) target.friends.push(user)
user.followers.slice(id, 1) user.followers.slice(id, 1)
await this.usersRepository.save(user) await this.usersRepository.save(user)
} else { } else target.followers.push(user)
target.followers.push(user)
}
await this.usersRepository.save(target) await this.usersRepository.save(target)
return 'OK' return 'OK'
} }
async findByCode (code: string) {
const user = await this.usersRepository.findOneBy({ authToken: code })
if (user == null) throw new BadRequestException('User not found')
return user
}
async turnOnTwoFactorAuthentication (ftId: number) {
return await this.usersRepository.update({ ftId }, { twoFA: true})
}
} }

8
front/volume/src/App.svelte

@ -32,7 +32,7 @@
import type { Friend } from "./components/Friends.svelte"; import type { Friend } from "./components/Friends.svelte";
import type { ChannelsType } from "./components/Channels.svelte"; import type { ChannelsType } from "./components/Channels.svelte";
import { store, getUser, login, API_URL } from "./Auth"; import { store, getUser, login, verify, API_URL } from "./Auth";
import FakeLogin from "./FakeLogin.svelte"; import FakeLogin from "./FakeLogin.svelte";
// Single Page Application config // Single Page Application config
@ -197,6 +197,8 @@
<div> <div>
{#if $store === null} {#if $store === null}
<h1><button type="button" on:click={login}>Log In</button></h1> <h1><button type="button" on:click={login}>Log In</button></h1>
{:else if $store.twoFA === true && $store.isVerified === false}
<h1><button type="button" on:click={verify}>verify</button></h1>
{:else} {:else}
<Navbar <Navbar
{clickProfile} {clickProfile}
@ -255,9 +257,7 @@
{/if} {/if}
{#if appState === APPSTATE.PROFILE} {#if appState === APPSTATE.PROFILE}
<div on:click={resetAppState} on:keydown={resetAppState}> <div on:click={resetAppState} on:keydown={resetAppState}>
<Profile user={$store} edit={1} <Profile user={$store} edit={1} on:view-history={openIdHistory} />
on:view-history={openIdHistory}
/>
</div> </div>
{/if} {/if}
{#if appState === APPSTATE.PROFILE_ID} {#if appState === APPSTATE.PROFILE_ID}

11
front/volume/src/Auth.ts

@ -29,6 +29,17 @@ export function login() {
window.location.replace(API_URL + "/log/in"); window.location.replace(API_URL + "/log/in");
} }
export function verify() {
fetch(API_URL + "/log/verify", {
method: "get",
mode: "cors",
credentials: "include",
});
alert(
"We have sent you an email to verify your account. Check the mailbox which is linked to your 42's profile."
);
}
export function logout() { export function logout() {
window.location.replace(API_URL + "/log/out"); window.location.replace(API_URL + "/log/out");
store.set(null); store.set(null);

4
front/volume/src/components/Friends.svelte

@ -19,8 +19,8 @@
if (response.ok) { if (response.ok) {
alert("Invitation send."); alert("Invitation send.");
} else { } else {
const error = (await response.json()).message const error = (await response.json()).message;
alert("Invitation failed: " + error);; alert("Invitation failed: " + error);
} }
} }
</script> </script>

23
front/volume/src/components/Profile.svelte

@ -6,7 +6,7 @@
matchs: number; matchs: number;
winrate: number; winrate: number;
rank: number; rank: number;
is2faEnabled: boolean; twoFA: boolean;
} }
</script> </script>
@ -35,7 +35,16 @@
async function handle2fa(event: Event) { async function handle2fa(event: Event) {
event.preventDefault(); event.preventDefault();
alert("Trying to " + (user.is2faEnabled ? "disable" : "enable") + " 2FA"); let response = await fetch(API_URL, {
headers: { "content-type": "application/json" },
method: "POST",
body: JSON.stringify(user),
credentials: "include",
});
if (response.ok) {
alert("Succefully " + (user.twoFA ? "disabled" : "enabled") + " 2FA");
user.twoFA = !user.twoFA;
}
} }
</script> </script>
@ -66,7 +75,11 @@
{/if} {/if}
</div> </div>
<div class="profile-body"> <div class="profile-body">
<p><button on:click={() => dispatch("view-history", user.ftId)}>View History</button></p> <p>
<button on:click={() => dispatch("view-history", user.ftId)}
>View History</button
>
</p>
<p>Wins: {user.wins}</p> <p>Wins: {user.wins}</p>
<p>Looses: {user.looses}</p> <p>Looses: {user.looses}</p>
<p>Winrate: {user.winrate}%</p> <p>Winrate: {user.winrate}%</p>
@ -84,10 +97,10 @@
> >
</form> </form>
<button type="button" on:click={handle2fa}> <button type="button" on:click={handle2fa}>
{#if user.is2faEnabled} {#if user.twoFA}
Disable 2FA Disable 2FA
{:else} {:else}
Enable user.2FA Enable 2FA
{/if} {/if}
</button> </button>
<button id="logout" type="button" on:click={logout}>Log Out</button> <button id="logout" type="button" on:click={logout}>Log Out</button>

Loading…
Cancel
Save