Skip to content

WIP: Introduced handlers for backend architecture#333

Open
stasel wants to merge 1 commit into
mainfrom
backend-arch-rework
Open

WIP: Introduced handlers for backend architecture#333
stasel wants to merge 1 commit into
mainfrom
backend-arch-rework

Conversation

@stasel

@stasel stasel commented May 3, 2026

Copy link
Copy Markdown
Member

No description provided.

Copilot AI review requested due to automatic review settings May 3, 2026 23:21
@stasel stasel added the BE Backend ticket label May 3, 2026
@stasel stasel added this to Dojo May 3, 2026
@HackYourFutures HackYourFutures temporarily deployed to dojo-backend-arch-rewor-ebbbym May 3, 2026 23:21 Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the server-side HTTP architecture by introducing a dedicated handlers/ layer (Express request/response concerns) and converting controllers into “pure” services that return values and throw typed HttpErrors.

Changes:

  • Added handlers/* classes and a shared sendError helper to translate controller errors into HTTP responses.
  • Refactored routers to depend on handlers instead of controllers, and updated index.ts wiring accordingly.
  • Refactored multiple controllers to remove Express dependencies and to throw HttpError subclasses for error cases.

Reviewed changes

Copilot reviewed 39 out of 39 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
server/src/routes/UserRouter.ts Switches user routes to call UserHandler methods.
server/src/routes/TraineesRouter.ts Switches trainee-related routes to handler instances across subdomains.
server/src/routes/SearchRouter.ts Uses SearchHandler instead of controller.
server/src/routes/GeographyRouter.ts Uses GeographyHandler instead of controller.
server/src/routes/DashboardRouter.ts Uses DashboardHandler instead of controller.
server/src/routes/CohortsRouter.ts Uses CohortsHandler instead of controller.
server/src/routes/AuthenticationRouter.ts Routes login/logout/session via AuthenticationHandler.
server/src/middlewares/AuthMiddleware.ts Updates token cookie constant import to handler layer.
server/src/index.ts Rewires dependency graph: services → repositories → pure controllers → HTTP handlers → routers.
server/src/handlers/index.ts Barrel exports for handler layer.
server/src/handlers/UserHandler.ts New HTTP handler for users with zod body parsing + error translation.
server/src/handlers/TraineeHandler.ts New HTTP handler for base trainee CRUD endpoints.
server/src/handlers/TestHandler.ts New HTTP handler for trainee tests endpoints.
server/src/handlers/StrikeHandler.ts New HTTP handler for trainee strikes endpoints.
server/src/handlers/SearchHandler.ts New HTTP handler that wraps controller results in the existing search response shape.
server/src/handlers/ProfilePictureHandler.ts New HTTP handler managing upload + calling controller for storage updates.
server/src/handlers/LetterHandler.ts New HTTP handler streaming generated letters to the client.
server/src/handlers/InteractionHandler.ts New HTTP handler for trainee interactions endpoints.
server/src/handlers/Handler.ts Adds sendError helper to map HttpError → structured HTTP responses.
server/src/handlers/GeographyHandler.ts New HTTP handler for countries/cities search.
server/src/handlers/EmploymentHistoryHandler.ts New HTTP handler for employment-history endpoints.
server/src/handlers/DashboardHandler.ts New HTTP handler for dashboard response.
server/src/handlers/CohortsHandler.ts New HTTP handler for cohorts endpoint.
server/src/handlers/AuthenticationHandler.ts New HTTP handler for login (sets cookie), logout, and session.
server/src/errors/index.ts Adds barrel export for error types.
server/src/errors/HttpError.ts Introduces HttpError + common HTTP subclasses.
server/src/controllers/UserController.ts Converts to pure controller API returning values and throwing HttpError.
server/src/controllers/Trainee/TraineeController.ts Converts to pure controller API (CRUD returning values / throwing HttpError).
server/src/controllers/Trainee/TestController.ts Converts to pure controller API and centralizes validation + HttpError throwing.
server/src/controllers/Trainee/StrikeController.ts Converts to pure controller API and centralizes validation + HttpError throwing.
server/src/controllers/Trainee/ProfilePictureController.ts Converts to pure controller API; returns URLs and cleans up temp files in finally.
server/src/controllers/Trainee/LetterController.ts Converts to pure controller API; returns stream + metadata.
server/src/controllers/Trainee/InteractionController.ts Converts to pure controller API and centralizes validation + HttpError throwing.
server/src/controllers/Trainee/EmploymentHistoryController.ts Converts to pure controller API and centralizes validation + HttpError throwing.
server/src/controllers/SearchController.ts Converts to pure controller API returning results only (handler formats response).
server/src/controllers/GeographyController.ts Converts to pure controller API returning arrays only.
server/src/controllers/DashboardController.ts Converts to pure controller API returning typed dashboard payload.
server/src/controllers/CohortsController.ts Converts to pure controller API returning cohorts data.
server/src/controllers/AuthenticationController.ts Converts to pure controller API returning { user, token } and throwing HttpError.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +67 to 69
async deleteTrainee(_traineeId: string): Promise<void> {
throw new Error('Not implemented');
}
Comment on lines +15 to +23
async getCohorts(req: Request, res: Response): Promise<void> {
const MAX_POSSIBLE_COHORT = 999;
let start = Number.parseInt(req.query.start as string) || 0;
let end = Number.parseInt(req.query.end as string) || MAX_POSSIBLE_COHORT;
start = Math.max(start, 0);
end = Math.min(end, MAX_POSSIBLE_COHORT);

const result = await this.controller.getCohorts({ start, end });
res.status(200).json(result);
Comment on lines +35 to +42
async updateEmploymentHistory(req: Request, res: Response, next: NextFunction): Promise<void> {
const history: EmploymentHistory = {
id: String(req.params.employmentHistoryID),
type: req.body?.type,
companyName: req.body?.companyName,
role: req.body?.role,
startDate: req.body?.startDate ? new Date(req.body.startDate) : (undefined as unknown as Date),
endDate: req.body?.endDate ? new Date(req.body.endDate) : undefined,
Comment on lines +36 to +41
const test: Test = {
id: String(req.params.testID),
date: req.body?.date,
type: req.body?.type,
result: req.body?.result,
score: Number.parseFloat(req.body?.score),
Comment on lines 43 to 45
const imageURL = new URL(this.getImageKey(trainee.id), baseURL).href;
const thumbnailURL = new URL(this.getThumbnailKey(trainee.id), baseURL).href;

@dojo-security-review

Copy link
Copy Markdown

🛑 Security Review — 3 issue(s) found

The PR is a clean architectural refactor (splitting Express handlers from business-logic controllers). Two cross-cutting patterns introduced security issues: reporter IDs accepted from the request body, and an unsanitized filename in a response header.

View all findings (3 issues)
File Line Severity OWASP Issue
server/src/handlers/InteractionHandler.ts 33, 52 🟠 High A01 Reporter ID accepted from req.body — any authenticated user can falsely attribute interactions to another user
server/src/handlers/StrikeHandler.ts 32, 50 🟠 High A01 Same reporter ID spoofing in strikes — authorship of strikes can be falsified
server/src/handlers/LetterHandler.ts 22 🟡 Medium A03 Unsanitized fileName interpolated into Content-Disposition header — potential HTTP response splitting via CRLF in trainee name

Generated by PR Security Review for issue #333 · ● 541.7K ·

@dojo-security-review dojo-security-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by PR Security Review for issue #333 · ● 541.7K

type,
title,
details,
reporterID: req.body?.reporterID || user.id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Security Issue: Reporter ID Spoofing / Broken Access Control

OWASP: A01 – Broken Access Control
Severity: High

Issue: The reporterID is accepted directly from req.body, allowing any authenticated user to attribute an interaction to a different user. An attacker can submit { "reporterID": "<victim-user-id>", ... } to falsely record that another staff member created the interaction. The same pattern appears again in updateInteraction (line 52), meaning an attacker can also rewrite the reporter on existing records.

Recommendation: Ignore the client-supplied reporterID entirely and always derive it from the authenticated session:

// addInteraction
const interaction: InteractionWithReporterID = {
  date,
  type,
  title,
  details,
  reporterID: user.id,  // always use the authenticated user — never trust req.body
} as InteractionWithReporterID;

Apply the same fix to updateInteraction (line 52).

reason,
date,
comments,
reporterID: req.body?.reporterID || user.id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Security Issue: Reporter ID Spoofing / Broken Access Control

OWASP: A01 – Broken Access Control
Severity: High

Issue: Same pattern as InteractionHandlerreporterID is accepted from req.body, so any authenticated user can record a strike and falsely attribute it to another staff member. The same flaw exists in updateStrike (line 50), allowing retroactive reassignment of authorship on existing strikes.

Recommendation: Derive reporterID exclusively from the authenticated session; never accept it from the request body:

const strike: StrikeWithReporterID = {
  reason,
  date,
  comments,
  reporterID: user.id,  // ignore req.body?.reporterID
} as StrikeWithReporterID;

Apply the same fix to updateStrike (line 50).

const { stream, fileName, contentType } = await this.controller.generateLetter({ traineeId, type });
res.status(200).header({
'Content-Type': contentType,
'Content-Disposition': `attachment; filename="${fileName}"`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Security Issue: HTTP Response Header Injection via Unsanitized Filename

OWASP: A03 – Injection
Severity: Medium

Issue: fileName is interpolated directly into the Content-Disposition header without sanitisation. fileName originates from trainee data (display name + letter type) which is ultimately user-controlled. If a trainee's name contains " characters, the filename parameter in the header is broken out of its quotes. More critically, if the name contains \r\n (CRLF), an attacker could inject arbitrary HTTP response headers (HTTP Response Splitting), enabling cache poisoning or header-based attacks.

Recommendation: Strip or encode unsafe characters from the filename before embedding it in the header. The simplest approach is to use encodeURIComponent and the filename* RFC 5987 format, or at minimum remove " and CRLF from the string:

const safeFileName = fileName.replace(/["\r\n]/g, '_');
res.status(200).header({
  'Content-Type': contentType,
  'Content-Disposition': `attachment; filename="\$\{safeFileName}"`,
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

BE Backend ticket

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants