Skip to content

3 layer backend architecture, testing suite, and minor improvements#135

Merged
jenul-ferdinand merged 262 commits intomainfrom
code-style-fixes
Feb 26, 2026
Merged

3 layer backend architecture, testing suite, and minor improvements#135
jenul-ferdinand merged 262 commits intomainfrom
code-style-fixes

Conversation

@jenul-ferdinand
Copy link
Copy Markdown
Member

@jenul-ferdinand jenul-ferdinand commented Dec 28, 2025

  • Setup a three layer backend architecture with an updated file structure, repositories/, services/, controllers/.

  • Because of this, we have improved testability especially for business logic in our backend services.

  • Testing suite can be found in tests/, using Jest with sample data from the test db as fixtures, mongodb-memory-server is used to test the service layer.

  • Previous "services" are now providers/. E.g., AiOverviewProvider.

  • Setup module alias importing e.g., "@models/unit" instead of "../../../../models/unit". This needed some tweaks so that intellisense works see backend/jsconfig.json and backend/package.json.

  • Old endpoints are V1 and new endpoints are V2.

  • The frontend garbage code has improved a bit more.

backend/infra/repositories/review.repository.js
- remove .populate('author') from findByUserId — author is injected on the
  frontend from the already-resolved user, only unit population is needed

frontend/src/app/routes/user-profile/user-profile.state.ts
- extract State interface into its own file with username, user, profileImg,
  isCurrentUser, and reviews: IReviewFullyPopulated[] fields

frontend/src/app/routes/user-profile/logout-button/
- add standalone LogoutButtonComponent for the logout action in the panel

frontend/src/app/routes/user-profile/profile-panel/
- add standalone ProfilePanelComponent that receives State as a required input
- gate logout button and personalisation message behind isCurrentUser check
- move flex: 1 to :host so the host element participates in the flex layout

frontend/src/app/routes/user-profile/user-profile.component.ts
- import Router, ReviewCardComponent, GetReviewService, SkeletonModule
- remove unused username property from the component class
- remove all debug console.log and console.info statements
- replace exhaustMap's inner map with switchMap to chain getReviewsByUser
  after resolving the user, mapping reviews to IReviewFullyPopulated by
  injecting the resolved user as author on each review
- add catchError on reviews fetch returning empty array so a failed reviews
  call does not break the whole state stream
- sort reviews by unit code level digit (index 3) with localeCompare fallback
  for deterministic ordering within the same level
- add logout() method that subscribes to userService.logout() and navigates
  to / only after the server confirms the session is cleared

frontend/src/app/routes/user-profile/user-profile.component.html
- replace static left panel markup with app-profile-panel inside @if (state)
- add @else skeleton with p-skeleton circle for avatar, bars for username,
  body lines, and button area to fill the panel space during load
- wrap reviews @for in @if (state) to prevent null access before first emit
- add @else with three review-group-skeleton blocks each containing unit
  header bars and a card-height skeleton to communicate layout while loading
- dynamic h2 heading: 'Your amazing contributions' for isCurrentUser,
  'Contributions' for other users' profiles, using state?. during loading

frontend/src/app/routes/user-profile/user-profile.component.scss
- change height: 100dvh to min-height: 100dvh so content pushes footer down
- add @media (max-width: 1150px) flex-direction: column for single column layout
- add .profile-panel-skeleton with flex: 1, matching panel background,
  border-radius, padding, and a skeleton-header row for avatar + username
- replace .left with .right as a CSS grid with grid-template-columns: 1fr 1fr
  and grid-auto-rows: min-content to prevent equal-height row stretching
- h2 spans both columns with grid-column: 1 / -1
- add @media (max-width: 1790px) collapsing grid to single column
- add .review-group-skeleton with margin-top: 1.5rem and column flex layout
- add .review-group with margin-top: 1.5rem and .unit-title-header with
  margin-bottom: 0.5rem for explicit spacing without transform offsets

frontend/src/app/shared/components/review-card/review-card.component.ts
- add RouterLink to imports for username navigation
- add 'profile' to variant union type: 'default' | 'compact' | 'profile'

