nicolas-arnaud
2 years ago
33 changed files with 2224 additions and 158 deletions
@ -1,3 +0,0 @@ |
|||||
POSTGRES_USER: postgres_usr |
|
||||
POSTGRES_PASSWORD: postgres_pw |
|
||||
POSTGRES_DB: transcendence |
|
@ -0,0 +1,14 @@ |
|||||
|
POSTGRES_HOST: postgres |
||||
|
POSTGRES_PORT: 5432 |
||||
|
POSTGRES_USER: postgres_usr |
||||
|
POSTGRES_PASSWORD: postgres_pw |
||||
|
POSTGRES_DB: transcendence |
||||
|
|
||||
|
BACK_PORT=3001 |
||||
|
|
||||
|
JWT_SECRET= |
||||
|
JWT_EXPIRATION_TIME_SECONDS=900 |
||||
|
|
||||
|
FT_OAUTH_CLIENT_ID= |
||||
|
FT_OAUTH_CLIENT_SECRET= |
||||
|
FT_OAUTH_CALLBACK_URL=http://localhost/ |
@ -0,0 +1,2 @@ |
|||||
|
.env |
||||
|
*/volume/.env |
@ -0,0 +1,28 @@ |
|||||
|
npm install; |
||||
|
cat >.env <<EOF |
||||
|
POSTGRES_HOST= $POSTGRES_HOST |
||||
|
POSTGRES_PORT= $POSTGRES_PORT |
||||
|
POSTGRES_USER= $POSTGRES_USER |
||||
|
POSTGRES_PASSWORD= $POSTGRES_PASSWORD |
||||
|
POSTGRES_DB= $POSTGRES_DB |
||||
|
|
||||
|
BACK_PORT=$BACK_PORT |
||||
|
|
||||
|
JWT_SECRET=$JWT_SECRET |
||||
|
JWT_EXPIRATION_TIME_SECONDS=$JWT_EXPIRATION_TIME_SECONDS |
||||
|
|
||||
|
FT_OAUTH_CLIENT_ID=$FT_OAUTH_CLIENT_ID |
||||
|
FT_OAUTH_CLIENT_SECRET=$FT_OAUTH_CLIENT_SECRET |
||||
|
FT_OAUTH_CALLBACK_URL=$FT_OAUTH_CALLBACK_URL |
||||
|
EOF |
||||
|
|
||||
|
if [[ $NODE_ENV == "production" ]]; then |
||||
|
npm run build && npm run start:prod; |
||||
|
elif [[ $NODE_ENV == "debug" ]]; then |
||||
|
npm run start:debug; |
||||
|
elif [[ $NODE_ENV == "check" ]]; then |
||||
|
npm run format && npm run lint; echo "=== FINISH ==="; |
||||
|
elif [[ $NODE_ENV == "development" ]]; then |
||||
|
npm run dev; |
||||
|
else echo "NODE_ENV value isn't known."; |
||||
|
fi; |
File diff suppressed because it is too large
@ -0,0 +1,28 @@ |
|||||
|
import { Controller, Get, Redirect, Req, UseGuards } from '@nestjs/common' |
||||
|
import { User } from 'src/auth/42.decorator' |
||||
|
import { AuthenticatedGuard } from 'src/auth/42-auth.guard' |
||||
|
import { Profile } from 'passport-42' |
||||
|
import { Request } from 'express' |
||||
|
|
||||
|
@Controller() |
||||
|
export class ApiController { |
||||
|
@Get('profile') |
||||
|
@UseGuards(AuthenticatedGuard) |
||||
|
profile (@User() user: Profile) { |
||||
|
return { user } |
||||
|
} |
||||
|
|
||||
|
@Get('logout') |
||||
|
@Redirect('/') |
||||
|
logOut (@Req() req: Request) { |
||||
|
req.logOut(function (err) { |
||||
|
if (err) return err |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
@Get('ranks') |
||||
|
getRanks (): number[] { |
||||
|
return [1, 2, 3] |
||||
|
} |
||||
|
} |
||||
|
|
@ -0,0 +1,10 @@ |
|||||
|
import { Module } from '@nestjs/common' |
||||
|
import { HttpModule } from '@nestjs/axios' |
||||
|
import { ApiController } from './api.controller' |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [HttpModule], |
||||
|
controllers: [ApiController], |
||||
|
}) |
||||
|
|
||||
|
export class ApiModule { } |
@ -1,7 +1,20 @@ |
|||||
import { Module } from '@nestjs/common' |
import { Module } from '@nestjs/common' |
||||
|
|
||||
|
import { ApiModule } from './api/api.module' |
||||
|
import { AuthModule } from './auth/auth.module' |
||||
|
import { ChatModule } from './chat/chat.module' |
||||
|
import { DbModule } from './db/db.module' |
||||
import { PongModule } from './pong/pong.module' |
import { PongModule } from './pong/pong.module' |
||||
|
import { UsersModule } from './users/users.module' |
||||
|
|
||||
@Module({ |
@Module({ |
||||
imports: [PongModule] |
imports: [ |
||||
|
ApiModule, |
||||
|
AuthModule, |
||||
|
ChatModule, |
||||
|
DbModule, |
||||
|
PongModule, |
||||
|
UsersModule |
||||
|
], |
||||
}) |
}) |
||||
export class AppModule { } |
export class AppModule { } |
||||
|
@ -0,0 +1,25 @@ |
|||||
|
import { |
||||
|
type ExecutionContext, |
||||
|
Injectable, |
||||
|
type CanActivate |
||||
|
} from '@nestjs/common' |
||||
|
import { AuthGuard } from '@nestjs/passport' |
||||
|
import { type Request } from 'express' |
||||
|
|
||||
|
@Injectable() |
||||
|
export class FtOauthGuard extends AuthGuard('42') { |
||||
|
async canActivate (context: ExecutionContext): Promise<boolean> { |
||||
|
const activate: boolean = (await super.canActivate(context)) as boolean |
||||
|
const request: Request = context.switchToHttp().getRequest() |
||||
|
await super.logIn(request) |
||||
|
return activate |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Injectable() |
||||
|
export class AuthenticatedGuard implements CanActivate { |
||||
|
async canActivate (context: ExecutionContext): Promise<boolean> { |
||||
|
const req: Request = context.switchToHttp().getRequest() |
||||
|
return req.isAuthenticated() |
||||
|
} |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
import { createParamDecorator, type ExecutionContext } from '@nestjs/common' |
||||
|
import { type Profile } from 'passport-42' |
||||
|
|
||||
|
export const User = createParamDecorator( |
||||
|
(data: unknown, ctx: ExecutionContext): Profile => { |
||||
|
const request = ctx.switchToHttp().getRequest() |
||||
|
return request.user |
||||
|
} |
||||
|
) |
@ -0,0 +1,42 @@ |
|||||
|
import { Injectable } from '@nestjs/common' |
||||
|
import { ConfigService } from '@nestjs/config' |
||||
|
import { PassportStrategy } from '@nestjs/passport' |
||||
|
import { Strategy, Profile, VerifyCallback } from 'passport-42' |
||||
|
import { UsersService } from 'src/users/users.service' |
||||
|
import { User } from 'src/users/user.entity' |
||||
|
|
||||
|
@Injectable() |
||||
|
export class FtStrategy extends PassportStrategy(Strategy, '42') { |
||||
|
constructor ( |
||||
|
private readonly configService: ConfigService, |
||||
|
private readonly usersService: UsersService |
||||
|
) { |
||||
|
super({ |
||||
|
clientID: configService.get<string>('FT_OAUTH_CLIENT_ID'), |
||||
|
clientSecret: configService.get<string>('FT_OAUTH_CLIENT_SECRET'), |
||||
|
callbackURL: configService.get<string>('FT_OAUTH_CALLBACK_URL'), |
||||
|
passReqToCallback: true |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
async validate ( |
||||
|
request: { session: { accessToken: string } }, |
||||
|
accessToken: string, |
||||
|
refreshToken: string, |
||||
|
profile: Profile, |
||||
|
cb: VerifyCallback |
||||
|
): Promise<any> { |
||||
|
request.session.accessToken = accessToken |
||||
|
console.log('accessToken', accessToken, 'refreshToken', refreshToken) |
||||
|
const id_42 = profile.id as number |
||||
|
console.log(profile) |
||||
|
if ((await this.usersService.getOneUser42(id_42)) === null) { |
||||
|
const newUser = new User() |
||||
|
newUser.id_42 = profile.id as number |
||||
|
newUser.username = profile.displayName as string |
||||
|
newUser.avatar = profile._json.image.versions.small as string |
||||
|
this.usersService.create(newUser) |
||||
|
} |
||||
|
return cb(null, profile) |
||||
|
} |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
import { Controller, Get, Redirect, UseGuards, Res, Req } from '@nestjs/common' |
||||
|
import { FtOauthGuard } from './42-auth.guard' |
||||
|
import { Response, Request } from 'express' |
||||
|
|
||||
|
@Controller('auth') |
||||
|
export class AuthController { |
||||
|
@Get('42') |
||||
|
@UseGuards(FtOauthGuard) |
||||
|
ftAuth () {} |
||||
|
|
||||
|
@Get('42/return') |
||||
|
@UseGuards(FtOauthGuard) |
||||
|
@Redirect('http://localhost:5000/') |
||||
|
ftAuthCallback ( |
||||
|
@Res({ passthrough: true }) response: Response, |
||||
|
@Req() request: Request |
||||
|
) { |
||||
|
console.log('cookie:', request.cookies['connect.sid']) |
||||
|
response.cookie('connect.sid', request.cookies['connect.sid']) |
||||
|
} |
||||
|
} |
@ -0,0 +1,14 @@ |
|||||
|
import { Module } from '@nestjs/common' |
||||
|
import { UsersModule } from 'src/users/users.module' |
||||
|
import { PassportModule } from '@nestjs/passport' |
||||
|
import { ConfigService } from '@nestjs/config' |
||||
|
import { AuthController } from './auth.controller' |
||||
|
import { FtStrategy } from './42.strategy' |
||||
|
import { SessionSerializer } from './session.serializer' |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [UsersModule, PassportModule], |
||||
|
providers: [ConfigService, FtStrategy, SessionSerializer], |
||||
|
controllers: [AuthController] |
||||
|
}) |
||||
|
export class AuthModule {} |
@ -0,0 +1,24 @@ |
|||||
|
import { Injectable } from '@nestjs/common' |
||||
|
import { PassportSerializer } from '@nestjs/passport' |
||||
|
import { type Profile } from 'passport-42' |
||||
|
|
||||
|
@Injectable() |
||||
|
export class SessionSerializer extends PassportSerializer { |
||||
|
constructor () { |
||||
|
super() |
||||
|
} |
||||
|
|
||||
|
serializeUser ( |
||||
|
user: Profile, |
||||
|
done: (err: Error | null, user: Profile) => void |
||||
|
): any { |
||||
|
done(null, user) |
||||
|
} |
||||
|
|
||||
|
deserializeUser ( |
||||
|
payload: Profile, |
||||
|
done: (err: Error | null, user: Profile) => void |
||||
|
) { |
||||
|
done(null, payload) |
||||
|
} |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
import { Module, forwardRef } from '@nestjs/common' |
||||
|
|
||||
|
import { TypeOrmModule } from '@nestjs/typeorm' |
||||
|
import { Message } from './entities/message.entity' |
||||
|
import { Channel } from './entities/channel.entity' |
||||
|
import { UsersModule } from 'src/users/users.module' |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [ |
||||
|
TypeOrmModule.forFeature([Channel, Message]), |
||||
|
forwardRef(() => UsersModule) |
||||
|
] |
||||
|
// providers: [ChatService]
|
||||
|
}) |
||||
|
export class ChatModule {} |
@ -0,0 +1,33 @@ |
|||||
|
import { |
||||
|
Entity, |
||||
|
PrimaryGeneratedColumn, |
||||
|
JoinTable, |
||||
|
OneToMany, |
||||
|
ManyToMany |
||||
|
} from 'typeorm' |
||||
|
|
||||
|
import { Message } from './message.entity' |
||||
|
import { User } from 'src/users/user.entity' |
||||
|
|
||||
|
@Entity('channel') |
||||
|
export class Channel { |
||||
|
@PrimaryGeneratedColumn() |
||||
|
id: number |
||||
|
|
||||
|
@OneToMany(() => Message, (message) => message.channel) |
||||
|
message: Message[] |
||||
|
|
||||
|
@ManyToMany(() => User) |
||||
|
@JoinTable({ |
||||
|
name: 'users_id', |
||||
|
joinColumn: { |
||||
|
name: 'channel', |
||||
|
referencedColumnName: 'id' |
||||
|
}, |
||||
|
inverseJoinColumn: { |
||||
|
name: 'user', |
||||
|
referencedColumnName: 'id' |
||||
|
} |
||||
|
}) |
||||
|
user: User[] |
||||
|
} |
@ -0,0 +1,28 @@ |
|||||
|
import { |
||||
|
BaseEntity, |
||||
|
Entity, |
||||
|
PrimaryGeneratedColumn, |
||||
|
Column, |
||||
|
JoinColumn, |
||||
|
ManyToOne |
||||
|
} from 'typeorm' |
||||
|
|
||||
|
import { Channel } from './channel.entity' |
||||
|
import { User } from 'src/users/user.entity' |
||||
|
|
||||
|
@Entity('message') |
||||
|
export class Message extends BaseEntity { |
||||
|
@PrimaryGeneratedColumn() |
||||
|
public id: number |
||||
|
|
||||
|
@Column() |
||||
|
public content: string |
||||
|
|
||||
|
@ManyToOne(() => User) |
||||
|
@JoinColumn({ name: 'author_id' }) |
||||
|
public author: User |
||||
|
|
||||
|
@ManyToOne(() => Channel) |
||||
|
@JoinColumn({ name: 'channel_id' }) |
||||
|
public channel: Channel |
||||
|
} |
@ -0,0 +1,36 @@ |
|||||
|
import { Module } from '@nestjs/common' |
||||
|
import { TypeOrmModule } from '@nestjs/typeorm' |
||||
|
import { ConfigModule, ConfigService } from '@nestjs/config' |
||||
|
import * as Joi from 'joi' |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [ |
||||
|
ConfigModule.forRoot({ |
||||
|
validationSchema: Joi.object({ |
||||
|
POSTGRES_HOST: Joi.string().required(), |
||||
|
POSTGRES_PORT: Joi.number().required(), |
||||
|
POSTGRES_USER: Joi.string().required(), |
||||
|
POSTGRES_PASSWORD: Joi.string().required(), |
||||
|
POSTGRES_DB: Joi.string().required(), |
||||
|
BACK_PORT: Joi.number(), |
||||
|
}) |
||||
|
}), |
||||
|
TypeOrmModule.forRootAsync({ |
||||
|
imports: [ConfigModule], |
||||
|
inject: [ConfigService], |
||||
|
useFactory: (configService: ConfigService) => ({ |
||||
|
type: 'postgres', |
||||
|
host: configService.get<string>('POSTGRES_HOST'), |
||||
|
port: configService.get<number>('POSTGRES_PORT'), |
||||
|
username: configService.get<string>('POSTGRES_USER'), |
||||
|
password: configService.get<string>('POSTGRES_PASSWORD'), |
||||
|
database: configService.get<string>('POSTGRES_DB'), |
||||
|
jwt_secret: configService.get<string>('JWT_SECRET'), |
||||
|
entities: [__dirname + '/../**/*.entity.ts'], |
||||
|
synchronize: true |
||||
|
}) |
||||
|
}), |
||||
|
] |
||||
|
}) |
||||
|
|
||||
|
export class DbModule { } |
@ -1,10 +1,38 @@ |
|||||
import { NestFactory } from '@nestjs/core' |
|
||||
import { WsAdapter } from '@nestjs/platform-ws' |
import { WsAdapter } from '@nestjs/platform-ws' |
||||
|
|
||||
|
import { Logger } from '@nestjs/common' |
||||
|
import { NestFactory } from '@nestjs/core' |
||||
import { AppModule } from './app.module' |
import { AppModule } from './app.module' |
||||
|
import * as session from 'express-session' |
||||
|
import * as passport from 'passport' |
||||
|
import { type NestExpressApplication } from '@nestjs/platform-express' |
||||
|
import * as cookieParser from 'cookie-parser' |
||||
|
|
||||
async function bootstrap (): Promise<void> { |
async function bootstrap () { |
||||
const app = await NestFactory.create(AppModule) |
const logger = new Logger() |
||||
|
const app = await NestFactory.create<NestExpressApplication>(AppModule) |
||||
|
const port = process.env.BACK_PORT |
||||
|
const cors = { |
||||
|
origin: ['http://localhost:80', 'http://localhost', '*'], |
||||
|
methods: 'GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS', |
||||
|
preflightContinue: false, |
||||
|
optionsSuccessStatus: 204, |
||||
|
credentials: true, |
||||
|
allowedHeaders: ['Accept', 'Content-Type', 'Authorization'] |
||||
|
} |
||||
|
app.use( |
||||
|
session({ |
||||
|
resave: false, |
||||
|
saveUninitialized: false, |
||||
|
secret: process.env.JWT_SECRET |
||||
|
}) |
||||
|
) |
||||
|
app.use(cookieParser()) |
||||
|
app.use(passport.initialize()) |
||||
|
app.use(passport.session()) |
||||
|
app.enableCors(cors) |
||||
app.useWebSocketAdapter(new WsAdapter(app)) |
app.useWebSocketAdapter(new WsAdapter(app)) |
||||
await app.listen(3001) |
await app.listen(port) |
||||
|
logger.log(`Application listening on port ${port}`) |
||||
} |
} |
||||
void bootstrap() |
bootstrap() |
||||
|
@ -0,0 +1,5 @@ |
|||||
|
declare module 'passport-42' { |
||||
|
export type Profile = any; |
||||
|
export type VerifyCallback = any; |
||||
|
export class Strategy {}; |
||||
|
} |
@ -0,0 +1,39 @@ |
|||||
|
import { |
||||
|
IsString, |
||||
|
IsNotEmpty, |
||||
|
IsEmail, |
||||
|
Length, |
||||
|
IsPositive, |
||||
|
IsOptional |
||||
|
} from 'class-validator' |
||||
|
|
||||
|
export class CreateUserDto { |
||||
|
@IsPositive() |
||||
|
@IsNotEmpty() |
||||
|
readonly id_42: number |
||||
|
|
||||
|
@IsString() |
||||
|
@IsNotEmpty() |
||||
|
readonly username: string |
||||
|
|
||||
|
@IsString() |
||||
|
@IsNotEmpty() |
||||
|
readonly avatar: string |
||||
|
} |
||||
|
|
||||
|
export class UpdateUserDto { |
||||
|
@IsPositive() |
||||
|
@IsNotEmpty() |
||||
|
readonly id_42: number |
||||
|
|
||||
|
@IsString() |
||||
|
@IsNotEmpty() |
||||
|
readonly username: string |
||||
|
|
||||
|
@IsString() |
||||
|
@IsNotEmpty() |
||||
|
readonly avatar: string |
||||
|
|
||||
|
@IsOptional() |
||||
|
readonly status: string |
||||
|
} |
@ -0,0 +1,19 @@ |
|||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm' |
||||
|
|
||||
|
@Entity() |
||||
|
export class User { |
||||
|
@PrimaryGeneratedColumn() |
||||
|
id: number |
||||
|
|
||||
|
@Column({ unique: true }) |
||||
|
id_42: number |
||||
|
|
||||
|
@Column({ unique: true }) |
||||
|
username: string |
||||
|
|
||||
|
@Column() |
||||
|
avatar: string |
||||
|
|
||||
|
@Column({ default: 'online' }) |
||||
|
status: string |
||||
|
} |
@ -0,0 +1,30 @@ |
|||||
|
import { |
||||
|
Controller, |
||||
|
Get, |
||||
|
Post, |
||||
|
Body, |
||||
|
Param, |
||||
|
ParseIntPipe |
||||
|
} from '@nestjs/common' |
||||
|
import { type User } from './user.entity' |
||||
|
import { UsersService } from './users.service' |
||||
|
import { CreateUserDto, UpdateUserDto } from './user.dto' |
||||
|
@Controller('users') |
||||
|
export class UsersController { |
||||
|
constructor (private readonly usersService: UsersService) {} |
||||
|
|
||||
|
@Get() |
||||
|
async getAllUsers (): Promise<User[]> { |
||||
|
return await this.usersService.getAllUsers() |
||||
|
} |
||||
|
|
||||
|
@Post() |
||||
|
async create (@Body() payload: CreateUserDto) { |
||||
|
return await this.usersService.create(payload) |
||||
|
} |
||||
|
|
||||
|
@Post(':id') |
||||
|
update (@Param('id', ParseIntPipe) id: number, @Body() user: UpdateUserDto) { |
||||
|
this.usersService.update(id, user) |
||||
|
} |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
import { Module } from '@nestjs/common' |
||||
|
import { TypeOrmModule } from '@nestjs/typeorm' |
||||
|
import { User } from './user.entity' |
||||
|
import { UsersController } from './users.controller' |
||||
|
import { UsersService } from './users.service' |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [TypeOrmModule.forFeature([User])], |
||||
|
controllers: [UsersController], |
||||
|
providers: [UsersService], |
||||
|
exports: [UsersService] |
||||
|
}) |
||||
|
export class UsersModule {} |
@ -0,0 +1,54 @@ |
|||||
|
import { |
||||
|
ConflictException, |
||||
|
Injectable, |
||||
|
NotFoundException |
||||
|
} from '@nestjs/common' |
||||
|
import { InjectRepository } from '@nestjs/typeorm' |
||||
|
import { Repository } from 'typeorm' |
||||
|
import { User } from './user.entity' |
||||
|
import { type CreateUserDto, type UpdateUserDto } from './user.dto' |
||||
|
|
||||
|
@Injectable() |
||||
|
export class UsersService { |
||||
|
constructor ( |
||||
|
@InjectRepository(User) private readonly usersRepository: Repository<User> |
||||
|
) {} |
||||
|
|
||||
|
async getAllUsers (): Promise<User[]> { |
||||
|
return await this.usersRepository.find({}) |
||||
|
} |
||||
|
|
||||
|
async getOneUser (username: string): Promise<User | null> { |
||||
|
return await this.usersRepository.findOneBy({ username }) |
||||
|
} |
||||
|
|
||||
|
async getOneUser42 (id_42: number): Promise<User | null> { |
||||
|
return await this.usersRepository.findOneBy({ id_42 }) |
||||
|
} |
||||
|
|
||||
|
async create (newUser: CreateUserDto) { |
||||
|
try { |
||||
|
const user = new User() |
||||
|
user.id_42 = newUser.id_42 |
||||
|
user.avatar = newUser.avatar |
||||
|
user.username = newUser.username |
||||
|
return await this.usersRepository.save(user) |
||||
|
} catch (err) { |
||||
|
throw new Error(`Error creating ${err} user ${err.message}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async findOne (id: number) { |
||||
|
const user = await this.usersRepository.findOneBy({ id }) |
||||
|
if (user == null) { |
||||
|
throw new NotFoundException(`User #${id} not found`) |
||||
|
} |
||||
|
return user |
||||
|
} |
||||
|
|
||||
|
async update (id: number, changes: UpdateUserDto) { |
||||
|
const user = await this.findOne(id) |
||||
|
await this.usersRepository.merge(user, changes) |
||||
|
return await this.usersRepository.save(user) |
||||
|
} |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"name": "transcendance", |
||||
|
"lockfileVersion": 3, |
||||
|
"requires": true, |
||||
|
"packages": {} |
||||
|
} |
Loading…
Reference in new issue