├── .dockerignore ├── front ├── .prettierrc.json ├── src │ ├── index.css │ ├── routes │ │ ├── Home.tsx │ │ ├── Landing.tsx │ │ ├── chat_modes │ │ │ ├── context.css │ │ │ ├── type │ │ │ │ └── chat.type.tsx │ │ │ ├── chatRoom.css │ │ │ ├── chatPreview.css │ │ │ ├── tags.css │ │ │ ├── settingCard.tsx │ │ │ ├── roomStatus.css │ │ │ └── card.css │ │ ├── UserInterface.tsx │ │ ├── Auth │ │ │ ├── SignIn.tsx │ │ │ ├── SignUp.tsx │ │ │ └── Auth.css │ │ ├── Chat.css │ │ ├── profile_types │ │ │ ├── private │ │ │ │ ├── users_relations │ │ │ │ │ ├── UsersRelations.tsx │ │ │ │ │ ├── BlockedList.tsx │ │ │ │ │ ├── PendingList.tsx │ │ │ │ │ └── FriendsList.tsx │ │ │ │ ├── TwoFA.tsx │ │ │ │ └── ModifyUserInfo.tsx │ │ │ ├── Profiles.css │ │ │ └── public │ │ │ │ └── UserPublicProfile.css │ │ ├── Watch.css │ │ ├── TwoFAValidation.tsx │ │ ├── gameRequestCard.tsx │ │ ├── game.interfaces.tsx │ │ └── LeaderBoard.css │ ├── ressources │ │ ├── imgs │ │ │ └── mvaldes.jpeg │ │ └── icons │ │ │ └── Icon_Pen.svg │ ├── Components │ │ ├── SimpleToolTip.tsx │ │ ├── Navbar.css │ │ └── Navbar.tsx │ ├── custom.d.ts │ ├── globals │ │ ├── variables.tsx │ │ ├── contexts.tsx │ │ └── Interfaces.tsx │ ├── queries │ │ ├── headers.tsx │ │ ├── otherUserQueries.tsx │ │ ├── gamesQueries.tsx │ │ ├── updateUserQueries.tsx │ │ ├── avatarQueries.tsx │ │ ├── userQueries.tsx │ │ ├── userFriendsQueries.tsx │ │ ├── twoFAQueries.tsx │ │ └── authQueries.tsx │ ├── ContextMenus │ │ ├── COnUserSimple.tsx │ │ └── COnUser.tsx │ ├── modals │ │ ├── MLogoutValid.tsx │ │ ├── MUploadAvatar.tsx │ │ └── MActivateTwoFA.tsx │ ├── hooks │ │ ├── UserInfoHooks.tsx │ │ └── AuthHooks.tsx │ ├── toasts │ │ └── TAlert.tsx │ ├── index.tsx │ ├── App.tsx │ └── App.css ├── .prettierignore ├── public │ ├── font │ │ └── clip.regular.ttf │ ├── index.html │ └── particlesjs-config.json ├── Dockerfile ├── .gitignore ├── tsconfig.json ├── package.json └── README.md ├── back ├── src │ ├── game │ │ ├── dto │ │ │ ├── index.ts │ │ │ └── game.dto.ts │ │ ├── interfaces │ │ │ ├── player.interface.ts │ │ │ ├── client.interface.ts │ │ │ ├── gameData.interface.ts │ │ │ └── room.interface.ts │ │ ├── watch │ │ │ └── watch.controller.ts │ │ ├── game.module.ts │ │ └── game.controller.ts │ ├── upload │ │ ├── dto │ │ │ ├── index.ts │ │ │ └── upload.dto.ts │ │ ├── utils │ │ │ └── upload.utils.ts │ │ ├── upload.module.ts │ │ ├── upload.service.ts │ │ └── upload.controller.ts │ ├── auth │ │ ├── filter │ │ │ ├── index.ts │ │ │ └── redirect.login.ts │ │ ├── interfaces │ │ │ ├── index.ts │ │ │ └── 42.interface.ts │ │ ├── dto │ │ │ ├── index.ts │ │ │ ├── 2fa.dto.ts │ │ │ └── auth.dto.ts │ │ ├── 2FA │ │ │ ├── index.ts │ │ │ ├── 2fa.controller.ts │ │ │ └── 2fa.service.ts │ │ ├── guard │ │ │ ├── index.ts │ │ │ ├── rt.guard.ts │ │ │ ├── 42auth.guard.ts │ │ │ └── jwt.guard.ts │ │ ├── strategy │ │ │ ├── index.ts │ │ │ ├── 42.strategy.ts │ │ │ ├── rt.strategy.ts │ │ │ └── jwt.strategy.ts │ │ ├── auth.module.ts │ │ └── auth.controller.ts │ ├── user │ │ ├── dto │ │ │ ├── index.ts │ │ │ ├── update.dto.ts │ │ │ └── user.dto.ts │ │ ├── statuses.ts │ │ ├── user.module.ts │ │ └── user.controller.spec.ts │ ├── decorators │ │ ├── index.ts │ │ ├── public.decorator.ts │ │ ├── get-current-user-decorator-id.ts │ │ └── get-current-user-decorator.ts │ ├── chat │ │ ├── chat.module.ts │ │ ├── filter │ │ │ └── transformation-filter.ts │ │ ├── dto │ │ │ └── chat.dto.ts │ │ └── type │ │ │ └── chat.type.ts │ ├── prisma │ │ ├── prisma.module.ts │ │ └── prisma.service.ts │ ├── main.ts │ └── app.module.ts ├── tsconfig.build.json ├── .prettierrc ├── prisma │ ├── migrations │ │ └── migration_lock.toml │ └── schema.prisma ├── Dockerfile ├── test │ ├── jest-e2e.json │ └── app.e2e-spec.ts ├── nest-cli.json ├── tsconfig.json ├── .gitignore ├── .eslintrc.js ├── .env ├── env.dev ├── README.md └── package.json ├── screenshots ├── chat.png ├── watch.png ├── invite_game.png ├── leaderboard.png ├── private_profile.png └── public_profile.png ├── .gitignore ├── nginx ├── nginx.conf └── ssl-certs │ ├── cert.crt │ └── cert.key ├── .env ├── yarn.lock ├── Makefile ├── .gitmessage.txt ├── docker-compose.yml └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /front/.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /front/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | -------------------------------------------------------------------------------- /back/src/game/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './game.dto'; 2 | -------------------------------------------------------------------------------- /back/src/upload/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './upload.dto'; 2 | -------------------------------------------------------------------------------- /back/src/auth/filter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './redirect.login'; 2 | -------------------------------------------------------------------------------- /front/ .prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage -------------------------------------------------------------------------------- /back/src/auth/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './42.interface'; 2 | -------------------------------------------------------------------------------- /back/src/auth/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.dto'; 2 | export * from './2fa.dto'; -------------------------------------------------------------------------------- /back/src/user/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.dto'; 2 | export * from './update.dto'; 3 | -------------------------------------------------------------------------------- /back/src/user/statuses.ts: -------------------------------------------------------------------------------- 1 | export enum Status { 2 | offline, 3 | online, 4 | inGame, 5 | } -------------------------------------------------------------------------------- /back/src/auth/2FA/index.ts: -------------------------------------------------------------------------------- 1 | export * from './2fa.controller'; 2 | export * from './2fa.service'; 3 | -------------------------------------------------------------------------------- /screenshots/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ft-transcendence/transcendence/HEAD/screenshots/chat.png -------------------------------------------------------------------------------- /screenshots/watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ft-transcendence/transcendence/HEAD/screenshots/watch.png -------------------------------------------------------------------------------- /front/src/routes/Home.tsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return
Home
; 3 | } 4 | -------------------------------------------------------------------------------- /screenshots/invite_game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ft-transcendence/transcendence/HEAD/screenshots/invite_game.png -------------------------------------------------------------------------------- /screenshots/leaderboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ft-transcendence/transcendence/HEAD/screenshots/leaderboard.png -------------------------------------------------------------------------------- /back/src/game/interfaces/player.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Player { 2 | roomId: number; 3 | playerNb: number; 4 | } 5 | -------------------------------------------------------------------------------- /screenshots/private_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ft-transcendence/transcendence/HEAD/screenshots/private_profile.png -------------------------------------------------------------------------------- /screenshots/public_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ft-transcendence/transcendence/HEAD/screenshots/public_profile.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node modules 2 | node_modules/ 3 | dist/ 4 | .nfs* 5 | *.swp 6 | # migrations/ 7 | package-lock.json 8 | .vscode/ 9 | -------------------------------------------------------------------------------- /back/src/auth/guard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.guard'; 2 | export * from './42auth.guard'; 3 | export * from './rt.guard'; 4 | -------------------------------------------------------------------------------- /back/src/auth/strategy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.strategy'; 2 | export * from './42.strategy'; 3 | export * from './rt.strategy'; 4 | -------------------------------------------------------------------------------- /back/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /front/public/font/clip.regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ft-transcendence/transcendence/HEAD/front/public/font/clip.regular.ttf -------------------------------------------------------------------------------- /front/src/ressources/imgs/mvaldes.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ft-transcendence/transcendence/HEAD/front/src/ressources/imgs/mvaldes.jpeg -------------------------------------------------------------------------------- /back/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "tabWidth": 4, 5 | "useTabs": true, 6 | "bracketSpacing": true 7 | } -------------------------------------------------------------------------------- /front/src/routes/Landing.tsx: -------------------------------------------------------------------------------- 1 | export default function Landing() { 2 | return ( 3 |
4 |

Landing

5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /back/src/game/interfaces/client.interface.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io'; 2 | 3 | export interface Client extends Socket { 4 | data: { id: number }; 5 | } 6 | -------------------------------------------------------------------------------- /back/src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './public.decorator'; 2 | export * from './get-current-user-decorator-id'; 3 | export * from './get-current-user-decorator'; 4 | -------------------------------------------------------------------------------- /back/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /back/src/upload/dto/upload.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsNumber } from 'class-validator'; 2 | 3 | export class uploadDto { 4 | @IsNumber() 5 | @IsNotEmpty() 6 | userId: number; 7 | } 8 | -------------------------------------------------------------------------------- /front/src/Components/SimpleToolTip.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from "react-bootstrap"; 2 | 3 | export const renderTooltip = (text: string) => ( 4 | {text} 5 | ); 6 | -------------------------------------------------------------------------------- /back/Dockerfile: -------------------------------------------------------------------------------- 1 | ### 2 | ## BACK END DOCKERFILE 3 | ### 4 | 5 | FROM node:lts-alpine 6 | 7 | COPY ./ /app/ 8 | 9 | WORKDIR /app 10 | 11 | RUN yarn install 12 | 13 | CMD ["yarn", "run", "start"] -------------------------------------------------------------------------------- /front/Dockerfile: -------------------------------------------------------------------------------- 1 | ### 2 | ## FRONT END DOCKERFILE 3 | ### 4 | 5 | FROM node:lts-alpine 6 | 7 | COPY ./ /app/ 8 | 9 | WORKDIR /app 10 | 11 | RUN yarn install 12 | 13 | CMD ["yarn", "run", "build"] -------------------------------------------------------------------------------- /front/src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: any; 3 | export default content; 4 | } 5 | 6 | declare module "*.jpeg" { 7 | const content: any; 8 | export default content; 9 | } 10 | -------------------------------------------------------------------------------- /front/src/routes/chat_modes/context.css: -------------------------------------------------------------------------------- 1 | .react-contexify__will-leave--scale { 2 | animation-duration: 0s !important; 3 | } 4 | 5 | .react-contexify .react-contexify__submenu { 6 | left: -100% !important; 7 | } 8 | -------------------------------------------------------------------------------- /back/src/auth/interfaces/42.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface to extract user data from 42 API 3 | */ 4 | 5 | export interface Profile_42 { 6 | id: number; 7 | username: string; 8 | email: string; 9 | avatar: string; 10 | } 11 | -------------------------------------------------------------------------------- /back/src/decorators/public.decorator.ts: -------------------------------------------------------------------------------- 1 | /* GLOBAL MODULES */ 2 | import { SetMetadata } from '@nestjs/common'; 3 | 4 | // Create @Public decorator - used to set public property 5 | export const Public = () => SetMetadata('isPublic', true); 6 | -------------------------------------------------------------------------------- /back/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /back/src/auth/guard/rt.guard.ts: -------------------------------------------------------------------------------- 1 | /* AUTH Guards */ 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | // Create RtGuard - used to validate refresh token 5 | export class RtGuard extends AuthGuard('jwt-refresh') { 6 | constructor() { 7 | super(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /front/src/globals/variables.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { IUserInputsRef } from "./Interfaces"; 3 | 4 | export const GUserInputsRefs: IUserInputsRef = { 5 | username: React.createRef(), 6 | email: React.createRef(), 7 | password: React.createRef(), 8 | }; 9 | -------------------------------------------------------------------------------- /front/src/globals/contexts.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | import { AuthContextType } from "./Interfaces"; 3 | 4 | export let AuthContext = createContext(null!); 5 | 6 | export function useAuth() { 7 | return useContext(AuthContext); 8 | } 9 | -------------------------------------------------------------------------------- /front/src/routes/UserInterface.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | import { CNavBar } from "../Components/Navbar"; 3 | import "../App.css"; 4 | 5 | export default function UserInterface() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /back/src/auth/guard/42auth.guard.ts: -------------------------------------------------------------------------------- 1 | /* GLOBAL MODULES */ 2 | import { Injectable } from '@nestjs/common'; 3 | /* GUARDS */ 4 | import { AuthGuard } from '@nestjs/passport'; 5 | 6 | // Create FortyTwoAuthGuard - used access 42 API 7 | @Injectable() 8 | export class FortyTwoAuthGuard extends AuthGuard('42auth') { 9 | constructor() { 10 | super(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /back/src/user/dto/update.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsEmail, IsNotEmpty, MaxLength } from 'class-validator'; 2 | 3 | export class UpdateUsernameDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | @MaxLength(32) 7 | username: string; 8 | } 9 | 10 | export class UpdateEmailDto { 11 | @IsString() 12 | @IsEmail() 13 | @MaxLength(50) 14 | @IsNotEmpty() 15 | email: string; 16 | } 17 | -------------------------------------------------------------------------------- /back/src/game/interfaces/gameData.interface.ts: -------------------------------------------------------------------------------- 1 | export interface GameData { 2 | paddleLeft?: number; 3 | paddleRight?: number; 4 | xBall?: number; 5 | yBall?: number; 6 | player1Name: string; 7 | player2Name: string; 8 | player1Avatar: number; 9 | player2Avater: number; 10 | player1Score: number; 11 | player2Score: number; 12 | gameID?: number; 13 | startTime?: Date; 14 | } 15 | -------------------------------------------------------------------------------- /back/src/game/watch/watch.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { GameService } from '../game.service'; 3 | 4 | @Controller('watch') 5 | export class WatchController { 6 | constructor(private gameService: GameService) {} 7 | 8 | //@Public() 9 | @Get('/') 10 | getOngoingGame() { 11 | return JSON.stringify(this.gameService.getGameList()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 1024; 3 | listen [::]:1024; 4 | 5 | root /var/www/transcendence; 6 | index index.html index.htm index.nginx-debian.html; 7 | 8 | server_name _; 9 | 10 | location /back/ { 11 | proxy_pass http://back:4000/; 12 | } 13 | 14 | location / { 15 | proxy_pass http://front:3000/; 16 | } 17 | } -------------------------------------------------------------------------------- /front/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ 2 | # for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /back/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "plugins": [ 7 | { 8 | "name": "@nestjs/swagger", 9 | "options": { 10 | "classValidatorShims": false, 11 | "introspectComments": true 12 | } 13 | } 14 | ] 15 | }, 16 | "root": "/app/src" 17 | } 18 | -------------------------------------------------------------------------------- /back/src/upload/utils/upload.utils.ts: -------------------------------------------------------------------------------- 1 | import { diskStorage } from 'multer'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | export const saveImageToStorage = { 5 | storage: diskStorage({ 6 | destination: process.env.UPLOAD_DIR, 7 | filename: async (request, file, callback) => { 8 | const fileExtension = '.' + file.mimetype.split('/')[1]; 9 | const fileName = uuidv4() + fileExtension; 10 | callback(undefined, fileName); 11 | }, 12 | }), 13 | }; 14 | -------------------------------------------------------------------------------- /back/src/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { PrismaModule } from 'src/prisma/prisma.module'; 3 | import { UserModule } from 'src/user/user.module'; 4 | import { ChatGateway } from './chat.gateway'; 5 | import { ChatService } from './chat.service'; 6 | 7 | @Module({ 8 | imports: [forwardRef(() => UserModule), forwardRef(() => PrismaModule)], 9 | providers: [ChatService, ChatGateway], 10 | exports: [ChatGateway, ChatService], 11 | }) 12 | export class ChatModule {} 13 | -------------------------------------------------------------------------------- /back/src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { GameModule } from 'src/game/game.module'; 3 | import { PrismaModule } from 'src/prisma/prisma.module'; 4 | import { UserController } from './user.controller'; 5 | import { UserService } from './user.service'; 6 | 7 | @Module({ 8 | imports: [GameModule, forwardRef(() => PrismaModule)], 9 | providers: [UserService], 10 | controllers: [UserController], 11 | exports: [UserService], 12 | }) 13 | export class UserModule {} 14 | -------------------------------------------------------------------------------- /back/src/upload/upload.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule } from '@nestjs/axios'; 2 | import { forwardRef, Module } from '@nestjs/common'; 3 | import { UserModule } from 'src/user/user.module'; 4 | import { UploadController } from './upload.controller'; 5 | import { UploadService } from './upload.service'; 6 | 7 | @Module({ 8 | imports: [forwardRef(() => HttpModule), forwardRef(() => UserModule)], 9 | providers: [UploadService], 10 | controllers: [UploadController], 11 | exports: [UploadService], 12 | }) 13 | export class UploadModule {} 14 | -------------------------------------------------------------------------------- /front/src/queries/headers.tsx: -------------------------------------------------------------------------------- 1 | export const authHeader = () => { 2 | let token = "Bearer " + localStorage.getItem("userToken"); 3 | let myHeaders = new Headers(); 4 | myHeaders.append("Authorization", token); 5 | return myHeaders; 6 | }; 7 | 8 | export const authContentHeader = () => { 9 | let token = "bearer " + localStorage.getItem("userToken"); 10 | let myHeaders = new Headers(); 11 | myHeaders.append("Authorization", token); 12 | myHeaders.append("Content-Type", "application/json"); 13 | return myHeaders; 14 | }; 15 | -------------------------------------------------------------------------------- /back/src/decorators/get-current-user-decorator-id.ts: -------------------------------------------------------------------------------- 1 | /* GLOBAL MODULES */ 2 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 3 | 4 | // Create @GetCurrentUser decorator - used to get current userID 5 | export const GetCurrentUserId = createParamDecorator( 6 | (data: undefined, context: ExecutionContext) => { 7 | // Get request from context 8 | const request = context.switchToHttp().getRequest(); 9 | // LOG 10 | // console.log('user', request.user); 11 | // Extract userID from request 12 | return request.user.id; 13 | }, 14 | ); 15 | -------------------------------------------------------------------------------- /front/src/ContextMenus/COnUserSimple.tsx: -------------------------------------------------------------------------------- 1 | import { Menu, Item } from "react-contexify"; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | export const COnUserSimple = (props: any) => { 5 | const navigate = useNavigate(); 6 | 7 | return ( 8 | 9 | { 12 | navigate("/app/public/" + props.who); 13 | window.location.reload(); 14 | }} 15 | > 16 | see profile 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | ################################################# 2 | ## DOCKER CONTAINER ENVIRONMENT VARIABLES ## 3 | ################################################# 4 | 5 | ## Nginx 6 | SITE_NAME=10.11.10.2 7 | SITE_DIR=/var/www/${SITE_NAME} 8 | 9 | ## Database 10 | POSTGRES_PASSWORD=secret 11 | POSTGRES_USER=prisma 12 | POSTGRES_DB=dbdata 13 | 14 | ## Ports 15 | BACK_PORT=4000 16 | FRONT_PORT=3000 17 | 18 | ## URLS 19 | SITE_URL=http://${SITE_NAME} 20 | FRONT_URL="${SITE_URL}:${FRONT_PORT}" 21 | BACK_URL="${SITE_URL}:${BACK_PORT}" 22 | SOCKET_URL="ws://${SITE_NAME}:${BACK_PORT}" -------------------------------------------------------------------------------- /back/src/user/user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserController } from './user.controller'; 3 | 4 | describe('UserController', () => { 5 | let controller: UserController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [UserController], 10 | }).compile(); 11 | 12 | controller = module.get(UserController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /back/src/prisma/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { PrismaService } from './prisma.service'; 3 | 4 | /* 5 | * The prisma module is the way for the code to connect to the database 6 | * We export from the module the stuff needed by the application 7 | */ 8 | 9 | @Global() //Makes PrismaService available to all modules without needing to import 10 | @Module({ 11 | providers: [PrismaService], 12 | exports: [PrismaService], //needed so the modules accessing the PrismaModule have access to the PrismaService 13 | }) 14 | export class PrismaModule {} 15 | -------------------------------------------------------------------------------- /back/src/auth/dto/2fa.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, IsNumber, IsString } from 'class-validator'; 2 | 3 | /** 4 | * DTO - Data Transfer Object 5 | */ 6 | 7 | // 2FA DTO 8 | export class TwoFactorDto { 9 | @IsNotEmpty() 10 | @IsString() 11 | username: string; 12 | 13 | @IsNotEmpty() 14 | @IsString() 15 | twoFAcode: string; 16 | } 17 | 18 | // 2FA User DTO 19 | export class TwoFactorUserDto { 20 | @IsNotEmpty() 21 | @IsEmail() 22 | email: string; 23 | 24 | @IsNotEmpty() 25 | @IsString() 26 | twoFAsecret: string; 27 | 28 | @IsNotEmpty() 29 | @IsNumber() 30 | id: number; 31 | } 32 | -------------------------------------------------------------------------------- /back/src/decorators/get-current-user-decorator.ts: -------------------------------------------------------------------------------- 1 | /* GLOBAL MODULES */ 2 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 3 | 4 | // @GetCurrentUser decorator - used to get current user 5 | export const GetCurrentUser = createParamDecorator( 6 | (data: string | undefined, context: ExecutionContext) => { 7 | // Get request from context 8 | const request = context.switchToHttp().getRequest(); 9 | // Extract user from request 10 | if (!data) { 11 | return request.user; 12 | } 13 | // Extract user data from request 14 | return request.user[data]; 15 | }, 16 | ); 17 | -------------------------------------------------------------------------------- /front/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src", "src/custom.d.ts"] 20 | 21 | } 22 | -------------------------------------------------------------------------------- /back/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /front/src/routes/Auth/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | 3 | export default function SignIn() { 4 | let navigate = useNavigate(); 5 | 6 | async function handleClick(event: any) { 7 | event.preventDefault(); 8 | navigate("/auth/signup"); 9 | } 10 | 11 | return ( 12 |
13 |