frontend/src/app/shared/components/review-card/review-card.component.html
- conditionally apply mt-5 via [class.mt-5]="variant !== 'profile'" to avoid
  Bootstrap's !important blocking CSS overrides on the profile page
- add [class.profile] binding for the new variant
- add [routerLink]="['/user', review.author.username]" to the user section
  so clicking the avatar or username navigates to that user's profile page

frontend/src/app/shared/components/review-card/review-card.component.scss
- add @media (hover: none) block forcing opacity: 1 on .top-right-button so
  touch devices always see the delete button since mouseenter never fires
- add cursor: pointer and width: fit-content on .user-section, and
  &:hover span { text-decoration: underline } to signal it is clickable
- extend compact block selector to .compact, .profile for shared sizing styles
  (padding: 14px, reduced font sizes, user-section sizing, border-radius: 12px)
- add .review-card-container.profile block: display: block on .top-right-button
  to re-enable it, and reduced .thumbs font-size: 1rem and span font-size: 0.8rem
  to fit reaction buttons inside the narrower two-column grid cells
- add .review-card-container.compact block: transform: none on hover and
  display: none on .top-right-button to opt out of the shared profile behaviour

frontend/src/app/shared/services/api/get-review.service.ts
- add getReviewsByUser(userId) returning Observable<IReviewFullyPopulated[]>
  hitting GET /reviews/user/:userId with the populated unit from the backend
@jenul-ferdinand jenul-ferdinand marked this pull request as ready for review February 26, 2026 12:29
Copilot AI review requested due to automatic review settings February 26, 2026 12:29
@jenul-ferdinand jenul-ferdinand merged commit 8fd0a48 into main Feb 26, 2026
2 checks passed
@jenul-ferdinand jenul-ferdinand deleted the code-style-fixes branch February 26, 2026 12:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Sets up a new backend architecture (repositories/services/controllers) with improved testability (Jest + mongodb-memory-server), introduces v2 endpoints, and applies a broad frontend refactor/cleanup (new auth/profile pages, formatting, constants, eslint/prettier).

Changes:

  • Backend: migrate to layered infra structure, add service + performance test suites, and add v2 routes/controllers/services/providers.
  • Frontend: introduce new /auth and /user/:username pages and refactor unit/review flows to use v2 models/state.
  • Tooling: add ESLint/Prettier configs + scripts and path alias/import ergonomics.

Reviewed changes

