Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@
"cookie-parser": "^1.4.7",
"crypto-js": "^4.2.0",
"jiti": "^2.6.1",
"nestjs-pino": "^4.6.0",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-jwt": "^4.0.1",
"passport-oauth2": "^1.8.0",
"pg": "^8.20.0",
"pino": "^10.3.1",
"pino-http": "^11.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"tournament-pairings": "^2.0.1",
Expand All @@ -68,15 +71,16 @@
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.6",
"@types/jest": "^30.0.0",
"@types/node": "^25.4.0",
"@types/node": "^25.5.0",
"@types/passport-local": "^1.0.38",
"@types/supertest": "^7.2.0",
"dotenv": "^17.3.1",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"globals": "^17.4.0",
"jest": "^30.2.0",
"jest": "^30.3.0",
"pino-pretty": "^13.1.3",
"prettier": "^3.8.1",
"source-map-support": "^0.5.21",
"supertest": "^7.2.2",
Expand Down
1,622 changes: 938 additions & 684 deletions api/pnpm-lock.yaml

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { DatabaseConfig } from "./DatabaseConfig";
import { GithubApiModule } from "./github-api/github-api.module";
import { ScheduleModule } from "@nestjs/schedule";
import { StatsModule } from "./stats/stats.module";
import { LoggerModule } from "nestjs-pino";
import { Request } from "express";

