3 layer backend architecture, testing suite, and minor improvements#135
3 layer backend architecture, testing suite, and minor improvements#135jenul-ferdinand merged 262 commits intomainfrom
Conversation
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
…s dont overflow in review cards
There was a problem hiding this comment.
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
/authand/user/:usernamepages 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
unitis 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 usethis.; emit the output directly (or via a component method) and keep the child component inside the scope whereunitexists (or bind from a component field).
<!-- Toast -->
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @if (unit$ | async; as unit) { | ||
| @if (userState$ | async; as state) { |
There was a problem hiding this comment.
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).
| (selectOnFocus)="(true)" | ||
| (focusOnHover)="(true)" |
There was a problem hiding this comment.
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").
| (selectOnFocus)="(true)" | |
| (focusOnHover)="(true)" | |
| [selectOnFocus]="true" | |
| [focusOnHover]="true" |
| <!-- Auth Overlay --> | ||
| @if (!authenticated && !error && selectedSetu) { |
There was a problem hiding this comment.
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.
| label="Login" | ||
| rounded="true" | ||
| icon="pi pi-sign-in" |
There was a problem hiding this comment.
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.
| icon="pi pi-sign-in" | |
| icon="pi pi-sign-in" | |
| routerLink="/auth" |
| [src]=" | ||
| notification.data.user.profileImg || | ||
| 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSWwfGUCDwrZZK12xVpCOqngxSpn0BDpq6ewQ&s' | ||
| " |
There was a problem hiding this comment.
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.
| const user = req.user; | ||
| if (!user) return res.status(404).json({ message: 'User context not found' }); |
There was a problem hiding this comment.
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.
| 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' }); | |
| } |
| static getAll = asyncHandler(async (req, res) => { | ||
| const units = await UnitService.fetchAll(); | ||
| return res.status(201).json(units); | ||
| }); |
There was a problem hiding this comment.
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 (Sucessfully → Successfully) if you keep a message response.
| 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, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
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 (Sucessfully → Successfully) if you keep a message response.
| 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); | ||
| }); |
There was a problem hiding this comment.
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.
| <a | ||
| href="https://www.facebook.com/WiredMonash" | ||
| target="_blank" | ||
| aria-label="Facebook" | ||
| ><i class="bi bi-facebook"></i | ||
| ></a> |
There was a problem hiding this comment.
Links that use target="_blank" should include rel="noopener noreferrer" to prevent reverse-tabnabbing. Apply this to the external social links in the footer.
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.jsonandbackend/package.json.Old endpoints are V1 and new endpoints are V2.
The frontend garbage code has improved a bit more.