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 { 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 { UsersModule } from './users/users.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 { Logger } from '@nestjs/common' |
|||
import { NestFactory } from '@nestjs/core' |
|||
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> { |
|||
const app = await NestFactory.create(AppModule) |
|||
async function bootstrap () { |
|||
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)) |
|||
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