Sign in.

14 |
15 | Don't have an account yet?   16 | {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} 17 | 18 | Sign up. 19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /back/src/game/interfaces/room.interface.ts: -------------------------------------------------------------------------------- 1 | import { Client } from './client.interface'; 2 | 3 | export interface Room { 4 | id: number; 5 | name: string; 6 | player1: Client; 7 | player1Name: string; 8 | player1Avatar: string; 9 | player1Disconnected?: boolean; 10 | player2?: Client; 11 | player2Name?: string; 12 | player2Avatar?: string; 13 | player2Disconnected?: boolean; 14 | paddleLeft: number; 15 | paddleLeftDir: number; 16 | paddleRight: number; 17 | paddleRightDir: number; 18 | player1Score: number; 19 | player2Score: number; 20 | xball?: number; 21 | yball?: number; 22 | xSpeed?: number; 23 | ySpeed?: number; 24 | private: boolean; 25 | ballSpeed?: number; 26 | } 27 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | clsx@^1.1.1: 6 | version "1.2.1" 7 | resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" 8 | integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== 9 | 10 | react-contexify@^5.0.0: 11 | version "5.0.0" 12 | resolved "https://registry.yarnpkg.com/react-contexify/-/react-contexify-5.0.0.tgz#11b477550a0ee5a9a144399bc17c7c56bbc60057" 13 | integrity sha512-2FIp7lxJ6dtfGr8EZ4uVV5p5TQjd0n2h/JU7PrejNIMiCeZWvSVPFh4lj1ZvjXosglBvP7q5JQQ8yUCdSaMSaw== 14 | dependencies: 15 | clsx "^1.1.1" 16 | -------------------------------------------------------------------------------- /back/.gitignore: -------------------------------------------------------------------------------- 1 | # environment variables 2 | .env.prod 3 | 4 | # compiled output 5 | /dist 6 | /node_modules 7 | 8 | # uploaded files 9 | /image_dir 10 | 11 | # prisma migrations 12 | # /prisma/migrations/* 13 | 14 | # Logs 15 | logs 16 | *.log 17 | npm-debug.log* 18 | pnpm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | lerna-debug.log* 22 | 23 | # OS 24 | .DS_Store 25 | 26 | # Tests 27 | /coverage 28 | /.nyc_output 29 | 30 | # IDEs and editors 31 | /.idea 32 | .project 33 | .classpath 34 | .c9/ 35 | *.launch 36 | .settings/ 37 | *.sublime-workspace 38 | 39 | # IDE - VSCode 40 | .vscode/* 41 | !.vscode/settings.json 42 | !.vscode/tasks.json 43 | !.vscode/launch.json 44 | !.vscode/extensions.json -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # TRANSCENDENCE MAKEFILE 2 | 3 | 4 | # Docker compose file location 5 | COMPOSE = docker-compose.yml 6 | 7 | # Change this to suit your compose version 8 | 9 | COMPOSE_CMD = docker-compose -f ${COMPOSE} 10 | # COMPOSE_CMD = docker compose -f ${COMPOSE} 11 | 12 | # BASIC COMPOSE COMMANDS 13 | build: 14 | ${COMPOSE_CMD} up --build 15 | 16 | up: 17 | ${COMPOSE_CMD} up 18 | 19 | down: 20 | ${COMPOSE_CMD} down 21 | 22 | # CLEAN DATABASES 23 | dbclean: down 24 | docker volume prune -f 25 | 26 | clean: dbclean 27 | rm -rf back/dist back/node_modules front/node_modules 28 | 29 | # PRUNE DOCKER CONTAINERS 30 | fclean: down clean 31 | docker system prune -af 32 | 33 | .PHONY: all build up down clean fclean -------------------------------------------------------------------------------- /back/src/auth/filter/redirect.login.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | HttpException, 6 | } from '@nestjs/common'; 7 | import { Response } from 'express'; 8 | import { UnauthorizedException } from '@nestjs/common'; 9 | 10 | @Catch(UnauthorizedException) 11 | export class RedirectOnLogin implements ExceptionFilter { 12 | catch(exception: HttpException, host: ArgumentsHost) { 13 | const context = host.switchToHttp(); 14 | const response = context.getResponse(); 15 | const status = exception.getStatus(); 16 | const url = new URL(process.env.SITE_URL); 17 | url.port = process.env.FRONT_PORT; 18 | url.pathname = '/auth/signin'; 19 | response.status(status).redirect(url.href); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /back/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /back/src/game/game.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { GameService } from './game.service'; 3 | import { ScheduleModule } from '@nestjs/schedule'; 4 | import { WatchController } from './watch/watch.controller'; 5 | import { GameController } from './game.controller'; 6 | import { GameGateway } from './game.gateway'; 7 | import { UserModule } from 'src/user/user.module'; 8 | import { AppModule } from 'src/app.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | ScheduleModule.forRoot(), 13 | forwardRef(() => AppModule), 14 | forwardRef(() => UserModule), 15 | ], 16 | providers: [GameService, GameGateway], 17 | controllers: [WatchController, GameController], 18 | exports: [GameService], 19 | }) 20 | export class GameModule {} 21 | -------------------------------------------------------------------------------- /back/src/chat/filter/transformation-filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, HttpException } from '@nestjs/common'; 2 | import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets'; 3 | 4 | @Catch(HttpException) 5 | export class HttpToWsFilter extends BaseWsExceptionFilter { 6 | catch(exception: HttpException, host: ArgumentsHost) { 7 | const properWsException = new WsException( 8 | exception.getResponse()['message'], 9 | ); 10 | super.catch(properWsException, host); 11 | } 12 | } 13 | 14 | @Catch(WsException) 15 | export class ProperWsFilter extends BaseWsExceptionFilter { 16 | catch(exception: WsException, host: ArgumentsHost) { 17 | const properWsException = new WsException(exception.getError()); 18 | super.catch(properWsException, host); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /front/src/modals/MLogoutValid.tsx: -------------------------------------------------------------------------------- 1 | import { Modal, Button } from "react-bootstrap"; 2 | 3 | export function MLogoutValid(props: any) { 4 | return ( 5 | 11 | 12 | Log Out 13 | 14 | Do you wish to log out ? 15 | 16 | 19 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /back/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | 'plugin:unicorn/recommended', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | ignorePatterns: ['.eslintrc.js'], 20 | rules: { 21 | '@typescript-eslint/interface-name-prefix': 'off', 22 | '@typescript-eslint/explicit-function-return-type': 'off', 23 | '@typescript-eslint/explicit-module-boundary-types': 'off', 24 | '@typescript-eslint/no-explicit-any': 'off', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /back/src/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PrismaClient } from '@prisma/client'; 4 | 5 | @Injectable() 6 | export class PrismaService extends PrismaClient { 7 | [x: string]: any; 8 | private _message: any; //prismaClient is an existing class allowing to connect to a db, it has some basic functions already 9 | public get message(): any { 10 | return this._message; 11 | } 12 | public set message(value: any) { 13 | this._message = value; 14 | } 15 | constructor(config: ConfigService) { 16 | //from dotenv module 17 | super({ 18 | //calls the constructor of the class being extended 19 | datasources: { 20 | db: { 21 | url: config.get('DATABASE_URL'), 22 | }, 23 | }, 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /front/src/queries/otherUserQueries.tsx: -------------------------------------------------------------------------------- 1 | import { authContentHeader } from "./headers"; 2 | 3 | export const getOtherUser = (otherUsername: number) => { 4 | let body = JSON.stringify({ 5 | otherId: otherUsername, 6 | }); 7 | return fetchGetOtherUser("get_user", body); 8 | }; 9 | 10 | const fetchGetOtherUser = async (url: string, body: any) => { 11 | let fetchUrl = process.env.REACT_APP_BACKEND_URL + "/users/" + url; 12 | try { 13 | const response = await fetch(fetchUrl, { 14 | method: "POST", 15 | headers: authContentHeader(), 16 | body: body, 17 | redirect: "follow", 18 | }); 19 | const result_1 = await response.json(); 20 | if (!response.ok) return "error"; 21 | return result_1; 22 | } catch (error) { 23 | return console.log("error", error); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 15 | 24 | Transcendence 25 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /back/.env: -------------------------------------------------------------------------------- 1 | # ENV FILE 2 | 3 | # This was inserted by `prisma init`: 4 | # Environment variables declared in this file are automatically made available to Prisma. 5 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 6 | 7 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 8 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 9 | 10 | # Database basics 11 | DB_HOST=postgres 12 | DB_PORT=5432 13 | DB_SCHEMA=public 14 | 15 | # Database URL 16 | DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:${DB_PORT}/${POSTGRES_DB}?schema=${DB_SCHEMA}&sslmode=prefer 17 | 18 | # Upload dir 19 | UPLOAD_DIR="./image_dir" 20 | 21 | # Environment 22 | ENVIRONMENT="PRODUCTION" -------------------------------------------------------------------------------- /front/src/ressources/icons/Icon_Pen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /front/src/queries/gamesQueries.tsx: -------------------------------------------------------------------------------- 1 | import { authContentHeader } from "./headers"; 2 | 3 | export const getGameStats = (otherUsername: number) => { 4 | let body = JSON.stringify({ 5 | otherId: otherUsername, 6 | }); 7 | return fetGameStats("get_game_history", body); 8 | }; 9 | 10 | const fetGameStats = async (url: string, body: any) => { 11 | let fetchUrl = process.env.REACT_APP_BACKEND_URL + "/users/" + url; 12 | try { 13 | const response = await fetch(fetchUrl, { 14 | method: "POST", 15 | headers: authContentHeader(), 16 | body: body, 17 | redirect: "follow", 18 | }); 19 | const result_1 = await response.json(); 20 | if (!response.ok) { 21 | console.log("POST error on ", url); 22 | return "error"; 23 | } 24 | return Object.assign(result_1); 25 | } catch (error) { 26 | return console.log("error", error); 27 | } 28 | }; -------------------------------------------------------------------------------- /back/src/auth/guard/jwt.guard.ts: -------------------------------------------------------------------------------- 1 | /* GLOBAL MODULES */ 2 | import { ExecutionContext, Injectable } from '@nestjs/common'; 3 | /* Attach METADATA */ 4 | import { Reflector } from '@nestjs/core'; 5 | /* AUTH Guard Passport Module */ 6 | import { AuthGuard } from '@nestjs/passport'; 7 | 8 | // Global guard to protect all route 9 | @Injectable() 10 | export class JwtGuard extends AuthGuard('jwt') { 11 | constructor(private reflector: Reflector) { 12 | super(); 13 | } 14 | // Validate function 15 | canActivate(context: ExecutionContext) { 16 | // Get metadata from context 17 | const isPublic = this.reflector.getAllAndOverride('isPublic', [ 18 | context.getHandler(), 19 | context.getClass(), 20 | ]); 21 | // if the route is public, return true 22 | if (isPublic) return true; 23 | // if the route is not public - pass to JwtGuard 24 | return super.canActivate(context); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitmessage.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Description 4 | 5 | * 6 | 7 | # 8 | # 50 char subject line 9 | # 10 | # 72 char wrapped description 11 | 12 | # 13 | # Emoji cheat sheet: 14 | # 15 | # 🚧 (:construction:) -> work in progress 16 | # ➕ (:heavy_plus_sign:) -> added a subfeature 17 | # 🐛 (:bug:) -> bug fix 18 | # 🚀 (:rocket:) -> launch new feature 19 | # 🎨 (:art:) -> style 20 | # 🔨 (:hammer:) -> refactoring 21 | # 🧪 (:test_tube:) -> testing 22 | # 📝 (:memo:) -> documentation 23 | # 🧹 (:broom:) -> code maintenance 24 | # 🗑️ (:wastebasket:) -> deleted files 25 | # 📦️ (:package:) -> package change 26 | # 💚 (:green_heart:) -> fix conflict 27 | -------------------------------------------------------------------------------- /front/src/hooks/UserInfoHooks.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | // export const useUsername = () => { 4 | // const [showUsername, setShowUsername] = useState(false); 5 | // const onClickEditUsername = () => setShowUsername((curent) => !curent); 6 | // const hideUsername = () => setShowUsername(false); 7 | // return (showUsername); 8 | // }; 9 | 10 | // export const [showEmail, setShowEmail] = useState(false); 11 | // export const onClickEditEmail = () => setShowEmail((curent) => !curent); 12 | // export const hideEmail = () => setShowEmail(false); 13 | 14 | // export const [showPhone, setShowPhone] = useState(false); 15 | // export const onClickEditPhone = () => setShowPhone((curent) => !curent); 16 | // export const hidePhone = () => setShowPhone(false); 17 | 18 | // export const [showPass, setShowPass] = useState(false); 19 | // export const onClickEditPass = () => setShowPass((curent) => !curent); 20 | // export const hidePass = () => setShowPass(false); 21 | -------------------------------------------------------------------------------- /back/src/game/dto/game.dto.ts: -------------------------------------------------------------------------------- 1 | import { UserDto } from 'src/user/dto'; 2 | import { IsNotEmpty, IsString, IsNumber, MaxLength } from 'class-validator'; 3 | 4 | /* 5 | * This is the type of game that is used in 6 | * -Latest Games- for a user 7 | */ 8 | export class SubjectiveGameDto { 9 | @IsNumber() 10 | @IsNotEmpty() 11 | userId: number; 12 | 13 | @IsNumber() 14 | @IsNotEmpty() 15 | opponentId: number; 16 | 17 | @IsString() 18 | @IsNotEmpty() 19 | @MaxLength(65_000) 20 | opponentAvatar: string; 21 | 22 | @IsString() 23 | @IsNotEmpty() 24 | opponentUsername: string; 25 | 26 | @IsString() 27 | @IsNotEmpty() 28 | opponentUser: UserDto; 29 | 30 | @IsNumber() 31 | @IsNotEmpty() 32 | opponentRank: number; 33 | 34 | @IsNumber() 35 | @IsNotEmpty() 36 | duration: number; 37 | 38 | @IsNumber() 39 | @IsNotEmpty() 40 | userScore: number; 41 | 42 | @IsNumber() 43 | @IsNotEmpty() 44 | opponentScore: number; 45 | 46 | @IsNotEmpty() 47 | victory: boolean; 48 | } 49 | -------------------------------------------------------------------------------- /back/src/auth/strategy/42.strategy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating 42 API Auth strategy 3 | */ 4 | 5 | import { Injectable } from '@nestjs/common'; 6 | import { PassportStrategy } from '@nestjs/passport'; 7 | 8 | import { Strategy } from 'passport-42'; 9 | import { AuthService } from '../auth.service'; 10 | 11 | import { Profile_42 } from '../interfaces/42.interface'; 12 | 13 | @Injectable() 14 | export class FortyTwoStrategy extends PassportStrategy(Strategy, '42auth') { 15 | /** 16 | * 42 API Auth strategy object constructor 17 | */ 18 | constructor(private readonly authService: AuthService) { 19 | super({ 20 | clientID: process.env.FORTYTWO_ID, 21 | clientSecret: process.env.FORTYTWO_SECRET, 22 | callbackURL: process.env.FORTYTWO_CALLBACK, 23 | profileFields: { 24 | id: 'id', 25 | username: 'login', 26 | email: 'email', 27 | avatar: 'image_url', 28 | }, 29 | }); 30 | } 31 | 32 | validate(accessToken: string, refreshToken: string, profile: Profile_42) { 33 | return profile; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /back/src/chat/dto/chat.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsArray, 3 | IsBoolean, 4 | IsEmail, 5 | IsNotEmpty, 6 | IsNumber, 7 | IsOptional, 8 | IsString, 9 | } from 'class-validator'; 10 | import { Tag } from '../type/chat.type'; 11 | 12 | export class ChannelDto { 13 | @IsString() 14 | @IsNotEmpty() 15 | name: string; 16 | 17 | @IsBoolean() 18 | private: boolean; 19 | 20 | @IsBoolean() 21 | isPassword: boolean; 22 | 23 | @IsOptional() 24 | @IsString() 25 | password: string; 26 | 27 | @IsEmail() 28 | email: string; 29 | 30 | @IsArray() 31 | @IsOptional() 32 | members: Array; 33 | } 34 | 35 | export class UseMessageDto { 36 | @IsEmail() 37 | @IsNotEmpty() 38 | email: string; 39 | 40 | @IsNumber() 41 | @IsNotEmpty() 42 | channelId: number; 43 | 44 | @IsString() 45 | @IsNotEmpty() 46 | msg: string; 47 | 48 | @IsNumber() 49 | @IsOptional() 50 | msgId: number; 51 | } 52 | 53 | export class DMDto { 54 | @IsEmail() 55 | @IsNotEmpty() 56 | email: string; 57 | 58 | @IsNumber() 59 | @IsNotEmpty() 60 | targetId: number; 61 | } 62 | -------------------------------------------------------------------------------- /front/src/routes/Auth/SignUp.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from "react-bootstrap"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { GUserInputsRefs } from "../../globals/variables"; 4 | 5 | export default function SignUp() { 6 | let navigate = useNavigate(); 7 | 8 | async function handleClick(event: any) { 9 | event.preventDefault(); 10 | navigate("/auth/signin"); 11 | } 12 | 13 | return ( 14 |
15 |

Sign up.

