From 9c706c2d8f94a380162330ab108f3f800022d6f8 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Mon, 27 Oct 2025 23:11:49 +0100
Subject: [PATCH 01/29] add JWT auth and GitHub OAuth strategies to auth module
---
api/src/auth/auth.controller.ts | 59 +++++++++++++++++++++++++++++++--
api/src/auth/auth.module.ts | 24 +++++++++++++-
api/src/auth/auth.service.ts | 11 +++++-
frontend/app/actions/axios.ts | 1 -
4 files changed, 90 insertions(+), 5 deletions(-)
diff --git a/api/src/auth/auth.controller.ts b/api/src/auth/auth.controller.ts
index 438edde2..3fcc9c62 100644
--- a/api/src/auth/auth.controller.ts
+++ b/api/src/auth/auth.controller.ts
@@ -1,4 +1,59 @@
-import { Controller } from "@nestjs/common";
+import {
+ BadRequestException,
+ Body,
+ Controller,
+ Get,
+ Post,
+ Req,
+ Res,
+ UseGuards,
+} from "@nestjs/common";
+import { AuthGuard } from "@nestjs/passport";
+import { Response, Request } from "express";
+import { AuthService } from "./auth.service";
+import { JwtAuthGuard } from "./jwt-auth.guard";
+import { ConfigService } from "@nestjs/config";
@Controller("auth")
-export class AuthController {}
+export class AuthController {
+ constructor(
+ private auth: AuthService,
+ private configService: ConfigService,
+ ) {}
+
+ @UseGuards(JwtAuthGuard)
+ @Post("login")
+ login(@Body() body: { token?: string }, @Res() res: Response) {
+ if (!body.token)
+ throw new BadRequestException("Token is required for login.");
+
+ res.cookie("token", body.token);
+ res.json({ message: "Login successful" });
+ }
+
+ @Get("/github/callback")
+ @UseGuards(AuthGuard("github"))
+ githubCallback(@Req() req: Request, @Res() res: Response) {
+ const user: any = (req as any).user;
+ const token = this.auth.signToken(user);
+ const redirectUrl = this.configService.getOrThrow(
+ "OAUTH_SUCCESS_REDIRECT_URL",
+ );
+ if (redirectUrl) {
+ res.cookie("token", token, {
+ httpOnly: true,
+ secure: true,
+ sameSite: "none",
+ maxAge: 7 * 24 * 60 * 60 * 1000
+ });
+ return res.redirect(redirectUrl);
+ }
+ return res.json({ token });
+ }
+
+ @Get("/me")
+ @UseGuards(JwtAuthGuard)
+ me(@Req() req: Request) {
+ return (req as any).user;
+ }
+}
diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts
index a7eac273..296fc574 100644
--- a/api/src/auth/auth.module.ts
+++ b/api/src/auth/auth.module.ts
@@ -1,9 +1,31 @@
import { Module } from "@nestjs/common";
+import { PassportModule } from "@nestjs/passport";
+import { JwtModule } from "@nestjs/jwt";
+import { ConfigModule, ConfigService } from "@nestjs/config";
import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
+import { JwtStrategy } from "./jwt.strategy";
+import { GithubOAuthStrategy } from "./github.strategy";
+import { UserModule } from "../user/user.module";
@Module({
- providers: [AuthService],
+ imports: [
+ ConfigModule,
+ UserModule,
+ PassportModule.register({ session: false }),
+ JwtModule.registerAsync({
+ imports: [ConfigModule],
+ inject: [ConfigService],
+ useFactory: (config: ConfigService) => ({
+ secret: config.getOrThrow("JWT_SECRET") || "dev-secret",
+ signOptions: {
+ expiresIn: "7d",
+ },
+ }),
+ }),
+ ],
+ providers: [AuthService, JwtStrategy, GithubOAuthStrategy],
controllers: [AuthController],
+ exports: [PassportModule, JwtModule],
})
export class AuthModule {}
diff --git a/api/src/auth/auth.service.ts b/api/src/auth/auth.service.ts
index 58b57505..966d3f38 100644
--- a/api/src/auth/auth.service.ts
+++ b/api/src/auth/auth.service.ts
@@ -1,4 +1,13 @@
import { Injectable } from "@nestjs/common";
+import { JwtService } from "@nestjs/jwt";
+import { UserEntity } from "../user/entities/user.entity";
@Injectable()
-export class AuthService {}
+export class AuthService {
+ constructor(private jwt: JwtService) {}
+
+ signToken(user: UserEntity) {
+ const payload = { sub: user.id, email: user.email, username: user.username };
+ return this.jwt.sign(payload);
+ }
+}
diff --git a/frontend/app/actions/axios.ts b/frontend/app/actions/axios.ts
index 2dcdbccd..3b7368a2 100644
--- a/frontend/app/actions/axios.ts
+++ b/frontend/app/actions/axios.ts
@@ -7,7 +7,6 @@ const axiosInstance = axios.create({
baseURL: process.env.BACKEND_URL,
headers: {
"Content-Type": "application/json",
- Authorization: `${process.env.BACKEND_SECRET}`,
},
});
From 8d7d9894d0e7afd6e1a80d983b74747e5ddcc7a6 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Mon, 27 Oct 2025 23:12:04 +0100
Subject: [PATCH 02/29] replace UserGuard with JwtAuthGuard and add GitHub
OAuth & JWT strategies to auth module
---
api/src/auth/github.strategy.ts | 68 +++++++++++++++++++++++++++++++
api/src/auth/jwt-auth.guard.ts | 5 +++
api/src/auth/jwt.strategy.ts | 31 ++++++++++++++
api/src/event/event.controller.ts | 17 ++++----
4 files changed, 113 insertions(+), 8 deletions(-)
create mode 100644 api/src/auth/github.strategy.ts
create mode 100644 api/src/auth/jwt-auth.guard.ts
create mode 100644 api/src/auth/jwt.strategy.ts
diff --git a/api/src/auth/github.strategy.ts b/api/src/auth/github.strategy.ts
new file mode 100644
index 00000000..30531c3c
--- /dev/null
+++ b/api/src/auth/github.strategy.ts
@@ -0,0 +1,68 @@
+import { Injectable } from "@nestjs/common";
+import { PassportStrategy } from "@nestjs/passport";
+import { Strategy as GitHubStrategy, Profile } from "passport-github2";
+import { ConfigService } from "@nestjs/config";
+import { UserService } from "../user/user.service";
+
+@Injectable()
+export class GithubOAuthStrategy extends PassportStrategy(
+ GitHubStrategy,
+ "github",
+) {
+ constructor(
+ config: ConfigService,
+ private users: UserService,
+ ) {
+ super({
+ clientID: config.getOrThrow("GITHUB_CLIENT_ID"),
+ clientSecret: config.getOrThrow("GITHUB_CLIENT_SECRET"),
+ callbackURL: config.getOrThrow("GITHUB_CALLBACK_URL"),
+ scope: ["user:email"],
+ });
+ }
+
+ async validate(
+ accessToken: string,
+ refreshToken: string,
+ profile: Profile,
+ done: (err: any, user?: any) => void,
+ ) {
+ try {
+ const githubId = profile.id;
+ const email = profile.emails && profile.emails[0]?.value;
+ const username =
+ profile.username || profile.displayName || email || githubId;
+ const name = profile.displayName || username;
+ const profilePicture = profile.photos && profile.photos[0]?.value;
+
+ let user = await this.users.getUserByGithubId(githubId);
+
+ if (!user) {
+ user = await this.users.createUser(
+ email || `${githubId}@users.noreply.github.com`,
+ username,
+ name,
+ profilePicture,
+ githubId,
+ accessToken,
+ false,
+ );
+ } else {
+ await this.users.updateUser(
+ user.id,
+ email || user.email,
+ username || user.username,
+ name || user.name,
+ profilePicture || user.profilePicture,
+ githubId,
+ accessToken,
+ );
+ user = await this.users.getUserById(user.id);
+ }
+
+ done(null, user);
+ } catch (err) {
+ done(err, undefined);
+ }
+ }
+}
diff --git a/api/src/auth/jwt-auth.guard.ts b/api/src/auth/jwt-auth.guard.ts
new file mode 100644
index 00000000..5a704e72
--- /dev/null
+++ b/api/src/auth/jwt-auth.guard.ts
@@ -0,0 +1,5 @@
+import { Injectable } from "@nestjs/common";
+import { AuthGuard } from "@nestjs/passport";
+
+@Injectable()
+export class JwtAuthGuard extends AuthGuard('jwt') {}
diff --git a/api/src/auth/jwt.strategy.ts b/api/src/auth/jwt.strategy.ts
new file mode 100644
index 00000000..c94b4704
--- /dev/null
+++ b/api/src/auth/jwt.strategy.ts
@@ -0,0 +1,31 @@
+import { Injectable, UnauthorizedException } from "@nestjs/common";
+import { PassportStrategy } from "@nestjs/passport";
+import { ExtractJwt, Strategy } from "passport-jwt";
+import { ConfigService } from "@nestjs/config";
+import { UserService } from "../user/user.service";
+
+export interface JwtPayload {
+ sub: string; // user id
+ email?: string;
+ username?: string;
+}
+
+@Injectable()
+export class JwtStrategy extends PassportStrategy(Strategy) {
+ constructor(private config: ConfigService, private users: UserService) {
+ super({
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
+ ignoreExpiration: false,
+ secretOrKey: config.getOrThrow("JWT_SECRET"),
+ });
+ }
+
+ async validate(payload: JwtPayload) {
+ const userId = payload.sub;
+ if (!userId) throw new UnauthorizedException();
+ // Load current user; if missing, reject token
+ const user = await this.users.getUserById(userId).catch(() => null);
+ if (!user) throw new UnauthorizedException();
+ return user; // attaches to request.user
+ }
+}
diff --git a/api/src/event/event.controller.ts b/api/src/event/event.controller.ts
index 8f56ffe7..3ae01cfd 100644
--- a/api/src/event/event.controller.ts
+++ b/api/src/event/event.controller.ts
@@ -15,7 +15,8 @@ import { TeamService } from "../team/team.service";
import { UserService } from "../user/user.service";
import { CreateEventDto } from "./dtos/createEventDto";
import { SetLockTeamsDateDto } from "./dtos/setLockTeamsDateDto";
-import { UserGuard, UserId } from "../guards/UserGuard";
+import { UserId } from "../guards/UserGuard";
+import { JwtAuthGuard } from "../auth/jwt-auth.guard";
@Controller("event")
export class EventController {
@@ -25,7 +26,7 @@ export class EventController {
private readonly userService: UserService,
) {}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Get("my")
async getMyEvents(@UserId() userId: string) {
return this.eventService.getEventsForUser(userId);
@@ -51,7 +52,7 @@ export class EventController {
return await this.eventService.getCurrentLiveEvent();
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Post()
createEvent(
@UserId() userId: string,
@@ -94,7 +95,7 @@ export class EventController {
return this.userService.getUserCountOfEvent(eventId);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Get(":id/isUserRegistered")
getEventByUserId(
@Param("id", new ParseUUIDPipe()) eventId: string,
@@ -103,7 +104,7 @@ export class EventController {
return this.eventService.isUserRegisteredForEvent(eventId, userId);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Get(":id/isEventAdmin")
isEventAdmin(
@Param("id", new ParseUUIDPipe()) eventId: string,
@@ -112,7 +113,7 @@ export class EventController {
return this.eventService.isEventAdmin(eventId, userId);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Put(":id/join")
async joinEvent(
@Param("id", new ParseUUIDPipe()) eventId: string,
@@ -136,7 +137,7 @@ export class EventController {
return this.userService.joinEvent(userId, eventId);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Put(":id/lock")
async lockEvent(
@Param("id", new ParseUUIDPipe()) eventId: string,
@@ -150,7 +151,7 @@ export class EventController {
return this.eventService.lockEvent(eventId);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Put(":id/lockTeamsDate")
async lockTeamsDate(
@Param("id", new ParseUUIDPipe()) eventId: string,
From c9d1e819ed12ad6a74b8a3fa52c0e91618b5e244 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Mon, 27 Oct 2025 23:12:26 +0100
Subject: [PATCH 03/29] update CORS configuration to use dynamic origin and
extend supported headers
---
api/src/main.ts | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/api/src/main.ts b/api/src/main.ts
index a7bd8d36..bdacba07 100644
--- a/api/src/main.ts
+++ b/api/src/main.ts
@@ -26,12 +26,18 @@ export const getRabbitmqConfig: any = (
async function bootstrap() {
const app = await NestFactory.create(AppModule);
+ const configService = app.get(ConfigService);
app.useGlobalPipes(new ValidationPipe());
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
app.useGlobalFilters(new TypeormExceptionFilter());
- app.enableCors();
+ app.enableCors({
+ origin: configService.getOrThrow("CORS_ORIGIN"),
+ methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
+ credentials: true,
+ allowedHeaders:
+ "Content-Type, Accept, Authorization, X-Requested-With, X-HTTP-Method-Override, X-Auth-Token, X-Refresh-Token",
+ });
- const configService = app.get(ConfigService);
app.connectMicroservice(
getRabbitmqConfig(configService, "game_results"),
From a867a5ccc607d425d1f1d6795175ef58768aade6 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Mon, 27 Oct 2025 23:12:46 +0100
Subject: [PATCH 04/29] replace `UserGuard` with `JwtAuthGuard` across
controllers and update dependencies for JWT authentication
---
api/package.json | 2 +
api/pnpm-lock.yaml | 119 ++++++++++++++++++++++
api/src/guards/UserGuard.ts | 35 +------
api/src/match/match.controller.ts | 21 ++--
api/src/team/team.controller.ts | 22 ++--
api/src/user/social-account.controller.ts | 5 +-
api/src/user/user.controller.ts | 5 +-
7 files changed, 151 insertions(+), 58 deletions(-)
diff --git a/api/package.json b/api/package.json
index 7b9db33a..45f250f5 100644
--- a/api/package.json
+++ b/api/package.json
@@ -31,6 +31,7 @@
"@nestjs/common": "^11.1.7",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.7",
+ "@nestjs/jwt": "^11.0.0",
"@nestjs/microservices": "^11.1.7",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.7",
@@ -45,6 +46,7 @@
"crypto-js": "^4.2.0",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
+ "passport-jwt": "^4.0.1",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
diff --git a/api/pnpm-lock.yaml b/api/pnpm-lock.yaml
index 20aac125..474ad1ab 100644
--- a/api/pnpm-lock.yaml
+++ b/api/pnpm-lock.yaml
@@ -20,6 +20,9 @@ importers:
'@nestjs/core':
specifier: ^11.1.7
version: 11.1.7(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.7)(@nestjs/platform-express@11.1.7)(reflect-metadata@0.2.2)(rxjs@7.8.2)
+ '@nestjs/jwt':
+ specifier: ^11.0.0
+ version: 11.0.1(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))
'@nestjs/microservices':
specifier: ^11.1.7
version: 11.1.7(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.7)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -62,6 +65,9 @@ importers:
passport-github2:
specifier: ^0.1.12
version: 0.1.12
+ passport-jwt:
+ specifier: ^4.0.1
+ version: 4.0.1
pg:
specifier: ^8.16.3
version: 8.16.3
@@ -853,6 +859,11 @@ packages:
'@nestjs/websockets':
optional: true
+ '@nestjs/jwt@11.0.1':
+ resolution: {integrity: sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==}
+ peerDependencies:
+ '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
+
'@nestjs/mapped-types@2.1.0':
resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==}
peerDependencies:
@@ -1183,6 +1194,9 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+ '@types/jsonwebtoken@9.0.10':
+ resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
+
'@types/luxon@3.7.1':
resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==}
@@ -1192,6 +1206,9 @@ packages:
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
+ '@types/ms@2.1.0':
+ resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
+
'@types/node@24.9.1':
resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==}
@@ -1710,6 +1727,9 @@ packages:
buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+ buffer-equal-constant-time@1.0.1:
+ resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
+
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -2013,6 +2033,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
+ ecdsa-sig-formatter@1.0.11:
+ resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
+
edmonds-blossom-fixed@1.0.1:
resolution: {integrity: sha512-wtpraSt4yJeUpNU8RGC4q2JBxsJbHFxI7/htm/mS4FgxSN90WBwmgE7QZwpcY70KJRqmfLCNxU49UIc+DfzLKQ==}
@@ -2722,6 +2745,16 @@ packages:
jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
+ jsonwebtoken@9.0.2:
+ resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
+ engines: {node: '>=12', npm: '>=6'}
+
+ jwa@1.4.2:
+ resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
+
+ jws@3.2.2:
+ resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
+
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -2759,12 +2792,33 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
+ lodash.includes@4.3.0:
+ resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
+
+ lodash.isboolean@3.0.3:
+ resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
+
+ lodash.isinteger@4.0.4:
+ resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
+
+ lodash.isnumber@3.0.3:
+ resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
+
+ lodash.isplainobject@4.0.6:
+ resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
+
+ lodash.isstring@4.0.1:
+ resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
+
lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+ lodash.once@4.1.1:
+ resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
+
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@@ -3013,6 +3067,9 @@ packages:
resolution: {integrity: sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==}
engines: {node: '>= 0.8.0'}
+ passport-jwt@4.0.1:
+ resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==}
+
passport-oauth2@1.8.0:
resolution: {integrity: sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==}
engines: {node: '>= 0.4.0'}
@@ -4707,6 +4764,12 @@ snapshots:
'@nestjs/microservices': 11.1.7(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.7)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/platform-express': 11.1.7(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.7)
+ '@nestjs/jwt@11.0.1(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))':
+ dependencies:
+ '@nestjs/common': 11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
+ '@types/jsonwebtoken': 9.0.10
+ jsonwebtoken: 9.0.2
+
'@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)':
dependencies:
'@nestjs/common': 11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -5022,12 +5085,19 @@ snapshots:
'@types/json-schema@7.0.15': {}
+ '@types/jsonwebtoken@9.0.10':
+ dependencies:
+ '@types/ms': 2.1.0
+ '@types/node': 24.9.1
+
'@types/luxon@3.7.1': {}
'@types/methods@1.1.4': {}
'@types/mime@1.3.5': {}
+ '@types/ms@2.1.0': {}
+
'@types/node@24.9.1':
dependencies:
undici-types: 7.16.0
@@ -5650,6 +5720,8 @@ snapshots:
buffer-crc32@0.2.13: {}
+ buffer-equal-constant-time@1.0.1: {}
+
buffer-from@1.1.2: {}
buffer-more-ints@1.0.0: {}
@@ -5905,6 +5977,10 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
+ ecdsa-sig-formatter@1.0.11:
+ dependencies:
+ safe-buffer: 5.2.1
+
edmonds-blossom-fixed@1.0.1: {}
ee-first@1.1.1: {}
@@ -6833,6 +6909,30 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
+ jsonwebtoken@9.0.2:
+ dependencies:
+ jws: 3.2.2
+ lodash.includes: 4.3.0
+ lodash.isboolean: 3.0.3
+ lodash.isinteger: 4.0.4
+ lodash.isnumber: 3.0.3
+ lodash.isplainobject: 4.0.6
+ lodash.isstring: 4.0.1
+ lodash.once: 4.1.1
+ ms: 2.1.3
+ semver: 7.7.3
+
+ jwa@1.4.2:
+ dependencies:
+ buffer-equal-constant-time: 1.0.1
+ ecdsa-sig-formatter: 1.0.11
+ safe-buffer: 5.2.1
+
+ jws@3.2.2:
+ dependencies:
+ jwa: 1.4.2
+ safe-buffer: 5.2.1
+
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -6862,10 +6962,24 @@ snapshots:
dependencies:
p-locate: 5.0.0
+ lodash.includes@4.3.0: {}
+
+ lodash.isboolean@3.0.3: {}
+
+ lodash.isinteger@4.0.4: {}
+
+ lodash.isnumber@3.0.3: {}
+
+ lodash.isplainobject@4.0.6: {}
+
+ lodash.isstring@4.0.1: {}
+
lodash.memoize@4.1.2: {}
lodash.merge@4.6.2: {}
+ lodash.once@4.1.1: {}
+
lodash@4.17.21: {}
log-symbols@4.1.0:
@@ -7078,6 +7192,11 @@ snapshots:
dependencies:
passport-oauth2: 1.8.0
+ passport-jwt@4.0.1:
+ dependencies:
+ jsonwebtoken: 9.0.2
+ passport-strategy: 1.0.0
+
passport-oauth2@1.8.0:
dependencies:
base64url: 3.0.1
diff --git a/api/src/guards/UserGuard.ts b/api/src/guards/UserGuard.ts
index 04542ad2..25ba6cb8 100644
--- a/api/src/guards/UserGuard.ts
+++ b/api/src/guards/UserGuard.ts
@@ -1,42 +1,11 @@
import {
- CanActivate,
createParamDecorator,
ExecutionContext,
- Injectable,
- UnauthorizedException,
} from "@nestjs/common";
-import { Observable } from "rxjs";
-import { ConfigService } from "@nestjs/config";
-import { USER_ID_KEY } from "./GuardConstants";
export const UserId = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
- return request.headers[USER_ID_KEY];
+ return request.user?.id;
},
-);
-
-@Injectable()
-export class UserGuard implements CanActivate {
- FRONTEND_SECRET: string;
-
- constructor(config: ConfigService) {
- this.FRONTEND_SECRET = config.getOrThrow("FRONTEND_SECRET");
- }
-
- canActivate(
- context: ExecutionContext,
- ): boolean | Promise | Observable {
- const request = context.switchToHttp().getRequest();
-
- const authorization = request.headers.authorization;
- if (authorization !== this.FRONTEND_SECRET)
- throw new UnauthorizedException();
-
- const userId = request.headers[USER_ID_KEY];
- if (!userId) throw new UnauthorizedException("User ID is required");
- request.userId = userId;
-
- return true;
- }
-}
+);
\ No newline at end of file
diff --git a/api/src/match/match.controller.ts b/api/src/match/match.controller.ts
index 98ac8971..e4b11be5 100644
--- a/api/src/match/match.controller.ts
+++ b/api/src/match/match.controller.ts
@@ -13,8 +13,9 @@ import {
import { MatchService } from "./match.service";
import { EventService } from "../event/event.service";
import { EventState } from "../event/entities/event.entity";
-import { UserGuard, UserId } from "../guards/UserGuard";
+import { UserId } from "../guards/UserGuard";
import { MatchEntity } from "./entites/match.entity";
+import { JwtAuthGuard } from "../auth/jwt-auth.guard";
@Controller("match")
export class MatchController {
@@ -25,7 +26,7 @@ export class MatchController {
private logger = new Logger("MatchController");
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Get("swiss/:eventId")
getSwissMatches(
@Param("eventId", ParseUUIDPipe) eventId: string,
@@ -39,7 +40,7 @@ export class MatchController {
);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Put("swiss/:eventId")
async startSwissMatches(
@Param("eventId", ParseUUIDPipe) eventId: string,
@@ -57,7 +58,7 @@ export class MatchController {
return await this.matchService.createNextSwissMatches(eventId);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Put("tournament/:eventId")
async startTournamentMatches(
@Param("eventId", ParseUUIDPipe) eventId: string,
@@ -75,7 +76,7 @@ export class MatchController {
return this.matchService.getTournamentTeamCount(eventId);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Get("tournament/:eventId")
getTournamentMatches(
@Param("eventId", ParseUUIDPipe) eventId: string,
@@ -89,7 +90,7 @@ export class MatchController {
);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Get("queue/:eventId")
async getQueueMatches(
@Param("eventId", ParseUUIDPipe) eventId: string,
@@ -98,7 +99,7 @@ export class MatchController {
return await this.matchService.getQueueMatches(eventId, userId);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Get("queue/:eventId/admin")
async getAllQueueMatches(
@Param("eventId", ParseUUIDPipe) eventId: string,
@@ -111,7 +112,7 @@ export class MatchController {
return await this.matchService.getAllQueueMatches(eventId);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Get("logs/:matchId")
async getMatchLogs(
@Param("matchId", ParseUUIDPipe) matchId: string,
@@ -125,7 +126,7 @@ export class MatchController {
return logs;
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Put("reveal/:matchId")
async revealMatch(
@Param("matchId", ParseUUIDPipe) matchId: string,
@@ -155,7 +156,7 @@ export class MatchController {
return await this.matchService.getMatchById(matchId);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Get("queue/:eventId/timeseries")
async getQueueMatchesTimeSeries(
@Param("eventId") eventId: string,
diff --git a/api/src/team/team.controller.ts b/api/src/team/team.controller.ts
index 430945ed..b91ad9c3 100644
--- a/api/src/team/team.controller.ts
+++ b/api/src/team/team.controller.ts
@@ -17,7 +17,6 @@ import { CreateTeamDto } from "./dtos/createTeamDto";
import { InviteUserDto } from "./dtos/inviteUserDto";
import { UserService } from "../user/user.service";
import { EventService } from "../event/event.service";
-import { UserGuard } from "../guards/UserGuard";
import { PermissionRole } from "../user/entities/user.entity";
import {
EventId,
@@ -28,6 +27,7 @@ import {
} from "../guards/TeamGuard";
import { EVENT_ID_PARAM, TEAM_ID_PARAM } from "../guards/GuardConstants";
import { TeamEntity } from "./entities/team.entity";
+import { JwtAuthGuard } from "../auth/jwt-auth.guard";
@Controller("team")
export class TeamController {
@@ -57,13 +57,13 @@ export class TeamController {
);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Get(`event/:${EVENT_ID_PARAM}/my`)
getMyTeamForEvent(@EventId eventId: string, @UserId("id") userId: string) {
return this.teamService.getTeamOfUserForEvent(eventId, userId);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Post(`event/:${EVENT_ID_PARAM}/create`)
async createTeam(
@UserId() userId: string,
@@ -87,7 +87,7 @@ export class TeamController {
return this.teamService.createTeam(createTeamDto.name, userId, eventId);
}
- @UseGuards(UserGuard, MyTeamGuards, TeamNotLockedGuard)
+ @UseGuards(JwtAuthGuard, MyTeamGuards, TeamNotLockedGuard)
@Put(`event/:${EVENT_ID_PARAM}/leave`)
async leaveTeam(@UserId() userId: string, @Team() team: TeamEntity) {
return this.teamService.leaveTeam(team.id, userId);
@@ -123,7 +123,7 @@ export class TeamController {
});
}
- @UseGuards(UserGuard, MyTeamGuards, TeamNotLockedGuard)
+ @UseGuards(JwtAuthGuard, MyTeamGuards, TeamNotLockedGuard)
@Post(`event/:${EVENT_ID_PARAM}/sendInvite`)
async sendInviteToTeam(
@EventId eventId: string,
@@ -156,7 +156,7 @@ export class TeamController {
);
}
- @UseGuards(UserGuard, MyTeamGuards, TeamNotLockedGuard)
+ @UseGuards(JwtAuthGuard, MyTeamGuards, TeamNotLockedGuard)
@Get(`event/:${EVENT_ID_PARAM}/searchInviteUsers/:searchQuery`)
async searchUsersForInvite(
@EventId eventId: string,
@@ -166,7 +166,7 @@ export class TeamController {
return this.userService.searchUsersForInvite(eventId, searchQuery, team.id);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Get(`event/:${EVENT_ID_PARAM}/pending`)
async getUserPendingInvites(
@UserId() userId: string,
@@ -175,7 +175,7 @@ export class TeamController {
return this.teamService.getTeamsUserIsInvitedTo(userId, eventId);
}
- @UseGuards(UserGuard, TeamNotLockedGuard)
+ @UseGuards(JwtAuthGuard, TeamNotLockedGuard)
@Put(`event/:${EVENT_ID_PARAM}/acceptInvite/:${TEAM_ID_PARAM}`)
async acceptTeamInvite(
@UserId() userId: string,
@@ -193,7 +193,7 @@ export class TeamController {
}
// TODO: eventId is not used here, should be removed
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Delete(`event/:${EVENT_ID_PARAM}/declineInvite/:${TEAM_ID_PARAM}`)
async declineTeamInvite(@UserId() userId: string, @TeamId teamId: string) {
if (!(await this.teamService.isUserInvitedToTeam(userId, teamId)))
@@ -202,7 +202,7 @@ export class TeamController {
return this.teamService.declineTeamInvite(userId, teamId);
}
- @UseGuards(UserGuard, MyTeamGuards, TeamNotLockedGuard)
+ @UseGuards(JwtAuthGuard, MyTeamGuards, TeamNotLockedGuard)
@Put(`event/:${EVENT_ID_PARAM}/queue/join`)
async joinQueue(@Team() team: TeamEntity) {
if (team.inQueue)
@@ -211,7 +211,7 @@ export class TeamController {
return this.teamService.joinQueue(team.id);
}
- @UseGuards(UserGuard, MyTeamGuards)
+ @UseGuards(JwtAuthGuard, MyTeamGuards)
@Get(`event/:${EVENT_ID_PARAM}/queue/state`)
async getQueueState(@Team() team: TeamEntity) {
return this.teamService.getQueueState(team.id);
diff --git a/api/src/user/social-account.controller.ts b/api/src/user/social-account.controller.ts
index fa847319..0384abc6 100644
--- a/api/src/user/social-account.controller.ts
+++ b/api/src/user/social-account.controller.ts
@@ -15,7 +15,8 @@ import {
SocialAccountEntity,
SocialPlatform,
} from "./entities/social-account.entity";
-import { UserGuard, UserId } from "../guards/UserGuard";
+import { UserId } from "../guards/UserGuard";
+import { JwtAuthGuard } from "../auth/jwt-auth.guard";
class LinkSocialAccountDto {
platform: SocialPlatform;
@@ -23,7 +24,7 @@ class LinkSocialAccountDto {
platformUserId: string;
}
-@UseGuards(UserGuard)
+@UseGuards(JwtAuthGuard)
@ApiTags("social-accounts")
@Controller("social-accounts")
export class SocialAccountController {
diff --git a/api/src/user/user.controller.ts b/api/src/user/user.controller.ts
index 97b05c02..3c33bab8 100644
--- a/api/src/user/user.controller.ts
+++ b/api/src/user/user.controller.ts
@@ -10,7 +10,8 @@ import {
} from "@nestjs/common";
import { UserService } from "./user.service";
import { CreateUserDto } from "./dtos/user.dto";
-import { UserGuard, UserId } from "../guards/UserGuard";
+import { UserId } from "../guards/UserGuard";
+import { JwtAuthGuard } from "../auth/jwt-auth.guard";
@Controller("user")
export class UserController {
@@ -44,7 +45,7 @@ export class UserController {
);
}
- @UseGuards(UserGuard)
+ @UseGuards(JwtAuthGuard)
@Get("canCreateEvent")
async canCreateEvent(@UserId() id: string) {
return this.userService.canCreateEvent(id);
From 187b6b082f3ba07690d879d9a8daa9fdd164ee69 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sat, 1 Nov 2025 17:00:32 +0100
Subject: [PATCH 05/29] migrate authentication to backend with JWT and
CredentialsProvider
---
frontend/app/utils/authOptions.ts | 124 ++++++++++++++++--------------
1 file changed, 67 insertions(+), 57 deletions(-)
diff --git a/frontend/app/utils/authOptions.ts b/frontend/app/utils/authOptions.ts
index 0198a902..8e2df078 100644
--- a/frontend/app/utils/authOptions.ts
+++ b/frontend/app/utils/authOptions.ts
@@ -1,71 +1,81 @@
import { NextAuthOptions } from "next-auth";
-import GithubProvider from "next-auth/providers/github";
-import axiosInstance from "@/app/actions/axios";
+import CredentialsProvider from "next-auth/providers/credentials";
+
+const BACKEND_BASE_URL =
+ process.env.BACKEND_URL || process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL;
export const authOptions: NextAuthOptions = {
+ session: {
+ strategy: "jwt",
+ },
providers: [
- GithubProvider({
- clientId: process.env.CLIENT_ID_GITHUB!,
- clientSecret: process.env.CLIENT_SECRET_GITHUB!,
- authorization: {
- params: {
- scope: "read:user user:email repo:invite",
- },
+ CredentialsProvider({
+ id: "backend",
+ name: "Backend",
+ credentials: {},
+ async authorize(_credentials, req) {
+ try {
+ if (!BACKEND_BASE_URL) {
+ console.error("Missing BACKEND URL env");
+ return null;
+ }
+
+ const cookieHeader = req.headers?.cookie || "";
+
+ // Try to extract the JWT set by the backend from cookies to also send as Bearer
+ const match = cookieHeader.match(/(?:^|; )token=([^;]+)/);
+ const token = match ? decodeURIComponent(match[1]) : undefined;
+
+ const res = await fetch(`${BACKEND_BASE_URL}/auth/me`, {
+ method: "GET",
+ headers: {
+ // Forward cookies so backend can read its own cookie if configured for shared domain
+ cookie: cookieHeader,
+ // Also send as Bearer since JwtStrategy extracts from Authorization header
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ Accept: "application/json",
+ },
+ });
+
+ if (!res.ok) return null;
+
+ const user = await res.json();
+ if (!user?.id) return null;
+
+ return {
+ id: user.id,
+ name: user.name || user.username,
+ email: user.email,
+ image: user.profilePicture,
+ } as any;
+ } catch (e) {
+ console.error("Authorize failed:", e);
+ return null;
+ }
},
}),
],
callbacks: {
- async signIn({ user, account, profile }) {
- if (account?.provider === "github") {
- const githubProfile = profile as any;
-
- try {
- const existingUser = (
- await axiosInstance.get(`user/github/${account.providerAccountId}`)
- ).data;
- if (!existingUser) {
- if (!account?.access_token) {
- throw new Error("No access token found");
- }
-
- await axiosInstance.post(`user/`, {
- email: user.email!,
- username: githubProfile?.login || user.name!,
- name: user.name! || githubProfile?.name!,
- profilePicture: user.image! || githubProfile?.avatar_url!,
- githubId: account.providerAccountId,
- githubAccessToken: account.access_token,
- });
- } else {
- await axiosInstance.put(`user/${existingUser.id}`, {
- email: user.email!,
- username: githubProfile?.login || existingUser.username,
- name: githubProfile?.name || existingUser.name,
- profilePicture:
- githubProfile?.avatar_url || existingUser.profilePicture,
- githubId: account.providerAccountId,
- githubAccessToken: account.access_token,
- });
- }
- } catch (e: any) {
- console.error("Error during sign in:", e);
- console.debug("response:", e?.response);
- return false;
- }
- }
-
+ async signIn() {
return true;
},
- async session({ session }) {
- if (!session.user?.email) {
- throw new Error("User email is not available in session");
+ async jwt({ token, user }) {
+ if (user) {
+ token.sub = (user as any).id || token.sub;
+ token.name = user.name || token.name;
+ token.email = user.email || token.email;
+ (token as any).profilePicture = (user as any).profilePicture;
}
- const dbUser = (
- await axiosInstance.get(`user/email/${session.user?.email}`)
- ).data;
-
- if (dbUser) session.user.id = dbUser.id;
-
+ return token;
+ },
+ async session({ session, token }) {
+ if (!session.user) session.user = { id: "", email: "", name: "" } as any;
+ (session.user as any).id =
+ (token.sub as string) || (session.user as any).id;
+ if (token.email) session.user.email = token.email as string;
+ if (token.name) session.user.name = token.name as string;
+ if ((token as any).profilePicture)
+ session.user.profilePicture = (token as any).profilePicture as string;
return session;
},
},
From 459129716fadce09fcc89305e29c6190810d696c Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sat, 1 Nov 2025 17:00:50 +0100
Subject: [PATCH 06/29] update axios instance to support dynamic backend URL
and include token in request headers
---
frontend/app/actions/axios.ts | 32 +++++++++++++++++++++-----------
1 file changed, 21 insertions(+), 11 deletions(-)
diff --git a/frontend/app/actions/axios.ts b/frontend/app/actions/axios.ts
index 3b7368a2..8dd418c5 100644
--- a/frontend/app/actions/axios.ts
+++ b/frontend/app/actions/axios.ts
@@ -1,31 +1,41 @@
import axios, { AxiosResponse } from "axios";
-import { getServerSession } from "next-auth/next";
-import { authOptions } from "@/app/utils/authOptions";
import { ServerActionResponse } from "@/app/actions/errors";
const axiosInstance = axios.create({
- baseURL: process.env.BACKEND_URL,
+ baseURL:
+ process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL || process.env.BACKEND_URL,
headers: {
"Content-Type": "application/json",
},
});
+function getTokenFromCookieString(cookieString: string): string | null {
+ const parts = cookieString.split(";");
+ for (const part of parts) {
+ const [k, ...rest] = part.trim().split("=");
+ if (k === "token") return decodeURIComponent(rest.join("="));
+ }
+ return null;
+}
+
axiosInstance.interceptors.request.use(
async (config) => {
- if (!process.env.BACKEND_SECRET) {
- config.baseURL = process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL;
- return config;
- }
+ if (process.env.BACKEND_URL) {
+ const cookieData = await require("next/headers").cookies();
+ const token = cookieData.get("token");
+ if (token) config.headers.Authorization = `Bearer ${token.value}`;
- if (config.url && config.url.startsWith("user/email/")) {
+ config.baseURL = process.env.BACKEND_URL;
return config;
}
- const session = await getServerSession(authOptions);
- config.headers.userId = session?.user?.id || "";
+
+ config.baseURL = process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL;
+ config.headers.Authorization = getTokenFromCookieString(
+ document.cookie || "",
+ );
return config;
},
(error) => {
- // Handle request errors
return Promise.reject(error);
},
);
From 4c5797c26006bd28b304d544df37f0dd2f5a0f20 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sat, 1 Nov 2025 17:01:32 +0100
Subject: [PATCH 07/29] refactor GitHub login flow to redirect to backend
authentication endpoint
---
api/src/guards/GuardConstants.ts | 1 -
frontend/components/github.tsx | 9 +++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/api/src/guards/GuardConstants.ts b/api/src/guards/GuardConstants.ts
index bf52bf6c..72e55cc9 100644
--- a/api/src/guards/GuardConstants.ts
+++ b/api/src/guards/GuardConstants.ts
@@ -1,3 +1,2 @@
-export const USER_ID_KEY = "userid";
export const EVENT_ID_PARAM = "eventId";
export const TEAM_ID_PARAM = "teamId";
diff --git a/frontend/components/github.tsx b/frontend/components/github.tsx
index 1f31887e..7b9b8cfb 100644
--- a/frontend/components/github.tsx
+++ b/frontend/components/github.tsx
@@ -1,13 +1,14 @@
import { Button } from "@heroui/react";
import { GithubIcon } from "./icons";
-import { signIn } from "next-auth/react";
export default function GithubLoginButton() {
- async function githubLogin() {
+ function githubLogin() {
try {
- await signIn("github");
+ const base =
+ process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL || process.env.BACKEND_URL;
+ window.location.href = `${base?.replace(/\/$/, "")}/auth/github/callback`;
} catch (error) {
- console.log("error while logging in:", error);
+ console.log("error while redirecting to login:", error);
}
}
From 4c6ef6334c44d5b46e816cc41ebd433d5699283d Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sat, 1 Nov 2025 17:01:40 +0100
Subject: [PATCH 08/29] update user type definition to replace optional image
field with required profilePicture
---
frontend/types/next-auth.d.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/types/next-auth.d.ts b/frontend/types/next-auth.d.ts
index 360ad258..99f6c1e4 100644
--- a/frontend/types/next-auth.d.ts
+++ b/frontend/types/next-auth.d.ts
@@ -6,7 +6,7 @@ declare module "next-auth" {
id: string;
email: string;
name: string;
- image?: string;
+ profilePicture: string;
};
}
}
From ca7f7448bb7a63e323189f33fc384ba100027e31 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sat, 1 Nov 2025 17:02:05 +0100
Subject: [PATCH 09/29] refactor authentication flow to utilize UserService for
fetching user details and update SSO page for seamless sign-in experience
---
api/src/auth/auth.controller.ts | 5 ++++-
api/src/guards/TeamGuard.ts | 4 ++--
frontend/app/auth/sso/page.tsx | 35 +++++++++++++++++++++++++++++++++
frontend/app/events/page.tsx | 4 ++--
4 files changed, 43 insertions(+), 5 deletions(-)
create mode 100644 frontend/app/auth/sso/page.tsx
diff --git a/api/src/auth/auth.controller.ts b/api/src/auth/auth.controller.ts
index 3fcc9c62..65f57562 100644
--- a/api/src/auth/auth.controller.ts
+++ b/api/src/auth/auth.controller.ts
@@ -13,12 +13,14 @@ import { Response, Request } from "express";
import { AuthService } from "./auth.service";
import { JwtAuthGuard } from "./jwt-auth.guard";
import { ConfigService } from "@nestjs/config";
+import { UserService } from "../user/user.service";
@Controller("auth")
export class AuthController {
constructor(
private auth: AuthService,
private configService: ConfigService,
+ private userService: UserService
) {}
@UseGuards(JwtAuthGuard)
@@ -54,6 +56,7 @@ export class AuthController {
@Get("/me")
@UseGuards(JwtAuthGuard)
me(@Req() req: Request) {
- return (req as any).user;
+ const user: any = (req as any).user;
+ return this.userService.getUserById(user.id);
}
}
diff --git a/api/src/guards/TeamGuard.ts b/api/src/guards/TeamGuard.ts
index 01e40656..1db0d5d5 100644
--- a/api/src/guards/TeamGuard.ts
+++ b/api/src/guards/TeamGuard.ts
@@ -9,7 +9,7 @@ import {
ParseUUIDPipe,
} from "@nestjs/common";
import { TeamService } from "../team/team.service";
-import { EVENT_ID_PARAM, TEAM_ID_PARAM, USER_ID_KEY } from "./GuardConstants";
+import { EVENT_ID_PARAM, TEAM_ID_PARAM } from "./GuardConstants";
import { TeamEntity } from "../team/entities/team.entity";
export const TeamId = Param(TEAM_ID_PARAM, new ParseUUIDPipe());
@@ -40,7 +40,7 @@ export class MyTeamGuards implements CanActivate {
async canActivate(context: ExecutionContext): Promise {
const request = context.switchToHttp().getRequest();
const eventId: string | undefined = request.params[EVENT_ID_PARAM];
- const userId: string | undefined = request.headers[USER_ID_KEY];
+ const userId: string | undefined = request.user?.id;
if (!eventId || !userId) return false;
diff --git a/frontend/app/auth/sso/page.tsx b/frontend/app/auth/sso/page.tsx
new file mode 100644
index 00000000..d85773ba
--- /dev/null
+++ b/frontend/app/auth/sso/page.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import { useEffect } from "react";
+import { signIn } from "next-auth/react";
+import { useRouter } from "next/navigation";
+
+export default function SsoPage() {
+ const router = useRouter();
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ await signIn("backend", { redirect: false });
+ if (!cancelled) {
+ router.replace("/");
+ router.refresh();
+ }
+ } catch (e) {
+ console.error("Failed to finalize SSO:", e);
+ if (!cancelled) router.replace("/auth/error");
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [router]);
+ return (
+
+
+
+
Finishing sign-in...
+
+
+ );
+}
diff --git a/frontend/app/events/page.tsx b/frontend/app/events/page.tsx
index 67db28ce..52a3e826 100644
--- a/frontend/app/events/page.tsx
+++ b/frontend/app/events/page.tsx
@@ -50,9 +50,9 @@ export default async function EventsPage() {
Discover and join upcoming coding competitions
{canCreate && (
-
+
)}
From 104b27c4efa49adba3af6bb0fe126b7102177b35 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sat, 1 Nov 2025 17:02:36 +0100
Subject: [PATCH 10/29] prettier
---
api/src/auth/auth.controller.ts | 4 ++--
api/src/auth/auth.service.ts | 6 +++++-
api/src/auth/jwt-auth.guard.ts | 2 +-
api/src/auth/jwt.strategy.ts | 5 ++++-
api/src/guards/UserGuard.ts | 7 ++-----
api/src/main.ts | 1 -
6 files changed, 14 insertions(+), 11 deletions(-)
diff --git a/api/src/auth/auth.controller.ts b/api/src/auth/auth.controller.ts
index 65f57562..19ce67d8 100644
--- a/api/src/auth/auth.controller.ts
+++ b/api/src/auth/auth.controller.ts
@@ -20,7 +20,7 @@ export class AuthController {
constructor(
private auth: AuthService,
private configService: ConfigService,
- private userService: UserService
+ private userService: UserService,
) {}
@UseGuards(JwtAuthGuard)
@@ -46,7 +46,7 @@ export class AuthController {
httpOnly: true,
secure: true,
sameSite: "none",
- maxAge: 7 * 24 * 60 * 60 * 1000
+ maxAge: 7 * 24 * 60 * 60 * 1000,
});
return res.redirect(redirectUrl);
}
diff --git a/api/src/auth/auth.service.ts b/api/src/auth/auth.service.ts
index 966d3f38..63097206 100644
--- a/api/src/auth/auth.service.ts
+++ b/api/src/auth/auth.service.ts
@@ -7,7 +7,11 @@ export class AuthService {
constructor(private jwt: JwtService) {}
signToken(user: UserEntity) {
- const payload = { sub: user.id, email: user.email, username: user.username };
+ const payload = {
+ sub: user.id,
+ email: user.email,
+ username: user.username,
+ };
return this.jwt.sign(payload);
}
}
diff --git a/api/src/auth/jwt-auth.guard.ts b/api/src/auth/jwt-auth.guard.ts
index 5a704e72..2e81dba6 100644
--- a/api/src/auth/jwt-auth.guard.ts
+++ b/api/src/auth/jwt-auth.guard.ts
@@ -2,4 +2,4 @@ import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
-export class JwtAuthGuard extends AuthGuard('jwt') {}
+export class JwtAuthGuard extends AuthGuard("jwt") {}
diff --git a/api/src/auth/jwt.strategy.ts b/api/src/auth/jwt.strategy.ts
index c94b4704..250dd2ba 100644
--- a/api/src/auth/jwt.strategy.ts
+++ b/api/src/auth/jwt.strategy.ts
@@ -12,7 +12,10 @@ export interface JwtPayload {
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
- constructor(private config: ConfigService, private users: UserService) {
+ constructor(
+ private config: ConfigService,
+ private users: UserService,
+ ) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
diff --git a/api/src/guards/UserGuard.ts b/api/src/guards/UserGuard.ts
index 25ba6cb8..00ea4d20 100644
--- a/api/src/guards/UserGuard.ts
+++ b/api/src/guards/UserGuard.ts
@@ -1,11 +1,8 @@
-import {
- createParamDecorator,
- ExecutionContext,
-} from "@nestjs/common";
+import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const UserId = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user?.id;
},
-);
\ No newline at end of file
+);
diff --git a/api/src/main.ts b/api/src/main.ts
index bdacba07..99ec37e3 100644
--- a/api/src/main.ts
+++ b/api/src/main.ts
@@ -38,7 +38,6 @@ async function bootstrap() {
"Content-Type, Accept, Authorization, X-Requested-With, X-HTTP-Method-Override, X-Auth-Token, X-Refresh-Token",
});
-
app.connectMicroservice(
getRabbitmqConfig(configService, "game_results"),
);
From 1d94eeb537eeba952f14fd182118265ed1590c8d Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 21:05:03 +0100
Subject: [PATCH 11/29] update exmaple .env files
---
api/.env.example | 13 +++++++++++++
frontend/.env.example | 9 ---------
2 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/api/.env.example b/api/.env.example
index 1a398cbf..9a68457c 100644
--- a/api/.env.example
+++ b/api/.env.example
@@ -8,3 +8,16 @@ FRONTEND_SECRET=FJIOERJGOERHGOIREHGIREHGROIEHGEORIGHOIOI3HRIO3232
API_SECRET_ENCRYPTION_KEY=c5o43icm5i43jio5j34p5
RABBITMQ_URL=amqp://guest:guest@localhost:5672
K8S_SERVICE_URL=http://localhost:9000
+
+
+JWT_SECRET=gerg43g3g34g
+GITHUB_CLIENT_ID=
+GITHUB_CLIENT_SECRET=
+GITHUB_CALLBACK_URL=http://localhost:4000/auth/github/callback
+OAUTH_SUCCESS_REDIRECT_URL=http://localhost:3000/auth/sso
+42_OAUTH_SUCCESS_REDIRECT_URL=http://localhost:3000/profile
+CORS_ORIGIN=http://localhost:3000
+
+FORTYTWO_CALLBACK_URL=http://localhost:4000/auth/42/callback
+FORTYTWO_CLIENT_ID=
+FORTYTWO_CLIENT_SECRET=
\ No newline at end of file
diff --git a/frontend/.env.example b/frontend/.env.example
index 156a54a4..0419ea47 100644
--- a/frontend/.env.example
+++ b/frontend/.env.example
@@ -1,17 +1,8 @@
# Authentication Settings
-# GitHub Authentication (Required for website sign-ins)
-# These are used for user login with NextAuth.js
-CLIENT_ID_GITHUB=xxx
-CLIENT_SECRET_GITHUB=xxx
NEXTAUTH_SECRET=ci5u439cnu59843uc53894
NEXTAUTH_URL=http://localhost:3000
-# 42 Intra Authentication (Optional for testing 42 account linking)
-# Only needed if you want to enable 42 school login
-NEXT_PUBLIC_FORTY_TWO_CLIENT_ID=xx
-FORTY_TWO_CLIENT_SECRET=xxx
-
# Backend Connection
From bc17b73d8b7cda71ea0487f9931c10ad5544a4ce Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 21:05:44 +0100
Subject: [PATCH 12/29] implement 42 OAuth authentication flow with callback
handling
---
api/src/auth/auth.controller.ts | 64 ++++++++++++++++++++++++++++++++-
1 file changed, 63 insertions(+), 1 deletion(-)
diff --git a/api/src/auth/auth.controller.ts b/api/src/auth/auth.controller.ts
index 19ce67d8..502edd6a 100644
--- a/api/src/auth/auth.controller.ts
+++ b/api/src/auth/auth.controller.ts
@@ -4,16 +4,21 @@ import {
Controller,
Get,
Post,
+ Query,
Req,
Res,
UseGuards,
} from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
-import { Response, Request } from "express";
+import { Request, Response } from "express";
import { AuthService } from "./auth.service";
import { JwtAuthGuard } from "./jwt-auth.guard";
import { ConfigService } from "@nestjs/config";
import { UserService } from "../user/user.service";
+import { UserId } from "../guards/UserGuard";
+import * as CryptoJS from "crypto-js";
+import { SocialAccountService } from "../user/social-account.service";
+import { SocialPlatform } from "../user/entities/social-account.entity";
@Controller("auth")
export class AuthController {
@@ -21,6 +26,7 @@ export class AuthController {
private auth: AuthService,
private configService: ConfigService,
private userService: UserService,
+ private socialAccountService: SocialAccountService,
) {}
@UseGuards(JwtAuthGuard)
@@ -53,6 +59,62 @@ export class AuthController {
return res.json({ token });
}
+ @Get("/42/getUrl")
+ @UseGuards(JwtAuthGuard)
+ getFortyTwoAuthUrl(@UserId() userId: string) {
+ const encryptedUserId = CryptoJS.AES.encrypt(
+ userId,
+ this.configService.getOrThrow("API_SECRET_ENCRYPTION_KEY"),
+ ).toString();
+
+ const base64EncodedEncryptedUserId = Buffer.from(encryptedUserId).toString("base64");
+
+ return `https://api.intra.42.fr/oauth/authorize?client_id=${this.configService.getOrThrow("FORTYTWO_CLIENT_ID")}&redirect_uri=${encodeURIComponent(this.configService.getOrThrow("FORTYTWO_CALLBACK_URL"))}&response_type=code&state=${base64EncodedEncryptedUserId}`;
+ }
+
+ @Get("/42/callback")
+ @UseGuards(AuthGuard("42"))
+ async fortyTwoCallback(
+ @Req()
+ request: Request & {
+ user: {
+ fortyTwoAccount: {
+ platformUserId: string;
+ username: string;
+ email: string;
+ };
+ }
+ },
+ @Res() res: Response,
+ @Query("state") encryptedUserId: string,
+ ) {
+ try{
+ const base64DecodedEncryptedUserId = Buffer.from(encryptedUserId, "base64").toString("utf-8");
+
+ const userId = CryptoJS.AES.decrypt(
+ base64DecodedEncryptedUserId,
+ this.configService.getOrThrow("API_SECRET_ENCRYPTION_KEY"),
+ ).toString(CryptoJS.enc.Utf8);
+ if (!userId) throw new BadRequestException("Invalid state parameter.");
+
+ await this.socialAccountService.upsertSocialAccountForUser({
+ userId,
+ platform: SocialPlatform.FORTYTWO,
+ platformUserId: request.user.fortyTwoAccount.platformUserId,
+ username: request.user.fortyTwoAccount.username,
+ });
+
+ const redirectUrl = this.configService.getOrThrow(
+ "42_OAUTH_SUCCESS_REDIRECT_URL",
+ );
+
+ return res.redirect(redirectUrl);
+ }catch(e){
+ console.log("Error in FortyTwo callback: ", e)
+ throw new BadRequestException("Invalid state parameter.");
+ }
+ }
+
@Get("/me")
@UseGuards(JwtAuthGuard)
me(@Req() req: Request) {
From b2652ecb4869a7006ea626e8bbff62764f9495bf Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 21:05:53 +0100
Subject: [PATCH 13/29] implement 42 OAuth authentication flow with callback
handling
---
api/src/auth/auth.module.ts | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts
index 296fc574..14369eac 100644
--- a/api/src/auth/auth.module.ts
+++ b/api/src/auth/auth.module.ts
@@ -6,6 +6,7 @@ import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
import { JwtStrategy } from "./jwt.strategy";
import { GithubOAuthStrategy } from "./github.strategy";
+import { FortyTwoOAuthStrategy } from "./fortytwo.strategy";
import { UserModule } from "../user/user.module";
@Module({
@@ -24,7 +25,12 @@ import { UserModule } from "../user/user.module";
}),
}),
],
- providers: [AuthService, JwtStrategy, GithubOAuthStrategy],
+ providers: [
+ AuthService,
+ JwtStrategy,
+ GithubOAuthStrategy,
+ FortyTwoOAuthStrategy,
+ ],
controllers: [AuthController],
exports: [PassportModule, JwtModule],
})
From 1ea1ae6443639d1874ff241c853520a621a4cfb2 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 21:06:13 +0100
Subject: [PATCH 14/29] refactor authentication flow to use axios for API calls
---
frontend/app/utils/authOptions.ts | 63 ++++++++++++++-----------------
1 file changed, 29 insertions(+), 34 deletions(-)
diff --git a/frontend/app/utils/authOptions.ts b/frontend/app/utils/authOptions.ts
index 8e2df078..b8d2a5a9 100644
--- a/frontend/app/utils/authOptions.ts
+++ b/frontend/app/utils/authOptions.ts
@@ -1,5 +1,7 @@
import { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
+import axiosInstance from "@/app/actions/axios";
+import { redirect } from "next/navigation";
const BACKEND_BASE_URL =
process.env.BACKEND_URL || process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL;
@@ -20,34 +22,19 @@ export const authOptions: NextAuthOptions = {
return null;
}
- const cookieHeader = req.headers?.cookie || "";
-
- // Try to extract the JWT set by the backend from cookies to also send as Bearer
- const match = cookieHeader.match(/(?:^|; )token=([^;]+)/);
- const token = match ? decodeURIComponent(match[1]) : undefined;
-
- const res = await fetch(`${BACKEND_BASE_URL}/auth/me`, {
- method: "GET",
- headers: {
- // Forward cookies so backend can read its own cookie if configured for shared domain
- cookie: cookieHeader,
- // Also send as Bearer since JwtStrategy extracts from Authorization header
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
- Accept: "application/json",
- },
- });
-
- if (!res.ok) return null;
-
- const user = await res.json();
- if (!user?.id) return null;
+ const res = await axiosInstance.get<{
+ id: string;
+ username: string;
+ email: string;
+ profilePicture: string;
+ }>(`/auth/me`);
return {
- id: user.id,
- name: user.name || user.username,
- email: user.email,
- image: user.profilePicture,
- } as any;
+ id: res.data.id,
+ name: res.data.username,
+ email: res.data.email,
+ image: res.data.profilePicture,
+ };
} catch (e) {
console.error("Authorize failed:", e);
return null;
@@ -64,18 +51,26 @@ export const authOptions: NextAuthOptions = {
token.sub = (user as any).id || token.sub;
token.name = user.name || token.name;
token.email = user.email || token.email;
- (token as any).profilePicture = (user as any).profilePicture;
}
return token;
},
async session({ session, token }) {
- if (!session.user) session.user = { id: "", email: "", name: "" } as any;
- (session.user as any).id =
- (token.sub as string) || (session.user as any).id;
- if (token.email) session.user.email = token.email as string;
- if (token.name) session.user.name = token.name as string;
- if ((token as any).profilePicture)
- session.user.profilePicture = (token as any).profilePicture as string;
+ try {
+ const res = await axiosInstance.get<{
+ id: string;
+ username: string;
+ email: string;
+ profilePicture: string;
+ }>(`/auth/me`);
+
+ session.user.id = res.data.id;
+ session.user.email = res.data.email;
+ session.user.name = res.data.username;
+ session.user.profilePicture = res.data.profilePicture;
+ } catch {
+ session.user.id = "";
+ }
+
return session;
},
},
From 6758e7a556187990892ddd51abbc685b9a83a159 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 21:07:08 +0100
Subject: [PATCH 15/29] implement 42 OAuth strategy for user authentication
---
api/package.json | 1 +
api/src/auth/fortytwo.strategy.ts | 64 ++++++++++++++++++++++++++++++
api/src/types/passport-oauth2.d.ts | 16 ++++++++
3 files changed, 81 insertions(+)
create mode 100644 api/src/auth/fortytwo.strategy.ts
create mode 100644 api/src/types/passport-oauth2.d.ts
diff --git a/api/package.json b/api/package.json
index 45f250f5..454f3dfa 100644
--- a/api/package.json
+++ b/api/package.json
@@ -47,6 +47,7 @@
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-jwt": "^4.0.1",
+ "passport-oauth2": "^1.8.0",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
diff --git a/api/src/auth/fortytwo.strategy.ts b/api/src/auth/fortytwo.strategy.ts
new file mode 100644
index 00000000..64fb5927
--- /dev/null
+++ b/api/src/auth/fortytwo.strategy.ts
@@ -0,0 +1,64 @@
+import { Injectable } from "@nestjs/common";
+import { PassportStrategy } from "@nestjs/passport";
+import { ConfigService } from "@nestjs/config";
+import { UserService } from "../user/user.service";
+import { SocialAccountService } from "../user/social-account.service";
+import { Strategy } from "passport-oauth2";
+
+interface FortyTwoProfile {
+ id: number;
+ login: string;
+ email: string | null;
+ image_url: string | null;
+ displayname: string | null;
+}
+
+@Injectable()
+export class FortyTwoOAuthStrategy extends PassportStrategy(Strategy, "42") {
+ constructor(
+ config: ConfigService,
+ private readonly users: UserService,
+ private readonly socialAccounts: SocialAccountService,
+ ) {
+ super({
+ authorizationURL: "https://api.intra.42.fr/oauth/authorize",
+ tokenURL: "https://api.intra.42.fr/oauth/token",
+ clientID: config.getOrThrow("FORTYTWO_CLIENT_ID"),
+ clientSecret: config.getOrThrow("FORTYTWO_CLIENT_SECRET"),
+ callbackURL: config.getOrThrow("FORTYTWO_CALLBACK_URL"),
+ });
+ }
+
+ async validate(
+ accessToken: string,
+ refreshToken: string,
+ _params: any,
+ done: (err: any, user?: any) => void,
+ ) {
+ try {
+ const res = await fetch("https://api.intra.42.fr/v2/me", {
+ headers: { Authorization: `Bearer ${accessToken}` },
+ });
+
+ if (!res.ok) {
+ console.error("42 API error:", res.status, await res.text());
+ return done(new Error(`42 API error: ${res.status}`));
+ }
+ const data = (await res.json()) as FortyTwoProfile;
+
+ const platformUserId = String(data.id);
+ const username = data.login;
+ const email = data.email ?? undefined;
+
+ done(null, {
+ fortyTwoAccount: {
+ platformUserId,
+ username,
+ email,
+ }
+ });
+ } catch (err) {
+ done(err, undefined);
+ }
+ }
+}
diff --git a/api/src/types/passport-oauth2.d.ts b/api/src/types/passport-oauth2.d.ts
new file mode 100644
index 00000000..015a2c9a
--- /dev/null
+++ b/api/src/types/passport-oauth2.d.ts
@@ -0,0 +1,16 @@
+declare module "passport-oauth2" {
+ import { Strategy as PassportStrategy } from "passport";
+
+ export interface StrategyOptions {
+ authorizationURL: string;
+ tokenURL: string;
+ clientID: string;
+ clientSecret: string;
+ callbackURL: string;
+ }
+
+ export class Strategy extends PassportStrategy {
+ constructor(options: StrategyOptions, verify?: any);
+ }
+}
+
From e20db1086faed85679699c6695c97f44b0e4e433 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 21:09:06 +0100
Subject: [PATCH 16/29] migrate 42 OAuth authentication to backend and refactor
social account linking
---
api/pnpm-lock.yaml | 3 +
api/src/user/social-account.controller.ts | 22 -------
api/src/user/social-account.service.ts | 73 ++++++++---------------
frontend/app/actions/social-accounts.ts | 12 ++++
frontend/hooks/use42Linking.ts | 61 +++++++------------
5 files changed, 61 insertions(+), 110 deletions(-)
diff --git a/api/pnpm-lock.yaml b/api/pnpm-lock.yaml
index 474ad1ab..94d0bfc1 100644
--- a/api/pnpm-lock.yaml
+++ b/api/pnpm-lock.yaml
@@ -68,6 +68,9 @@ importers:
passport-jwt:
specifier: ^4.0.1
version: 4.0.1
+ passport-oauth2:
+ specifier: ^1.8.0
+ version: 1.8.0
pg:
specifier: ^8.16.3
version: 8.16.3
diff --git a/api/src/user/social-account.controller.ts b/api/src/user/social-account.controller.ts
index 0384abc6..95bdbb35 100644
--- a/api/src/user/social-account.controller.ts
+++ b/api/src/user/social-account.controller.ts
@@ -30,28 +30,6 @@ class LinkSocialAccountDto {
export class SocialAccountController {
constructor(private readonly socialAccountService: SocialAccountService) {}
- @Post("link")
- @ApiOperation({ summary: "Link a social account to the authenticated user" })
- @ApiResponse({
- status: 200,
- description: "Social account linked successfully",
- })
- @ApiResponse({
- status: 409,
- description: "Social account already linked to another user",
- })
- async linkSocialAccount(
- @UserId() userId: string,
- @Body() linkDto: LinkSocialAccountDto,
- ): Promise {
- return await this.socialAccountService.linkSocialAccount(
- userId,
- linkDto.platform,
- linkDto.username,
- linkDto.platformUserId,
- );
- }
-
@Delete(":platform")
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
diff --git a/api/src/user/social-account.service.ts b/api/src/user/social-account.service.ts
index 25b65fec..c43c60ba 100644
--- a/api/src/user/social-account.service.ts
+++ b/api/src/user/social-account.service.ts
@@ -20,52 +20,6 @@ export class SocialAccountService {
private userRepository: Repository,
) {}
- async linkSocialAccount(
- userId: string,
- platform: SocialPlatform,
- username: string,
- platformUserId: string,
- ): Promise {
- // Check if user exists
- const user = await this.userRepository.findOne({ where: { id: userId } });
- if (!user) {
- throw new NotFoundException("User not found");
- }
-
- // Check if this platform account is already linked to another user
- const existingAccount = await this.socialAccountRepository.findOne({
- where: { platform, platformUserId },
- });
-
- if (existingAccount && existingAccount.userId !== userId) {
- throw new ConflictException(
- "This social account is already linked to another user",
- );
- }
-
- // Check if user already has an account for this platform
- const existingUserAccount = await this.socialAccountRepository.findOne({
- where: { userId, platform },
- });
-
- if (existingUserAccount) {
- // Update existing account
- existingUserAccount.username = username;
- existingUserAccount.platformUserId = platformUserId;
- return await this.socialAccountRepository.save(existingUserAccount);
- }
-
- // Create new social account link
- const socialAccount = this.socialAccountRepository.create({
- userId,
- platform,
- username,
- platformUserId,
- });
-
- return await this.socialAccountRepository.save(socialAccount);
- }
-
async unlinkSocialAccount(
userId: string,
platform: SocialPlatform,
@@ -95,4 +49,29 @@ export class SocialAccountService {
where: { userId, platform },
});
}
-}
+
+ async upsertSocialAccountForUser(params: {
+ userId: string;
+ platform: SocialPlatform;
+ username: string;
+ platformUserId: string;
+ }): Promise {
+ const existing = await this.socialAccountRepository.findOne({
+ where: { userId: params.userId, platform: params.platform },
+ });
+
+ if (existing) {
+ existing.username = params.username;
+ existing.platformUserId = params.platformUserId;
+ return this.socialAccountRepository.save(existing);
+ }
+
+ const entity = this.socialAccountRepository.create({
+ userId: params.userId,
+ platform: params.platform,
+ username: params.username,
+ platformUserId: params.platformUserId,
+ });
+ return this.socialAccountRepository.save(entity);
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/actions/social-accounts.ts b/frontend/app/actions/social-accounts.ts
index f86600e0..03d3d625 100644
--- a/frontend/app/actions/social-accounts.ts
+++ b/frontend/app/actions/social-accounts.ts
@@ -57,3 +57,15 @@ export async function getSocialAccountByPlatform(
);
}
}
+
+export async function getFortyTwoAuthUrl(): Promise {
+ try {
+ const response = await axiosInstance.get("/auth/42/getUrl");
+ return response.data;
+ } catch (error: any) {
+ console.error("Error fetching 42 auth URL:", error);
+ throw new Error(
+ error.response?.data?.message || "Failed to fetch 42 auth URL",
+ );
+ }
+}
diff --git a/frontend/hooks/use42Linking.ts b/frontend/hooks/use42Linking.ts
index 8ff2534d..7c5df09d 100644
--- a/frontend/hooks/use42Linking.ts
+++ b/frontend/hooks/use42Linking.ts
@@ -1,10 +1,7 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useSearchParams } from "next/navigation";
-import {
- OAUTH_URLS,
- OAUTH_CONFIG,
- OAUTH_PROVIDERS,
-} from "@/lib/constants/oauth";
+import { OAUTH_CONFIG, OAUTH_PROVIDERS } from "@/lib/constants/oauth";
+import { getFortyTwoAuthUrl } from "@/app/actions/social-accounts";
/**
* Simplified hook for handling 42 School OAuth integration
@@ -43,49 +40,31 @@ export function use42Linking(onSuccess?: () => void): Use42LinkingReturn {
}, []);
const initiate42OAuth = useCallback(() => {
- // Clear any previous messages and show immediate loading feedback
setMessage(null);
setIsInitiating(true);
processedRef.current = null;
- // Check if environment variable is available
- const clientId = process.env.NEXT_PUBLIC_FORTY_TWO_CLIENT_ID;
- if (!clientId) {
- console.error("NEXT_PUBLIC_FORTY_TWO_CLIENT_ID is not set");
- setMessage({
- type: "error",
- text: "OAuth configuration is missing. Please contact support.",
- });
- setIsInitiating(false);
- return;
- }
-
// Small delay to show the loading state before redirect
- setTimeout(() => {
- // Generate a cryptographically secure random state string
- const array = new Uint8Array(OAUTH_CONFIG.STATE_LENGTH);
- window.crypto.getRandomValues(array);
- const state = Array.from(array, (b) =>
- b.toString(16).padStart(2, "0"),
- ).join("");
- const authUrl = new URL(OAUTH_URLS.FORTY_TWO_AUTHORIZE);
+ setTimeout(async () => {
+ try {
+ // Ask backend for the 42 auth URL (which includes the encrypted user id in state)
+ const authUrl = await getFortyTwoAuthUrl();
- authUrl.searchParams.set("client_id", clientId);
- authUrl.searchParams.set(
- "redirect_uri",
- `${window.location.origin}/auth/callback/${OAUTH_PROVIDERS.FORTY_TWO}`,
- );
- authUrl.searchParams.set("response_type", "code");
- authUrl.searchParams.set("scope", "public");
- authUrl.searchParams.set("state", state);
-
- // Store state in sessionStorage for verification
- sessionStorage.setItem(
- OAUTH_CONFIG.SESSION_STORAGE_KEYS.OAUTH_STATE,
- state,
- );
+ if (!authUrl) {
+ throw new Error("No auth URL returned from backend");
+ }
- window.location.href = authUrl.toString();
+ window.location.href = authUrl;
+ } catch (err: any) {
+ console.error("Failed to initiate 42 OAuth via backend:", err);
+ setMessage({
+ type: "error",
+ text:
+ err?.message ||
+ "Failed to start 42 authentication. Please try again later.",
+ });
+ setIsInitiating(false);
+ }
}, OAUTH_CONFIG.LOADING_DELAY);
}, []);
From ae749584016f948c05b727ea016cb60b2f88daa3 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 21:09:38 +0100
Subject: [PATCH 17/29] extend JWT expiration to 30 days
---
api/src/auth/auth.module.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts
index 14369eac..f3884efc 100644
--- a/api/src/auth/auth.module.ts
+++ b/api/src/auth/auth.module.ts
@@ -20,7 +20,7 @@ import { UserModule } from "../user/user.module";
useFactory: (config: ConfigService) => ({
secret: config.getOrThrow("JWT_SECRET") || "dev-secret",
signOptions: {
- expiresIn: "7d",
+ expiresIn: "30d",
},
}),
}),
From 7747bbde02027ea7c7ca5b65526cf72caa1f5fd2 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 21:10:50 +0100
Subject: [PATCH 18/29] format code
---
api/src/auth/auth.controller.ts | 16 ++++++++++------
api/src/auth/fortytwo.strategy.ts | 2 +-
api/src/types/passport-oauth2.d.ts | 1 -
api/src/user/social-account.service.ts | 3 +--
4 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/api/src/auth/auth.controller.ts b/api/src/auth/auth.controller.ts
index 502edd6a..42e3cdba 100644
--- a/api/src/auth/auth.controller.ts
+++ b/api/src/auth/auth.controller.ts
@@ -67,7 +67,8 @@ export class AuthController {
this.configService.getOrThrow("API_SECRET_ENCRYPTION_KEY"),
).toString();
- const base64EncodedEncryptedUserId = Buffer.from(encryptedUserId).toString("base64");
+ const base64EncodedEncryptedUserId =
+ Buffer.from(encryptedUserId).toString("base64");
return `https://api.intra.42.fr/oauth/authorize?client_id=${this.configService.getOrThrow("FORTYTWO_CLIENT_ID")}&redirect_uri=${encodeURIComponent(this.configService.getOrThrow("FORTYTWO_CALLBACK_URL"))}&response_type=code&state=${base64EncodedEncryptedUserId}`;
}
@@ -83,13 +84,16 @@ export class AuthController {
username: string;
email: string;
};
- }
+ };
},
@Res() res: Response,
@Query("state") encryptedUserId: string,
) {
- try{
- const base64DecodedEncryptedUserId = Buffer.from(encryptedUserId, "base64").toString("utf-8");
+ try {
+ const base64DecodedEncryptedUserId = Buffer.from(
+ encryptedUserId,
+ "base64",
+ ).toString("utf-8");
const userId = CryptoJS.AES.decrypt(
base64DecodedEncryptedUserId,
@@ -109,8 +113,8 @@ export class AuthController {
);
return res.redirect(redirectUrl);
- }catch(e){
- console.log("Error in FortyTwo callback: ", e)
+ } catch (e) {
+ console.log("Error in FortyTwo callback: ", e);
throw new BadRequestException("Invalid state parameter.");
}
}
diff --git a/api/src/auth/fortytwo.strategy.ts b/api/src/auth/fortytwo.strategy.ts
index 64fb5927..76bbc58d 100644
--- a/api/src/auth/fortytwo.strategy.ts
+++ b/api/src/auth/fortytwo.strategy.ts
@@ -55,7 +55,7 @@ export class FortyTwoOAuthStrategy extends PassportStrategy(Strategy, "42") {
platformUserId,
username,
email,
- }
+ },
});
} catch (err) {
done(err, undefined);
diff --git a/api/src/types/passport-oauth2.d.ts b/api/src/types/passport-oauth2.d.ts
index 015a2c9a..a1a5892f 100644
--- a/api/src/types/passport-oauth2.d.ts
+++ b/api/src/types/passport-oauth2.d.ts
@@ -13,4 +13,3 @@ declare module "passport-oauth2" {
constructor(options: StrategyOptions, verify?: any);
}
}
-
diff --git a/api/src/user/social-account.service.ts b/api/src/user/social-account.service.ts
index c43c60ba..37313e62 100644
--- a/api/src/user/social-account.service.ts
+++ b/api/src/user/social-account.service.ts
@@ -1,6 +1,5 @@
import {
Injectable,
- ConflictException,
NotFoundException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
@@ -74,4 +73,4 @@ export class SocialAccountService {
});
return this.socialAccountRepository.save(entity);
}
-}
\ No newline at end of file
+}
From 767924cf5ae277ed01cbda84cb03cbee2e8fa53b Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 21:19:27 +0100
Subject: [PATCH 19/29] update readme
---
api/README.md | 18 ++++++++++++++++++
frontend/README.md | 18 ------------------
2 files changed, 18 insertions(+), 18 deletions(-)
diff --git a/api/README.md b/api/README.md
index 55ba0d0e..38ac903d 100644
--- a/api/README.md
+++ b/api/README.md
@@ -106,3 +106,21 @@ This service runs as both:
* **Microservice** - RabbitMQ message consumer for:
* `game_results` - Match result processing
* `github-service-results` - GitHub operation results
+
+## Environment Variables
+
+### GitHub OAuth (Required)
+
+1. Go to GitHub → Settings → Developer settings → OAuth Apps
+2. Register a new OAuth app
+3. Set Authorization callback URL: `http://localhost:4000/auth/github/callback`
+4. Add Client ID and Secret to `.env.local`
+
+### 42 School OAuth (Optional)
+
+For testing account linking functionality:
+
+1. Go to 42 School → Settings → API → Applications
+2. Create a new application
+3. Set Redirect URI: `http://localhost:4000/auth/42/callback`
+4. Add Client ID and Client Secret to `.env.local`
diff --git a/frontend/README.md b/frontend/README.md
index 263a416f..d4d00ae5 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -44,21 +44,3 @@ brew install pnpm
- **Build:** `pnpm build` (auto-updates wiki and changelog)
- **Start:** `pnpm start`
-
-## Environment Variables
-
-### GitHub OAuth (Required)
-
-1. Go to GitHub → Settings → Developer settings → OAuth Apps
-2. Register a new OAuth app
-3. Set Authorization callback URL: `http://localhost:3000/api/auth/callback`
-4. Add Client ID and Secret to `.env.local`
-
-### 42 School OAuth (Optional)
-
-For testing account linking functionality:
-
-1. Go to 42 School → Settings → API → Applications
-2. Create a new application
-3. Set Redirect URI: `http://localhost:3000/auth/callback/42`
-4. Add Client ID and Client Secret to `.env.local`
From 00ccea1d284b5bb3eb6b38c894a9d1be277c65a2 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 21:20:08 +0100
Subject: [PATCH 20/29] remove unused endpoints
---
api/src/user/user.controller.ts | 33 ---------------------------------
1 file changed, 33 deletions(-)
diff --git a/api/src/user/user.controller.ts b/api/src/user/user.controller.ts
index 3c33bab8..13ec89fd 100644
--- a/api/src/user/user.controller.ts
+++ b/api/src/user/user.controller.ts
@@ -1,15 +1,10 @@
import {
- Body,
Controller,
Get,
Param,
- ParseUUIDPipe,
- Post,
- Put,
UseGuards,
} from "@nestjs/common";
import { UserService } from "./user.service";
-import { CreateUserDto } from "./dtos/user.dto";
import { UserId } from "../guards/UserGuard";
import { JwtAuthGuard } from "../auth/jwt-auth.guard";
@@ -17,34 +12,6 @@ import { JwtAuthGuard } from "../auth/jwt-auth.guard";
export class UserController {
constructor(private readonly userService: UserService) {}
- @Post()
- async createUser(@Body() user: CreateUserDto) {
- return this.userService.createUser(
- user.email,
- user.username,
- user.name,
- user.profilePicture,
- user.githubId,
- user.githubAccessToken,
- );
- }
-
- @Put(":id")
- async updateUser(
- @Body() user: CreateUserDto,
- @Param("id", new ParseUUIDPipe()) id: string,
- ) {
- return this.userService.updateUser(
- id,
- user.email,
- user.username,
- user.name,
- user.profilePicture,
- user.githubId,
- user.githubAccessToken,
- );
- }
-
@UseGuards(JwtAuthGuard)
@Get("canCreateEvent")
async canCreateEvent(@UserId() id: string) {
From 0d7e78f98234f0cefa70860215a8c148d1586efa Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 21:25:48 +0100
Subject: [PATCH 21/29] sync cookie time with jwt token expire time
---
api/src/auth/auth.controller.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/src/auth/auth.controller.ts b/api/src/auth/auth.controller.ts
index 42e3cdba..ffcc5835 100644
--- a/api/src/auth/auth.controller.ts
+++ b/api/src/auth/auth.controller.ts
@@ -52,7 +52,7 @@ export class AuthController {
httpOnly: true,
secure: true,
sameSite: "none",
- maxAge: 7 * 24 * 60 * 60 * 1000,
+ maxAge: 30 * 24 * 60 * 60 * 1000,
});
return res.redirect(redirectUrl);
}
From d9019106c41a68a48865584f7a5211e55c5d7883 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 21:27:12 +0100
Subject: [PATCH 22/29] remove unused code
---
api/src/auth/auth.module.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts
index f3884efc..9dc7effc 100644
--- a/api/src/auth/auth.module.ts
+++ b/api/src/auth/auth.module.ts
@@ -18,7 +18,7 @@ import { UserModule } from "../user/user.module";
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
- secret: config.getOrThrow("JWT_SECRET") || "dev-secret",
+ secret: config.getOrThrow("JWT_SECRET"),
signOptions: {
expiresIn: "30d",
},
From 59e7ff9685f7f92eeffbc15c9dc078ce32d93ffb Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 22:20:52 +0100
Subject: [PATCH 23/29] merge
---
.githooks/pre-commit | 4 ----
frontend/package.json | 5 +++++
2 files changed, 5 insertions(+), 4 deletions(-)
delete mode 100755 .githooks/pre-commit
diff --git a/.githooks/pre-commit b/.githooks/pre-commit
deleted file mode 100755
index b06bebc4..00000000
--- a/.githooks/pre-commit
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-
-# lint & format visualizer
-cd frontend && pnpm exec lint-staged --allow-empty
diff --git a/frontend/package.json b/frontend/package.json
index e13bddb4..5e6d2732 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -83,5 +83,10 @@
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3"
+ },
+ "lint-staged": {
+ "*.{js,jsx,ts,tsx,json,css,scss,md}": [
+ "eslint --fix"
+ ]
}
}
From bfab25f708558afab8814a408238fe28bfcbf4f0 Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 22:21:55 +0100
Subject: [PATCH 24/29] remove unused property
---
frontend/app/utils/authOptions.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/app/utils/authOptions.ts b/frontend/app/utils/authOptions.ts
index 02634dab..33c2f215 100644
--- a/frontend/app/utils/authOptions.ts
+++ b/frontend/app/utils/authOptions.ts
@@ -54,7 +54,7 @@ export const authOptions: NextAuthOptions = {
}
return token;
},
- async session({ session, _ }) {
+ async session({ session }) {
try {
const res = await axiosInstance.get<{
id: string;
From ab0a7938b8f5e380365ca2d85075d92a3d1cadcb Mon Sep 17 00:00:00 2001
From: Emil Ebert
Date: Sun, 16 Nov 2025 22:23:56 +0100
Subject: [PATCH 25/29] fix build
---
frontend/app/profile/ProfileClient.tsx | 2 +-
frontend/app/utils/authOptions.ts | 2 +-
frontend/layouts/basic-navbar.tsx | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/app/profile/ProfileClient.tsx b/frontend/app/profile/ProfileClient.tsx
index bf375bb3..f914c561 100644
--- a/frontend/app/profile/ProfileClient.tsx
+++ b/frontend/app/profile/ProfileClient.tsx
@@ -47,7 +47,7 @@ function ProfileContent() {
name={session!.user?.name}
description={session!.user?.email}
avatarProps={{
- src: session!.user?.image || "/placeholder-avatar.png",
+ src: session!.user?.profilePicture || "/placeholder-avatar.png",
size: "lg",
isBordered: true,
}}
diff --git a/frontend/app/utils/authOptions.ts b/frontend/app/utils/authOptions.ts
index 33c2f215..f7339a7a 100644
--- a/frontend/app/utils/authOptions.ts
+++ b/frontend/app/utils/authOptions.ts
@@ -32,7 +32,7 @@ export const authOptions: NextAuthOptions = {
id: res.data.id,
name: res.data.username,
email: res.data.email,
- image: res.data.profilePicture,
+ profilePicture: res.data.profilePicture,
};
}
catch (e) {
diff --git a/frontend/layouts/basic-navbar.tsx b/frontend/layouts/basic-navbar.tsx
index 32b94f35..7ba79289 100644
--- a/frontend/layouts/basic-navbar.tsx
+++ b/frontend/layouts/basic-navbar.tsx
@@ -190,7 +190,7 @@ export const Navbar = forwardRef(