@Module({
imports: [
Expand All @@ -20,6 +22,44 @@ import { StatsModule } from "./stats/stats.module";
isGlobal: true,
envFilePath: ".env",
}),
LoggerModule.forRoot({
pinoHttp: {
autoLogging: true,
transport:
process.env.NODE_ENV !== "production"
? {
target: "pino-pretty",
options: {
singleLine: true,
colorize: true,
ignore: "pid,hostname",
messageFormat: "[{context}] {msg}",
},
}
: undefined,
customProps: (req: Request) => {
const customReq = req as Request & { user?: { id: string } };
const user = customReq.user;
return {
userId: user?.id,
};
},
serializers: {
req: (req) => {
return {
id: req.id,
method: req.method,
url: req.url,
};
},
res: (res) => {
return {
statusCode: res.statusCode,
};
},
},
},
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => {
Expand Down
12 changes: 9 additions & 3 deletions api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Req,
Res,
UseGuards,
Logger,
} from "@nestjs/common";
import { UserEntity } from "../user/entities/user.entity";
import { AuthGuard } from "@nestjs/passport";
Expand All @@ -21,6 +22,8 @@ import { SocialPlatform } from "../user/entities/social-account.entity";

@Controller("auth")
export class AuthController {
private readonly logger = new Logger(AuthController.name);

constructor(
private readonly auth: AuthService,
private readonly configService: ConfigService,
Expand All @@ -32,6 +35,8 @@ export class AuthController {
@UseGuards(AuthGuard("github"))
githubCallback(@Req() req: Request, @Res() res: Response) {
const user = req.user as UserEntity;
this.logger.log({ action: "github_login", userId: user.id });

const token = this.auth.signToken(user);
const redirectUrl = this.configService.getOrThrow<string>(
"OAUTH_SUCCESS_REDIRECT_URL",
Expand Down Expand Up @@ -105,19 +110,20 @@ export class AuthController {
username: request.user.fortyTwoAccount.username,
});

this.logger.log({ action: "fortytwo_link", userId });

const redirectUrl = this.configService.getOrThrow<string>(
"OAUTH_42_SUCCESS_REDIRECT_URL",
);

return res.redirect(redirectUrl);
} catch (e) {
// Use a more detailed log, and preserve specific error messages for BadRequestException
console.error("Error in FortyTwo callback:", e);
if (e instanceof BadRequestException) {
throw e;
}

throw new BadRequestException(
e && typeof e.message === "string"
e instanceof Error
? `Invalid state parameter: ${e.message}`
: "Invalid state parameter.",
);
Expand Down
57 changes: 57 additions & 0 deletions api/src/common/AllExceptionsFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from "@nestjs/common";
import { HttpAdapterHost } from "@nestjs/core";

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);

constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

catch(exception: unknown, host: ArgumentsHost): void {
if (host.getType() !== "http") {
return;
}

const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const httpStatus =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;

const responseBody = {
statusCode: httpStatus,
timestamp: new Date().toISOString(),
path: httpAdapter.getRequestUrl(ctx.getRequest()),
message:
exception instanceof HttpException
? exception.message
: "Internal server error",
};

if (httpStatus >= 500) {
if (exception instanceof Error) {
this.logger.error(exception.message, exception.stack);
} else {
this.logger.error(`Unhandled Exception: ${exception}`);
}
} else {
// 4xx errors
if (exception instanceof Error) {
this.logger.warn(`HTTP Exception: ${exception.message}`);
} else {
this.logger.warn(`HTTP Exception: ${exception}`);
}
}

httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
}
}
12 changes: 10 additions & 2 deletions api/src/common/TypeormExceptionFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,28 @@ import {
Catch,
ArgumentsHost,
HttpStatus,
Logger,
} from "@nestjs/common";
import { Response } from "express";
import { Response, Request } from "express";

@Catch(EntityNotFoundError)
export class TypeormExceptionFilter implements ExceptionFilter {
catch(exception: EntityNotFoundError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();

// Log the error
Logger.error(
`Entity not found for request ${request.method} ${request.url}: ${exception.message}`,
exception.stack,
"TypeormExceptionFilter",
);

response.status(HttpStatus.NOT_FOUND).json({
statusCode: HttpStatus.NOT_FOUND,
timestamp: new Date().toISOString(),
message: "Entity not found",
error: exception.message,
});
}
}
2 changes: 1 addition & 1 deletion api/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export const enum LockKeys {
AUTO_LOCK_EVENTS = 12345,
CREATE_TEAM_REPOS = 12346,
PROCESS_QUEUE_MATCHES = 12347,
}
}
1 change: 0 additions & 1 deletion api/src/event/dtos/createEventDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,4 @@ export class CreateEventDto {
@ApiProperty()
@IsBoolean()
isPrivate: boolean;

}
1 change: 0 additions & 1 deletion api/src/event/dtos/updateEventSettingsDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,3 @@ export class UpdateEventSettingsDto {
@IsString()
serverConfig?: string;
}

63 changes: 63 additions & 0 deletions api/src/event/event.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Put,
UnauthorizedException,
UseGuards,
Logger,
} from "@nestjs/common";
import { EventService } from "./event.service";
import { TeamService } from "../team/team.service";
Expand All @@ -24,6 +25,8 @@ import { UserId } from "../guards/UserGuard";

@Controller("event")
export class EventController {
private readonly logger = new Logger(EventController.name);

constructor(
private readonly eventService: EventService,
private readonly teamService: TeamService,
Expand Down Expand Up @@ -91,6 +94,12 @@ export class EventController {
"You are not authorized to create events.",
);

this.logger.log({
action: "attempt_create_event",
userId,
eventName: createEventDto.name,
});

return this.eventService.createEvent(
userId,
createEventDto.name,
Expand Down Expand Up @@ -165,6 +174,8 @@ export class EventController {
throw new BadRequestException("Event has not started yet.");
}

this.logger.log({ action: "attempt_join_event", userId, eventId });

return this.userService.joinEvent(userId, eventId);
}

Expand All @@ -179,6 +190,8 @@ export class EventController {
"You are not authorized to lock this event.",
);

this.logger.log({ action: "attempt_lock_event", userId, eventId });

return this.eventService.lockEvent(eventId);
}

Expand All @@ -193,6 +206,8 @@ export class EventController {
"You are not authorized to unlock teams for this event.",
);

this.logger.log({ action: "attempt_unlock_event", userId, eventId });

return this.eventService.unlockEvent(eventId);
}

Expand All @@ -208,6 +223,13 @@ export class EventController {
"You are not authorized to lock teams for this event.",
);

this.logger.log({
action: "attempt_set_lock_teams_date",
userId,
eventId,
repoLockDate: body.repoLockDate,
});

if (!body.repoLockDate)
return this.eventService.setTeamsLockedDate(eventId, null);
return this.eventService.setTeamsLockedDate(
Expand All @@ -228,6 +250,12 @@ export class EventController {
"You are not authorized to update settings for this event.",
);

this.logger.log({
action: "attempt_update_event_settings",
userId,
eventId,
});

return this.eventService.updateEventSettings(eventId, body);
}

Expand All @@ -253,6 +281,13 @@ export class EventController {
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
throw new UnauthorizedException("You are not an admin of this event");
}

this.logger.log({
action: "attempt_add_event_admin",
userId,
eventId,
newAdminId,
});
return this.eventService.addEventAdmin(eventId, newAdminId);
}

Expand All @@ -266,6 +301,13 @@ export class EventController {
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
throw new UnauthorizedException("You are not an admin of this event");
}

this.logger.log({
action: "attempt_remove_event_admin",
userId,
eventId,
removedAdminId: adminIdToRemove,
});
return this.eventService.removeEventAdmin(eventId, adminIdToRemove);
}

Expand All @@ -285,6 +327,13 @@ export class EventController {
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
throw new UnauthorizedException("You are not an admin of this event");
}

this.logger.log({
action: "attempt_create_starter_template",
userId,
eventId,
templateName: body.name,
});
return this.eventService.createStarterTemplate(
eventId,
body.name,
Expand All @@ -304,6 +353,13 @@ export class EventController {
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
throw new UnauthorizedException("You are not an admin of this event");
}

this.logger.log({
action: "attempt_update_starter_template",
userId,
eventId,
templateId,
});
return this.eventService.updateStarterTemplate(eventId, templateId, body);
}

Expand All @@ -317,6 +373,13 @@ export class EventController {
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
throw new UnauthorizedException("You are not an admin of this event");
}

this.logger.log({
action: "attempt_delete_starter_template",
userId,
eventId,
templateId,
});
return this.eventService.deleteStarterTemplate(eventId, templateId);
}
}
Loading
Loading