Copilot reviewed 175 out of 238 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
frontend/src/app/shared/models/review.model.ts Formatting/refactor
frontend/src/app/shared/models/notification.model.ts Type fixes
frontend/src/app/shared/models/ai-overview.model.ts Removed model
frontend/src/app/shared/interceptors/csrf.interceptor.ts Refactor/format
frontend/src/app/shared/interceptors/auth.interceptor.ts Minor fix
frontend/src/app/shared/helpers.ts Removed helper
frontend/src/app/shared/constants/constants.ts New constants
frontend/src/app/shared/constants.ts Deleted old constants
frontend/src/app/shared/components/write-review-unit/write-review-unit.component.scss Formatting
frontend/src/app/shared/components/unit-review-header/unit-review-header.component.scss Import/path + cleanup
frontend/src/app/shared/components/unit-review-header/unit-review-header.component.html Async/state refactor
frontend/src/app/shared/components/unit-card/unit-card.component.scss Formatting
frontend/src/app/shared/components/unit-card/unit-card.component.html Template refactor
frontend/src/app/shared/components/shiny-monstar-title/shiny-monstar-title.component.ts New component
frontend/src/app/shared/components/shiny-monstar-title/shiny-monstar-title.component.scss New styles
frontend/src/app/shared/components/shiny-monstar-title/shiny-monstar-title.component.html New template
frontend/src/app/shared/components/setu-card/setu-card.component.ts Auth refactor
frontend/src/app/shared/components/setu-card/setu-card.component.scss Formatting
frontend/src/app/shared/components/setu-card/setu-card.component.html Auth gating changes
frontend/src/app/shared/components/review-card/review-card.component.scss Variant styles
frontend/src/app/shared/components/review-card/report-review/report-review.component.ts Formatting
frontend/src/app/shared/components/review-card/report-review/report-review.component.html Template changes
frontend/src/app/shared/components/rating/rating.component.ts Formatting
frontend/src/app/shared/components/rating/rating.component.html Formatting
frontend/src/app/shared/components/notifications/notifications-popup/notifications-popup.component.ts Import reorder
frontend/src/app/shared/components/notifications/notifications-popup/notifications-popup.component.html Formatting
frontend/src/app/shared/components/notifications/notification-card/notification-card.component.ts Import reorder
frontend/src/app/shared/components/notifications/notification-card/notification-card.component.html Template changes
frontend/src/app/shared/components/navbar/navbar.component.scss Import/path cleanup
frontend/src/app/shared/components/footer/footer.component.ts Formatting
frontend/src/app/shared/components/footer/footer.component.html Formatting
frontend/src/app/shared/components/ai-overview/ai-overview.component.ts v2 model + rxjs cleanup
frontend/src/app/shared/components/ai-overview/ai-overview.component.scss Import/path + formatting
frontend/src/app/shared/components/ai-overview/ai-overview.component.html Template simplification
frontend/src/app/routes/verified/verified.component.ts Formatting
frontend/src/app/routes/user-profile/user-profile.state.ts New state
frontend/src/app/routes/user-profile/user-profile.component.ts New route/page
frontend/src/app/routes/user-profile/user-profile.component.scss New styles
frontend/src/app/routes/user-profile/user-profile.component.html New template
frontend/src/app/routes/user-profile/profile-panel/profile-panel.component.ts New component
frontend/src/app/routes/user-profile/profile-panel/profile-panel.component.scss New styles
frontend/src/app/routes/user-profile/profile-panel/profile-panel.component.html New template
frontend/src/app/routes/user-profile/logout-button/logout-button.component.ts New component
frontend/src/app/routes/user-profile/logout-button/logout-button.component.scss New styles
frontend/src/app/routes/user-profile/logout-button/logout-button.component.html New template
frontend/src/app/routes/unit-overview/unit-overview.component.scss Import/path
frontend/src/app/routes/unit-overview/unit-overview.component.html v2 unit data binding
frontend/src/app/routes/unit-map/unit-map.component.ts Formatting/import reorder
frontend/src/app/routes/unit-map/unit-map.component.scss Import/path
frontend/src/app/routes/unit-map/unit-map.component.html Formatting
frontend/src/app/routes/terms-and-conds/terms-and-conds.component.ts Formatting
frontend/src/app/routes/terms-and-conds/terms-and-conds.component.scss Import/path
frontend/src/app/routes/setu-overview/setu-overview.component.ts Constants import + cleanup
frontend/src/app/routes/setu-overview/setu-overview.component.scss Formatting
frontend/src/app/routes/setu-overview/setu-overview.component.html Formatting
frontend/src/app/routes/reset-password/reset-password.component.ts Formatting/providers
frontend/src/app/routes/reset-password/reset-password.component.html Formatting
frontend/src/app/routes/not-found/not-found.component.ts Formatting
frontend/src/app/routes/home/home.component.html Use new title component
frontend/src/app/routes/changelog/changelog.component.ts Formatting/import reorder
frontend/src/app/routes/changelog/changelog.component.scss Formatting
frontend/src/app/routes/changelog/changelog.component.html Control-flow formatting
frontend/src/app/routes/auth/auth.component.ts New route/page
frontend/src/app/routes/auth/auth.component.scss New styles
frontend/src/app/routes/auth/auth.component.html New template
frontend/src/app/routes/auth/auth-google-button/auth-google-button.component.ts New component
frontend/src/app/routes/auth/auth-google-button/auth-google-button.component.scss New styles
frontend/src/app/routes/auth/auth-google-button/auth-google-button.component.html New template
frontend/src/app/routes/about/about.component.ts Import reorder
frontend/src/app/routes/about/about.component.scss Formatting
frontend/src/app/routes/about/about.component.html Formatting
frontend/src/app/app.routes.ts Add auth/profile routes
frontend/src/app/app.config.ts Add APP_INITIALIZER auth
frontend/src/app/app.component.ts Providers/imports cleanup
frontend/src/app/app.component.scss Import/path
frontend/public/changelog.json Add 2026 entry
frontend/package.json Add lint/format deps/scripts
frontend/eslint.config.mjs New ESLint config
frontend/angular.json IncludePaths for styles
frontend/README.md Minor edit
frontend/.prettierrc New prettier config
frontend/.prettierignore New ignore
frontend/.mcp.json Removed
frontend/.editorconfig Trailing whitespace setting
frontend/.claude/settings.local.json Removed
backend/utils/success.js Removed utility
backend/tests/services/user.service.test.js New tests
backend/tests/services/unit.service.test.js New tests
backend/tests/services/review.service.test.js New tests
backend/tests/services/jest.setup.js New test setup
backend/tests/performance/v2.units.popular.yml New perf script
backend/tests/performance/v1.units.popular.yml New perf script
backend/tests/performance/units.api.test.js New perf test
backend/tests/performance/runArtillery.js New runner
backend/tests/performance/mocks/redis.mock.js New mock
backend/tests/performance/jest.setup.js New perf setup
backend/services/token.service.js Removed legacy
backend/services/redis.service.js Removed legacy
backend/server.js Wire new infra + middleware
backend/routes/v2/units.js Removed legacy v2
backend/package.json Add jest/eslint/aliases
backend/models/user.js Alias imports + JSDoc
backend/models/review.js JSDoc + cleanup
backend/models/notification.js JSDoc + cleanup
backend/jsconfig.json Add path aliases
backend/jest.config.js Multi-project jest
backend/infra/utilities/verifyToken.js Alias import fix
backend/infra/utilities/generateSitemap.js Alias imports + dotenv quiet
backend/infra/utilities/errors.js New error classes
backend/infra/services/user.service.js New service layer
backend/infra/services/unit.service.js New service layer
backend/infra/services/notification.service.js New service layer
backend/infra/routes/v2/users.js New v2 routes
backend/infra/routes/v2/units.js New v2 routes
backend/infra/routes/v2/reviews.js New v2 routes
backend/infra/routes/v1/units.js Alias imports + tweaks
backend/infra/routes/v1/setus.js Alias imports
backend/infra/routes/v1/reviews.js Alias imports + formatting
backend/infra/routes/v1/notifications.js Alias imports + formatting
backend/infra/routes/v1/github.js Formatting
backend/infra/routes/v1/auth.js Token provider swap
backend/infra/routes/v1/admin.js Provider swaps
backend/infra/repositories/unit.repository.js New repository
backend/infra/repositories/notification.repository.js New repository
backend/infra/providers/token.provider.js New provider
backend/infra/providers/tagManager.provider.js Alias imports
backend/infra/providers/cloudinary.provider.js dotenv quiet
backend/infra/providers/cache.provider.js New provider
backend/infra/providers/aiOverview.provider.js Rename/provider
backend/infra/middleware/user.middleware.js New middleware
backend/infra/middleware/error.middleware.js New middleware
backend/infra/middleware/admin.middleware.js New middleware
backend/infra/controllers/user.controller.js New controller
backend/infra/controllers/unit.controller.js New controller
backend/infra/controllers/review.controller.js New controller
backend/infra/README.md New docs
backend/eslint.config.mjs New ESLint config
backend/CLAUDE.md Removed
backend/AGENTS.md Doc tweak
backend/.mcp.json Removed
backend/.editorconfig New editorconfig
backend/.claude/settings.local.json Removed
README.md Badges + doc path change
.vscode/settings.json Workspace tooling
.mcp.json Removed
.contextive/schema.json New schema
.contextive/definitions.yml New definitions
.claude/settings.local.json Removed
Files not reviewed (1)
  • frontend/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)