16 |
17 | Already registered?   18 | {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} 19 | 20 | Sign in. 21 | 22 |
23 | 24 | USERNAME 25 | 30 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /back/src/auth/strategy/rt.strategy.ts: -------------------------------------------------------------------------------- 1 | /* GLOBAL MODULES */ 2 | import { Injectable } from '@nestjs/common'; 3 | /* AUTH Passport Module */ 4 | import { PassportStrategy } from '@nestjs/passport'; 5 | import { ExtractJwt, Strategy } from 'passport-jwt'; 6 | /* REQUEST */ 7 | import { Request } from 'express'; 8 | 9 | @Injectable() 10 | export class RtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { 11 | constructor() { 12 | super({ 13 | // Extract Token from header - Authorization: Bearer 14 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 15 | secretOrKey: process.env.JWT_SECRET, 16 | // Pass request to callback function 17 | passReqToCallback: true, 18 | }); 19 | } 20 | // Validate function used by Passport Module 21 | async validate(request: Request, data: any) { 22 | // switch Token to refresh Token 23 | const refreshToken = request 24 | .get('authorization') 25 | .replace('Bearer ', '') 26 | .trim(); 27 | 28 | // LOG IN CONSOLE 29 | // console.log(data); 30 | 31 | // return data and append refreshToken 32 | return { 33 | ...data, 34 | refreshToken, 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /front/src/toasts/TAlert.tsx: -------------------------------------------------------------------------------- 1 | import { ToastContainer } from "react-bootstrap"; 2 | import Toast from "react-bootstrap/Toast"; 3 | 4 | // const getDate = () => { 5 | // let newDate = new Date(); 6 | // let formattedDate = newDate.getMonth() + 1 + "/" + newDate.getDate(); 7 | // return formattedDate; 8 | // }; 9 | 10 | const getTime = () => { 11 | let newDate = new Date(); 12 | const time = newDate.getHours() + "h" + newDate.getMinutes(); 13 | return time; 14 | }; 15 | 16 | export const TAlert = (props: any) => { 17 | return ( 18 | 19 | props.setShow(false)} 21 | show={props.show} 22 | delay={5000} 23 | autohide 24 | > 25 | 26 | 27 | Notification Alert 28 | {getTime()} 29 | 30 | {props.text} 31 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /back/src/user/dto/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { Exclude } from 'class-transformer'; 2 | import { IsNotEmpty, IsString, IsNumber, MaxLength } from 'class-validator'; 3 | 4 | /* 5 | * DTO = Data Transfer Object 6 | * watch for changes in the user model depending on Shu Yen's work :) 7 | */ 8 | 9 | export class UserDto { 10 | //Data transfer object 11 | @IsNumber() 12 | @IsNotEmpty() 13 | id: number; 14 | 15 | @IsString() 16 | @IsNotEmpty() 17 | username: string; 18 | 19 | @IsString() 20 | @IsNotEmpty() 21 | email: string; 22 | 23 | @IsString() 24 | @IsNotEmpty() 25 | @MaxLength(65_000) 26 | avatar: string; 27 | 28 | @IsNumber() 29 | @IsNotEmpty() 30 | gamesWon: number; 31 | 32 | @IsNumber() 33 | @IsNotEmpty() 34 | gamesLost: number; 35 | 36 | @IsNumber() 37 | @IsNotEmpty() 38 | gamesPlayed: number; 39 | 40 | @IsNumber() 41 | @IsNotEmpty() 42 | rank: number; 43 | 44 | @IsNumber() 45 | @IsNotEmpty() 46 | score: number; 47 | 48 | added: number[]; 49 | adding: number[]; 50 | friends: number[]; 51 | 52 | blocked: number[]; 53 | blocking: number[]; 54 | blocks: number[]; 55 | 56 | @Exclude() 57 | hash: string; 58 | 59 | @Exclude() 60 | hashedRtoken: string; 61 | } 62 | -------------------------------------------------------------------------------- /back/src/auth/dto/auth.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsEmail, 3 | IsNotEmpty, 4 | IsNumber, 5 | IsString, 6 | MaxLength, 7 | MinLength, 8 | } from 'class-validator'; 9 | 10 | /** 11 | * DTO - Data Transfer Object 12 | */ 13 | 14 | // SignUp DTO 15 | export class SignUpDto { 16 | @IsEmail() 17 | @IsNotEmpty() 18 | @MaxLength(50) 19 | email: string; 20 | 21 | @IsString() 22 | @IsNotEmpty() 23 | @MinLength(8) 24 | @MaxLength(32) 25 | password: string; 26 | 27 | @IsString() 28 | @IsNotEmpty() 29 | @MaxLength(32) 30 | username: string; 31 | } 32 | // Signin DTO 33 | export class SignInDto { 34 | @IsString() 35 | @IsNotEmpty() 36 | password: string; 37 | 38 | @IsString() 39 | @IsNotEmpty() 40 | username: string; 41 | } 42 | 43 | // 42 API DTO 44 | export class Auth42Dto { 45 | @IsNumber() 46 | @IsNotEmpty() 47 | id: number; 48 | 49 | @IsEmail() 50 | @IsNotEmpty() 51 | email: string; 52 | 53 | @IsString() 54 | @IsNotEmpty() 55 | username: string; 56 | 57 | @IsString() 58 | @IsNotEmpty() 59 | avatar: string; 60 | } 61 | 62 | // Auth Tokens DTO 63 | export class AuthTokenDto { 64 | @IsString() 65 | @IsNotEmpty() 66 | access_token: string; 67 | 68 | @IsString() 69 | @IsNotEmpty() 70 | refresh_token: string; 71 | } 72 | -------------------------------------------------------------------------------- /back/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AuthController } from './auth.controller'; 4 | import { AuthService } from './auth.service'; 5 | /* JASON WEB TOKEN AUTH MODULE */ 6 | import { JwtModule } from '@nestjs/jwt'; 7 | import { jwtStrategy, RtStrategy, FortyTwoStrategy } from './strategy'; 8 | /* USER Module */ 9 | import { forwardRef } from '@nestjs/common'; 10 | import { UserModule } from 'src/user/user.module'; 11 | import { TwoFAController, TwoFactorService } from './2FA'; 12 | import { UploadModule } from 'src/upload/upload.module'; 13 | import { HttpModule } from '@nestjs/axios'; 14 | import { ChatModule } from 'src/chat/chat.module'; 15 | import { AppModule } from 'src/app.module'; 16 | 17 | @Module({ 18 | imports: [ 19 | JwtModule.register({}), 20 | forwardRef(() => AppModule), 21 | forwardRef(() => UserModule), 22 | forwardRef(() => ChatModule), 23 | forwardRef(() => UploadModule), 24 | forwardRef(() => HttpModule), 25 | ], 26 | controllers: [AuthController, TwoFAController], 27 | providers: [ 28 | AuthService, 29 | TwoFactorService, 30 | jwtStrategy, 31 | RtStrategy, 32 | FortyTwoStrategy, 33 | ], 34 | exports: [AuthService], 35 | }) 36 | export class AuthModule {} 37 | -------------------------------------------------------------------------------- /front/src/routes/Chat.css: -------------------------------------------------------------------------------- 1 | .zone-diff { 2 | display: flex; 3 | height: 100vh; 4 | width: 100vw; 5 | background-color: #252a2a; 6 | border-color: grey; 7 | } 8 | 9 | .card-disappear-click-zone { 10 | position: fixed; 11 | height: 100vh; 12 | width: 100vw; 13 | background-color: rgba(255, 255, 255, 0.3); 14 | } 15 | 16 | .add-zone { 17 | position: fixed; 18 | top: 50%; 19 | left: 50%; 20 | transform: translateX(-50%) translateY(-50%); 21 | animation-name: mymove; 22 | animation-duration: 0.5s; 23 | animation-timing-function: ease; 24 | } 25 | 26 | @keyframes mymove { 27 | from {transform: translateX(-50%) translateY(-50%) scale(0.95); opacity: 0;} 28 | to {transform: translateX(-50%) translateY(-50%) scale(1); opacity: 1;} 29 | } 30 | 31 | /* ===== Scrollbar CSS ===== */ 32 | /* Firefox */ 33 | * { 34 | scrollbar-width: none; 35 | scrollbar-color: #C3CEDA #071330; 36 | } 37 | 38 | /* Chrome, Edge, and Safari */ 39 | *::-webkit-scrollbar { 40 | width: 12px; 41 | } 42 | 43 | *::-webkit-scrollbar-track { 44 | background: #2c3447; 45 | } 46 | 47 | *::-webkit-scrollbar-thumb { 48 | background-color: #C3CEDA; 49 | border-radius: 10px; 50 | border: 3px solid #2c3447; 51 | } -------------------------------------------------------------------------------- /back/src/game/game.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Req } from '@nestjs/common'; 2 | import { GameService } from './game.service'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | 5 | @ApiTags('Game') 6 | @Controller('game') 7 | export class GameController { 8 | constructor(private gameService: GameService) {} 9 | 10 | /* CREATE */ 11 | 12 | @Post('/save_game') 13 | async saveGame( 14 | @Body('id') id: number, 15 | @Body('userId1') userId1: number, 16 | @Body('userId2') userId2: number, 17 | @Body('score1') score1: number, 18 | @Body('score2') score2: number, 19 | @Body('startTime') startTime: Date, 20 | @Body('endTime') endTime: Date, 21 | ) { 22 | // console.log('Going through saveGame in game.controller'); 23 | const result = await this.gameService.saveGame( 24 | id, 25 | userId1, 26 | userId2, 27 | score1, 28 | score2, 29 | startTime, 30 | endTime, 31 | ); 32 | return result; 33 | } 34 | 35 | /* READ */ 36 | 37 | @Get('get_game') 38 | getGame(@Body('otherId') otherId: number) { 39 | return this.gameService.getGame(otherId); 40 | } 41 | 42 | @Get('get_last_games') 43 | getLastGames() { 44 | // console.log('Going through getLastGames in game.controller'); 45 | return this.gameService.getLastGames(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /back/src/upload/upload.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { createWriteStream } from 'node:fs'; 4 | import { UserService } from 'src/user/user.service'; 5 | import { v4 as uuidv4 } from 'uuid'; 6 | 7 | @Injectable() 8 | export class UploadService { 9 | constructor( 10 | private readonly httpService: HttpService, 11 | private readonly userService: UserService, 12 | ) {} 13 | 14 | async download_avatar(id: number, avatarURL: string): Promise { 15 | // Generate uuid filename 16 | const fileExtension = '.' + avatarURL.split('.').pop(); 17 | const filename = uuidv4() + fileExtension; 18 | const path = process.env.UPLOAD_DIR + '/' + filename; 19 | // update user avatar 20 | await this.userService.updateAvatar(id, filename); 21 | // create a streamable file 22 | const writer = createWriteStream(path); 23 | // get avatar from CDN 24 | const response = await this.httpService.axiosRef({ 25 | url: avatarURL, 26 | method: 'GET', 27 | responseType: 'stream', 28 | }); 29 | // pipe to file 30 | response.data.pipe(writer); 31 | // return Promise when file is downloaded 32 | return new Promise((resolve, reject) => { 33 | writer.on('finish', resolve); 34 | writer.on('error', reject); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /back/src/auth/strategy/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | /* GLOBAL MODULES */ 2 | import { Injectable } from '@nestjs/common'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | /* AUTH PassportStrategy */ 5 | import { PassportStrategy } from '@nestjs/passport'; 6 | /* AUTH JWT */ 7 | import { ExtractJwt, Strategy } from 'passport-jwt'; 8 | 9 | /** 10 | * Creating a JWT strategy 11 | */ 12 | 13 | @Injectable() 14 | export class jwtStrategy extends PassportStrategy(Strategy, 'jwt') { 15 | /** 16 | * JWT strategy object constructor 17 | */ 18 | constructor(private prisma: PrismaService) { 19 | super({ 20 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 21 | secretOrKey: process.env.JWT_SECRET, 22 | }); 23 | } 24 | 25 | /** 26 | * Validate function used by Passport Module 27 | */ 28 | async validate(data: { sub: number; email: string; is2FA: boolean }) { 29 | const user = await this.prisma.user.findUnique({ 30 | where: { 31 | id: data.sub, 32 | }, 33 | }); 34 | // if user is logged out return 401 35 | if (!user.hashedRtoken) return; 36 | // remove sensitive data 37 | if (user) delete user.hash; 38 | // if the user is not found user == NULL 39 | // 401 forbidden is returned. 40 | if (!user.twoFA) { 41 | return user; 42 | } 43 | if (data.is2FA) { 44 | return user; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /front/src/ContextMenus/COnUser.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { Menu, Item } from "react-contexify"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { NotifCxt } from "../App"; 5 | import { addFriendQuery } from "../queries/userFriendsQueries"; 6 | 7 | export const COnUser = (props: any) => { 8 | const navigate = useNavigate(); 9 | const notif = useContext(NotifCxt); 10 | 11 | const handleClick = (otherId: number, otherUsername:string) => { 12 | const addFriend = async () => { 13 | const result = await addFriendQuery(otherId); 14 | if (result !== "error") { 15 | notif?.setNotifText("Friend request sent to " + otherUsername + "!"); 16 | } else notif?.setNotifText("Could not send friend request :(."); 17 | notif?.setNotifShow(true); 18 | }; 19 | addFriend(); 20 | }; 21 | 22 | return ( 23 | 24 | { 27 | navigate("/app/public/" + props.who); 28 | window.location.reload(); 29 | }} 30 | > 31 | see profile 32 | 33 | { 35 | handleClick(props.who, props.username); 36 | }} 37 | > 38 | add as friend 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /nginx/ssl-certs/cert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDlzCCAn+gAwIBAgIUIjRSsPT/2nwSoEyz6iVM8C1BJF0wDQYJKoZIhvcNAQEL 3 | BQAwWzELMAkGA1UEBhMCRlIxDjAMBgNVBAcMBVBhcmlzMQswCQYDVQQKDAI0MjEW 4 | MBQGA1UEAwwNc3NoYWt5YS40Mi5mcjEXMBUGCgmSJomT8ixkAQEMB3NzaGFreWEw 5 | HhcNMjIwNjEwMTAxNjA3WhcNMjMwNjEwMTAxNjA3WjBbMQswCQYDVQQGEwJGUjEO 6 | MAwGA1UEBwwFUGFyaXMxCzAJBgNVBAoMAjQyMRYwFAYDVQQDDA1zc2hha3lhLjQy 7 | LmZyMRcwFQYKCZImiZPyLGQBAQwHc3NoYWt5YTCCASIwDQYJKoZIhvcNAQEBBQAD 8 | ggEPADCCAQoCggEBAK/adfC0MlsnIaEQZAWsb9p/X9zXrazLwULz8oEwm+Lc/r1l 9 | pRBFqYJ08Fi2ARoOJrTdt3r2zEItYW2/RlIMKUrazohqJn7Xz8nIPLI+VSTLVeb+ 10 | 4efn6mEVm6kCUoHuWoddnow89ixSxSfwHK+nOkJbO1a7fkLy+T9yDUer9tSdClfQ 11 | zqtLZmNWzVjQ06pkzUU6KiOxlB5eLlkuAG3IvjWo4z7Yg22HEulcFAx0t+jlsomV 12 | Zgt9/pz/S7Xtq1V3MIaAvPt4Lm2AQPemHQqRcf8l3ddUpiQltVG3/yh5ruYzqasq 13 | ZW3u4wGFT1ArVjVmKZbhBjRhTcna79ixiLt705ECAwEAAaNTMFEwHQYDVR0OBBYE 14 | FDzLWj5//Ijx8mUMBO4W1Gj/xl09MB8GA1UdIwQYMBaAFDzLWj5//Ijx8mUMBO4W 15 | 1Gj/xl09MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBACdB5ZWk 16 | +p9VX/koU0/Ky8RaSSDNbkgpcXY7kYSqBgiLGcw2FdRrpXZ6el8sWKU7SR5eb+6m 17 | BpiMqgnvhAsvbauBXPSkoM9M3rl/nhLbahDkqhP47/R5q13P1CdWNUXzNmRRfWfo 18 | 1qzOOtKxwugsS/RnI2EW7NdNKEZZU3so8Nlo+T3HT9+KTBOYhi1Hx4pTiILR2kxc 19 | 0XHwm3PDv95Jy3Lc+VYKlH8PU5Mt+Lz27h/0C6W+7jDWHz4cnC/JM8qLp+1UnnuA 20 | lymTKxQKKzv1WNzZUy3oMC898JlI9kJO4sIU3ZyE/VoW80FgDEEO/NHg4sbcqrNx 21 | IiacYpvJK0xafTY= 22 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /back/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory, Reflector } from '@nestjs/core'; 3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 4 | import { AppModule } from './app.module'; 5 | import { JwtGuard } from './auth/guard'; 6 | import { PrismaService } from './prisma/prisma.service'; 7 | 8 | /* Start the app */ 9 | async function bootstrap() { 10 | const app = await NestFactory.create(AppModule); 11 | 12 | // Swagger - OPENAPI 13 | const config = new DocumentBuilder() 14 | .setTitle('Transcendence API') 15 | .setDescription('The API for the Transcendence game') 16 | .setVersion('0.0.1') 17 | .addTag('transcendence') 18 | .build(); 19 | const document = SwaggerModule.createDocument(app, config); 20 | SwaggerModule.setup('api', app, document); 21 | 22 | // Enable CORS 23 | app.enableCors({ 24 | origin: process.env.FRONT_URL, 25 | }); 26 | 27 | // Setup Prisma 28 | app.get(PrismaService); 29 | 30 | // setup app to use validation pipe 31 | app.useGlobalPipes( 32 | new ValidationPipe({ 33 | whitelist: true, 34 | }), 35 | ); 36 | 37 | // set JwtGuard as a global guard 38 | const reflector = new Reflector(); 39 | app.useGlobalGuards(new JwtGuard(reflector)); 40 | 41 | // start api to listen on port 4000 42 | await app.listen(process.env.BACK_PORT); 43 | } 44 | 45 | // eslint-disable-next-line unicorn/prefer-top-level-await 46 | bootstrap(); 47 | -------------------------------------------------------------------------------- /back/env.dev: -------------------------------------------------------------------------------- 1 | ###################################### 2 | ##### DEVELOPMENT ENVIRONMENT FILE 3 | ###################################### 4 | 5 | # This was inserted by `prisma init`: 6 | # Environment variables declared in this file are automatically made available to Prisma. 7 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 8 | 9 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 10 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 11 | 12 | DB_HOST=postgres 13 | DB_PORT=5432 14 | DB_SCHEMA=public 15 | 16 | # Database URL 17 | DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:${DB_PORT}/${POSTGRES_DB}?schema=${DB_SCHEMA}&sslmode=prefer 18 | 19 | # Upload dir 20 | UPLOAD_DIR="./image_dir" 21 | 22 | # this is our jason web token secret hash 23 | JWT_SECRET="super_secret_jwt_token" 24 | 25 | # Token expiration time 26 | ACCESS_TOKEN_EXPIRATION="300m" 27 | REFRESH_TOKEN_EXPIRATION="1y" 28 | 29 | # App name for Authenticator 30 | MY_2FA_APP_NAME="ft_transcendence" 31 | 32 | DEFAULT_AVATAR="https://cdn-icons-png.flaticon.com/512/983/983980.png?w=740&t=st=1662017577~exp=1662018177~hmac=db050abcf21d70f42b8967d790041a3f22ae12908366ac4d46b6a49b2483adf4" 33 | 34 | # Callback URL for 42 35 | CALLBACK_URL_42=auth/42/callback 36 | 37 | # Environment 38 | ENVIRONMENT="PRODUCTION" 39 | 40 | # 42 API 41 | FORTYTWO_ID="your_id_here" 42 | 43 | FORTYTWO_SECRET="your_secret_here" 44 | 45 | FORTYTWO_CALLBACK=${SITE_URL}:${BACK_PORT}/${CALLBACK_URL_42} 46 | -------------------------------------------------------------------------------- /front/src/queries/updateUserQueries.tsx: -------------------------------------------------------------------------------- 1 | import { authContentHeader, authHeader } from "./headers"; 2 | 3 | export const updateAvatarQuery = (file: any) => { 4 | var formdata = new FormData(); 5 | formdata.append("avatar", file.files[0], "avatar.jpeg"); 6 | 7 | return fetchPost(formdata, "update_avatar", authHeader, file); 8 | }; 9 | 10 | export const updateUsernameQuery = (username: string) => { 11 | var raw = JSON.stringify({ 12 | username: username, 13 | }); 14 | return fetchPost(raw, "update_username", authContentHeader, username); 15 | }; 16 | 17 | export const updateEmailQuery = (email: string) => { 18 | var raw = JSON.stringify({ 19 | email: email, 20 | }); 21 | return fetchPost(raw, "update_email", authContentHeader, email); 22 | }; 23 | 24 | const fetchPost = async ( 25 | bodyContent: any, 26 | url: string, 27 | header: any, 28 | data: string 29 | ) => { 30 | let fetchUrl = process.env.REACT_APP_BACKEND_URL + "/users/" + url; 31 | 32 | try { 33 | const response = await fetch(fetchUrl, { 34 | method: "POST", 35 | headers: header(), 36 | body: bodyContent, 37 | redirect: "follow", 38 | }); 39 | await response.json(); 40 | if (!response.ok) { 41 | console.log("POST error on ", url); 42 | return "error"; 43 | } 44 | storeUserModif(url, data); 45 | return "success"; 46 | } catch (error) { 47 | return console.log("error", error); 48 | } 49 | }; 50 | 51 | const storeUserModif = (url: string, data: string) => { 52 | if (url === "update_username") localStorage.setItem("userName", data); 53 | if (url === "update_email") localStorage.setItem("userEmail", data); 54 | if (url === "update_avatar") localStorage.setItem("userPicture", data); 55 | }; 56 | -------------------------------------------------------------------------------- /front/src/queries/avatarQueries.tsx: -------------------------------------------------------------------------------- 1 | const authFileHeader = () => { 2 | let token = "bearer " + localStorage.getItem("userToken"); 3 | let myHeaders = new Headers(); 4 | myHeaders.append("Authorization", token); 5 | return myHeaders; 6 | }; 7 | 8 | export const uploadAvatarQuery = (fileInput: any) => { 9 | var formdata = new FormData(); 10 | 11 | formdata.append("avatar", fileInput, "name.png"); 12 | return fetchAvatar("POST", formdata, authFileHeader(), "avatar"); 13 | }; 14 | 15 | export const getAvatarQuery = () => { 16 | return fetchAvatar("GET", null, authFileHeader(), "avatar"); 17 | }; 18 | 19 | export const getUserAvatarQuery = (otherId: number) => { 20 | let body = JSON.stringify({ 21 | userId: otherId, 22 | }); 23 | let header = authFileHeader(); 24 | header.append("Content-Type", "application/json"); 25 | return fetchAvatar("POST", body, header, "getavatar"); 26 | }; 27 | 28 | const fetchAvatar = async ( 29 | method: string, 30 | body: any, 31 | header: any, 32 | url: string 33 | ) => { 34 | let fetchUrl = process.env.REACT_APP_BACKEND_URL + "/upload/" + url; 35 | 36 | let requestOptions: RequestInit | undefined; 37 | if (method === "POST") 38 | requestOptions = { 39 | method: method, 40 | headers: header, 41 | body: body, 42 | redirect: "follow", 43 | }; 44 | else 45 | requestOptions = { 46 | method: method, 47 | headers: header, 48 | redirect: "follow", 49 | }; 50 | 51 | try { 52 | const response = await fetch(fetchUrl, requestOptions); 53 | const result_1 = await response.blob(); 54 | if (!response.ok) { 55 | return "error"; 56 | } 57 | return result_1; 58 | } catch (error) { 59 | console.log("error", error); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /front/src/queries/userQueries.tsx: -------------------------------------------------------------------------------- 1 | import { authHeader } from "./headers"; 2 | 3 | export const getUserBlocked = () => { 4 | return fetchGet("get_blocked", storeFriendsInfo); 5 | }; 6 | 7 | export const getUserPending = () => { 8 | return fetchGet("get_pending", storeFriendsInfo); 9 | }; 10 | 11 | export const getUserData = () => { 12 | return fetchGet("me", storeUserInfo); 13 | }; 14 | 15 | export const getLeaderBoard = () => { 16 | return fetchGet("get_leaderboard", storeLeaderBoardInfo); 17 | }; 18 | 19 | const fetchGet = async (url: string, callback: any) => { 20 | let fetchUrl = process.env.REACT_APP_BACKEND_URL + "/users/" + url; 21 | try { 22 | const response = await fetch(fetchUrl, { 23 | method: "GET", 24 | headers: authHeader(), 25 | body: null, 26 | redirect: "follow", 27 | }); 28 | const result_1 = await response.json(); 29 | if (!response.ok) { 30 | return "error"; 31 | } 32 | return callback(result_1); 33 | } catch (error) { 34 | return console.log("error", error); 35 | } 36 | }; 37 | 38 | export const storeUserInfo = (result: any) => { 39 | localStorage.setItem("userID", result.id); 40 | localStorage.setItem("userName", result.username); 41 | localStorage.setItem("userEmail", result.email); 42 | localStorage.setItem("userPicture", result.avatar); 43 | localStorage.setItem("userGamesWon", result.gamesWon); 44 | localStorage.setItem("userGamesLost", result.gamesLost); 45 | localStorage.setItem("userGamesPlayed", result.gamesPlayed); 46 | localStorage.setItem("userAuth", result.twoFA); 47 | }; 48 | 49 | export const storeFriendsInfo = (result: any) => { 50 | return result; 51 | }; 52 | 53 | export const storeLeaderBoardInfo = (result: any) => { 54 | return result; 55 | }; 56 | -------------------------------------------------------------------------------- /front/src/routes/profile_types/private/users_relations/UsersRelations.tsx: -------------------------------------------------------------------------------- 1 | import { Col, Card, Container, Nav, Navbar } from "react-bootstrap"; 2 | import { NavLink, Outlet } from "react-router-dom"; 3 | 4 | export const UsersRelations = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /front/src/routes/profile_types/private/TwoFA.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { Row, Col } from "react-bootstrap"; 3 | import { NotifCxt } from "../../../App"; 4 | import { twoFAOff } from "../../../queries/twoFAQueries"; 5 | 6 | export const TwoFA = (props: any) => { 7 | const notif = useContext(NotifCxt); 8 | 9 | const handleTurnOff = (e: any) => { 10 | e.preventDefault(); 11 | const twoFADeactivate = async () => { 12 | const result = await twoFAOff(); 13 | if (!result) { 14 | notif?.setNotifText("Cannot deactivate 2FA. Please try again."); 15 | notif?.setNotifShow(true); 16 | } else { 17 | notif?.setNotifText("2FA successfully deactivated."); 18 | notif?.setNotifShow(true); 19 | props.onDeactivate(); 20 | localStorage.setItem("userAuth", "false"); 21 | } 22 | }; 23 | twoFADeactivate(); 24 | }; 25 | 26 | return ( 27 |
28 | 29 | 30 |
31 | 32 | {props.auth === "true" 33 | ? "Two Factor authentifcation enabled" 34 | : "Two Factor authentifcation disabled"} 35 | 36 |
37 | 38 | 39 | 48 | 49 |
50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /back/src/chat/type/chat.type.ts: -------------------------------------------------------------------------------- 1 | import { Player } from 'src/game/interfaces/player.interface'; 2 | 3 | export type oneSuggestion = { 4 | catagory: string; 5 | picture: string; 6 | name: string; 7 | id: number; 8 | data_id: number; 9 | }; 10 | 11 | export type chatPreview = { 12 | id: number; 13 | dm: boolean; 14 | name: string; 15 | isPassword: boolean; 16 | updateAt: string; 17 | lastMsg: string; 18 | unreadCount?: number; 19 | ownerEmail: string; 20 | ownerId: number; 21 | }; 22 | 23 | // eslint-disable-next-line unicorn/prevent-abbreviations 24 | export type oneMsg = { 25 | msgId: number; 26 | id: number; 27 | channelId: number; 28 | email: string; 29 | username: string; 30 | msg: string; 31 | createAt: string; 32 | updateAt: string; 33 | isInvite: boolean; 34 | }; 35 | 36 | export type oneUser = { 37 | online: boolean; 38 | username: string; 39 | id: number; 40 | email: string; 41 | isOwner: boolean; 42 | isAdmin: boolean; 43 | isInvited: boolean; 44 | isMuted: boolean; 45 | isFriend: boolean; 46 | }; 47 | 48 | export type updateUser = { 49 | selfEmail: string | null; 50 | otherId: number; 51 | }; 52 | 53 | export type Tag = { 54 | id: number; 55 | name: string; 56 | }; 57 | 58 | export type updateChannel = { 59 | channelId: number; 60 | dm: boolean; 61 | email: string | null; 62 | password: string; 63 | targetId: number; 64 | private: boolean; 65 | isPassword: boolean; 66 | newPassword: string; 67 | }; 68 | 69 | export type mute = { 70 | duration: number; 71 | email: string; 72 | channelId: number; 73 | }; 74 | 75 | export type gameInvitation = { 76 | gameInfo: Player; 77 | inviterId: number; 78 | inviterName: string; 79 | targetId: number; 80 | }; 81 | 82 | export type fetchDM = { 83 | channelId: number; 84 | targetId: number; 85 | }; 86 | -------------------------------------------------------------------------------- /nginx/ssl-certs/cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCv2nXwtDJbJyGh 3 | EGQFrG/af1/c162sy8FC8/KBMJvi3P69ZaUQRamCdPBYtgEaDia03bd69sxCLWFt 4 | v0ZSDClK2s6IaiZ+18/JyDyyPlUky1Xm/uHn5+phFZupAlKB7lqHXZ6MPPYsUsUn 5 | 8ByvpzpCWztWu35C8vk/cg1Hq/bUnQpX0M6rS2ZjVs1Y0NOqZM1FOiojsZQeXi5Z 6 | LgBtyL41qOM+2INthxLpXBQMdLfo5bKJlWYLff6c/0u17atVdzCGgLz7eC5tgED3 7 | ph0KkXH/Jd3XVKYkJbVRt/8oea7mM6mrKmVt7uMBhU9QK1Y1ZimW4QY0YU3J2u/Y 8 | sYi7e9ORAgMBAAECggEAbW6GPF72gzDrxX4csEcpaBAhyJ1Sz5gQUSjbvTdRmrCp 9 | uZTaTjD7llpNAvdn4h3ySlU7C8MvQvNYkOIZ0Y8N1y4onk1oXUDRZqE73DPb6kS6 10 | 1T/Btv3kJbw7nMX8MCzymxBQTLbW/qgXVo4eW9S19Xsuhv+wR9tJ7gh8aSCDPrTe 11 | +IvinVp64Nu/abBUXhS6MnbApc4YKgJkROu374Ok7fw/poeNk8ljyhGW6X2NLU5W 12 | NLhH2LRDWBs5LxpEjzovbG1Pcd38U9z3K2zoeOMITmuJ4OeZncARIDL9UWDInI4F 13 | zW9iPN6VY5HmtS1cC32D9Kwb0hyLgScZ1Djp9GElwQKBgQDbEUXEuB6XFuLmMEvh 14 | c4GsHmXUNe2i06O8dgvmcRjsibjReCIuqunKb2Cu5bmxKtYuy0RQlDdpTAMxveXz 15 | mEy3O+4q8uriwvReSJ7lrBBrJ5WEztH+/cefsjKG26cBwlMBEFPDm/kSFzDMJa8I 16 | aCM6S4AiVBkN26QrRlAVn2NxqQKBgQDNgByN3lP/E/edOtZsQgqER2IHTEm4iJLF 17 | jZav/XSKleQyBfDAasQnWwIf04MGHzPBanOHdwLqYbJ7QyOvN79JrOr9p1KlrMKq 18 | bpL9W3TZD2TB7wweY1Bp5ezE8qBVdyVcCJlbW5ssOkyDeRmQNviUUz5Jm6vDbpIx 19 | vdUhMpxTqQKBgAaKpH+0U82FNG9VP97SmSuvmLDWj2sOUNRe3goalHjzv94ZbUPh 20 | fKu72bI/T+U3dz/ceATD+EefqnTAy+4TPS83jewgZt0wnyV1m7EWC0N34iIeHu0z 21 | XNOq+ZFHW9xUli/w8d9kd/KPWLiv42Mn4O1rtb7QrhLY3TUhm2CZ6tlJAoGBALyi 22 | QpO/Z58Xtw2uurD5FipCVCuazXkEdKW+yHmX0Gh+GtAIP+yAlLoDgjmwr+7oqVt7 23 | 2byZdCWttGYNRS5Ln92DStm3w9esc6x6f5btYp700GpsgdVLGL98fRB0HtUU30hR 24 | 5GPF/PRmN2HfwaVtsFSG7QAoU0y7rJrYRFGK4ZCxAoGAbqPQIs2qb3H3049NAADF 25 | lmvVaXvCHM3XNboo3g52m9bekig1zdc9mDxcnLmRHD5T/pEvRx0fyUgxn7/N4R+V 26 | BPNmrOIraiSY4ABZiHPhn+7H1JVDtuUTne4Iei8lcvRTnvytxAX6v9O6lBvYtejk 27 | UBjT4s4Tz78gRyTpRevZFzw= 28 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /front/src/routes/Watch.css: -------------------------------------------------------------------------------- 1 | .Row { 2 | cursor: pointer; 3 | } 4 | 5 | .Row:hover { 6 | transform: scale(1.2); 7 | } 8 | 9 | .Row:active 10 | { 11 | transform: scale(0.9); 12 | transition: all 0.2s cubic-bezier(.45,1.71,.41,.97); 13 | } 14 | 15 | .LittleAvatar { 16 | margin: auto; 17 | width: 3vh; 18 | height: 3vh; 19 | border: 2px solid #F9F9F9; 20 | border-radius: 50%; 21 | padding: 15px; 22 | /* background: #D8D8D8; */ 23 | } 24 | 25 | th, td { 26 | padding-top: 10px; 27 | padding-bottom: 20px; 28 | padding-left: 30px; 29 | padding-right: 40px; 30 | } 31 | 32 | p { 33 | text-align: center; 34 | } 35 | 36 | .Refresh_button { 37 | background-color: Transparent; 38 | margin: auto; 39 | min-height: 30px; 40 | width: 10em; 41 | cursor: pointer; 42 | border: 1px solid rgb(255, 255, 255); 43 | color: white; 44 | opacity: 1; 45 | height: 2.3em; 46 | border-radius: 5px; 47 | transition: opacity 1s; 48 | align-items: center; 49 | } 50 | 51 | .Info-watch { 52 | margin: 2% auto; 53 | height: 20%; 54 | overflow: none; 55 | display: flex; 56 | flex-direction: row; 57 | justify-content: space-between; 58 | align-content: space-between; 59 | height: 7vh; 60 | width: calc(177vh * 0.45); 61 | /*background-image: radial-gradient(circle at 50% -40%, rgb(192,69,69) -180%, rgb(57,80,127) 270%);*/ 62 | /*border: 2px solid rgb(255, 255, 255); 63 | border-radius: 15px;*/ 64 | /*box-shadow: 0px 0px 5px 5px rgb(80, 200, 255), inset 0px 0px 5px 4px rgb(0, 190, 255);*/ 65 | } 66 | 67 | .Page-top-watch { 68 | margin: auto; 69 | height: 30%; 70 | width: 100%; 71 | overflow: auto; 72 | display: flex; 73 | flex-direction: column; 74 | /*justify-content: center;*/ 75 | align-items: center; 76 | } -------------------------------------------------------------------------------- /front/src/globals/Interfaces.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch } from "react"; 2 | 3 | export interface AuthContextType { 4 | user: string | null; 5 | signin: (user: string | null, callback: VoidFunction) => void; 6 | signout: (callback: VoidFunction) => void; 7 | } 8 | 9 | export interface IUserInputsRef { 10 | username: React.RefObject; 11 | email: React.RefObject; 12 | password: React.RefObject; 13 | } 14 | 15 | export interface IUserInfo { 16 | username: string | null; 17 | email: string | null; 18 | password: string | null; 19 | clear: any; 20 | } 21 | 22 | export interface ItableRow { 23 | key: number; 24 | userModel: { username: string; avatar: string; id: number; status: number }; 25 | } 26 | 27 | export interface IUserStatus { 28 | key: number; 29 | userModel: { id: number; status: number }; 30 | } 31 | 32 | export interface userModel { 33 | id: number; 34 | username: string; 35 | avatar: string; 36 | friends: Array; 37 | gamesLost: number; 38 | gamesPlayed: number; 39 | gamesWon: number; 40 | playTime: number; 41 | rank: number; 42 | score: number; 43 | winRate: number; 44 | } 45 | 46 | export interface gameModel { 47 | userId: number; 48 | opponentId: number; 49 | opponentUsername: string; 50 | opponentRank: number; 51 | duration: number; 52 | userScore: number; 53 | opponentScore: number; 54 | victory: boolean; 55 | } 56 | export class Users { 57 | key: number = 0; 58 | game: gameModel = { 59 | userId: 0, 60 | opponentId: 0, 61 | opponentUsername: "", 62 | opponentRank: 0, 63 | duration: 0, 64 | userScore: 0, 65 | opponentScore: 0, 66 | victory: false, 67 | }; 68 | } 69 | 70 | export interface INotifCxt { 71 | setNotifShow: Dispatch>; 72 | setNotifText: Dispatch>; 73 | } -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.3.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.2", 10 | "@types/node": "^16.11.39", 11 | "@types/react": "^18.0.12", 12 | "@types/react-dom": "^18.0.5", 13 | "@types/react-tag-autocomplete": "^6.1.1", 14 | "bootstrap": "^5.1.3", 15 | "bootstrap-icons": "^1.9.1", 16 | "emoji-picker-react": "^3.5.1", 17 | "focus-trap-react": "^9.0.2", 18 | "match-sorter": "^6.3.1", 19 | "react": "^18.1.0", 20 | "react-bootstrap": "^2.4.0", 21 | "react-bootstrap-icons": "^1.8.4", 22 | "react-contexify": "^5.0.0", 23 | "react-dom": "^18.1.0", 24 | "react-router-dom": "^6.3.0", 25 | "react-scripts": "5.0.1", 26 | "react-search-autocomplete": "^7.2.2", 27 | "react-switch": "^7.0.0", 28 | "react-tag-autocomplete": "^6.3.0", 29 | "react-tsparticles": "^2.1.0", 30 | "socket.io-client": "^4.5.1", 31 | "tsparticles": "^2.1.0", 32 | "typescript": "^4.7.3", 33 | "web-vitals": "^2.1.4" 34 | }, 35 | "scripts": { 36 | "start": "react-scripts start", 37 | "build": "react-scripts build", 38 | "test": "react-scripts test", 39 | "eject": "react-scripts eject" 40 | }, 41 | "eslintConfig": { 42 | "extends": [ 43 | "react-app", 44 | "react-app/jest" 45 | ] 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "devDependencies": { 60 | "file-loader": "^6.2.0", 61 | "prettier": "2.7.1", 62 | "webpack": "^5.73.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /front/src/modals/MUploadAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | import { Modal, Button, Form } from "react-bootstrap"; 3 | import { NotifCxt } from "../App"; 4 | import { uploadAvatarQuery } from "../queries/avatarQueries"; 5 | 6 | export function MUploadAvatar(props: any) { 7 | const notif = useContext(NotifCxt); 8 | const [newAvatar, setNewAvatar] = useState(); 9 | 10 | const onChange = (e: any) => { 11 | setNewAvatar(e.target.files[0]); 12 | }; 13 | 14 | const handleSubmit = (event: any) => { 15 | if (newAvatar) { 16 | const uploadAvatar = async () => { 17 | const result_1 = await uploadAvatarQuery(newAvatar); 18 | if (result_1 !== "error") { 19 | props.isAvatarUpdated(); 20 | props.onHide(); 21 | } else { 22 | notif?.setNotifText( 23 | "Unable to upload avatar. Please try again later." 24 | ); 25 | notif?.setNotifShow(true); 26 | } 27 | }; 28 | uploadAvatar(); 29 | } 30 | }; 31 | 32 | return ( 33 | 40 | 41 | 42 | Upload Avatar 43 | 44 | 45 | 46 | 47 | Choose an image to use as your avatar. 48 | 49 | 50 | 51 | 52 | 55 | 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /back/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthModule } from './auth/auth.module'; 3 | import { UserModule } from './user/user.module'; 4 | import { PrismaModule } from './prisma/prisma.module'; 5 | import { ConfigModule } from '@nestjs/config'; 6 | import { GameModule } from './game/game.module'; 7 | import { ChatModule } from './chat/chat.module'; 8 | import { JwtModule } from '@nestjs/jwt'; 9 | import { AppGateway } from './app.gateway'; 10 | import { UploadModule } from './upload/upload.module'; 11 | import { MulterModule } from '@nestjs/platform-express'; 12 | 13 | // Set the env file path 14 | let environmentFilePath = '.env'; 15 | 16 | if (process.env.ENVIRONMENT === 'PRODUCTION') { 17 | environmentFilePath = '.env.prod'; 18 | } else if (process.env.ENVIRONMENT === 'DEVELOPMENT') { 19 | environmentFilePath = '.env.dev'; 20 | } 21 | 22 | // Log 23 | console.log(`Running in ` + process.env.ENVIRONMENT + ` mode`); 24 | console.log('Using environment file: ' + environmentFilePath); 25 | console.log('Using port: ' + process.env.PORT); 26 | console.log('Using upload dir: ' + process.env.UPLOAD_DIR); 27 | 28 | /* 29 | * This one is the main module, it will import all the others. 30 | */ 31 | 32 | @Module({ 33 | imports: [ 34 | ConfigModule.forRoot({ 35 | // set path to .env file 36 | envFilePath: environmentFilePath, 37 | // global import 38 | isGlobal: true, 39 | // expand variables 40 | expandVariables: true, 41 | }), 42 | MulterModule, 43 | AuthModule, 44 | GameModule, 45 | UserModule, 46 | PrismaModule, 47 | ChatModule, 48 | JwtModule.register({ secret: process.env.JWT_SECRET }), 49 | UploadModule, 50 | ], 51 | providers: [AppGateway], 52 | exports: [AppGateway], 53 | }) 54 | export class AppModule {} 55 | 56 | console.log('API KEY: ' + process.env.FORTYTWO_SECRET); 57 | console.log('API UID: ' + process.env.FORTYTWO_ID); 58 | console.log('API CALLBACK: ' + process.env.FORTYTWO_CALLBACK); 59 | console.log('2FA APP NAME: ' + process.env.MY_2FA_APP_NAME); 60 | console.log('SITE URL: ' + process.env.SITE_URL); 61 | -------------------------------------------------------------------------------- /front/src/queries/userFriendsQueries.tsx: -------------------------------------------------------------------------------- 1 | import { authContentHeader } from "./headers"; 2 | 3 | export const getUserFriends = (otherId: number) => { 4 | let body = JSON.stringify({ 5 | otherId: otherId, 6 | }); 7 | return fetchGet("get_friends", authContentHeader, body); 8 | }; 9 | 10 | export const addFriendQuery = (otherId: number) => { 11 | let body = JSON.stringify({ 12 | otherId: otherId, 13 | }); 14 | return fetchGet("add_friend", authContentHeader, body); 15 | }; 16 | 17 | export const removeFriendQuery = (otherId: number) => { 18 | let body = JSON.stringify({ 19 | otherId: otherId, 20 | }); 21 | return fetchGet("rm_friend", authContentHeader, body); 22 | }; 23 | 24 | export const blockUserQuery = (otherId: number) => { 25 | let body = JSON.stringify({ 26 | otherId: otherId, 27 | }); 28 | return fetchGet("block_user", authContentHeader, body); 29 | }; 30 | 31 | export const unblockUserQuery = (otherId: number) => { 32 | let body = JSON.stringify({ 33 | otherId: otherId, 34 | }); 35 | return fetchGet("unblock_user", authContentHeader, body); 36 | }; 37 | 38 | export const cancelInviteQuery = (otherId: number) => { 39 | let body = JSON.stringify({ 40 | otherId: otherId, 41 | }); 42 | return fetchGet("cancel_invite", authContentHeader, body); 43 | }; 44 | 45 | export const denyInviteQuery = (otherId: number) => { 46 | let body = JSON.stringify({ 47 | otherId: otherId, 48 | }); 49 | return fetchGet("deny_invite", authContentHeader, body); 50 | }; 51 | 52 | const fetchGet = async (url: string, header: any, body: any) => { 53 | let fetchUrl = process.env.REACT_APP_BACKEND_URL + "/users/" + url; 54 | try { 55 | const response = await fetch(fetchUrl, { 56 | method: "POST", 57 | headers: header(), 58 | body: body, 59 | redirect: "follow", 60 | }); 61 | const result = await response.json(); 62 | if (!response.ok) { 63 | console.log("POST error on ", url); 64 | return "error"; 65 | } 66 | return result; 67 | } catch (error) { 68 | return console.log("error", error); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /back/src/auth/2FA/2fa.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, HttpCode, Post, Res } from '@nestjs/common'; 2 | import { GetCurrentUser, Public } from 'src/decorators'; 3 | import { TwoFactorDto, TwoFactorUserDto } from '../dto'; 4 | import { TwoFactorService } from './2fa.service'; 5 | import { Response } from 'express'; 6 | import { ApiHeader, ApiResponse, ApiTags } from '@nestjs/swagger'; 7 | 8 | @Controller('/auth/2fa') 9 | @ApiTags('Two Factor Authentication') 10 | @ApiHeader({ 11 | name: 'Two Factor Authentication', 12 | description: 'Two Factor Authentication', 13 | }) 14 | export class TwoFAController { 15 | constructor(private twoFAservice: TwoFactorService) {} 16 | 17 | /* TWO FACTOR AUTHENTIFICATION */ 18 | 19 | /** 20 | * /2FA/turn-on - turn on 2FA 21 | */ 22 | @Post('/turn-on') 23 | @ApiResponse({ status: 401, description: 'Invalid 2FA code' }) 24 | @HttpCode(200) 25 | async turn_on( 26 | @Body() { twoFAcode }: any, 27 | @GetCurrentUser() user: TwoFactorUserDto, 28 | ) { 29 | const tokens = await this.twoFAservice.turn_on(twoFAcode, user); 30 | return tokens; 31 | } 32 | 33 | /** 34 | * /2FA/turn-off - turn off 2FA 35 | */ 36 | @Post('/turn-off') 37 | @HttpCode(200) 38 | async turn_off(@GetCurrentUser() user: TwoFactorUserDto) { 39 | const tokens = await this.twoFAservice.turn_off(user); 40 | return tokens; 41 | } 42 | 43 | /** 44 | * /2fa/authenticate - authenticate 2FA 45 | */ 46 | @Public() 47 | @ApiResponse({ status: 401, description: 'Invalid 2FA code' }) 48 | @Post('/authenticate') 49 | async authenticate(@Body() dto: TwoFactorDto) { 50 | // LOG 51 | //console.log('auth 2fa', dto); 52 | return this.twoFAservice.authenticate(dto); 53 | } 54 | 55 | /** 56 | * /2fa/generate - generate a new 2FA QR code 57 | */ 58 | @Post('/generate') 59 | async generate_2fa( 60 | @Res() response: Response, 61 | @GetCurrentUser('email') email: string, 62 | ) { 63 | const { onetimepathurl } = await this.twoFAservice.generate2FA(email); 64 | const qrcode = await this.twoFAservice.generate2FAQRCode( 65 | onetimepathurl, 66 | ); 67 | return response.json(qrcode); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /front/src/queries/twoFAQueries.tsx: -------------------------------------------------------------------------------- 1 | import { storeToken } from "./authQueries"; 2 | import { getUserData } from "./userQueries"; 3 | 4 | /* Generate 2FA QR code */ 5 | export const twoFAGenerate = () => { 6 | return fetchPost(null, "generate", null); 7 | }; 8 | 9 | /* Validate 2FA code when signin in */ 10 | export const twoFAAuth = ( 11 | twoFAcode: string, 12 | email: string, 13 | userSignIn: any 14 | ) => { 15 | let raw = JSON.stringify({ 16 | username: email, 17 | twoFAcode: twoFAcode, 18 | }); 19 | return fetchPost(raw, "authenticate", userSignIn); 20 | }; 21 | 22 | /* Turn on 2FA for signed in user */ 23 | export const twoFAOn = (code: string) => { 24 | let raw = JSON.stringify({ 25 | twoFAcode: code, 26 | }); 27 | console.log("TURN ON"); 28 | return fetchPost(raw, "turn-on", null); 29 | }; 30 | 31 | export const twoFAOff = () => { 32 | return fetchPost(null, "turn-off", null); 33 | }; 34 | 35 | const authRawHeader = () => { 36 | let token = "bearer " + localStorage.getItem("userToken"); 37 | let myHeaders = new Headers(); 38 | myHeaders.append("Authorization", token); 39 | myHeaders.append("Content-Type", "application/json"); 40 | return myHeaders; 41 | }; 42 | 43 | const fetchPost = async (body: any, url: string, userSignIn: any) => { 44 | let fetchUrl = process.env.REACT_APP_BACKEND_URL + "/auth/2fa/" + url; 45 | 46 | try { 47 | const response = await fetch(fetchUrl, { 48 | method: "POST", 49 | headers: authRawHeader(), 50 | body: body, 51 | redirect: "follow", 52 | }); 53 | const result_1 = await response.json(); 54 | if (!response.ok) { 55 | console.log("POST error on ", url); 56 | return null; 57 | } 58 | if (url !== "generate") { 59 | storeToken(result_1); 60 | if (url === "authenticate") { 61 | if (localStorage.getItem("userToken")) { 62 | await getUserData(); 63 | if (localStorage.getItem("userName")) userSignIn(); 64 | else return null; 65 | } 66 | } 67 | } 68 | return result_1; 69 | } catch (error) { 70 | return console.log("error", error); 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /front/src/routes/Auth/Auth.css: -------------------------------------------------------------------------------- 1 | /* AUTHENTICATION */ 2 | 3 | .Auth-form-container { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | .Auth-form-content { 12 | padding-left: 12%; 13 | padding-right: 12%; 14 | } 15 | 16 | .Auth-form { 17 | justify-content: left; 18 | align-items: left; 19 | padding-top: 30px; 20 | padding-bottom: 50px; 21 | box-sizing: border-box; 22 | 23 | width: 30%; 24 | min-width: 400px; 25 | max-width: 400px; 26 | 27 | background: var(--cards-color-gray-up); 28 | box-shadow: inset 5px 4px 30px #23262b, inset -4px -5px 30px #434851; 29 | border-radius: 10px; 30 | } 31 | 32 | .form-test { 33 | font-family: "IBM Plex Mono", monospace; 34 | color: var(--text-color-white); 35 | letter-spacing: +2px; 36 | font-weight: 400; 37 | font-size: 12px; 38 | line-height: 20px; 39 | } 40 | 41 | .form-control { 42 | font-family: "IBM Plex Mono", monospace; 43 | color: var(--text-color-gray); 44 | background: #ecf0f3; 45 | box-shadow: inset 4px 4px 5px #c3c3c3, inset -6px -4px 5px #ffffff; 46 | border-radius: 17.5px; 47 | font-weight: 400; 48 | font-size: 12px; 49 | line-height: 20px; 50 | } 51 | 52 | .form-help-block { 53 | padding-top: 5px; 54 | padding-bottom: 10px; 55 | display: flex; 56 | align-items: flex-end; 57 | font-family: "Roboto" sans-serif; 58 | font-style: normal; 59 | font-weight: 200; 60 | font-size: 10px; 61 | opacity: 0.8; 62 | } 63 | 64 | .Auth-form-title { 65 | font-family: "Roboto" sans-serif; 66 | font-style: normal; 67 | font-weight: 500; 68 | font-size: 30px; 69 | line-height: 20px; 70 | 71 | letter-spacing: -0.4px; 72 | color: var(--text-color-white); 73 | } 74 | 75 | .text-secondary { 76 | padding-top: 16px; 77 | padding-bottom: 36px; 78 | display: flex; 79 | align-items: flex-end; 80 | font-family: "Roboto" sans-serif; 81 | font-style: normal; 82 | font-weight: 400; 83 | font-size: 12px; 84 | line-height: 14px; 85 | 86 | letter-spacing: -0.4px; 87 | color: var(--text-color-white); 88 | opacity: 0.8; 89 | } 90 | 91 | /* ////////////////////////////////////////////// */ 92 | -------------------------------------------------------------------------------- /front/src/Components/Navbar.css: -------------------------------------------------------------------------------- 1 | @import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css"); 2 | 3 | :root { 4 | --radius-size: 50px; 5 | --icons-size: 1.2rem; 6 | --border-size: 15px; 7 | --padding-size: 80px; 8 | } 9 | 10 | .toolbar-bigger { 11 | z-index: 800; 12 | height: 500px; 13 | width: 80px; 14 | position: absolute; 15 | left: -1.5px; 16 | top: 50%; 17 | 18 | transform: translate(-45%, -50%); 19 | transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); 20 | } 21 | .toolbar-bigger:hover { 22 | transform: translate(0, -50%); 23 | } 24 | 25 | .toolbar { 26 | position: absolute; 27 | height: 90%; 28 | box-shadow: 2.5px 2.5px 5px rgba(31, 33, 38, 0.25), -5px -5px 15px #363c45; 29 | border-radius: 0% var(--radius-size) var(--radius-size) 0%; 30 | background-image: linear-gradient( 31 | 175deg, 32 | rgba(67, 97, 238, 50), 33 | rgba(0, 0, 0, 0) 34 | ); 35 | padding: 1.5px; 36 | } 37 | 38 | .toolbar-top { 39 | width: 100%; 40 | height: 100%; 41 | border-radius: 0% var(--radius-size) var(--radius-size) 0%; 42 | background-color: #31353c; 43 | padding-top: var(--padding-size); 44 | padding-right: calc(var(--icons-size) + var(--border-size)); 45 | padding-left: var(--border-size); 46 | padding-bottom: var(--padding-size); 47 | display: flex; 48 | flex-direction: column; 49 | } 50 | 51 | .toolbar-top.space-around { 52 | justify-content: space-between; 53 | } 54 | 55 | .icons { 56 | font-size: var(--icons-size); 57 | position: absolute; 58 | } 59 | 60 | .icons.thin.hide { 61 | opacity: 0; 62 | color: #4361ee; 63 | transition: opacity 500ms ease-in-out, color 500ms ease-in-out; 64 | } 65 | 66 | .icons.thin.current { 67 | color: #d9d9d9; 68 | opacity: 100; 69 | transition: opacity 500ms ease-in-out, color 500ms ease-in-out; 70 | } 71 | .icons.thick.hide { 72 | opacity: 0; 73 | color: #4361ee; 74 | transition: opacity 500ms ease-in-out, color 500ms ease-in-out; 75 | } 76 | 77 | .icons.thick.current { 78 | color: #4361ee; 79 | opacity: 100; 80 | transition: opacity 500ms ease-in-out, color 500ms ease-in-out; 81 | } 82 | 83 | #clickableIcon { 84 | cursor: pointer; 85 | } 86 | -------------------------------------------------------------------------------- /front/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /front/src/Components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useLocation, useNavigate } from "react-router-dom"; 3 | import { useAuth } from "../globals/contexts"; 4 | import { MLogoutValid } from "../modals/MLogoutValid"; 5 | import "./Navbar.css"; 6 | 7 | const GetIcons = (props: any) => { 8 | const navigate = useNavigate(); 9 | const location = useLocation(); 10 | let auth = useAuth(); 11 | const [modalShow, setModalShow] = useState(false); 12 | 13 | const url = props.url; 14 | const fill = 15 | url === "private-profile" 16 | ? "person" 17 | : url === "leaderboard" 18 | ? "trophy" 19 | : url === "chat" 20 | ? "chat-left-dots" 21 | : url === "game" 22 | ? "dpad" 23 | : url === "watch" 24 | ? "play-btn" 25 | : "box-arrow-right"; 26 | 27 | return ( 28 |
29 | setModalShow(false)} 32 | onSubmit={() => { 33 | setModalShow(false); 34 | auth.signout(() => navigate("/auth/signin")); 35 | }} 36 | /> 37 |
38 | setModalShow(true) 46 | : () => navigate("/app/" + url) 47 | } 48 | /> 49 | navigate("/app/" + url)} 55 | /> 56 |
57 |
58 | ); 59 | }; 60 | 61 | export const CNavBar = () => { 62 | return ( 63 |
64 |
65 |
66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 |
75 |
76 |
77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /front/src/hooks/AuthHooks.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useState } from "react"; 2 | import { useLocation, Navigate, matchPath } from "react-router-dom"; 3 | import { AuthContext, useAuth } from "../globals/contexts"; 4 | import { logOut } from "../queries/authQueries"; 5 | 6 | export const RedirectWhenAuth = ({ children }: { children: JSX.Element }) => { 7 | const location = useLocation(); 8 | if ( 9 | matchPath(location.pathname, "/auth/signin") && 10 | localStorage!.getItem("userLogged") === "true" 11 | ) 12 | return ( 13 | 14 | ); 15 | return children; 16 | }; 17 | 18 | export const RequireAuth = ({ children }: { children: JSX.Element }) => { 19 | const auth = useAuth(); 20 | const location = useLocation(); 21 | 22 | if (localStorage!.getItem("userLogged")! === "true") 23 | auth.signin(localStorage.getItem("userName"), () => {}); 24 | else return ; 25 | return children; 26 | }; 27 | 28 | export const AuthProvider = ({ children }: { children: ReactNode }) => { 29 | const [user, setUser] = useState(null); 30 | 31 | const signin = (newUser: string | null, callback: VoidFunction) => { 32 | return fakeAuthProvider.signin(() => { 33 | setUser(newUser); 34 | localStorage.setItem("userLogged", "true"); 35 | callback(); 36 | }); 37 | }; 38 | 39 | const signout = (callback: VoidFunction) => { 40 | return fakeAuthProvider.signout(() => { 41 | const postLogout = async () => { 42 | const result = await logOut(); 43 | if (result !== "error") { 44 | setUser(null); 45 | localStorage.clear(); 46 | localStorage.setItem("userLogged", "false"); 47 | callback(); 48 | } 49 | }; 50 | postLogout(); 51 | }); 52 | }; 53 | const value = { user, signin, signout }; 54 | 55 | return {children}; 56 | }; 57 | 58 | const fakeAuthProvider = { 59 | isAuthenticated: false, 60 | signin(callback: VoidFunction) { 61 | fakeAuthProvider.isAuthenticated = true; 62 | setTimeout(callback, 1); // fake async 63 | }, 64 | signout(callback: VoidFunction) { 65 | fakeAuthProvider.isAuthenticated = false; 66 | setTimeout(callback, 1); 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | volumes: 4 | data: 5 | uploads: 6 | 7 | networks: 8 | backend: 9 | 10 | services: 11 | # FRONTEND 12 | front: 13 | image: node:lts-alpine 14 | container_name: frontend 15 | working_dir: /app 16 | command: sh -c "yarn install 17 | && yarn run build 18 | && yarn global add serve 19 | && serve -s build -l 3000" 20 | environment: 21 | - PUBLIC_URL=${FRONT_URL} 22 | - REACT_APP_BACKEND_URL=${BACK_URL} 23 | - REACT_APP_BACKEND_SOCKET=${SOCKET_URL} 24 | volumes: 25 | - ./front:/app 26 | ports: 27 | - "3000:3000" 28 | restart: unless-stopped 29 | networks: 30 | - backend 31 | depends_on: 32 | - back 33 | # BACKEND 34 | back: 35 | image: node:lts-alpine 36 | container_name: backend 37 | working_dir: /app 38 | command: sh -c "yarn install 39 | && npx prisma migrate deploy 40 | && yarn run start" 41 | environment: 42 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 43 | - POSTGRES_USER=${POSTGRES_USER} 44 | - POSTGRES_DB=${POSTGRES_DB} 45 | - SITE_URL=${SITE_URL} 46 | - FRONT_URL=${FRONT_URL} 47 | - BACK_URL=${BACK_URL} 48 | - FRONT_PORT=${FRONT_PORT} 49 | - BACK_PORT=${BACK_PORT} 50 | volumes: 51 | - ./back:/app 52 | - uploads:/app/image_dir 53 | ports: 54 | - "4000:4000" 55 | - "5555:5555" 56 | restart: unless-stopped 57 | networks: 58 | - backend 59 | depends_on: 60 | - postgres 61 | # POSTGRES 62 | postgres: 63 | image: postgres:14-alpine 64 | container_name: postgres 65 | ports: 66 | - 5432:5432 67 | restart: unless-stopped 68 | volumes: 69 | - data:/var/lib/postgresql/data 70 | environment: 71 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 72 | - POSTGRES_USER=${POSTGRES_USER} 73 | - POSTGRES_DB=${POSTGRES_DB} 74 | networks: 75 | - backend 76 | # NGINX 77 | nginx: 78 | image: nginx:stable-alpine 79 | container_name: nginx 80 | volumes: 81 | - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf 82 | ## - ./nginx/ssl-certs/cert.crt:/cert.crt 83 | ## - ./nginx/ssl-certs/cert.key:/cert.key 84 | ports: 85 | - "1024:1024" 86 | # command: sh -c "tail -f /dev/null" 87 | restart: unless-stopped 88 | networks: 89 | - backend 90 | depends_on: 91 | - back 92 | -------------------------------------------------------------------------------- /front/src/routes/chat_modes/type/chat.type.tsx: -------------------------------------------------------------------------------- 1 | import { Player } from "../../game.interfaces"; 2 | 3 | export type oneSuggestion = { 4 | catagory: string; 5 | picture: string; 6 | name: string; 7 | id: number; 8 | data_id: number; 9 | } 10 | 11 | export type chatPreview = { 12 | id: number; 13 | dm: boolean; 14 | name: string; 15 | isPassword: boolean; 16 | updateAt: string; 17 | lastMsg: string; 18 | unreadCount?: number; 19 | ownerEmail: string; 20 | ownerId: number; 21 | isBlocked: boolean; 22 | } 23 | 24 | export type newChannel = { 25 | name: string; 26 | private: boolean; 27 | isPassword: boolean; 28 | password: string; 29 | email: string | null; 30 | members: Tag[]; 31 | } 32 | 33 | export type newDM = { 34 | email: string | null; 35 | targetId: number; 36 | } 37 | 38 | export type fetchDM = { 39 | channelId: number; 40 | targetId: number; 41 | } 42 | 43 | export type Tag = { 44 | id: number | string; 45 | name: string; 46 | } 47 | 48 | export type updateChannel = { 49 | channelId: number | undefined; 50 | dm: boolean; 51 | email: string | null; 52 | password: string; 53 | targetId: number | string; 54 | private: boolean; 55 | isPassword: boolean; 56 | newPassword: string; 57 | } 58 | 59 | export type useMsg = { 60 | email: string | null; 61 | channelId: number; 62 | msg: string; 63 | msgId: number; 64 | } 65 | 66 | export type oneMsg = { 67 | msgId: number; 68 | id: number; 69 | channelId: number; 70 | email: string; 71 | username: string; 72 | msg: string; 73 | createAt: string; 74 | updateAt: string; 75 | isInvite: boolean; 76 | } 77 | 78 | export type oneUser = { 79 | username: string; 80 | id: number; 81 | email: string; 82 | isOwner: boolean; 83 | isAdmin: boolean; 84 | isInvited: boolean; 85 | isMuted: boolean; 86 | isFriend: boolean; 87 | isOnline: boolean; 88 | isBlocked: boolean; 89 | } 90 | 91 | export type updateUser = { 92 | selfEmail: string | null; 93 | otherId: number; 94 | } 95 | 96 | export type setting = { 97 | private: boolean; 98 | isPassword: boolean; 99 | } 100 | 101 | export type mute = { 102 | duration: number; 103 | email: string; 104 | channelId: number; 105 | } 106 | 107 | export type gameInvitation = { 108 | gameInfo: Player; 109 | inviterId: number; 110 | inviterName: string; 111 | targetId: number; 112 | } -------------------------------------------------------------------------------- /front/public/particlesjs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "particles": { 3 | "number": { 4 | "value": 48, 5 | "density": { 6 | "enable": true, 7 | "value_area": 2083.1330320802504 8 | } 9 | }, 10 | "color": { 11 | "value": "#dc5151" 12 | }, 13 | "shape": { 14 | "type": "circle", 15 | "stroke": { 16 | "width": 0, 17 | "color": "#000000" 18 | }, 19 | "polygon": { 20 | "nb_sides": 4 21 | }, 22 | "image": { 23 | "src": "img/github.svg", 24 | "width": 100, 25 | "height": 100 26 | } 27 | }, 28 | "opacity": { 29 | "value": 0, 30 | "random": true, 31 | "anim": { 32 | "enable": false, 33 | "speed": 0, 34 | "opacity_min": 1, 35 | "sync": false 36 | } 37 | }, 38 | "size": { 39 | "value": 0, 40 | "random": true, 41 | "anim": { 42 | "enable": false, 43 | "speed": 40, 44 | "size_min": 0.1, 45 | "sync": false 46 | } 47 | }, 48 | "line_linked": { 49 | "enable": true, 50 | "distance": 208.31330320802505, 51 | "color": "#ffffff", 52 | "opacity": 0.2964458545652664, 53 | "width": 0.9614460148062693 54 | }, 55 | "move": { 56 | "enable": true, 57 | "speed": 6, 58 | "direction": "none", 59 | "random": false, 60 | "straight": false, 61 | "out_mode": "out", 62 | "bounce": false, 63 | "attract": { 64 | "enable": false, 65 | "rotateX": 600, 66 | "rotateY": 1200 67 | } 68 | } 69 | }, 70 | "interactivity": { 71 | "detect_on": "canvas", 72 | "events": { 73 | "onhover": { 74 | "enable": true, 75 | "mode": "grab" 76 | }, 77 | "onclick": { 78 | "enable": false, 79 | "mode": "push" 80 | }, 81 | "resize": true 82 | }, 83 | "modes": { 84 | "grab": { 85 | "distance": 400, 86 | "line_linked": { 87 | "opacity": 1 88 | } 89 | }, 90 | "bubble": { 91 | "distance": 400, 92 | "size": 40, 93 | "duration": 2, 94 | "opacity": 8, 95 | "speed": 3 96 | }, 97 | "repulse": { 98 | "distance": 200, 99 | "duration": 0.4 100 | }, 101 | "push": { 102 | "particles_nb": 4 103 | }, 104 | "remove": { 105 | "particles_nb": 2 106 | } 107 | } 108 | }, 109 | "retina_detect": false 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Contributors][contributors-shield]][contributors-url] 2 | [![Commits][commits-shield]][commits-url] 3 | [![Pull Requests][pr-closed-shield]][pr-closed-url] 4 | ![Project passed][project-shield] 5 | 6 | # Transcendence 7 | 8 | Transcendence is a 42 school project, to learn about web developpement and SPA. 9 | The goal is to create a web app to play Pong, and socialize with other users. 10 | 11 | ## Built With 12 | ___ 13 | * [React](https://reactjs.org/) - A JavaScript library for building user interfaces 14 | * [NestJS](https://nestjs.com/) - A progressive Node.js framework for server-side applications. 15 | * [PostgreSQL](https://www.postgresql.org/) - Open source object-relational database system 16 | * [NGINX](https://www.nginx.com/) - Open-source HTTP server and reverse proxy 17 | 18 | ## Deployment 19 | ___ 20 | - run ```make``` 21 | - go to ```http://localhost:3000/``` 22 | 23 | ## Tools 24 | 25 | The project was built using these tools. 26 | 27 | ### Organisation 28 | 29 | - [Notion](https://ft-transcendence42.notion.site/Ft_transcendence-wiki-1df70bf999bb4290ab9729dc5aeb742b) 30 | - Figma 31 | 32 | ### Front-end 33 | 34 | - [React Bootstrap](https://react-bootstrap.github.io/) 35 | - [React Router](https://reactrouter.com/en/main) 36 | - [React Contextify](https://fkhadra.github.io/react-contexify/) 37 | 38 | ### Back-end 39 | 40 | - [Prisma](https://www.prisma.io/) 41 | - [PassportJS](https://www.passportjs.org/) 42 | 43 | and more... 44 | 45 | ## Previews 46 | ___ 47 | ### Private profile 48 | ![Alt text](/screenshots/private_profile.png "Private profile screenshot") 49 | 50 | ### Public profile 51 | ![Alt text](/screenshots/public_profile.png "Public profile screenshot") 52 | 53 | ### Leaderboard 54 | ![Alt text](/screenshots/leaderboard.png "Leaderboard screenshot") 55 | 56 | ### Chat 57 | ![Alt text](/screenshots/chat.png "Chat screenshot") 58 | 59 | ### Watch Game 60 | ![Alt text](/screenshots/watch.png "Watch screenshot") 61 | 62 | ## Contributors 63 | ___ 64 | - [Anthony Trupheme](https://github.com/antrup) 65 | - [Florian Cavillon](https://github.com/fcavillo) 66 | - [Marie-Lou Valdes](https://github.com/mvaldes42) 67 | - [Satcheen Shakya](https://github.com/5atchm1n) 68 | - [Shu Yen Lu](https://github.com/shuyenla) 69 | 70 | 71 | [contributors-shield]: https://img.shields.io/github/contributors/ft-transcendence/transcendence 72 | [contributors-url]: https://github.com/ft-transcendence/transcendence/graphs/contributors 73 | 74 | [commits-shield]: https://img.shields.io/github/last-commit/ft-transcendence/transcendence 75 | [commits-url]: https://github.com/ft-transcendence/transcendence/graphs/commit-activity 76 | 77 | [pr-closed-shield]: https://img.shields.io/github/issues-pr-closed/ft-transcendence/transcendence 78 | [pr-closed-url]: https://github.com/ft-transcendence/transcendence/pulls?q=is%3Apr+is%3Aclosed 79 | 80 | [project-shield]:https://img.shields.io/badge/project%20passed-100%25-brightgreen 81 | -------------------------------------------------------------------------------- /front/src/queries/authQueries.tsx: -------------------------------------------------------------------------------- 1 | import { authHeader } from "./headers"; 2 | import { getUserData } from "./userQueries"; 3 | 4 | let myHeaders = new Headers(); 5 | myHeaders.append("Content-Type", "application/json"); 6 | 7 | // const NavigateTwoFA = (username: string) => { 8 | // console.log("username navigate: ", username); 9 | // let navigate = useNavigate(); 10 | // navigate("/2FA?user=" + username); 11 | // }; 12 | 13 | const fetchPost = async ( 14 | raw: string, 15 | userInfo: any, 16 | userSignIn: any, 17 | url: string 18 | ) => { 19 | let fetchUrl = process.env.REACT_APP_BACKEND_URL + "/auth/" + url; 20 | 21 | try { 22 | const response = await fetch(fetchUrl, { 23 | method: "POST", 24 | headers: myHeaders, 25 | body: raw, 26 | redirect: "follow", 27 | }); 28 | const result_1 = await response.json(); 29 | if (!response.ok) { 30 | console.log("POST error on ", url); 31 | return "error: " + url; 32 | } 33 | // check if user is 2FA 34 | if (result_1.twoFA) { 35 | // redirect to 2FA page 36 | const url = "/2FA?user=" + result_1.username; 37 | window.location.href = url; 38 | // NavigateTwoFA(rest.username); 39 | } else { 40 | userInfo.clear(); 41 | storeToken(result_1); 42 | if (localStorage.getItem("userToken")) { 43 | await getUserData(); 44 | if (localStorage.getItem("userName")) userSignIn(); 45 | else return "error"; 46 | } 47 | } 48 | } catch (error) { 49 | return console.log("error", error); 50 | } 51 | }; 52 | 53 | export const signIn = (userInfo: any, userSignIn: any) => { 54 | let raw = JSON.stringify({ 55 | username: userInfo.email, 56 | password: userInfo.password, 57 | }); 58 | return fetchPost(raw, userInfo, userSignIn, "signin"); 59 | }; 60 | 61 | export const signUp = (userInfo: any, userSignIn: any) => { 62 | let raw = JSON.stringify({ 63 | email: userInfo.email, 64 | password: userInfo.password, 65 | username: userInfo.username, 66 | }); 67 | return fetchPost(raw, userInfo, userSignIn, "signup"); 68 | }; 69 | 70 | export const storeToken = (token: any) => { 71 | localStorage.setItem("userToken", token.access_token); 72 | localStorage.setItem("userRefreshToken", token.refresh_token); 73 | }; 74 | 75 | export const logOut = () => { 76 | return fetchPostLogout(); 77 | }; 78 | 79 | const fetchPostLogout = async () => { 80 | let fetchUrl = process.env.REACT_APP_BACKEND_URL + "/auth/logout"; 81 | 82 | try { 83 | const response = await fetch(fetchUrl, { 84 | method: "POST", 85 | headers: authHeader(), 86 | redirect: "follow", 87 | }); 88 | const result_1 = await response.text(); 89 | if (!response.ok) { 90 | console.log("POST error on logout"); 91 | return "error"; 92 | } 93 | return result_1; 94 | } catch (error) { 95 | return console.log("error", error); 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /front/src/routes/TwoFAValidation.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from "react"; 2 | import { Button, Form } from "react-bootstrap"; 3 | import { useLocation, useNavigate } from "react-router-dom"; 4 | import { NotifCxt } from "../App"; 5 | import { useAuth } from "../globals/contexts"; 6 | import { twoFAAuth } from "../queries/twoFAQueries"; 7 | 8 | export default function TwoFAValidation() { 9 | const notif = useContext(NotifCxt); 10 | let location = useLocation(); 11 | let navigate = useNavigate(); 12 | let auth = useAuth(); 13 | let username = localStorage.getItem("userName"); 14 | 15 | const [twoFACode, setCode] = useState(""); 16 | 17 | const handleInputChange = (e: any) => { 18 | const { value } = e.target; 19 | setCode(value); 20 | }; 21 | 22 | // get username from redirect URL 23 | useEffect(() => { 24 | const urlUsername = location.search.split("=")[1]; 25 | if (urlUsername) { 26 | localStorage.setItem("userName", urlUsername); 27 | navigate("/2FA"); 28 | } 29 | }, [location.search, navigate]); 30 | 31 | const handleSubmit = (e: any) => { 32 | e.preventDefault(); 33 | 34 | const userSignIn = () => { 35 | let username = localStorage.getItem("userName"); 36 | if (username) 37 | auth.signin(username, () => { 38 | navigate("/app/private-profile", { replace: true }); 39 | }); 40 | }; 41 | if (username !== "undefined" && username) { 42 | const twoFAValid = async (username: string) => { 43 | const result = await twoFAAuth(twoFACode, username, userSignIn); 44 | if (!result) { 45 | notif?.setNotifShow(true); 46 | notif?.setNotifText("Incorrect code. Please try again."); 47 | } 48 | }; 49 | twoFAValid(username); 50 | } else console.log("username is undefined"); 51 | }; 52 | 53 | return ( 54 |
55 |
56 |
57 |
58 |

Login with 2FA.

59 |
60 | Open your favorite authentication app, and enter the corresponding 61 | code. 62 |
63 | 64 | 71 | 72 | 83 |
84 |
85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /front/src/routes/gameRequestCard.tsx: -------------------------------------------------------------------------------- 1 | import "./chat_modes/card.css"; 2 | import { useEffect, useState } from "react"; 3 | import { socket } from "../App" 4 | import { useNavigate } from "react-router-dom"; 5 | import { Player } from "./game.interfaces"; 6 | import { getUserAvatarQuery } from "../queries/avatarQueries"; 7 | import { gameInvitation } from "./chat_modes/type/chat.type"; 8 | 9 | export function GameRequestCard({game, gameRequest, onGameRequest} 10 | : { game: gameInvitation | undefined, 11 | gameRequest: boolean, 12 | onGameRequest: () => void}) { 13 | 14 | const navigate = useNavigate(); 15 | const [avatarURL, setAvatarURL] = useState(""); 16 | 17 | useEffect(() => { 18 | if (game) 19 | { 20 | const getAvatar = async () => { 21 | const result: undefined | string | Blob | MediaSource = 22 | await getUserAvatarQuery(game!.inviterId); 23 | 24 | if (result !== undefined && result instanceof Blob) { 25 | setAvatarURL(URL.createObjectURL(result)); 26 | } 27 | } 28 | getAvatar(); 29 | } 30 | }, [game]); 31 | 32 | const joinGame = () => { 33 | socket.emit("join_private", {roomId: game!.gameInfo.roomId}, (player: Player) =>{ 34 | if (player.roomId !== undefined && player.playerNb !== undefined) { 35 | localStorage.setItem("roomid", player.roomId.toString()); 36 | localStorage.setItem("playernb", player.playerNb.toString()); 37 | onGameRequest(); 38 | navigate("/app/privateGame"); 39 | } 40 | else 41 | { 42 | socket.disconnect(); 43 | socket.connect(); 44 | onGameRequest(); 45 | } 46 | }); 47 | } 48 | 49 | const declineGame = () => { 50 | socket.disconnect(); 51 | socket.connect(); 52 | socket.emit("decline game", (game)); 53 | onGameRequest(); 54 | } 55 | 56 | return ( 57 | <> 58 |
59 |
GAME INVITATION
60 |
61 |
62 |
66 |
{game?.inviterName}
67 |
invited you to a game
68 |
69 |
70 |
72 | JOIN
73 |
75 | DECLINE
76 |
77 |
78 | 79 | ) 80 | } -------------------------------------------------------------------------------- /front/src/routes/game.interfaces.tsx: -------------------------------------------------------------------------------- 1 | import { Socket } from "socket.io-client"; 2 | 3 | export interface Game_data { 4 | paddleLeft?: number; 5 | paddleRight?: number; 6 | xBall?: number; 7 | yBall?: number; 8 | player1Name: string; 9 | player2Name: string; 10 | player1Avatar: number; 11 | player2Avater: number; 12 | player1Score: number; 13 | player2Score: number; 14 | gameID?: number; 15 | } 16 | 17 | export interface Player { 18 | roomId: number; 19 | playerNb: number; 20 | } 21 | 22 | export interface Coordinates { 23 | x?: number; 24 | y?: number; 25 | showBall: boolean; 26 | } 27 | 28 | export interface StatePong { 29 | ballX?: number; 30 | ballY?: number; 31 | paddleLeftY?: number; 32 | paddleRightY?: number; 33 | gameStarted: boolean; 34 | roomId: number; 35 | showStartButton: boolean; 36 | playerNumber: number; 37 | player1Score: number; 38 | player2Score: number; 39 | msgType: number; 40 | player1Name: string; 41 | player2Name: string; 42 | game_list: Game_data_extended[]; 43 | isSettingsShown?: boolean; 44 | settingsState: "up" | "down" | "none"; 45 | buttonState?: "Start" | "New Game" | "Cancel"; 46 | avatarP1URL: string, 47 | avatarP2URL: string, 48 | soloGame: boolean, 49 | redirectChat?: boolean, 50 | } 51 | 52 | export interface PropsPong { 53 | pvtGame?: boolean, 54 | roomId?: number, 55 | playerNumber?: number, 56 | socket?: Socket; 57 | } 58 | 59 | export interface Button { 60 | clickHandler: any; 61 | showButton: boolean; 62 | buttonText?: "Start" | "New Game" | "Cancel" | "Solo mode"; 63 | } 64 | 65 | export interface ButtonState { 66 | showButton: boolean; 67 | buttonText?: "Start" | "New Game" | "Cancel"; 68 | } 69 | 70 | export interface SoloButtonProps { 71 | clickHandler: any; 72 | showButton: boolean; 73 | buttonText?: "Play again" | "Quit"; 74 | } 75 | 76 | export interface SoloButtonState { 77 | showButton: boolean; 78 | buttonText?: "Play again" | "Quit"; 79 | } 80 | 81 | export interface PropsSoloPong { 82 | clickHandler: any; 83 | } 84 | 85 | export interface Msg { 86 | showMsg: boolean; 87 | type: number; 88 | } 89 | 90 | export interface MsgState { 91 | showMsg: boolean; 92 | type: number; 93 | } 94 | 95 | export interface MsgSolo { 96 | showMsg: boolean; 97 | type: number; 98 | score: number; 99 | } 100 | 101 | export interface MsgSoloState { 102 | showMsg: boolean; 103 | type: number; 104 | score: number; 105 | } 106 | 107 | export interface PaddleProps { 108 | ystart?: number; 109 | y?: number; 110 | side: string; 111 | show: boolean; 112 | } 113 | 114 | export interface StatePaddle { 115 | y?: number; 116 | side: string; 117 | show: boolean; 118 | } 119 | 120 | export interface SettingsProps { 121 | onClickClose: any; 122 | onKeyDown: any; 123 | message: string; 124 | } 125 | 126 | export interface SettingsState { 127 | message: string; 128 | } 129 | 130 | export interface Game_data_extended extends Game_data { 131 | avatar1URL: string; 132 | avatar2URL: string; 133 | } 134 | 135 | export interface StateSoloPong { 136 | ballX: number; 137 | ballY: number; 138 | paddleLeftY: number; 139 | gameStarted: boolean; 140 | player1Score: number; 141 | player1Name: string; 142 | isSettingsShown?: boolean; 143 | settingsState: "up" | "down" | "none"; 144 | avatarP1URL: string, 145 | } 146 | -------------------------------------------------------------------------------- /front/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; 3 | import App from "./App"; 4 | import Game from "./routes/Game"; 5 | import Auth from "./routes/Auth/Auth"; 6 | import SignIn from "./routes/Auth/SignIn"; 7 | import SignUp from "./routes/Auth/SignUp"; 8 | import Chat from "./routes/Chat"; 9 | import UserInterface from "./routes/UserInterface"; 10 | import { AuthProvider, RedirectWhenAuth, RequireAuth } from "./hooks/AuthHooks"; 11 | import TwoFAValidation from "./routes/TwoFAValidation"; 12 | import Watch from "./routes/Watch"; 13 | import LeaderBoard from "./routes/LeaderBoard"; 14 | import UserPrivateProfile from "./routes/profile_types/private/UserPrivateProfile"; 15 | import { BlockedList } from "./routes/profile_types/private/users_relations/BlockedList"; 16 | import { FriendsList } from "./routes/profile_types/private/users_relations/FriendsList"; 17 | import { PendingList } from "./routes/profile_types/private/users_relations/PendingList"; 18 | import UserPublicProfile from "./routes/profile_types/public/UserPublicProfile"; 19 | import "./index.css"; 20 | 21 | const root = ReactDOM.createRoot(document.getElementById("root")!); 22 | root.render( 23 | 24 | 25 | 26 | }> 27 | } /> 28 | } /> 29 | }> 30 | } /> 31 | 35 | 36 | 37 | } 38 | /> 39 | } /> 40 | } /> 41 | 42 | 43 | 47 | 48 | 49 | } 50 | > 51 | } /> 52 | }> 53 | } /> 54 | } /> 55 | } /> 56 | } /> 57 | 58 | } /> 59 | 63 | 64 | 65 | } 66 | /> 67 | } /> 68 | } /> 69 | } /> 70 | } /> 71 | } /> 72 | 73 | } /> 74 | 75 | 76 | 77 | 78 | ); 79 | -------------------------------------------------------------------------------- /front/src/routes/profile_types/private/users_relations/BlockedList.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from "react"; 2 | import { ItableRow, IUserStatus } from "../../../../globals/Interfaces"; 3 | import { getUserAvatarQuery } from "../../../../queries/avatarQueries"; 4 | import { getUserBlocked } from "../../../../queries/userQueries"; 5 | import { DisplayRow } from "./DisplayRowUsers"; 6 | import { UsersStatusCxt } from "../../../../App"; 7 | import { Spinner } from "react-bootstrap"; 8 | 9 | export const BlockedList = () => { 10 | const usersStatus = useContext(UsersStatusCxt); 11 | 12 | const [blockedList, setBlockedList] = useState( 13 | undefined 14 | ); 15 | 16 | const [isFetched, setFetched] = useState("false"); 17 | const [isUpdated, setUpdate] = useState(false); 18 | 19 | let blocked: ItableRow[] = []; 20 | 21 | useEffect(() => { 22 | const fetchDataBlocked = async () => { 23 | const result = await getUserBlocked(); 24 | if (result !== "error") return result; 25 | }; 26 | 27 | const fetchDataBlockedAvatar = async (otherId: number) => { 28 | const result: undefined | string | Blob | MediaSource = 29 | await getUserAvatarQuery(otherId); 30 | if (result !== "error") return result; 31 | else 32 | return "https://img.myloview.fr/stickers/default-avatar-profile-in-trendy-style-for-social-media-user-icon-400-228654852.jpg"; 33 | }; 34 | 35 | const fetchData = async () => { 36 | let fetchedBlocked = await fetchDataBlocked(); 37 | 38 | if (fetchedBlocked !== undefined && fetchedBlocked.length !== 0) { 39 | for (let i = 0; i < fetchedBlocked.length; i++) { 40 | let newRow: ItableRow = { 41 | key: i, 42 | userModel: { username: "", avatar: "", id: 0, status: -1 }, 43 | }; 44 | newRow.userModel.id = fetchedBlocked[i].id; 45 | newRow.userModel.username = fetchedBlocked[i].username; 46 | let found = undefined; 47 | if (usersStatus) { 48 | found = usersStatus.find( 49 | (x: IUserStatus) => x.key === fetchedBlocked[i].id 50 | ); 51 | if (found) newRow.userModel.status = found.userModel.status; 52 | } 53 | 54 | let avatar = await fetchDataBlockedAvatar(fetchedBlocked[i].id); 55 | 56 | if (avatar !== undefined && avatar instanceof Blob) 57 | newRow.userModel.avatar = URL.createObjectURL(avatar); 58 | else if (avatar) newRow.userModel.avatar = avatar; 59 | blocked.push(newRow); 60 | } 61 | } 62 | setBlockedList(blocked); 63 | setFetched("true"); 64 | }; 65 | 66 | fetchData(); 67 | // eslint-disable-next-line react-hooks/exhaustive-deps 68 | }, [isUpdated, usersStatus]); 69 | 70 | return ( 71 |
72 | {isFetched === "true" ? ( 73 | blockedList?.length !== 0 ? ( 74 | blockedList!.map((h, index) => { 75 | return ( 76 | 83 | ); 84 | }) 85 | ) : ( 86 | No blocked users. 87 | ) 88 | ) : ( 89 | 90 | )} 91 |
92 | ); 93 | }; 94 | 95 | //isFetched === "error" to add 96 | -------------------------------------------------------------------------------- /front/src/routes/profile_types/private/users_relations/PendingList.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from "react"; 2 | import { ItableRow, IUserStatus } from "../../../../globals/Interfaces"; 3 | import { getUserAvatarQuery } from "../../../../queries/avatarQueries"; 4 | import { getUserPending } from "../../../../queries/userQueries"; 5 | import { DisplayRow } from "./DisplayRowUsers"; 6 | import { UsersStatusCxt } from "../../../../App"; 7 | import { Spinner } from "react-bootstrap"; 8 | 9 | export const PendingList = () => { 10 | const usersStatus = useContext(UsersStatusCxt); 11 | 12 | const [pendingList, setPendingList] = useState( 13 | undefined 14 | ); 15 | 16 | const [isFetched, setFetched] = useState("false"); 17 | const [isUpdated, setUpdate] = useState(false); 18 | 19 | let pending: ItableRow[] = []; 20 | 21 | useEffect(() => { 22 | const fetchDataPending = async () => { 23 | const result = await getUserPending(); 24 | if (result !== "error") return result; 25 | }; 26 | 27 | const fetchDataPendingAvatar = async (otherId: number) => { 28 | const result: undefined | string | Blob | MediaSource = 29 | await getUserAvatarQuery(otherId); 30 | if (result !== "error") return result; 31 | else 32 | return "https://img.myloview.fr/stickers/default-avatar-profile-in-trendy-style-for-social-media-user-icon-400-228654852.jpg"; 33 | }; 34 | 35 | const fetchData = async () => { 36 | let fetchedPending = await fetchDataPending(); 37 | 38 | if (fetchedPending !== undefined && fetchedPending.length !== 0) { 39 | for (let i = 0; i < fetchedPending.length; i++) { 40 | let newRow: ItableRow = { 41 | key: i, 42 | userModel: { username: "", avatar: "", id: 0, status: -1 }, 43 | }; 44 | newRow.userModel.id = fetchedPending[i].id; 45 | newRow.userModel.username = fetchedPending[i].username; 46 | let found = undefined; 47 | if (usersStatus) { 48 | found = usersStatus.find( 49 | (x: IUserStatus) => x.key === fetchedPending[i].id 50 | ); 51 | if (found) newRow.userModel.status = found.userModel.status; 52 | } 53 | 54 | let avatar = await fetchDataPendingAvatar(fetchedPending[i].id); 55 | 56 | if (avatar !== undefined && avatar instanceof Blob) 57 | newRow.userModel.avatar = URL.createObjectURL(avatar); 58 | else if (avatar) newRow.userModel.avatar = avatar; 59 | pending.push(newRow); 60 | } 61 | } 62 | setPendingList(pending); 63 | setFetched("true"); 64 | }; 65 | 66 | fetchData(); 67 | // eslint-disable-next-line react-hooks/exhaustive-deps 68 | }, [isUpdated, usersStatus]); 69 | 70 | return ( 71 |
72 | {isFetched === "true" ? ( 73 | pendingList?.length !== 0 ? ( 74 | pendingList!.map((h, index) => { 75 | return ( 76 | 83 | ); 84 | }) 85 | ) : ( 86 | No friend requests. 87 | ) 88 | ) : ( 89 | 90 | )} 91 |
92 | ); 93 | }; 94 | 95 | //isFetched === "error" to add 96 | -------------------------------------------------------------------------------- /front/src/routes/chat_modes/chatRoom.css: -------------------------------------------------------------------------------- 1 | .chat-room-zone { 2 | background-color: inherit; 3 | flex: 1; 4 | display: flex; 5 | flex-direction: column; 6 | box-shadow: 0px 0px 96px 0px rgb(0 0 0 / 18%); 7 | } 8 | 9 | .lock-icon { 10 | margin: auto; 11 | } 12 | 13 | .brief-info { 14 | background-color: #191e1e; 15 | height: 60px; 16 | display: flex; 17 | } 18 | 19 | .chat-name { 20 | padding-left: 20px; 21 | padding-top: 15px; 22 | color: white; 23 | } 24 | 25 | .flex-empty-block { 26 | flex: 1; 27 | } 28 | 29 | 30 | .brief-info .setting-icon { 31 | margin-top: 2px; 32 | padding: 15px; 33 | opacity: 0.85; 34 | cursor: pointer; 35 | } 36 | 37 | .brief-info .setting-icon:hover { 38 | opacity: 1; 39 | } 40 | 41 | .msg-stream { 42 | background-color: #273438; 43 | padding: 10px; 44 | flex: 1; 45 | overflow: overlay; 46 | } 47 | 48 | .self { 49 | margin-top: 10px; 50 | margin-left: auto; 51 | margin-right: 18px; 52 | padding-top: 10px; 53 | padding-bottom: .5px; 54 | padding-left: 15px; 55 | padding-right: 15px; 56 | width: fit-content; 57 | background-color: white; 58 | text-align: right; 59 | border-top-left-radius: 10px; 60 | border-bottom-left-radius: 10px; 61 | border-radius: 10px; 62 | max-width: 70%; 63 | word-break: break-word; 64 | } 65 | 66 | .other { 67 | margin-top: 10px; 68 | margin-left: 8px; 69 | margin-right: auto; 70 | padding-top: 10px; 71 | padding-bottom: 0.5px; 72 | padding-left: 15px; 73 | padding-right: 20px; 74 | width: fit-content; 75 | background-color: whitesmoke; 76 | text-align: left; 77 | border-top-left-radius: 10px; 78 | border-bottom-left-radius: 10px; 79 | border-radius: 10px; 80 | max-width: 70%; 81 | word-break: break-word; 82 | } 83 | 84 | .msg-string { 85 | color: black; 86 | } 87 | 88 | .msg-sender { 89 | margin-bottom: auto; 90 | font-size: 12px; 91 | text-align: left; 92 | color:rgba(37, 36, 36, 0.884); 93 | } 94 | 95 | 96 | .msg-sent-time { 97 | background-color: mediumaquamarine; 98 | color: black; 99 | } 100 | 101 | .msg-input-zone { 102 | background-color: #334a50; 103 | padding: 10px; 104 | display: flex; 105 | box-shadow: 0px 0px 35px 0px rgb(0 0 0 / 5%); 106 | position: relative; 107 | } 108 | 109 | .msg-input-area { 110 | background-color: transparent; 111 | padding-left: 10px; 112 | padding-bottom: 5px; 113 | height: 35px; 114 | outline: none; 115 | border: none; 116 | color: white; 117 | flex: 1; 118 | } 119 | 120 | #msg::placeholder { 121 | color: white; 122 | } 123 | 124 | .pickerBox 125 | { 126 | position: absolute; 127 | right: 0; 128 | bottom: 60px; 129 | } 130 | 131 | .emoji-button 132 | { 133 | padding: 3px; 134 | cursor: pointer; 135 | } 136 | 137 | .emoji-picker-react input.emoji-search 138 | { 139 | height: 37px; 140 | } 141 | 142 | .emoji-picker-react section::-webkit-scrollbar 143 | { 144 | display: none; 145 | } 146 | 147 | .emoji-disappear-click-zone { 148 | position: absolute; 149 | height: 100vh; 150 | width: 100vw; 151 | top: 0px; 152 | left: 0px; 153 | } -------------------------------------------------------------------------------- /back/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /front/src/routes/profile_types/private/users_relations/FriendsList.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from "react"; 2 | import { ItableRow, IUserStatus } from "../../../../globals/Interfaces"; 3 | import { getUserAvatarQuery } from "../../../../queries/avatarQueries"; 4 | import { getUserFriends } from "../../../../queries/userFriendsQueries"; 5 | import { DisplayRow } from "./DisplayRowUsers"; 6 | import { UsersStatusCxt } from "../../../../App"; 7 | import { Spinner } from "react-bootstrap"; 8 | 9 | export const FriendsList = () => { 10 | const usersStatus = useContext(UsersStatusCxt); 11 | 12 | const [friendsList, setFriendsList] = useState( 13 | undefined 14 | ); 15 | 16 | const [isFetched, setFetched] = useState("false"); 17 | const [isUpdated, setUpdate] = useState(false); 18 | 19 | let friends: ItableRow[] = []; 20 | 21 | useEffect(() => { 22 | const fetchDataFriends = async () => { 23 | const id = localStorage.getItem("userID"); 24 | if (id) { 25 | const result = await getUserFriends(+id); 26 | if (result !== "error") return result; 27 | } 28 | }; 29 | 30 | const fetchDataFriendsAvatar = async (otherId: number) => { 31 | const result: undefined | string | Blob | MediaSource = 32 | await getUserAvatarQuery(otherId); 33 | if (result !== "error") return result; 34 | else 35 | return "https://img.myloview.fr/stickers/default-avatar-profile-in-trendy-style-for-social-media-user-icon-400-228654852.jpg"; 36 | }; 37 | 38 | const fetchData = async () => { 39 | let fetchedFriends = await fetchDataFriends(); 40 | 41 | if (fetchedFriends !== undefined && fetchedFriends.length !== 0) { 42 | for (let i = 0; i < fetchedFriends.length; i++) { 43 | let newRow: ItableRow = { 44 | key: i, 45 | userModel: { username: "", avatar: "", id: 0, status: -1 }, 46 | }; 47 | newRow.userModel.id = fetchedFriends[i].id; 48 | newRow.userModel.username = fetchedFriends[i].username; 49 | let found = undefined; 50 | if (usersStatus) { 51 | found = usersStatus.find( 52 | (x: IUserStatus) => x.key === fetchedFriends[i].id 53 | ); 54 | if (found) newRow.userModel.status = found.userModel.status; 55 | } 56 | 57 | let avatar = await fetchDataFriendsAvatar(fetchedFriends[i].id); 58 | 59 | if (avatar !== undefined && avatar instanceof Blob) 60 | newRow.userModel.avatar = URL.createObjectURL(avatar); 61 | else if (avatar) newRow.userModel.avatar = avatar; 62 | friends.push(newRow); 63 | } 64 | } 65 | setFriendsList(friends); 66 | setFetched("true"); 67 | }; 68 | 69 | fetchData(); 70 | // eslint-disable-next-line react-hooks/exhaustive-deps 71 | }, [isUpdated, usersStatus]); 72 | 73 | return ( 74 |
75 | {isFetched === "true" ? ( 76 | friendsList?.length !== 0 ? ( 77 | friendsList!.map((h, index) => { 78 | return ( 79 | 86 | ); 87 | }) 88 | ) : ( 89 | No friends. 90 | ) 91 | ) : ( 92 | 93 | )} 94 |
95 | ); 96 | }; 97 | 98 | //isFetched === "error" to add 99 | -------------------------------------------------------------------------------- /back/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Unlike TypeORM where entities are created and set inside the modules, 5 | // Prisma handles everything from this file where the models are set 6 | 7 | 8 | generator client { 9 | provider = "prisma-client-js" 10 | // output = "../node_modules/@prisma/client" 11 | 12 | } 13 | 14 | datasource db { 15 | provider = "postgresql" 16 | url = env("DATABASE_URL") 17 | } 18 | 19 | model User { 20 | id Int @id @default(autoincrement()) 21 | id42 Int 22 | 23 | createdAt DateTime @default(now()) 24 | updatedAt DateTime @updatedAt 25 | 26 | email String @unique 27 | username String? @unique 28 | hash String 29 | avatar String? 30 | 31 | hashedRtoken String? 32 | twoFAsecret String? 33 | twoFA Boolean? @default(false) 34 | 35 | gamesWon Int @default(0) 36 | gamesLost Int @default(0) 37 | gamesPlayed Int @default(0) 38 | gameHistory Int[] 39 | 40 | 41 | winRate Float? 42 | playTime Int @default(0) 43 | score Int @default(1200) 44 | rank Int? 45 | 46 | friends Int[] 47 | adding Int[] 48 | added Int[] 49 | 50 | blocks Int[] 51 | blocking Int[] 52 | blocked Int[] 53 | 54 | owner Channel[] @relation("owner") 55 | admin Channel[] @relation("admin") 56 | member Channel[] @relation("member") 57 | invited Channel[] @relation("invite") 58 | chanBlocked Channel[] @relation("blocked") 59 | 60 | Muted Mute[] 61 | 62 | messages Msg[] 63 | 64 | @@unique([id, email]) 65 | @@map("users") //mapped to another name 66 | } 67 | 68 | model Channel { 69 | id Int @id @default(autoincrement()) 70 | 71 | name String @default(uuid()) 72 | picture String? 73 | createdAt DateTime @default(now()) 74 | updatedAt DateTime @updatedAt 75 | 76 | dm Boolean @default(false) 77 | private Boolean @default(false) 78 | isPassword Boolean @default(false) 79 | password String? 80 | 81 | owners User[] @relation("owner") 82 | admins User[] @relation("admin") 83 | members User[] @relation("member") 84 | inviteds User[] @relation("invite") 85 | blocked User[] @relation("blocked") 86 | 87 | muted Mute[] 88 | 89 | messages Msg[] 90 | 91 | @@unique([id]) 92 | } 93 | 94 | model Msg { 95 | id Int @id @default(autoincrement()) 96 | 97 | msg String 98 | history String[] 99 | 100 | unsent Boolean @default(false) 101 | 102 | createdAt DateTime @default(now()) 103 | updatedAt DateTime @updatedAt 104 | 105 | owner User @relation(fields: [userId], references: [id]) 106 | userId Int 107 | channel Channel @relation(fields: [cid], references: [id]) 108 | cid Int 109 | } 110 | 111 | model Mute { 112 | id Int @id @default(autoincrement()) 113 | 114 | 115 | finishAt DateTime 116 | checkAt DateTime @default(now()) 117 | finished Boolean @default(false) 118 | 119 | muted User @relation(fields: [userId], references: [id]) 120 | userId Int 121 | channel Channel @relation(fields: [cid], references: [id]) 122 | cid Int 123 | } 124 | 125 | model Game { 126 | id Int @id @unique 127 | 128 | player1 Int 129 | player2 Int 130 | score1 Int 131 | score2 Int 132 | 133 | startTime DateTime 134 | endTime DateTime 135 | duration Int? //in milliseconds 136 | 137 | 138 | } -------------------------------------------------------------------------------- /front/src/modals/MActivateTwoFA.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from "react"; 2 | import { Modal, Button, Form, Spinner } from "react-bootstrap"; 3 | import { NotifCxt } from "../App"; 4 | import { twoFAGenerate, twoFAOn } from "../queries/twoFAQueries"; 5 | 6 | export function Activate2FA(props: any) { 7 | const notif = useContext(NotifCxt); 8 | const [image, setImage] = useState(""); 9 | const [FACodeModal, setCodeModal] = useState(""); 10 | 11 | const handleInputChange = (e: any) => { 12 | const { value } = e.target; 13 | setCodeModal(value); 14 | }; 15 | 16 | const getQRCode = async () => { 17 | const result = await twoFAGenerate(); 18 | if (!result) 19 | setImage( 20 | "https://user-images.githubusercontent.com/24848110/33519396-7e56363c-d79d-11e7-969b-09782f5ccbab.png" 21 | ); 22 | else setImage(result); 23 | }; 24 | 25 | useEffect(() => { 26 | if (props.show && image === "") getQRCode(); 27 | // eslint-disable-next-line react-hooks/exhaustive-deps 28 | }, [props.show]); 29 | 30 | const handleSubmit = (e: any) => { 31 | e.preventDefault(); 32 | const twoFAActivate = async () => { 33 | const result = await twoFAOn(FACodeModal); 34 | if (!result) { 35 | notif?.setNotifText("Wrong code. Please try again."); 36 | notif?.setNotifShow(true); 37 | } else { 38 | props.onHide(); 39 | props.onSubmit(); 40 | localStorage.setItem("userAuth", "true"); 41 | notif?.setNotifText( 42 | "TwoFA activated. A code will be asked on each login." 43 | ); 44 | notif?.setNotifShow(true); 45 | } 46 | }; 47 | twoFAActivate(); 48 | }; 49 | 50 | return ( 51 | 57 | 58 | 59 | Scan Your QR Code 60 | 61 | 62 | 63 | {image ? ( 64 |
65 | { 69 | e.preventDefault(); 70 | getQRCode(); 71 | }} 72 | /> 73 |
74 | 2FA 75 |
76 |
77 | ) : ( 78 | 79 | )} 80 | 81 | Enter Code 82 | 89 | 90 | Enter code from your authenticator app. 91 | 92 | 93 |
94 | 95 | 98 | 107 | 108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /front/src/routes/chat_modes/chatPreview.css: -------------------------------------------------------------------------------- 1 | .preview-zone { 2 | background-color: inherit; 3 | width: 250px; 4 | } 5 | 6 | .preview-chat-search { 7 | display: flex; 8 | height: 60px; 9 | } 10 | 11 | .input-search { 12 | padding: 10px; 13 | margin-top: 1px; 14 | width: 190px; 15 | } 16 | 17 | .input-search .wrapper{ 18 | border-radius: 10px !important; 19 | } 20 | 21 | .input-search .clear-icon{ 22 | margin-top: -1px; 23 | } 24 | 25 | 26 | .input-search li { 27 | width: 170px; 28 | height: 70px; 29 | padding: 10px !important; 30 | } 31 | 32 | .input-search ul { 33 | padding-bottom: 10px !important; 34 | } 35 | 36 | .search-result { 37 | /* margin: auto; */ 38 | height: 60px; 39 | padding: 10px; 40 | line-height: 0.8; 41 | } 42 | 43 | .search-icon 44 | { 45 | margin-left: 17px; 46 | } 47 | 48 | li .search-icon 49 | { 50 | margin-left: 10px; 51 | } 52 | 53 | 54 | .result-type { 55 | /* font-size: 14px; */ 56 | color: grey; 57 | } 58 | 59 | .result { 60 | color: rgb(75, 74, 74); 61 | } 62 | 63 | .add-room-button { 64 | background-color: grey; 65 | margin-top: 10px; 66 | margin-left: 5px; 67 | margin-right: 15px; 68 | border-radius: 50%; 69 | height: 40px; 70 | width: 40px; 71 | padding-top: 4px; 72 | text-align: center; 73 | color: white; 74 | font-size: 20px; 75 | cursor: pointer; 76 | } 77 | 78 | .preview-chat-list 79 | { 80 | overflow: overlay; 81 | height: calc(100% - 100px); 82 | } 83 | 84 | .preview-chat 85 | { 86 | height: 80px; 87 | display: flex; 88 | transition: all 0.3s ease; 89 | cursor: pointer; 90 | user-select: none; 91 | 92 | } 93 | 94 | .preview-chat:hover 95 | { 96 | background-color: rgba(255, 255, 255, 0.2) !important; 97 | } 98 | 99 | .preview-chat:active 100 | { 101 | background-color: rgba(255, 255, 255, 0.25) !important; 102 | } 103 | 104 | 105 | .preview-chat > div 106 | { 107 | display: flex; 108 | padding: 10px; 109 | width: 100%; 110 | transition: all 0.3s ease; 111 | } 112 | 113 | .preview-chat:hover > div 114 | { 115 | transform: scale(0.97); 116 | } 117 | 118 | .preview-chat:active > div 119 | { 120 | transform: scale(0.95); 121 | } 122 | 123 | .preview-chat-img 124 | { 125 | background-color: white; 126 | margin-top: 5px; 127 | border-radius: 50%; 128 | height: 45px; 129 | width: 45px; 130 | } 131 | 132 | .preview-chat-info 133 | { 134 | padding-left: 20px; 135 | width: 80%; 136 | display: flex; 137 | 138 | } 139 | 140 | .preview-chat-info-1 141 | { 142 | flex: 4; 143 | height: 100%; 144 | width: 80%; 145 | 146 | } 147 | 148 | .preview-chat-name 149 | { 150 | color: white; 151 | font-size: large; 152 | white-space: nowrap; 153 | overflow: hidden; 154 | text-overflow: ellipsis; 155 | text-align: left; 156 | margin-bottom: auto; 157 | } 158 | 159 | .preview-chat-msg 160 | { 161 | color: rgba(167, 167, 167, 0.479); 162 | white-space: nowrap; 163 | overflow: hidden; 164 | text-overflow: ellipsis; 165 | text-align: left; 166 | } 167 | 168 | .preview-chat-info-2 169 | { 170 | flex: 1; 171 | width: 80%; 172 | } 173 | 174 | .preview-chat-time { 175 | background: greenyellow; 176 | } 177 | 178 | .preview-chat-unread { 179 | background: purple; 180 | } -------------------------------------------------------------------------------- /front/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | import "bootstrap/dist/css/bootstrap.min.css"; 3 | import "./App.css"; 4 | import { createContext, useEffect, useState } from "react"; 5 | import { io } from "socket.io-client"; 6 | import { INotifCxt, IUserStatus } from "./globals/Interfaces"; 7 | import { TAlert } from "./toasts/TAlert"; 8 | import { GameRequestCard } from "./routes/gameRequestCard"; 9 | import { gameInvitation } from "./routes/chat_modes/type/chat.type"; 10 | 11 | let LoginStatus = { 12 | islogged: false, 13 | setUserName: () => {}, 14 | }; 15 | 16 | export const UsernameCxt = createContext(LoginStatus); 17 | 18 | export const UsersStatusCxt = createContext( 19 | undefined 20 | ); 21 | 22 | export const NotifCxt = createContext(undefined); 23 | 24 | const socketOptions = { 25 | transportOptions: { 26 | polling: { 27 | extraHeaders: { 28 | Token: localStorage.getItem("userToken"), 29 | }, 30 | }, 31 | }, 32 | }; 33 | 34 | export const socket = io(`${process.env.REACT_APP_BACKEND_SOCKET}`, socketOptions); 35 | 36 | export default function App() { 37 | const [usersStatus, setUsersStatus] = useState( 38 | undefined 39 | ); 40 | const [notifShow, setNotifShow] = useState(false); 41 | const [notifText, setNotifText] = useState("error"); 42 | const [gameRequest, setGameRequest] = useState(false); 43 | const [gameInfo, setGameInfo] = useState( 44 | undefined 45 | ); 46 | 47 | let userstatusTab: IUserStatus[] = []; 48 | 49 | useEffect(() => { 50 | socket.on("update-status", (data, str: string) => { 51 | // eslint-disable-next-line react-hooks/exhaustive-deps 52 | userstatusTab = []; 53 | for (let i = 0; i <= data.length - 1; i++) { 54 | let newUser: IUserStatus = { 55 | key: data[i][0], 56 | userModel: { id: 0, status: -1 }, 57 | }; 58 | newUser.userModel.id = data[i][0]; 59 | newUser.userModel.status = data[i][1]; 60 | userstatusTab.push(newUser); 61 | } 62 | setUsersStatus(userstatusTab); 63 | }); 64 | // return () => { 65 | // socket.off('update-status') 66 | // } 67 | }, [usersStatus]); 68 | 69 | useEffect(() => { 70 | socket.on("game invitation", (game: gameInvitation) => { 71 | setGameRequest(true); 72 | setGameInfo(game); 73 | 74 | return () => { 75 | socket.off("game invitation"); 76 | }; 77 | }); 78 | }, []); 79 | 80 | return ( 81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 |
92 |
event.stopPropagation()} 95 | > 96 | { 100 | setGameRequest((old) => { 101 | return !old; 102 | }); 103 | }} 104 | /> 105 |
106 |
107 |
108 |
109 |
110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /back/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "back", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prisma:dev:deploy": "prisma migrate deploy", 10 | "prisma:studio": "npx prisma studio &", 11 | "db:dev:restart": "yarn db:dev:rm && yarn db:dev:up", 12 | "db:dev:rm": "docker compose rm postgres -s -f -v", 13 | "db:dev:up": "docker compose up postgres -d", 14 | "prebuild": "rimraf dist", 15 | "build": "nest build", 16 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 17 | "start": "nest start", 18 | "start:dev": "nest start --watch", 19 | "start:debug": "nest start --debug --watch", 20 | "start:prod": "node dist/main", 21 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 22 | "test": "jest", 23 | "test:watch": "jest --watch", 24 | "test:cov": "jest --coverage", 25 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 26 | "test:e2e": "jest --config ./test/jest-e2e.json" 27 | }, 28 | "dependencies": { 29 | "@nestjs/axios": "^0.1.0", 30 | "@nestjs/common": "^9.0.8", 31 | "@nestjs/config": "^2.2.0", 32 | "@nestjs/core": "^9.0.8", 33 | "@nestjs/jwt": "^9.0.0", 34 | "@nestjs/passport": "^9.0.0", 35 | "@nestjs/platform-express": "^9.0.8", 36 | "@nestjs/platform-socket.io": "^9.0.8", 37 | "@nestjs/schedule": "^2.1.0", 38 | "@nestjs/swagger": "^6.0.5", 39 | "@nestjs/websockets": "^9.0.8", 40 | "@prisma/client": "^3.15.1", 41 | "@types/otplib": "^10.0.0", 42 | "@types/passport-jwt": "^3.0.6", 43 | "@types/passport-local": "^1.0.34", 44 | "@types/qrcode": "^1.4.2", 45 | "argon2": "^0.28.5", 46 | "async-mutex": "^0.3.2", 47 | "bcrypt": "^5.0.1", 48 | "class-transformer": "^0.5.1", 49 | "class-validator": "^0.13.2", 50 | "dotenv": "^16.0.1", 51 | "file-type": "^17.1.6", 52 | "moment": "^2.29.4", 53 | "otplib": "^12.0.1", 54 | "passport": "^0.6.0", 55 | "passport-42": "^1.2.6", 56 | "passport-jwt": "^4.0.0", 57 | "passport-local": "^1.0.0", 58 | "pg": "^8.7.3", 59 | "qrcode": "^1.5.1", 60 | "reflect-metadata": "^0.1.13", 61 | "rimraf": "^3.0.2", 62 | "rxjs": "^7.2.0", 63 | "typeorm": "^0.3.6", 64 | "webpack": "^5.73.0" 65 | }, 66 | "devDependencies": { 67 | "@nestjs/cli": "^8.2.7", 68 | "@nestjs/schematics": "^8.0.0", 69 | "@nestjs/testing": "^8.0.0", 70 | "@types/bcrypt": "^5.0.0", 71 | "@types/express": "^4.17.13", 72 | "@types/jest": "27.5.0", 73 | "@types/multer": "^1.4.7", 74 | "@types/node": "^16.0.0", 75 | "@types/supertest": "^2.0.11", 76 | "@typescript-eslint/eslint-plugin": "^5.0.0", 77 | "@typescript-eslint/parser": "^5.0.0", 78 | "eslint": "^8.19.0", 79 | "eslint-config-prettier": "^8.3.0", 80 | "eslint-plugin-prettier": "^4.0.0", 81 | "eslint-plugin-unicorn": "^43.0.1", 82 | "jest": "28.0.3", 83 | "prettier": "^2.3.2", 84 | "prisma": "^3.15.1", 85 | "source-map-support": "^0.5.20", 86 | "supertest": "^6.1.3", 87 | "ts-jest": "28.0.1", 88 | "ts-loader": "^9.2.3", 89 | "ts-node": "^10.0.0", 90 | "tsconfig-paths": "4.0.0", 91 | "typescript": "^4.3.5" 92 | }, 93 | "jest": { 94 | "moduleFileExtensions": [ 95 | "js", 96 | "json", 97 | "ts" 98 | ], 99 | "rootDir": "src", 100 | "testRegex": ".*\\.spec\\.ts$", 101 | "transform": { 102 | "^.+\\.(t|j)s$": "ts-jest" 103 | }, 104 | "collectCoverageFrom": [ 105 | "**/*.(t|j)s" 106 | ], 107 | "coverageDirectory": "../coverage", 108 | "testEnvironment": "node" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /back/src/upload/upload.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Body, 4 | Controller, 5 | Get, 6 | HttpCode, 7 | HttpStatus, 8 | ParseFilePipeBuilder, 9 | Post, 10 | Res, 11 | UploadedFile, 12 | UseInterceptors, 13 | } from '@nestjs/common'; 14 | import { FileInterceptor } from '@nestjs/platform-express'; 15 | import { saveImageToStorage } from './utils/upload.utils'; 16 | import { UserService } from 'src/user/user.service'; 17 | import { GetCurrentUserId } from 'src/decorators'; 18 | import { PrismaService } from 'src/prisma/prisma.service'; 19 | import { Response } from 'express'; 20 | import { uploadDto } from './dto'; 21 | import { ReadableStreamWithFileType } from 'file-type'; 22 | import { ApiHeader, ApiResponse, ApiTags } from '@nestjs/swagger'; 23 | 24 | @ApiTags('Upload') 25 | @ApiHeader({ name: 'Avatars', description: 'Upload and Retrieve avatars' }) 26 | @Controller('upload') 27 | export class UploadController { 28 | constructor( 29 | private readonly userService: UserService, 30 | private prisma: PrismaService, 31 | ) {} 32 | 33 | /** 34 | * Upload image to storage 35 | * 36 | * @param userId : number - id of user 37 | * @param body : any - body of request 38 | * @param file : Express.Multer.File - file of request 39 | * @returns object containing filename, originalname, path of file 40 | */ 41 | @Post('/avatar') 42 | @ApiResponse({ status: 400, description: 'Invalid File' }) 43 | @ApiResponse({ status: 422, description: 'Invalid File' }) 44 | @HttpCode(201) 45 | @UseInterceptors(FileInterceptor('avatar', saveImageToStorage)) 46 | async uploadAvatar( 47 | @GetCurrentUserId() userId: number, 48 | @Body() body: ReadableStreamWithFileType, 49 | @UploadedFile( 50 | new ParseFilePipeBuilder() 51 | .addFileTypeValidator({ 52 | fileType: 'jpeg|jpg|png', 53 | }) 54 | .addMaxSizeValidator({ 55 | // eslint-disable-next-line unicorn/numeric-separators-style 56 | maxSize: 1000000, 57 | }) 58 | .build({ 59 | errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, 60 | }), 61 | ) 62 | file: Express.Multer.File, 63 | ) { 64 | // LOG 65 | console.log(file); 66 | 67 | await this.userService.updateAvatar(userId, file.filename); 68 | const response = { 69 | filename: file.filename, 70 | originalname: file.originalname, 71 | path: file.path, 72 | }; 73 | return response; 74 | } 75 | 76 | /** 77 | * Return the avatar of any given user 78 | * 79 | * @param body : any - body of request 80 | * @param response : Response - Express response object 81 | * @returns Avatar of user with given id 82 | */ 83 | 84 | @Post('/getavatar') 85 | @HttpCode(200) 86 | @ApiResponse({ status: 400, description: 'Provide a userId' }) 87 | @ApiResponse({ status: 400, description: 'User not found' }) 88 | async getAvatarByUserId( 89 | @Body() body: uploadDto, 90 | @Res() response: Response, 91 | ) { 92 | if (!body.userId) { 93 | throw new BadRequestException('Provide a userId'); 94 | } 95 | const { userId } = body; 96 | const user = await this.prisma.user.findUnique({ 97 | where: { 98 | id: userId, 99 | }, 100 | }); 101 | if (!user) { 102 | throw new BadRequestException('User not found'); 103 | } 104 | return response.sendFile(user.avatar, { root: process.env.UPLOAD_DIR }); 105 | } 106 | 107 | /** 108 | * Return the avatar of current user 109 | * 110 | * @param userId : number - id of user 111 | * @param response : Response - Express response object 112 | * @returns Current User avatar 113 | */ 114 | 115 | @Get('/avatar') 116 | @HttpCode(200) 117 | async getAvatar( 118 | @GetCurrentUserId() userId: number, 119 | @Res() response: Response, 120 | ) { 121 | const user = await this.prisma.user.findUnique({ 122 | where: { 123 | id: userId, 124 | }, 125 | }); 126 | return response.sendFile(user.avatar, { 127 | root: process.env.UPLOAD_DIR, 128 | }); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /back/src/auth/2FA/2fa.service.ts: -------------------------------------------------------------------------------- 1 | /* GLOBAL MODULES */ 2 | import { 3 | forwardRef, 4 | Inject, 5 | Injectable, 6 | UnauthorizedException, 7 | } from '@nestjs/common'; 8 | import { Response } from 'express'; 9 | import { PrismaService } from 'src/prisma/prisma.service'; 10 | import { AuthService } from '../auth.service'; 11 | /* Decorators */ 12 | /* 2FA Modules */ 13 | import { authenticator } from 'otplib'; 14 | import { toDataURL } from 'qrcode'; 15 | /* DTOs */ 16 | import { TwoFactorDto, TwoFactorUserDto } from '../dto'; 17 | 18 | /** 19 | * TWO FACTOR AUTHENTICATION SERVICE 20 | */ 21 | @Injectable() 22 | export class TwoFactorService { 23 | constructor( 24 | private prisma: PrismaService, 25 | @Inject(forwardRef(() => AuthService)) 26 | private authservice: AuthService, 27 | ) {} 28 | 29 | /* Redirect 2FA Enabled signin */ 30 | signin_2FA(response: Response, username: string) { 31 | const url = new URL(process.env.SITE_URL); 32 | url.port = process.env.FRONT_PORT; 33 | url.pathname = '2FA'; 34 | url.searchParams.append('username', username); 35 | response.status(302).redirect(url.href); 36 | } 37 | 38 | /* Turn on 2FA for existing user */ 39 | async turn_on(twoFAcode: any, user: TwoFactorUserDto) { 40 | // destructure data 41 | const { email, twoFAsecret, id } = user; 42 | // Check is 2FA code is valid 43 | const isValid = await this.verify2FAcode(twoFAcode, twoFAsecret); 44 | // If invalid, throw error 401 45 | if (!isValid) throw new UnauthorizedException('Invalid 2FA code'); 46 | // Enable 2FA for user (add method to user module ?) 47 | await this.prisma.user.update({ 48 | where: { email: email }, 49 | data: { twoFA: true }, 50 | }); 51 | const tokens = await this.authservice.signin_jwt(id, email, true); 52 | return tokens; 53 | } 54 | 55 | /* Turn off 2FA for existing user */ 56 | async turn_off(user: TwoFactorUserDto) { 57 | const { email, id } = user; 58 | await this.prisma.user.update({ 59 | where: { id: id }, 60 | data: { twoFA: false }, 61 | }); 62 | const tokens = await this.authservice.signin_jwt(id, email, false); 63 | return tokens; 64 | } 65 | 66 | /* Generate a new 2FA for user */ 67 | async generate2FA(email: string) { 68 | // Generate a 2FA secret 69 | const secret = authenticator.generateSecret(); 70 | // Create a URL for the QR code 71 | const onetimepathurl = authenticator.keyuri( 72 | email, 73 | process.env.MY_2FA_APP_NAME, 74 | secret, 75 | ); 76 | // Add the secret to the user 77 | await this.prisma.user.update({ 78 | where: { email: email }, 79 | data: { twoFAsecret: secret }, 80 | }); 81 | return { 82 | secret, 83 | onetimepathurl, 84 | }; 85 | } 86 | 87 | /* Authenticate signin using 2FA */ 88 | async authenticate(dto: TwoFactorDto) { 89 | // destructure dto 90 | const { username, twoFAcode } = dto; 91 | // find user by email or username 92 | const [user] = await this.prisma.user.findMany({ 93 | where: { OR: [{ email: username }, { username: username }] }, 94 | }); 95 | if (!user) { 96 | throw new UnauthorizedException('Invalid User'); 97 | } 98 | // destructure data 99 | const { id, email, twoFAsecret } = user; 100 | // check if code is valid 101 | const isValidCode = await this.verify2FAcode(twoFAcode, twoFAsecret); 102 | // if invalid code, throw error 103 | if (!isValidCode) { 104 | throw new UnauthorizedException('Invalid 2FA code'); 105 | } 106 | // generate tokens 107 | const tokens = await this.authservice.signin_jwt(id, email, true); 108 | await this.authservice.updateRefreshToken(id, tokens.refresh_token); 109 | return tokens; 110 | } 111 | 112 | /* Verify 2FA code */ 113 | async verify2FAcode(code: string, twoFAsecret: string) { 114 | return authenticator.verify({ 115 | token: code, 116 | secret: twoFAsecret, 117 | }); 118 | } 119 | 120 | /* Generate a QR code for the user */ 121 | async generate2FAQRCode(onetimepathurl: string) { 122 | // Generate a QR code from the URL 123 | return toDataURL(onetimepathurl); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /back/src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AUTHENTIFICATION CONTROLLER 3 | */ 4 | 5 | /* GLOBAL MODULES */ 6 | import { 7 | Body, 8 | Controller, 9 | Get, 10 | HttpCode, 11 | Logger, 12 | Post, 13 | Req, 14 | Res, 15 | UseFilters, 16 | UseGuards, 17 | } from '@nestjs/common'; 18 | import { Response } from 'express'; 19 | /* CUSTOM DECORATORS */ 20 | import { GetCurrentUser, GetCurrentUserId, Public } from 'src/decorators'; 21 | /* INTERFACE FOR 42 API */ 22 | import { Profile_42 } from './interfaces/42.interface'; 23 | /* AUTH MODULES */ 24 | import { AuthService } from './auth.service'; 25 | import { FortyTwoAuthGuard } from './guard'; 26 | import { RtGuard } from './guard'; 27 | /* AUTH DTOs */ 28 | import { SignUpDto, SignInDto } from './dto'; 29 | import { TwoFactorService } from './2FA/2fa.service'; 30 | import { ApiHeader, ApiResponse, ApiTags } from '@nestjs/swagger'; 31 | import { RedirectOnLogin } from './filter'; 32 | 33 | // AUTH CONTROLLER - /auth 34 | @ApiTags('authentification') 35 | @ApiHeader({ 36 | name: 'Authorization', 37 | description: 'Jason Web Token as Bearer Token', 38 | }) 39 | @Controller('auth') 40 | export class AuthController { 41 | constructor( 42 | private authService: AuthService, 43 | private twoFAService: TwoFactorService, 44 | ) {} 45 | 46 | logger = new Logger('Authentification'); 47 | /** 48 | * /signup - create account 49 | * Creates a new user email/username/password 50 | */ 51 | @Public() 52 | @ApiResponse({ status: 403, description: 'Credentials already exist' }) 53 | @Post('/signup') 54 | signup(@Body() dto: SignUpDto) { 55 | this.logger.log('User Signup: ' + dto.username); 56 | return this.authService.signup(dto); 57 | } 58 | 59 | /** 60 | * /signin - sign in to API 61 | * Signs an existing user email/username and password 62 | */ 63 | @Public() 64 | @ApiResponse({ status: 403, description: 'Invalid Credentials' }) 65 | @Post('/signin') 66 | signin(@Body() dto: SignInDto) { 67 | this.logger.log('User Signup: ' + dto.username); 68 | return this.authService.signin(dto); 69 | } 70 | 71 | /** 72 | * /logout - logout from API 73 | * Deletes the refresh token from the database 74 | */ 75 | @Post('logout') 76 | @HttpCode(200) 77 | @ApiResponse({ status: 401, description: 'Unauthorized' }) 78 | logout( 79 | @GetCurrentUserId() userId: number, 80 | @GetCurrentUser('username') username: string, 81 | ) { 82 | // LOG 83 | this.logger.log('User logout ' + username); 84 | return this.authService.signout(userId); 85 | } 86 | 87 | /* REFRESH TOKEN CALLBACK */ 88 | 89 | /** 90 | * Updates Tokens for signed in user 91 | * Work in progress 92 | */ 93 | @Public() 94 | @UseGuards(RtGuard) 95 | @HttpCode(200) 96 | @Post('/refresh') 97 | refresh( 98 | @GetCurrentUserId() userId: number, 99 | @GetCurrentUser('refreshToken') refreshToken: string, 100 | ) { 101 | return this.authService.refresh_token(userId, refreshToken); 102 | } 103 | /* 42 API */ 104 | 105 | /** 106 | * Signin using 42 API => HREF front 107 | * Work in progress 108 | */ 109 | @Public() 110 | @UseGuards(FortyTwoAuthGuard) 111 | @Get('42') 112 | signin_42() { 113 | console.log('42 API signin'); 114 | } 115 | 116 | /** 117 | * 42 Callback URI 118 | * Creates user or signin if user already exists 119 | */ 120 | @Public() 121 | @UseGuards(FortyTwoAuthGuard) 122 | @UseFilters(RedirectOnLogin) 123 | @Get('42/callback') 124 | async callback_42(@Req() request: any, @Res() response: Response) { 125 | // Generate token using API response 126 | const user = await this.authService.signin_42( 127 | request.user as Profile_42, 128 | ); 129 | const { username, twoFA, id, email } = user; 130 | // LOG 131 | this.logger.log('42 API signin ' + username); 132 | return twoFA 133 | ? this.twoFAService.signin_2FA(response, username) 134 | : this.authService.signin_42_token(response, id, email); 135 | } 136 | 137 | /** 138 | * Testing basic /auth route 139 | */ 140 | @Public() 141 | @Get('/') 142 | test_auth() { 143 | return this.authService.test_route(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /front/src/routes/profile_types/Profiles.css: -------------------------------------------------------------------------------- 1 | /* PRIVATE PROFILE */ 2 | 3 | .app-title { 4 | font-family: "IBM Plex Mono"; 5 | font-style: normal; 6 | font-weight: 400; 7 | font-size: 20px; 8 | line-height: 20px; 9 | letter-spacing: -0.4px; 10 | color: var(--text-color-white); 11 | } 12 | 13 | .profile-username-text { 14 | font-weight: 500; 15 | font-size: 25px; 16 | line-height: 20px; 17 | color: var(--text-color-white); 18 | } 19 | 20 | .caption { 21 | font-family: "IBM Plex Mono"; 22 | font-style: normal; 23 | padding-top: 5px; 24 | font-size: 12px; 25 | } 26 | .caption:hover { 27 | text-decoration: underline; 28 | color: var(--buttons-color-blue); 29 | } 30 | 31 | /* PRIVATE PROFILE / CARDS */ 32 | 33 | .profile-card { 34 | background: #31353c; 35 | box-shadow: -10px -5px 30px 5px #34373c, 10px 5px 30px 5px #24282d; 36 | border-radius: 10px; 37 | } 38 | 39 | .modify-card { 40 | background: #2e3238; 41 | box-shadow: inset 5px 4px 30px #23262b, inset -4px -5px 30px #434851; 42 | border-radius: 10px; 43 | max-height: 80%; 44 | } 45 | 46 | /* PRIVATE PROFILE / AVATAR */ 47 | 48 | .profile-pic-round { 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | height: 150px; 53 | width: 150px; 54 | border-radius: 50%; 55 | background: var(--buttons-color-gray-disabled); 56 | box-shadow: 7px 7px 30px #1f2427, -7px -7px 30px #485057; 57 | } 58 | 59 | .profile-pic-inside { 60 | height: 98%; 61 | width: 98%; 62 | border-radius: 50%; 63 | } 64 | 65 | .edit-round-icon { 66 | padding: 3%; 67 | width: 30px; 68 | height: 30px; 69 | border-radius: 50%; 70 | background: var(--buttons-color-blue); 71 | transition: 0.3s ease; 72 | outline: none; 73 | outline-color: #556fe8; 74 | } 75 | 76 | .edit-round-icon:hover.edit-round-icon:active.edit-round-icon:focus { 77 | outline-style: solid; 78 | outline-width: 1px; 79 | outline-offset: -1px; 80 | outline-color: #3c58dd; 81 | background-color: var(--buttons-color-blue-selected); 82 | box-shadow: inset 2px 5px 10px rgba(15, 21, 49, 0.25), 83 | inset -2px -5px 10px #556fe8; 84 | } 85 | 86 | /* PRIVATE PROFILE / FRIENDS LIST */ 87 | 88 | .profile-pic-round-sm { 89 | position: relative; 90 | display: flex; 91 | align-items: center; 92 | justify-content: center; 93 | } 94 | 95 | .profile-pic-wrapper { 96 | height: 3.1em; 97 | width: 3.1em; 98 | display: flex; 99 | align-items: center; 100 | justify-content: center; 101 | border-radius: 50%; 102 | } 103 | 104 | .profile-pic-wrapper.ingame { 105 | outline: 0.125em solid; 106 | outline-color: var(--buttons-color-blue); 107 | } 108 | 109 | .profile-pic-inside-sm { 110 | height: 90%; 111 | width: 90%; 112 | border-radius: 50%; 113 | } 114 | 115 | .status-private { 116 | position: absolute; 117 | left: 3em; 118 | top: calc(3em - 0.8em + 0.125em); 119 | width: 0.8em; 120 | height: 0.8em; 121 | border-radius: 50%; 122 | transition: 0.5s ease; 123 | } 124 | 125 | .status-private.online { 126 | background: var(--basic-color-green); 127 | } 128 | 129 | .status-private.offline { 130 | background: grey; 131 | } 132 | 133 | .status-private.ingame { 134 | background: var(--basic-color-green); 135 | } 136 | 137 | /* //////////////////////////////////////////////////// */ 138 | 139 | /* GAMES LIST */ 140 | 141 | .profile-pic-wrapper-sm { 142 | height: 2.1em; 143 | width: 2.1em; 144 | display: flex; 145 | align-items: center; 146 | justify-content: center; 147 | border-radius: 50%; 148 | } 149 | 150 | .profile-pic-wrapper-sm.ingame { 151 | outline: 0.125em solid; 152 | outline-color: var(--buttons-color-blue); 153 | } 154 | 155 | .status-private-sm { 156 | position: absolute; 157 | left: calc(2em + 0.25em); 158 | top: calc(2em - 0.25em); 159 | width: 0.5em; 160 | height: 0.5em; 161 | border-radius: 50%; 162 | } 163 | 164 | .status-private-sm.online { 165 | background: var(--basic-color-green); 166 | } 167 | 168 | .status-private-sm.offline { 169 | background: grey; 170 | } 171 | 172 | .status-private-sm.ingame { 173 | background: var(--basic-color-green); 174 | } 175 | -------------------------------------------------------------------------------- /front/src/routes/chat_modes/tags.css: -------------------------------------------------------------------------------- 1 | /** 2 | *
3 | *
4 | * 7 | *
8 | *