frontend/src/app/shared/components/unit-review-header/unit-review-header.component.html:1

  • unit is scoped to the @if (unit$ | async; as unit) block, but it’s referenced later outside that block, which will fail template compilation. Also, Angular templates should not use this.; emit the output directly (or via a component method) and keep the child component inside the scope where unit exists (or bind from a component field).
<!-- Toast -->

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

Comment on lines +4 to +5
@if (unit$ | async; as unit) {
@if (userState$ | async; as state) {
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

unit is scoped to the @if (unit$ | async; as unit) block, but it’s referenced later outside that block, which will fail template compilation. Also, Angular templates should not use this.; emit the output directly (or via a component method) and keep the child component inside the scope where unit exists (or bind from a component field).

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +22
(selectOnFocus)="(true)"
(focusOnHover)="(true)"
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

selectOnFocus and focusOnHover are inputs on PrimeNG dropdown, but they’re currently bound as events via (…). This won’t set the intended behavior; switch them to property bindings (e.g. [selectOnFocus]="true" and [focusOnHover]="true").

Suggested change
(selectOnFocus)="(true)"
(focusOnHover)="(true)"
[selectOnFocus]="true"
[focusOnHover]="true"

Copilot uses AI. Check for mistakes.
<!-- Auth Overlay -->
@if (!authenticated && !error && selectedSetu) {
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The “Login” button in the locked overlay no longer triggers any action (the previous click handler was removed). Hook this up to the new auth flow (e.g., navigate to /auth or open whatever login UX replaced the dialog) so users can actually proceed.

Copilot uses AI. Check for mistakes.
label="Login"
rounded="true"
icon="pi pi-sign-in"
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The “Login” button in the locked overlay no longer triggers any action (the previous click handler was removed). Hook this up to the new auth flow (e.g., navigate to /auth or open whatever login UX replaced the dialog) so users can actually proceed.

Suggested change
icon="pi pi-sign-in"
icon="pi pi-sign-in"
routerLink="/auth"

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +7
[src]="
notification.data.user.profileImg ||
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSWwfGUCDwrZZK12xVpCOqngxSpn0BDpq6ewQ&s'
"
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The (error) binding doesn’t do anything (it evaluates a string literal but never assigns it to the image source). Use an error handler that sets the event target’s src (or bind to a component method) so broken profile images reliably fall back.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +12
const user = req.user;
if (!user) return res.status(404).json({ message: 'User context not found' });
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

req.user here is the decoded JWT payload set by userMiddleware (likely { id, isAdmin, iat, exp }), not the full user document. If the frontend expects profile fields (username, email, profileImg, etc.), this endpoint should resolve the user from the database (e.g., via UserService.validate(req.cookies.access_token) or repository lookup) and return a normalized user DTO.

Suggested change
const user = req.user;
if (!user) return res.status(404).json({ message: 'User context not found' });
const accessToken = req.cookies && req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({ message: 'Not authenticated' });
}
const user = await UserService.validate(accessToken);
if (!user) {
return res.status(404).json({ message: 'User context not found' });
}

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +14
static getAll = asyncHandler(async (req, res) => {
const units = await UnitService.fetchAll();
return res.status(201).json(units);
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

getAll should return 200 (not 201, which indicates creation). Also 204 No Content must not include a response body; either return 200 with the JSON payload or keep 204 and send no body. While touching this, fix the typo in the message (SucessfullySuccessfully) if you keep a message response.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +69
static updateByUnitcode = asyncHandler(async (req, res) => {
const updatedUnit = await UnitService.modifyByUnitcode(
req.params.unitcode,
req.body
);
return res.status(204).json({
msg: `Sucessfully updated ${req.params.unitcode}`,
unit: updatedUnit,
});
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

getAll should return 200 (not 201, which indicates creation). Also 204 No Content must not include a response body; either return 200 with the JSON payload or keep 204 and send no body. While touching this, fix the typo in the message (SucessfullySuccessfully) if you keep a message response.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +105
static toggleReaction = asyncHandler(async (req, res) => {
const reviewId = req.params.reviewId;
const { userId, reactionType } = req.body;

if (!['like', 'dislike'].includes(reactionType)) {
return res.status(400).json({
error: 'Invalid reaction type. Must be "like" or "dislike"',
});
}

const result = await ReviewService.toggleReaction(
reviewId,
userId,
reactionType
);

return res.status(200).json(result);
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

userId is taken from the request body even though the route is protected by userMiddleware. That allows a client to spoof reactions on behalf of other users. Use req.user.id as the actor ID and ignore/omit userId from the payload.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +12
<a
href="https://www.facebook.com/WiredMonash"
target="_blank"
aria-label="Facebook"
><i class="bi bi-facebook"></i
></a>
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

Links that use target="_blank" should include rel="noopener noreferrer" to prevent reverse-tabnabbing. Apply this to the external social links in the footer.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants