Skip to content

Auth0 is removed - #117

Merged
maximgorbatyuk merged 2 commits into
mainfrom
auth0-removal
Jan 27, 2026
Merged

maximgorbatyuk merged 2 commits into
mainfrom
auth0-removal

Conversation

@maximgorbatyuk

@maximgorbatyuk maximgorbatyuk commented Jan 27, 2026 •

Copy link
Copy Markdown
Member

Summary by CodeRabbit

Release Notes

  • New Features

    • New authentication pages: login, registration, password reset, and email verification flows with OAuth support (Google, GitHub)
    • M2M clients management for API access administration
    • Enhanced user account management: unlock locked accounts, resend verification emails, reset failed login attempts
  • Refactor

    • Modernized authentication system for improved security and flexibility
  • Style

    • Improved theme consistency across all components using unified design tokens

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 27, 2026 •

Copy link
Copy Markdown

Walkthrough

This PR transitions the application from Auth0/OIDC authentication to a custom token-based system with dedicated API endpoints, introduces M2M client management in the admin module, adds user security features (email verification, account lockout), implements new authentication pages for login/register/password recovery, and updates styling with theme variables.

Changes

Cohort / File(s) Summary
Build & Configuration
angular.json, package.json, src/environments/environment*.ts, src/environments/environment.local.ts
Added stylePreprocessorOptions and local build configuration; introduced npm scripts for local/SSR workflows; removed Auth0/OIDC dependencies; updated environment configs to use new auth callback URLs and removed Auth0-specific settings.
Authentication Infrastructure
src/app/shared/services/auth/*, src/app/services/auth-api.service.ts, src/app/shared/interceptors/auth-interceptor.ts
Replaced AuthService with token-based flow replacing OIDC manager; introduced TokenStorageService for token persistence; added AuthApiService wrapping REST endpoints for register, login, password reset, OAuth URLs; enhanced auth-interceptor with 401 handling and token refresh logic; removed Auth0ServerMockService and OidcUserManager.
Authentication Pages
src/app/modules/home/components/login-page/*, src/app/modules/home/components/register-page/*, src/app/modules/home/components/forgot-password-page/*, src/app/modules/home/components/reset-password-page/*, src/app/modules/home/components/verify-email-page/*, src/app/modules/home/home.module.ts, src/app/modules/home/home.routing-module.ts
Added five new authentication page components with templates and styling (login, register, forgot-password, reset-password, verify-email); includes form validation, password strength checking, OAuth button stubs, and state management.
OAuth Callback Handling
src/app/modules/home/components/auth-callback/auth-callback.component.ts
Extended OAuth callback component with token parsing from query params and URL fragments, token handling workflow, and fallback to legacy flow; added parseFragment and handleOAuthTokens helper methods.
M2M Clients Feature
src/app/modules/admin/services/m2m-clients.service.ts, src/app/modules/admin/components/m2m-clients/*, src/app/modules/admin/admin-routing.module.ts, src/app/modules/admin/admin.module.ts, src/app/models/m2m-client.model.ts
Introduced complete M2M client management: new service with CRUD operations, three new components (list/create/detail) with forms and status management, routing integration, and TypeScript models with scope and token response types.
User Management Extensions
src/app/modules/admin/components/users/users-admin-page/*, src/app/services/user-admin.service.ts
Extended users admin page with email confirmation and account lockout filters; added four new action methods (unlockUser, resetFailedAttempts, forceVerifyEmail, resendVerification); expanded user model with security-related fields.
Theme & UI Styling
src/_warm-theme.scss, src/app/shared/components/*/*.scss, src/app/modules/*/**/*.scss
Added new CSS variables (--warm-btn-contrast-text, --warm-spinner-track, --warm-modal-overlay); systematically replaced hardcoded white/gold colors with theme variables across ~25 component stylesheets; updated spinner and modal overlay styling.
Data Models & Extended Types
src/app/models/application-user.ts, src/app/models/extended/application-user-extended.ts
Introduced AuthProvider enum; extended ApplicationUser schema with security fields (authProvider, failedLoginAttempts, lockedUntil, isLockedOut, hasPendingEmailVerification); added corresponding getters to ApplicationUserExtended.
Session & SSR Module
src/app/shared/services/auth/auth.session.service.ts, src/app/app.module.server.ts
Replaced IdToken storage with AuthInfo (accessToken, expiresAt); removed Auth0 server-side mock and authentication service provisioning from SSR module.
Navbar & Navigation
src/app/components/navbar/navbar.component.ts, src/app/components/admin-navbar/admin-navbar.component.ts
Replaced Router-based navigation for login/logout in navbar; removed SpinnerService dependency; added M2M Clients menu item to admin toolbar.
Utility Services
src/app/services/api.service.ts
Added getBaseUrl() accessor to expose base API URL.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant AuthCallback as Auth Callback<br/>Component
    participant AuthService
    participant TokenStorage as Token Storage<br/>Service
    participant Backend
    participant Router
    
    Browser->>AuthCallback: Redirect from OAuth provider<br/>with tokens in URL
    AuthCallback->>AuthCallback: Parse tokens from query/fragment
    AuthCallback->>AuthService: handleOAuthTokens(accessToken,<br/>refreshToken, expiresIn)
    AuthService->>TokenStorage: Store tokens & expiry
    TokenStorage->>TokenStorage: Save to localStorage
    AuthService->>Backend: Fetch current user
    Backend-->>AuthService: User data
    AuthService->>AuthService: Check if MFA required
    alt MFA Required
        AuthCallback->>Browser: Show MFA UI
    else MFA Not Required
        AuthService->>Router: Navigate to /me
        Router->>Browser: Redirect to dashboard
    end
Loading
sequenceDiagram
    participant Admin
    participant M2MCreate as M2M Create<br/>Component
    participant M2MService
    participant Backend
    participant Admin2 as Admin<br/>Display
    
    Admin->>M2MCreate: Navigate to create page
    Admin->>M2MCreate: Fill form (name, scopes,<br/>rate limits)
    Admin->>M2MCreate: Submit form
    M2MCreate->>M2MService: create(request)
    M2MService->>Backend: POST /api/m2m-clients
    Backend-->>M2MService: Created client with secret
    M2MService-->>M2MCreate: M2mClientCreateResponse
    M2MCreate->>M2MCreate: Display secret (copy button)
    Admin->>M2MCreate: View client details
    M2MCreate->>M2MService: getById(clientId)
    M2MService->>Backend: GET /api/m2m-clients/:id
    Backend-->>M2MService: Client details
    M2MService-->>Admin2: Render detail view
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Added skill and claude file #116: Updates warm theme variables and applies them across component stylesheets — directly related to theme variable changes in this PR.
  • SSR #111: Modifies app.module.server.ts and SSR auth handling — related to the Auth0 mock service removal and SSR module updates.
  • companies mvp #74: Extends admin navigation with new menu items — related to admin-navbar changes and admin routing additions.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Auth0 is removed' accurately summarizes the primary change: removing Auth0 integration and replacing it with a custom authentication system using tokens and APIs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@maximgorbatyuk
maximgorbatyuk merged commit e076ab9 into main Jan 27, 2026
1 check passed

@coderabbitai coderabbitai 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.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/app/modules/home/components/auth-callback/auth-callback.component.ts (1)

231-245: Logic bug: Success state is immediately overwritten.

Lines 242-244 execute unconditionally after the if (result.result) block, overwriting the success state. This means showTotpInvalid will always be true and showInfoblock will always be false after the subscription callback runs, regardless of verification success.

Additionally, this subscription lacks untilDestroyed(this) for proper cleanup.

🐛 Proposed fix
 validateTotp(): void {
   if (this.totpCode == null || this.totpCode.length !== 6) {
     this.showTotpInvalid = true;
     this.totpCodeSent = false;
     return;
   }

-  this.totpService.verifyTotp(this.totpCode).subscribe((result) => {
+  this.totpService.verifyTotp(this.totpCode)
+    .pipe(untilDestroyed(this))
+    .subscribe((result) => {
     if (result.result) {
       this.showTotpInvalid = false;
       this.showInfoblock = true;
       this.totpCodeSent = true;

-      this.authService.getCurrentUserFromBackend().subscribe((user) => {
+      this.authService.getCurrentUserFromBackend()
+        .pipe(untilDestroyed(this))
+        .subscribe((user) => {
         this.redirectToMainPageOrUrl();
       });
+    } else {
+      this.showInfoblock = false;
+      this.showTotpInvalid = true;
+      this.totpCodeSent = false;
     }
-
-    this.showInfoblock = false;
-    this.showTotpInvalid = true;
-    this.totpCodeSent = false;
   });
 }
src/app/shared/services/auth/auth.service.ts (1)

72-81: Only clear the session on auth-related errors in getCurrentUserFromBackend.

The current catchError clears the session for all errors (network failures, 5xx, etc.), which logs out users on transient failures. Clear the session only for 401 (invalid token) and 403 (forbidden) status codes.

Suggested fix
+import { HttpErrorResponse } from "@angular/common/http";

-      catchError(() => {
-        this.clearSession();
-        return of(null);
-      }),
+      catchError((error: HttpErrorResponse) => {
+        if (error.status === 401 || error.status === 403) {
+          this.clearSession();
+        }
+        return of(null);
+      }),
🤖 Fix all issues with AI agents
In `@src/app/modules/admin/components/m2m-clients/m2m-client-create.component.ts`:
- Around line 56-78: Before performing the create request in onSubmit, reset the
previous create state so stale secrets/flags aren't shown: clear
this.createdClient (set to null/undefined) and reset any "copied" flag/state
(e.g., this.copied = false) and ensure isLoading is set appropriately; then
proceed with validation and call this.m2mClientsService.create(...). Also ensure
error and success branches still set isLoading and createdClient as before so
state stays consistent.

In `@src/app/modules/admin/components/m2m-clients/m2m-client-detail.component.ts`:
- Around line 46-68: Validate the route id in ngOnInit/loadClient to avoid NaN
lookups and ensure isLoading is cleared on error: in ngOnInit parse the id from
this.route.snapshot.params["id"] and if it is not a finite number call
this.alertService.error(...) and navigate back to "/admin/m2m-clients" without
calling loadClient; in loadClient ensure this.isLoading is set to true at start
and set to false in both the subscribe.next and subscribe.error handlers (before
navigating) so the spinner is cleared even when the lookup fails; reference
ngOnInit, loadClient, isLoading, this.route.snapshot.params, and the subscribe
error handler to locate changes.
- Around line 38-43: The editForm's rate limit fields lack the same validators
as the create form; update the FormGroup created in editForm (this.editForm =
this.fb.group(...)) to add Validators.min(1) and Validators.max(10000) to
rateLimitPerMinute and Validators.min(1) and Validators.max(1000000) to
rateLimitPerDay so validation matches the create page (keep existing
required/maxlength rules for name/description).

In
`@src/app/modules/admin/components/m2m-clients/m2m-clients-list.component.html`:
- Around line 87-113: The three icon-only action buttons in the m2m-clients-list
template (the buttons that call viewClient(client), toggleStatus(client) and
deleteClient(client)) need accessible labels: add an appropriate aria-label
attribute to each button (e.g., aria-label="View/Edit client",
aria-label="Deactivate client" / "Activate client" based on client.isActive, and
aria-label="Delete client") and mark the inner <i> icons as decorative by adding
aria-hidden="true" (or role="img" with aria-hidden) so screen readers announce
the button action instead of the icon; ensure the toggle button's aria-label
updates based on client.isActive to match the title text.

In
`@src/app/modules/admin/components/users/users-admin-page/users-admin-page.component.html`:
- Around line 168-215: The icon-only action buttons in the users admin row (the
buttons that call openUserRolesForm(item), unlockUser(item),
resetFailedAttempts(item), resendVerification(item), and forceVerifyEmail(item))
lack accessible names; add aria-label attributes to each button (e.g.,
aria-label="Change role", "Unlock user", "Reset failed attempts", "Resend
verification email", "Force verify email") matching the button's action/title so
screen readers can announce them.

In `@src/app/modules/home/components/auth-callback/auth-callback.component.ts`:
- Around line 32-39: Remove all direct console.log calls in
auth-callback.component.ts (e.g., the debug lines in AuthCallbackComponent that
log window.location.href, this.route.snapshot.queryParams,
this.route.snapshot.fragment and similar console statements across the file at
the listed ranges). Replace them with a proper logging mechanism or gated debug
logging: either call the app logger service (e.g., this.logger.debug(...)) or
wrap logs behind an environment/debug flag so nothing sensitive is printed in
production; ensure you update all occurrences referenced (around the ngOnInit
and other methods that use console.log) rather than leaving any stray console
statements.
- Around line 162-174: The parseFragment function currently uses const [key,
value] = pair.split("=") which truncates values containing '=' (e.g., base64
tokens); change the split logic in parseFragment to find the first '=' (use
pair.indexOf('=')) and slice the key as pair.slice(0, idx) and the value as
pair.slice(idx + 1) so the value retains any '=' characters, then
decodeURIComponent both key and value and handle missing '=' (skip or treat
value as empty) accordingly.

In
`@src/app/modules/home/components/forgot-password-page/forgot-password-page.component.html`:
- Around line 24-33: The email input in forgot-password-page.component (the
<input> with id="email" and [(ngModel)]="email") uses an English placeholder;
update it to Russian or add i18n metadata per guidelines — either replace
placeholder="Enter your email address" with the appropriate Russian string or
add an i18n attribute (e.g.,
i18n-placeholder="@@forgotPassword.emailPlaceholder") and provide the localized
text in the translation files so the placeholder matches the Russian UI.

In `@src/app/modules/home/components/login-page/login-page.component.html`:
- Around line 85-89: The loading text "Signing in..." is not localized; update
the template around the isLoading span (referencing the isLoading conditional,
spinner-wrapper span and spinner-border) to use the app's i18n approach: either
replace the hardcoded English with the Russian equivalent or add i18n metadata
(e.g., i18n attribute) or use the translation key (e.g., a translate pipe or
i18n key like LOGIN.SIGNING_IN) so the loading text is localized consistently
with the rest of the page.
- Around line 62-71: The password visibility toggle button is currently removed
from keyboard focus with tabindex="-1"; remove that attribute so the button is
focusable, and add accessible state/label bindings: bind aria-pressed to
showPassword and set an aria-label that changes with showPassword (e.g., "Show
password"/"Hide password") so screen readers get context; keep the existing
(click)="togglePasswordVisibility()" handler (and class "password-toggle" and
icon logic using showPassword) unchanged.

In `@src/app/modules/home/components/login-page/login-page.component.ts`:
- Around line 55-81: Replace the nested subscribe pattern in the login flow by
flattening the observables: call authService.loginWithEmail(this.email,
this.password).pipe(untilDestroyed(this), switchMap(() =>
authService.getCurrentUserFromBackend()), untilDestroyed(this)).subscribe({...})
so that login errors are handled in the single subscription error handler (using
alertService.error and setting this.isLoading = false) and successful completion
(or error from getCurrentUserFromBackend if you still want to ignore it)
navigates to "/me" via router.navigate; ensure you preserve setting
this.isLoading = false on both success and error paths and keep references to
the methods loginWithEmail and getCurrentUserFromBackend and the alertService
and router usage.
- Around line 37-44: The queryParams subscription in login-page.component.ts
(this.route.queryParams.subscribe) leaks because it isn't cleaned up; update the
component to unsubscribe on destroy by either piping the observable through
untilDestroyed(this) (or a takeUntil(this.destroy$) pattern) before subscribing,
or store the Subscription and call unsubscribe in ngOnDestroy; ensure you
reference the existing this.route.queryParams subscription and add/implement
ngOnDestroy (or destroy$) to reliably dispose the subscription when the
component is destroyed.

In `@src/app/modules/home/components/register-page/register-page.component.html`:
- Around line 104-109: Remove the tabindex="-1" from the password visibility
toggle buttons so they remain keyboard-focusable, and add an appropriate
aria-label (e.g., aria-label="Show password" / "Hide password" managed by the
component) to each button; update the click handler usage of
togglePasswordVisibility() in the register-page component to also set or reflect
the accessible label/state (and apply the same changes to the second
password-toggle button occurrence).
- Around line 49-70: Fix typos and mixed-language strings in the registration
template by updating the visible Russian text for the form fields: correct the
first name input placeholder bound to firstName from "Выше имя" to "Ваше имя"
and the last name input placeholder bound to lastName from "Ваша файмилия" to
"Ваша фамилия"; then scan the other inputs referenced in this template (e.g.,
ids/names like email, password, confirmPassword or any placeholders/labels
around lines mentioned) and replace any English/misspelled strings with proper
Russian equivalents so all labels/placeholders are consistent.

In
`@src/app/modules/home/components/reset-password-page/reset-password-page.component.html`:
- Around line 43-53: Add accessible labels to the password visibility toggle
buttons by adding an aria-label that reflects the current action (e.g., "Show
password" when showPassword is false and "Hide password" when showPassword is
true). Update the <button> elements that call togglePasswordVisibility() (the
password-toggle buttons referencing the showPassword property and
bi-eye/bi-eye-slash classes) to include an [attr.aria-label] binding that
toggles between the two labels, and apply the same change to the second toggle
button used elsewhere on the page.

In
`@src/app/modules/home/components/reset-password-page/reset-password-page.component.ts`:
- Around line 68-70: The setTimeout used to call
this.router.navigate(["/login"]) can fire after the component is destroyed; add
a class property (e.g., private redirectTimeoutId: any) to store the return
value of setTimeout where it is invoked (in the success handler that currently
calls setTimeout), and implement ngOnDestroy() to
clearTimeout(this.redirectTimeoutId) and null it to avoid the callback running
after destruction; ensure you still call router.navigate only when the timeout
exists and component is alive.

In
`@src/app/modules/home/components/verify-email-page/verify-email-page.component.ts`:
- Around line 17-23: The component VerifyEmailPageComponent currently uses the
raw token in URLs and starts a delayed navigation that can fire after the
component is destroyed; update any place that builds a URL or route with the
token (e.g., where token is read in ngOnInit and where resend or verify links
are constructed) to wrap the token with encodeURIComponent(token) to avoid URL
path issues, and add a class property (e.g., redirectTimer: any) to store the
timer ID and implement ngOnDestroy to call clearTimeout(this.redirectTimer) so
the pending navigation is cancelled when the component is torn down; ensure any
places that set the timer (setTimeout) assign to this.redirectTimer.

In `@src/app/services/auth-api.service.ts`:
- Around line 104-106: The getDeviceInfo method directly accesses
navigator.userAgent which will throw during SSR; update getDeviceInfo to guard
access (e.g., check typeof navigator !== 'undefined' or typeof window !==
'undefined' or use globalThis) and return a safe fallback string (like empty
string or 'server') when navigator is not present so server rendering won't
error; locate and modify the getDeviceInfo() function to implement this guard
and fallback.
🧹 Nitpick comments (39)
src/app/shared/components/dialogs/confirm-dialog/confirm-dialog.component.scss (1)

6-10: Good use of CSS variable for theming.

The switch to var(--warm-modal-overlay) aligns well with the warm theme changes across the PR. Consider adding a fallback value for resilience if the variable is undefined in some context:

 .modal {
-  background-color: var(--warm-modal-overlay);
+  background-color: var(--warm-modal-overlay, rgba(0, 0, 0, 0.6));
   -webkit-animation: fadein 0.2s;
   animation: fadein 0.2s;
 }
src/app/modules/home/components/auth-callback/auth-callback.component.ts (2)

186-222: Refactor nested subscription using RxJS operators.

Nested subscriptions are an anti-pattern that makes the code harder to test and manage. Use higher-order mapping operators like switchMap instead.

♻️ Proposed refactor
+import { switchMap, filter } from "rxjs/operators";
+import { of } from "rxjs";
...

 private handleOAuthTokens(
   accessToken: string,
   refreshToken: string,
   expiresIn: number,
 ): void {
   this.authService
     .handleOAuthCallback(accessToken, refreshToken, expiresIn)
     .pipe(
       untilDestroyed(this),
+      switchMap((user) => {
+        if (!user) {
+          this.showErrorBlock = true;
+          this.showInfoblock = false;
+          return of(null);
+        }
+        return this.authService.completeAuthentication();
+      }),
     )
-    .subscribe({
-      next: (user) => {
-        if (user) {
-          this.authService
-            .completeAuthentication()
-            .pipe(untilDestroyed(this))
-            .subscribe((totpCheck) => {
-              if (totpCheck.isMfaEnabled) {
-                this.showMfaBlock = true;
-                this.showInfoblock = false;
-              } else {
-                this.redirectToMainPageOrUrl();
-              }
-            });
-        } else {
-          this.showErrorBlock = true;
-          this.showInfoblock = false;
-        }
-      },
+    .subscribe({
+      next: (totpCheck) => {
+        if (!totpCheck) return;
+        if (totpCheck.isMfaEnabled) {
+          this.showMfaBlock = true;
+          this.showInfoblock = false;
+        } else {
+          this.redirectToMainPageOrUrl();
+        }
+      },
       error: (err) => {
         console.error("[AuthCallback] handleOAuthCallback error:", err);
         this.showErrorBlock = true;
         this.showInfoblock = false;
       },
     });
 }

53-56: Clarify the test detection logic.

The comment suggests this check is for detecting test execution, but queryParams == null using loose equality is an unusual way to detect tests. If Angular's test utilities set up routes differently, consider using a more explicit test detection mechanism or environment flag.

src/environments/environment.staging.ts (1)

1-12: Consider deriving auth redirect URIs from baseUrl to prevent drift.
Keeps staging config consistent if the host changes later.

♻️ Suggested refactor
-export const environment = {
+const baseUrl = "http://localhost:4200";
+
+export const environment = {
   production: true,
   staging: true,
   isUnderMaintenance: false,
   type: "staging",
-  baseUrl: "http://localhost:4200",
+  baseUrl,
   resourceApiURI: "https://api.techinterview.space",
   name: "staging",
   auth: {
-    redirectUri: "http://localhost:4200/auth-callback",
-    postLogoutRedirectUri: "http://localhost:4200/logout-callback",
+    redirectUri: `${baseUrl}/auth-callback`,
+    postLogoutRedirectUri: `${baseUrl}/logout-callback`,
   },
src/app/services/user-admin.service.ts (1)

50-76: Consider DRY-ing the repeated action POSTs.
These four methods share the same URL pattern and response shape; a small private helper would reduce duplication and centralize path changes.

♻️ Example refactor
+  private postUserAction(
+    userId: number,
+    action: "unlock" | "reset-failed-attempts" | "resend-verification" | "force-verify-email",
+  ): Observable<{ message: string }> {
+    return this.api.post<{ message: string }>(`${this.apiUrl}${userId}/${action}`, {});
+  }
+
   unlockUser(userId: number): Observable<{ message: string }> {
-    return this.api.post<{ message: string }>(
-      `${this.apiUrl}${userId}/unlock`,
-      {},
-    );
+    return this.postUserAction(userId, "unlock");
   }
@@
   resetFailedAttempts(userId: number): Observable<{ message: string }> {
-    return this.api.post<{ message: string }>(
-      `${this.apiUrl}${userId}/reset-failed-attempts`,
-      {},
-    );
+    return this.postUserAction(userId, "reset-failed-attempts");
   }
@@
   resendVerification(userId: number): Observable<{ message: string }> {
-    return this.api.post<{ message: string }>(
-      `${this.apiUrl}${userId}/resend-verification`,
-      {},
-    );
+    return this.postUserAction(userId, "resend-verification");
   }
@@
   forceVerifyEmail(userId: number): Observable<{ message: string }> {
-    return this.api.post<{ message: string }>(
-      `${this.apiUrl}${userId}/force-verify-email`,
-      {},
-    );
+    return this.postUserAction(userId, "force-verify-email");
   }
src/app/modules/admin/components/m2m-clients/m2m-clients-list.component.html (1)

37-68: Add trackBy functions and extract complex template expressions into component methods.

The *ngFor loops on clients (line 38) and scopes (line 61) lack trackBy functions, causing unnecessary DOM recreation during change detection. Additionally, the inline ternary operator with slice() (lines 50–52), the clientId.slice(0, 12) (line 57), and scopes.slice(0, 3) (line 61) violate the coding guideline requiring templates to avoid complex expressions.

Create three component methods—trackByClientId(), displayDescription(), displayClientId(), and visibleScopes()—and update the template to use them. This improves performance, readability, and maintainability.

♻️ Suggested template adjustments
-        <tr
-          *ngFor="let client of clients"
-          [class.inactive-row]="!client.isActive"
-        >
+        <tr
+          *ngFor="let client of clients; trackBy: trackByClientId"
+          [class.inactive-row]="!client.isActive"
+        >
@@
-            <div class="client-description" *ngIf="client.description">
-              {{
-                client.description.length > 50
-                  ? client.description.slice(0, 50) + "..."
-                  : client.description
-              }}
-            </div>
+            <div class="client-description" *ngIf="client.description">
+              {{ displayDescription(client) }}
+            </div>
@@
-            <code class="client-id">{{ client.clientId.slice(0, 12) }}...</code>
+            <code class="client-id">{{ displayClientId(client) }}</code>
@@
-            <span
-              *ngFor="let scope of client.scopes.slice(0, 3)"
-              class="badge badge-scope"
-            >
+            <span
+              *ngFor="let scope of visibleScopes(client); trackBy: trackByScope"
+              class="badge badge-scope"
+            >
src/app/modules/admin/components/salaries/historical-data-templates/historical-data-templates.component.scss (1)

32-77: Consider alternatives to ::ng-deep (deprecated).

::ng-deep is deprecated and may be removed in future Angular versions. While it's currently necessary for styling third-party components like ng-select, consider:

  • Using the component's native theming/styling API if available
  • Moving these styles to a global stylesheet with proper scoping

For now, this usage is acceptable given the need to style ng-select internals.

src/app/modules/salaries/components/add-salary/add-salary.component.scss (1)

76-79: Duplicate responsive rules for .form-container.

Lines 76-79 and 108-110 both define responsive styles for .form-container at the max-width: 768px breakpoint with slightly different padding values (1.5rem vs 1.5rem 1rem). The second rule (lines 108-110) will override the first.

Consider consolidating these into a single media query block for clarity.

Suggested consolidation
 `@media` (max-width: 768px) {
-  padding: 1.5rem;
+  padding: 1.5rem 1rem;
   border-radius: 12px;
 }

Then remove lines 107-115.

Also applies to: 107-115

src/app/modules/companies/components/company-review-block/company-review-block.component.scss (1)

186-194: Consider using CSS variables for hardcoded rgba values.

The tag background colors use hardcoded rgba values which won't adapt to theme changes. Consider defining these as CSS variables in the theme file for consistency.

Suggested approach
     &.positive {
-      background-color: rgba(129, 178, 154, 0.15);
+      background-color: var(--warm-success-bg-subtle);
       color: var(--warm-success);
     }

     &.negative {
-      background-color: rgba(217, 85, 80, 0.15);
+      background-color: var(--warm-danger-bg-subtle);
       color: var(--warm-danger);
     }

Define the corresponding variables in your theme file.

src/app/modules/companies/components/add-review-page/add-review-page.component.scss (1)

141-145: Consider using a CSS variable for the focus ring color.

The hardcoded rgba(224, 122, 95, 0.15) won't adapt to theme changes. This pattern appears in multiple files.

Suggested approach
     &:focus {
       border-color: var(--warm-accent-primary);
-      box-shadow: 0 0 0 3px rgba(224, 122, 95, 0.15);
+      box-shadow: 0 0 0 3px var(--warm-input-focus-ring);
       outline: none;
     }
src/app/modules/admin/components/m2m-clients/m2m-client-detail.component.scss (2)

35-44: Consider using a CSS variable for the alert background.

The hardcoded rgba(255, 193, 7, 0.15) won't adapt to theme changes.

Suggested approach
 .secret-alert {
   display: flex;
   justify-content: space-between;
   align-items: flex-start;
-  background-color: rgba(255, 193, 7, 0.15);
+  background-color: var(--warm-warning-bg-subtle);
   border: 1px solid var(--warm-warning);

71-74: Consider defining a CSS variable for the monospace font stack.

The font-family "JetBrains Mono", monospace is repeated multiple times (lines 72, 123, 226, 275). A CSS variable would improve maintainability.

Suggested approach

Define in your theme file:

--warm-font-mono: "JetBrains Mono", monospace;

Then use throughout:

   .secret-input {
-    font-family: "JetBrains Mono", monospace;
+    font-family: var(--warm-font-mono);
     font-size: 0.9rem;
   }
src/app/modules/home/components/register-page/register-page.component.scss (3)

37-43: Consider using a CSS variable for the heading font.

The font-family is hardcoded. If --warm-font-heading is already defined (as seen in other components), use it here for consistency.

Suggested approach
 .auth-title {
-  font-family: "Lora", Georgia, serif;
+  font-family: var(--warm-font-heading);
   font-size: 1.75rem;

50-67: Consider using a CSS variable for the success background.

The hardcoded rgba(107, 142, 35, 0.1) won't adapt to theme changes, similar to other alert backgrounds in this PR.

Suggested approach
 .success-message {
   display: flex;
   align-items: flex-start;
   gap: 1rem;
   padding: 1.25rem;
-  background-color: rgba(107, 142, 35, 0.1);
+  background-color: var(--warm-success-bg-subtle);
   border: 1px solid var(--warm-success);

170-178: Consider using a CSS variable for the focus ring.

Same pattern as other files - hardcoded rgba(224, 122, 95, 0.15) could be var(--warm-input-focus-ring) for consistency.

src/app/shared/services/auth/token-storage.service.ts (1)

24-37: Consider wrapping localStorage operations in try-catch.

While the browser check is good for SSR, localStorage operations can throw exceptions in certain scenarios (storage quota exceeded, private browsing mode in some browsers, security restrictions). This could cause runtime errors.

💡 Optional: Add defensive error handling
 setTokens(
   accessToken: string,
   refreshToken: string,
   expiresIn: number,
 ): void {
   if (!this.isBrowser) {
     return;
   }

-  const expiresAt = Date.now() + expiresIn * 1000;
-  localStorage.setItem(this.ACCESS_TOKEN_KEY, accessToken);
-  localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken);
-  localStorage.setItem(this.EXPIRES_AT_KEY, expiresAt.toString());
+  try {
+    const expiresAt = Date.now() + expiresIn * 1000;
+    localStorage.setItem(this.ACCESS_TOKEN_KEY, accessToken);
+    localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken);
+    localStorage.setItem(this.EXPIRES_AT_KEY, expiresAt.toString());
+  } catch (e) {
+    console.error('Failed to store tokens:', e);
+  }
 }
src/app/shared/services/auth/auth.session.service.ts (3)

21-30: Redundant assertion inside truthy block.

On line 23, Assertion.notNull(authInfo, "authInfo") is called after already checking if (authInfo) on line 22. The assertion will never fail since authInfo is guaranteed to be truthy at that point.

♻️ Remove redundant assertion
 set auth(authInfo: AuthInfo | null) {
   if (authInfo) {
-    Assertion.notNull(authInfo, "authInfo");
     this.session.setItem(this.authorizationStorageSessionKey, authInfo);
     this.session.setItem(this.authorizationTimestampSessionKey, Date.now());
   } else {
     this.session.removeItem(this.authorizationStorageSessionKey);
     this.session.removeItem(this.authorizationTimestampSessionKey);
   }
 }

32-41: Redundant null coalescing operator.

On line 40, authInfo ?? null is unnecessary because line 36 already returns null if authInfo == null. At line 40, authInfo is guaranteed to be non-null.

♻️ Simplify return statement
 get auth(): AuthInfo | null {
   const authInfo = this.session.getItem<AuthInfo>(
     this.authorizationStorageSessionKey,
   );
   if (authInfo == null || this.sessionExpired) {
     return null;
   }

-  return authInfo ?? null;
+  return authInfo;
 }

43-50: Same redundant assertion pattern in applicationUser setter.

The Assertion.notNull(user, "user") on line 45 is redundant since user is already confirmed truthy by the if (user) check.

♻️ Remove redundant assertion
 set applicationUser(user: ApplicationUser | null) {
   if (user) {
-    Assertion.notNull(user, "user");
     this.session.setItem(this.applicationUserStorageSessionKey, user);
   } else {
     this.session.removeItem(this.applicationUserStorageSessionKey);
   }
 }
src/app/modules/admin/services/m2m-clients.service.ts (1)

1-19: Use ApiService wrapper for consistency with the codebase pattern.

This service uses HttpClient directly, while 16+ other services in the codebase use the ApiService wrapper. Consider refactoring to use ApiService to leverage centralized request handling (base URL, headers, error transformation, and retry strategies).

src/app/modules/admin/components/m2m-clients/m2m-client-create.component.html (1)

48-63: Add trackBy for scope lists to prevent DOM churn.

Without trackBy, Angular recreates items on each array update, which can reset checkbox focus/selection. Please verify and add trackBy handlers in the component (e.g., by scope.value).

♻️ Suggested template update
-            <div *ngFor="let scope of availableScopes" class="scope-item">
+            <div
+              *ngFor="let scope of availableScopes; trackBy: trackByScope"
+              class="scope-item"
+            >

-          <span
-            *ngFor="let scope of createdClient.scopes"
-            class="badge badge-scope"
-          >
+          <span
+            *ngFor="let scope of createdClient.scopes; trackBy: trackByScopeValue"
+            class="badge badge-scope"
+          >

Also applies to: 163-168

src/app/modules/home/components/register-page/register-page.component.ts (1)

96-106: Keep submit disabled when passwords mismatch.

isFormValid() doesn't include the confirmation match, so the submit button can enable even when the passwords differ. Folding the check into validation avoids the extra warning step.

♻️ Suggested update
-      this.confirmPassword.trim() !== "" &&
+      this.confirmPassword.trim() !== "" &&
+      this.password === this.confirmPassword &&
src/app/modules/home/components/reset-password-page/reset-password-page.component.ts (3)

31-31: Consider moving title setup to ngOnInit for consistency.

Setting the title in the constructor works, but moving it to ngOnInit would be more consistent with Angular lifecycle patterns and align with how ngOnDestroy resets the title.


98-117: Duplicated password validation logic between isPasswordStrong() and getPasswordStrength().

Both methods compute hasLower, hasUpper, and hasDigit independently. Consider consolidating this logic to avoid duplication and ensure consistency.

♻️ Proposed refactor
+  private getPasswordCriteria(): { hasLower: boolean; hasUpper: boolean; hasDigit: boolean; hasSpecial: boolean } {
+    return {
+      hasLower: /[a-z]/.test(this.newPassword),
+      hasUpper: /[A-Z]/.test(this.newPassword),
+      hasDigit: /\d/.test(this.newPassword),
+      hasSpecial: /[!@#$%^&*(),.?":{}|<>]/.test(this.newPassword),
+    };
+  }
+
   isPasswordStrong(): boolean {
-    const hasLower = /[a-z]/.test(this.newPassword);
-    const hasUpper = /[A-Z]/.test(this.newPassword);
-    const hasDigit = /\d/.test(this.newPassword);
-    return hasLower && hasUpper && hasDigit;
+    const { hasLower, hasUpper, hasDigit } = this.getPasswordCriteria();
+    return hasLower && hasUpper && hasDigit;
   }

   getPasswordStrength(): "weak" | "medium" | "strong" {
     if (this.newPassword.length < 8) return "weak";
-    const hasLower = /[a-z]/.test(this.newPassword);
-    const hasUpper = /[A-Z]/.test(this.newPassword);
-    const hasDigit = /\d/.test(this.newPassword);
-    const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(this.newPassword);
+    const { hasLower, hasUpper, hasDigit, hasSpecial } = this.getPasswordCriteria();
     const score = [hasLower, hasUpper, hasDigit, hasSpecial].filter(Boolean).length;
     if (score >= 4 && this.newPassword.length >= 12) return "strong";
     if (score >= 3) return "medium";
     return "weak";
   }

8-13: Consider adding changeDetection: ChangeDetectionStrategy.OnPush for better performance.

Since this component uses explicit state management with properties like isLoading, successMessage, and errorMessage, it's a good candidate for OnPush change detection.

src/app/modules/home/components/reset-password-page/reset-password-page.component.scss (1)

55-55: Consider using CSS variables for the rgba background colors.

The hardcoded rgba(192, 80, 77, 0.1) and rgba(107, 142, 35, 0.1) values may not adapt well to theme changes. Consider defining CSS variables for these semi-transparent backgrounds or deriving them from the existing --warm-danger and --warm-success tokens.

Also applies to: 75-75

src/app/modules/home/components/reset-password-page/reset-password-page.component.html (1)

55-60: Repeated method calls in template may impact performance.

getPasswordStrength() is called twice in the template (lines 56, 57-58), and isFormValid() is called on every change detection cycle (line 106). Consider caching these values in the component or using getter properties with OnPush change detection.

Also applies to: 105-107

src/app/modules/home/components/verify-email-page/verify-email-page.component.html (1)

37-62: Consider wrapping the resend section in a <form> element.

The email input and submit button work without a form wrapper, but wrapping them in <form> would enable keyboard submission (Enter key) and improve semantic structure.

♻️ Proposed refactor
     <!-- Resend Form -->
     <div *ngIf="canResend && !successMessage" class="resend-section">
       <p class="resend-info">
         Enter your email address to receive a new verification link.
       </p>

+      <form (ngSubmit)="resendVerification()">
         <div class="form-group">
           <label for="email" class="form-label">Email</label>
           <input
             type="email"
             id="email"
             class="form-control"
             [(ngModel)]="email"
             name="email"
             placeholder="Enter your email address"
             [disabled]="isResending"
             autocomplete="email"
           />
         </div>

         <button
-          type="button"
+          type="submit"
           class="btn-submit"
-          (click)="resendVerification()"
           [disabled]="isResending || !isEmailValid()"
         >
           <span *ngIf="!isResending">Resend Verification Email</span>
           <span *ngIf="isResending" class="spinner-wrapper">
             <span class="spinner-border spinner-border-sm" role="status"></span>
             Sending...
           </span>
         </button>
+      </form>
     </div>
src/app/modules/admin/components/m2m-clients/m2m-client-create.component.scss (1)

75-137: Add small-screen stacking for flex rows/actions.

On narrow screens, .form-row and .form-actions stay in a single row and can overflow. Consider stacking them below 600px like .scopes-grid does. As per coding guidelines.

♻️ Suggested responsive tweaks
 .form-row {
   display: flex;
   gap: 1rem;
+  flex-wrap: wrap;
 
   .half {
     flex: 1;
   }
+
+  `@media` (max-width: 600px) {
+    flex-direction: column;
+  }
 }
@@
 .form-actions {
   display: flex;
   gap: 0.75rem;
   margin-top: 1.5rem;
+
+  `@media` (max-width: 600px) {
+    flex-direction: column;
+  }
 }
src/app/modules/home/components/forgot-password-page/forgot-password-page.component.ts (1)

54-58: Consider extracting the shared email validation regex.

The email regex /^[^\s@]+@[^\s@]+\.[^\s@]+$/ is duplicated across LoginPageComponent, RegisterPageComponent, and this component. Consider extracting it to a shared utility or validator.

♻️ Suggested refactor

Create a shared validation utility:

// src/app/shared/utils/validators.ts
export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export function isValidEmail(email: string): boolean {
  return email.trim() !== '' && EMAIL_REGEX.test(email);
}

Then use in components:

+import { isValidEmail } from '@shared/utils/validators';

 isEmailValid(): boolean {
-  return (
-    this.email.trim() !== "" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email)
-  );
+  return isValidEmail(this.email);
 }
src/app/modules/admin/components/m2m-clients/m2m-clients-list.component.ts (3)

72-79: Consider using the app's confirm dialog component instead of native confirm().

The codebase has a confirm-dialog component (referenced in src/app/shared/components/dialogs/confirm-dialog/confirm-dialog.component.ts). Using it would provide a consistent UX across the application.


55-70: Direct state mutation may cause inconsistency.

Line 62 directly mutates client.isActive based on the assumed toggle result rather than using the API response. If the server returns a different state, the UI will be out of sync.

♻️ Suggested improvement
 toggleStatus(client: M2mClientModel): void {
   const action = client.isActive
     ? this.m2mClientsService.deactivate(client.id)
     : this.m2mClientsService.activate(client.id);

   action.pipe(untilDestroyed(this)).subscribe({
-    next: () => {
-      client.isActive = !client.isActive;
+    next: (updatedClient) => {
+      const index = this.clients.findIndex(c => c.id === client.id);
+      if (index !== -1) {
+        this.clients[index] = updatedClient;
+      }
       this.alertService.success(
-        `Client ${client.isActive ? "activated" : "deactivated"}`,
+        `Client ${updatedClient.isActive ? "activated" : "deactivated"}`,
       );
     },
     error: (err) =>
       this.alertService.error(err.error?.message || "Action failed"),
   });
 }

This assumes the API returns the updated client. If it doesn't, the current approach is acceptable but should reload from the server for correctness.


28-28: Empty ngOnDestroy is intentional.

The empty body is required for the untilDestroyed operator to function correctly. Consider adding a brief comment to clarify this for future maintainers.

- ngOnDestroy(): void {}
+ ngOnDestroy(): void {
+   // Required for untilDestroyed operator
+ }
src/app/modules/admin/components/m2m-clients/m2m-client-detail.component.html (3)

59-63: Add trackBy function for *ngFor to improve performance.

When iterating over arrays, using trackBy helps Angular identify which items changed, reducing unnecessary DOM updates.

- *ngFor="let scope of generatedToken.scopes"
+ *ngFor="let scope of generatedToken.scopes; trackBy: trackByScope"

Add to component:

trackByScope(index: number, scope: string): string {
  return scope;
}

182-196: Add trackBy function for available scopes iteration.

Same as above, consider adding trackBy for the availableScopes loop.

- <div *ngFor="let scope of availableScopes" class="scope-toggle">
+ <div *ngFor="let scope of availableScopes; trackBy: trackByScopeValue" class="scope-toggle">

26-31: Security consideration: Secret input should use appropriate input type.

The secret is displayed in a type="text" input. While it's intentional to show the secret for copying, consider if masking with a toggle would be more appropriate to prevent shoulder-surfing.

src/app/modules/home/components/login-page/login-page.component.ts (1)

103-105: Add a comment noting ngOnDestroy is required for untilDestroyed.

Similar to other components, it helps maintainability to document why ngOnDestroy exists.

src/app/modules/admin/components/m2m-clients/m2m-client-create.component.ts (1)

81-87: Handle clipboard failures instead of assuming success.

navigator.clipboard.writeText is async and can reject (permissions/HTTP); consider handling failure and only toggling the copied state after success.

♻️ Suggested hardening
   copySecret(): void {
-    if (this.createdClient) {
-      navigator.clipboard.writeText(this.createdClient.clientSecret);
-      this.secretCopied = true;
-      setTimeout(() => (this.secretCopied = false), 2000);
-    }
+    if (!this.createdClient) {
+      return;
+    }
+    if (!navigator.clipboard) {
+      this.alertService.error("Clipboard is not available in this browser.");
+      return;
+    }
+    navigator.clipboard
+      .writeText(this.createdClient.clientSecret)
+      .then(() => {
+        this.secretCopied = true;
+        setTimeout(() => (this.secretCopied = false), 2000);
+      })
+      .catch(() => this.alertService.error("Failed to copy client secret."));
   }
src/app/modules/home/components/verify-email-page/verify-email-page.component.ts (1)

103-107: Consider centralizing the email regex used across auth pages.

The same pattern appears in src/app/modules/home/components/forgot-password-page/forgot-password-page.component.ts and src/app/modules/home/components/login-page/login-page.component.ts. A shared validator/helper reduces drift.

Comment on lines +56 to +78
onSubmit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}

this.isLoading = true;
this.m2mClientsService
.create(this.form.value)
.pipe(untilDestroyed(this))
.subscribe({
next: (response) => {
this.isLoading = false;
this.createdClient = response;
this.alertService.success("M2M client created successfully");
},
error: (err) => {
this.isLoading = false;
this.alertService.error(
err.error?.message || "Failed to create client",
);
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reset create state before new submit to avoid stale secrets.

If a previous creation succeeded, a later failed submit can leave the old createdClient (and “copied” state) visible.

🛠️ Suggested fix
   onSubmit(): void {
     if (this.form.invalid) {
       this.form.markAllAsTouched();
       return;
     }
 
     this.isLoading = true;
+    this.createdClient = null;
+    this.secretCopied = false;
     this.m2mClientsService
       .create(this.form.value)
       .pipe(untilDestroyed(this))
       .subscribe({
🤖 Prompt for AI Agents
In `@src/app/modules/admin/components/m2m-clients/m2m-client-create.component.ts`
around lines 56 - 78, Before performing the create request in onSubmit, reset
the previous create state so stale secrets/flags aren't shown: clear
this.createdClient (set to null/undefined) and reset any "copied" flag/state
(e.g., this.copied = false) and ensure isLoading is set appropriately; then
proceed with validation and call this.m2mClientsService.create(...). Also ensure
error and success branches still set isLoading and createdClient as before so
state stays consistent.

Comment on lines +38 to +43
this.editForm = this.fb.group({
name: ["", [Validators.required, Validators.maxLength(200)]],
description: ["", Validators.maxLength(1000)],
rateLimitPerMinute: [null],
rateLimitPerDay: [null],
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find files related to m2m-clients component
find . -path "*/m2m-clients*" -type f -name "*.ts" | head -20

Repository: Techinterview-space/frontend

Length of output: 360


🏁 Script executed:

#!/bin/bash
# List all TypeScript files in the m2m-clients directory
ls -la src/app/modules/admin/components/m2m-clients/ 2>/dev/null || echo "Directory not found, searching..."
find . -name "*m2m*" -type f | grep -E "\.(ts|component)$"

Repository: Techinterview-space/frontend

Length of output: 1249


🏁 Script executed:

#!/bin/bash
# Look for the create component file
find . -name "*m2m-client-create*" -o -name "*m2m-clients-create*" | head -10

Repository: Techinterview-space/frontend

Length of output: 309


🏁 Script executed:

#!/bin/bash
# Read the m2m-client-create component to see the form group definition
cat -n src/app/modules/admin/components/m2m-clients/m2m-client-create.component.ts

Repository: Techinterview-space/frontend

Length of output: 3804


🏁 Script executed:

#!/bin/bash
# Also check the detail component to see the exact lines mentioned
cat -n src/app/modules/admin/components/m2m-clients/m2m-client-detail.component.ts

Repository: Techinterview-space/frontend

Length of output: 8098


Align rate‑limit validation with the create form.

The create page enforces min(1)–max(10000) for rateLimitPerMinute and min(1)–max(1000000) for rateLimitPerDay; the edit form currently has no validators. Add these constraints to maintain consistency.

🛠️ Suggested fix
       name: ["", [Validators.required, Validators.maxLength(200)]],
       description: ["", Validators.maxLength(1000)],
-      rateLimitPerMinute: [null],
-      rateLimitPerDay: [null],
+      rateLimitPerMinute: [null, [Validators.min(1), Validators.max(10000)]],
+      rateLimitPerDay: [null, [Validators.min(1), Validators.max(1000000)]],
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.editForm = this.fb.group({
name: ["", [Validators.required, Validators.maxLength(200)]],
description: ["", Validators.maxLength(1000)],
rateLimitPerMinute: [null],
rateLimitPerDay: [null],
});
this.editForm = this.fb.group({
name: ["", [Validators.required, Validators.maxLength(200)]],
description: ["", Validators.maxLength(1000)],
rateLimitPerMinute: [null, [Validators.min(1), Validators.max(10000)]],
rateLimitPerDay: [null, [Validators.min(1), Validators.max(1000000)]],
});
🤖 Prompt for AI Agents
In `@src/app/modules/admin/components/m2m-clients/m2m-client-detail.component.ts`
around lines 38 - 43, The editForm's rate limit fields lack the same validators
as the create form; update the FormGroup created in editForm (this.editForm =
this.fb.group(...)) to add Validators.min(1) and Validators.max(10000) to
rateLimitPerMinute and Validators.min(1) and Validators.max(1000000) to
rateLimitPerDay so validation matches the create page (keep existing
required/maxlength rules for name/description).

Comment on lines +46 to +68
ngOnInit(): void {
const id = +this.route.snapshot.params["id"];
this.loadClient(id);
}

ngOnDestroy(): void {}

loadClient(id: number): void {
this.isLoading = true;
this.m2mClientsService
.getById(id)
.pipe(untilDestroyed(this))
.subscribe({
next: (client) => {
this.client = client;
this.editForm.patchValue(client);
this.isLoading = false;
},
error: () => {
this.alertService.error("Failed to load client");
this.router.navigate(["/admin/m2m-clients"]);
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against invalid route IDs and clear loading on error.

Prevents NaN lookups and avoids leaving the spinner on if navigation is blocked.

🛠️ Suggested fix
   ngOnInit(): void {
-    const id = +this.route.snapshot.params["id"];
+    const id = Number(this.route.snapshot.params["id"]);
+    if (!Number.isFinite(id)) {
+      this.alertService.error("Invalid client id.");
+      this.router.navigate(["/admin/m2m-clients"]);
+      return;
+    }
     this.loadClient(id);
   }
@@
         error: () => {
+          this.isLoading = false;
           this.alertService.error("Failed to load client");
           this.router.navigate(["/admin/m2m-clients"]);
         },
🤖 Prompt for AI Agents
In `@src/app/modules/admin/components/m2m-clients/m2m-client-detail.component.ts`
around lines 46 - 68, Validate the route id in ngOnInit/loadClient to avoid NaN
lookups and ensure isLoading is cleared on error: in ngOnInit parse the id from
this.route.snapshot.params["id"] and if it is not a finite number call
this.alertService.error(...) and navigate back to "/admin/m2m-clients" without
calling loadClient; in loadClient ensure this.isLoading is set to true at start
and set to false in both the subscribe.next and subscribe.error handlers (before
navigating) so the spinner is cleared even when the lookup fails; reference
ngOnInit, loadClient, isLoading, this.route.snapshot.params, and the subscribe
error handler to locate changes.

Comment on lines +87 to +113
<button
class="btn btn-outline-primary"
(click)="viewClient(client)"
title="View/Edit"
>
<i class="bi bi-pencil"></i>
</button>
<button
class="btn"
[ngClass]="
client.isActive
? 'btn-outline-warning'
: 'btn-outline-success'
"
(click)="toggleStatus(client)"
[title]="client.isActive ? 'Deactivate' : 'Activate'"
>
<i
class="bi"
[ngClass]="client.isActive ? 'bi-pause' : 'bi-play'"
></i>
</button>
<button
class="btn btn-outline-danger"
(click)="deleteClient(client)"
title="Delete"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add accessible labels for icon-only action buttons.
Icon-only buttons are not announced by screen readers, which blocks keyboard/screen-reader users from understanding actions. Add aria-label on the buttons and mark icons as decorative. As per coding guidelines, ensure accessibility in templates.

✅ Suggested fix
-              <button
-                class="btn btn-outline-primary"
-                (click)="viewClient(client)"
-                title="View/Edit"
-              >
-                <i class="bi bi-pencil"></i>
-              </button>
+              <button
+                class="btn btn-outline-primary"
+                (click)="viewClient(client)"
+                title="View/Edit"
+                aria-label="View or edit client"
+              >
+                <i class="bi bi-pencil" aria-hidden="true"></i>
+              </button>
@@
-              <button
-                class="btn"
-                [ngClass]="
-                  client.isActive
-                    ? 'btn-outline-warning'
-                    : 'btn-outline-success'
-                "
-                (click)="toggleStatus(client)"
-                [title]="client.isActive ? 'Deactivate' : 'Activate'"
-              >
-                <i
-                  class="bi"
-                  [ngClass]="client.isActive ? 'bi-pause' : 'bi-play'"
-                ></i>
-              </button>
+              <button
+                class="btn"
+                [ngClass]="
+                  client.isActive
+                    ? 'btn-outline-warning'
+                    : 'btn-outline-success'
+                "
+                (click)="toggleStatus(client)"
+                [title]="client.isActive ? 'Deactivate' : 'Activate'"
+                [attr.aria-label]="client.isActive ? 'Deactivate client' : 'Activate client'"
+              >
+                <i
+                  class="bi"
+                  [ngClass]="client.isActive ? 'bi-pause' : 'bi-play'"
+                  aria-hidden="true"
+                ></i>
+              </button>
@@
-              <button
-                class="btn btn-outline-danger"
-                (click)="deleteClient(client)"
-                title="Delete"
-              >
-                <i class="bi bi-trash"></i>
-              </button>
+              <button
+                class="btn btn-outline-danger"
+                (click)="deleteClient(client)"
+                title="Delete"
+                aria-label="Delete client"
+              >
+                <i class="bi bi-trash" aria-hidden="true"></i>
+              </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
class="btn btn-outline-primary"
(click)="viewClient(client)"
title="View/Edit"
>
<i class="bi bi-pencil"></i>
</button>
<button
class="btn"
[ngClass]="
client.isActive
? 'btn-outline-warning'
: 'btn-outline-success'
"
(click)="toggleStatus(client)"
[title]="client.isActive ? 'Deactivate' : 'Activate'"
>
<i
class="bi"
[ngClass]="client.isActive ? 'bi-pause' : 'bi-play'"
></i>
</button>
<button
class="btn btn-outline-danger"
(click)="deleteClient(client)"
title="Delete"
>
<button
class="btn btn-outline-primary"
(click)="viewClient(client)"
title="View/Edit"
aria-label="View or edit client"
>
<i class="bi bi-pencil" aria-hidden="true"></i>
</button>
<button
class="btn"
[ngClass]="
client.isActive
? 'btn-outline-warning'
: 'btn-outline-success'
"
(click)="toggleStatus(client)"
[title]="client.isActive ? 'Deactivate' : 'Activate'"
[attr.aria-label]="client.isActive ? 'Deactivate client' : 'Activate client'"
>
<i
class="bi"
[ngClass]="client.isActive ? 'bi-pause' : 'bi-play'"
aria-hidden="true"
></i>
</button>
<button
class="btn btn-outline-danger"
(click)="deleteClient(client)"
title="Delete"
aria-label="Delete client"
>
<i class="bi bi-trash" aria-hidden="true"></i>
</button>
🤖 Prompt for AI Agents
In `@src/app/modules/admin/components/m2m-clients/m2m-clients-list.component.html`
around lines 87 - 113, The three icon-only action buttons in the
m2m-clients-list template (the buttons that call viewClient(client),
toggleStatus(client) and deleteClient(client)) need accessible labels: add an
appropriate aria-label attribute to each button (e.g., aria-label="View/Edit
client", aria-label="Deactivate client" / "Activate client" based on
client.isActive, and aria-label="Delete client") and mark the inner <i> icons as
decorative by adding aria-hidden="true" (or role="img" with aria-hidden) so
screen readers announce the button action instead of the icon; ensure the toggle
button's aria-label updates based on client.isActive to match the title text.

Comment on lines +168 to +215
<div class="btn-group btn-group-sm" role="group">
<button
type="button"
class="btn btn-outline-primary btn-sm"
(click)="openUserRolesForm(item)"
title="Изменить роль"
>
<i class="bi bi-person-gear"></i>
</button>
<button
*ngIf="item.isLockedOut"
type="button"
class="btn btn-outline-success btn-sm"
(click)="unlockUser(item)"
title="Разблокировать"
>
<i class="bi bi-unlock"></i>
</button>
<button
*ngIf="
item.failedLoginAttempts && item.failedLoginAttempts > 0
"
type="button"
class="btn btn-outline-warning btn-sm"
(click)="resetFailedAttempts(item)"
title="Сбросить попытки"
>
<i class="bi bi-arrow-counterclockwise"></i>
</button>
<button
*ngIf="!item.emailConfirmed"
type="button"
class="btn btn-outline-info btn-sm"
(click)="resendVerification(item)"
title="Отправить письмо"
>
<i class="bi bi-envelope"></i>
</button>
<button
*ngIf="!item.emailConfirmed"
type="button"
class="btn btn-outline-success btn-sm"
(click)="forceVerifyEmail(item)"
title="Подтвердить email"
>
<i class="bi bi-check-circle"></i>
</button>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add accessible labels to icon-only action buttons.

Icon-only buttons should expose an accessible name so screen readers can announce the action. As per coding guidelines, please add aria-label attributes.

🔧 Suggested update
-                  <button
+                  <button
                     type="button"
                     class="btn btn-outline-primary btn-sm"
                     (click)="openUserRolesForm(item)"
                     title="Изменить роль"
+                    aria-label="Изменить роль"
                   >
                     <i class="bi bi-person-gear"></i>
                   </button>
                   <button
                     *ngIf="item.isLockedOut"
                     type="button"
                     class="btn btn-outline-success btn-sm"
                     (click)="unlockUser(item)"
                     title="Разблокировать"
+                    aria-label="Разблокировать"
                   >
                     <i class="bi bi-unlock"></i>
                   </button>
                   <button
                     *ngIf="
                       item.failedLoginAttempts && item.failedLoginAttempts > 0
                     "
                     type="button"
                     class="btn btn-outline-warning btn-sm"
                     (click)="resetFailedAttempts(item)"
                     title="Сбросить попытки"
+                    aria-label="Сбросить попытки"
                   >
                     <i class="bi bi-arrow-counterclockwise"></i>
                   </button>
                   <button
                     *ngIf="!item.emailConfirmed"
                     type="button"
                     class="btn btn-outline-info btn-sm"
                     (click)="resendVerification(item)"
                     title="Отправить письмо"
+                    aria-label="Отправить письмо"
                   >
                     <i class="bi bi-envelope"></i>
                   </button>
                   <button
                     *ngIf="!item.emailConfirmed"
                     type="button"
                     class="btn btn-outline-success btn-sm"
                     (click)="forceVerifyEmail(item)"
                     title="Подтвердить email"
+                    aria-label="Подтвердить email"
                   >
                     <i class="bi bi-check-circle"></i>
                   </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="btn-group btn-group-sm" role="group">
<button
type="button"
class="btn btn-outline-primary btn-sm"
(click)="openUserRolesForm(item)"
title="Изменить роль"
>
<i class="bi bi-person-gear"></i>
</button>
<button
*ngIf="item.isLockedOut"
type="button"
class="btn btn-outline-success btn-sm"
(click)="unlockUser(item)"
title="Разблокировать"
>
<i class="bi bi-unlock"></i>
</button>
<button
*ngIf="
item.failedLoginAttempts && item.failedLoginAttempts > 0
"
type="button"
class="btn btn-outline-warning btn-sm"
(click)="resetFailedAttempts(item)"
title="Сбросить попытки"
>
<i class="bi bi-arrow-counterclockwise"></i>
</button>
<button
*ngIf="!item.emailConfirmed"
type="button"
class="btn btn-outline-info btn-sm"
(click)="resendVerification(item)"
title="Отправить письмо"
>
<i class="bi bi-envelope"></i>
</button>
<button
*ngIf="!item.emailConfirmed"
type="button"
class="btn btn-outline-success btn-sm"
(click)="forceVerifyEmail(item)"
title="Подтвердить email"
>
<i class="bi bi-check-circle"></i>
</button>
</div>
<div class="btn-group btn-group-sm" role="group">
<button
type="button"
class="btn btn-outline-primary btn-sm"
(click)="openUserRolesForm(item)"
title="Изменить роль"
aria-label="Изменить роль"
>
<i class="bi bi-person-gear"></i>
</button>
<button
*ngIf="item.isLockedOut"
type="button"
class="btn btn-outline-success btn-sm"
(click)="unlockUser(item)"
title="Разблокировать"
aria-label="Разблокировать"
>
<i class="bi bi-unlock"></i>
</button>
<button
*ngIf="
item.failedLoginAttempts && item.failedLoginAttempts > 0
"
type="button"
class="btn btn-outline-warning btn-sm"
(click)="resetFailedAttempts(item)"
title="Сбросить попытки"
aria-label="Сбросить попытки"
>
<i class="bi bi-arrow-counterclockwise"></i>
</button>
<button
*ngIf="!item.emailConfirmed"
type="button"
class="btn btn-outline-info btn-sm"
(click)="resendVerification(item)"
title="Отправить письмо"
aria-label="Отправить письмо"
>
<i class="bi bi-envelope"></i>
</button>
<button
*ngIf="!item.emailConfirmed"
type="button"
class="btn btn-outline-success btn-sm"
(click)="forceVerifyEmail(item)"
title="Подтвердить email"
aria-label="Подтвердить email"
>
<i class="bi bi-check-circle"></i>
</button>
</div>
🤖 Prompt for AI Agents
In
`@src/app/modules/admin/components/users/users-admin-page/users-admin-page.component.html`
around lines 168 - 215, The icon-only action buttons in the users admin row (the
buttons that call openUserRolesForm(item), unlockUser(item),
resetFailedAttempts(item), resendVerification(item), and forceVerifyEmail(item))
lack accessible names; add aria-label attributes to each button (e.g.,
aria-label="Change role", "Unlock user", "Reset failed attempts", "Resend
verification email", "Force verify email") matching the button's action/title so
screen readers can announce them.

Comment on lines +104 to +109
<button
type="button"
class="password-toggle"
(click)="togglePasswordVisibility()"
tabindex="-1"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep password visibility toggles keyboard-accessible.

tabindex="-1" removes the buttons from the tab order; remove it and add aria-label for screen readers.

♿ Suggested accessibility tweak
-              tabindex="-1"
+              aria-label="Показать или скрыть пароль"

-              tabindex="-1"
+              aria-label="Показать или скрыть подтверждение пароля"

Also applies to: 143-148

🤖 Prompt for AI Agents
In `@src/app/modules/home/components/register-page/register-page.component.html`
around lines 104 - 109, Remove the tabindex="-1" from the password visibility
toggle buttons so they remain keyboard-focusable, and add an appropriate
aria-label (e.g., aria-label="Show password" / "Hide password" managed by the
component) to each button; update the click handler usage of
togglePasswordVisibility() in the register-page component to also set or reflect
the accessible label/state (and apply the same changes to the second
password-toggle button occurrence).

Comment on lines +43 to +53
<button
type="button"
class="password-toggle"
(click)="togglePasswordVisibility()"
tabindex="-1"
>
<i
class="bi"
[ngClass]="showPassword ? 'bi-eye-slash' : 'bi-eye'"
></i>
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add aria-label to password toggle buttons for accessibility.

Screen reader users won't understand the purpose of these buttons without accessible labels.

♿ Proposed fix for the first toggle (lines 43-53)
             <button
               type="button"
               class="password-toggle"
               (click)="togglePasswordVisibility()"
               tabindex="-1"
+              [attr.aria-label]="showPassword ? 'Hide password' : 'Show password'"
             >
♿ Proposed fix for the second toggle (lines 82-92)
             <button
               type="button"
               class="password-toggle"
               (click)="toggleConfirmPasswordVisibility()"
               tabindex="-1"
+              [attr.aria-label]="showConfirmPassword ? 'Hide password' : 'Show password'"
             >

Also applies to: 82-92

🤖 Prompt for AI Agents
In
`@src/app/modules/home/components/reset-password-page/reset-password-page.component.html`
around lines 43 - 53, Add accessible labels to the password visibility toggle
buttons by adding an aria-label that reflects the current action (e.g., "Show
password" when showPassword is false and "Hide password" when showPassword is
true). Update the <button> elements that call togglePasswordVisibility() (the
password-toggle buttons referencing the showPassword property and
bi-eye/bi-eye-slash classes) to include an [attr.aria-label] binding that
toggles between the two labels, and apply the same change to the second toggle
button used elsewhere on the page.

Comment on lines +68 to +70
setTimeout(() => {
this.router.navigate(["/login"]);
}, 2000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential memory leak with setTimeout if component is destroyed early.

If the user navigates away before the 2-second delay completes, the setTimeout callback will still fire and attempt navigation. Consider storing the timeout reference and clearing it in ngOnDestroy.

🔧 Proposed fix

Add a property to track the timeout:

+  private redirectTimeout: ReturnType<typeof setTimeout> | null = null;

Update the success handler:

-          setTimeout(() => {
-            this.router.navigate(["/login"]);
-          }, 2000);
+          this.redirectTimeout = setTimeout(() => {
+            this.router.navigate(["/login"]);
+          }, 2000);

Clear in ngOnDestroy:

  ngOnDestroy(): void {
+    if (this.redirectTimeout) {
+      clearTimeout(this.redirectTimeout);
+    }
    this.titleService.resetTitle();
  }
🤖 Prompt for AI Agents
In
`@src/app/modules/home/components/reset-password-page/reset-password-page.component.ts`
around lines 68 - 70, The setTimeout used to call
this.router.navigate(["/login"]) can fire after the component is destroyed; add
a class property (e.g., private redirectTimeoutId: any) to store the return
value of setTimeout where it is invoked (in the success handler that currently
calls setTimeout), and implement ngOnDestroy() to
clearTimeout(this.redirectTimeoutId) and null it to avoid the callback running
after destruction; ensure you still call router.navigate only when the timeout
exists and component is alive.

Comment on lines +17 to +23
isLoading = true;
successMessage: string | null = null;
errorMessage: string | null = null;
email = "";
canResend = false;
isResending = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Encode the token and clear the redirect timer on destroy.

This avoids URL path issues and prevents a delayed navigation after the component is torn down.

🛠️ Suggested fix
   isLoading = true;
   successMessage: string | null = null;
   errorMessage: string | null = null;
   email = "";
   canResend = false;
   isResending = false;
+  private redirectTimeoutId: ReturnType<typeof setTimeout> | null = null;
@@
     this.http
       .get<{ success: boolean; message: string }>(
-        `${environment.resourceApiURI}/api/auth/verify-email/${token}`,
+        `${environment.resourceApiURI}/api/auth/verify-email/${encodeURIComponent(token)}`,
       )
       .pipe(untilDestroyed(this))
       .subscribe({
         next: () => {
           this.isLoading = false;
           this.successMessage =
             "Email verified successfully! Redirecting to sign in...";
-          setTimeout(() => {
+          this.redirectTimeoutId = setTimeout(() => {
             this.router.navigate(["/login"], {
               queryParams: { verified: "true" },
             });
           }, 2000);
         },
@@
   ngOnDestroy(): void {
+    if (this.redirectTimeoutId) {
+      clearTimeout(this.redirectTimeoutId);
+      this.redirectTimeoutId = null;
+    }
     this.titleService.resetTitle();
   }

Also applies to: 46-70, 109-111

🤖 Prompt for AI Agents
In
`@src/app/modules/home/components/verify-email-page/verify-email-page.component.ts`
around lines 17 - 23, The component VerifyEmailPageComponent currently uses the
raw token in URLs and starts a delayed navigation that can fire after the
component is destroyed; update any place that builds a URL or route with the
token (e.g., where token is read in ngOnInit and where resend or verify links
are constructed) to wrap the token with encodeURIComponent(token) to avoid URL
path issues, and add a class property (e.g., redirectTimer: any) to store the
timer ID and implement ngOnDestroy to call clearTimeout(this.redirectTimer) so
the pending navigation is cancelled when the component is torn down; ensure any
places that set the timer (setTimeout) assign to this.redirectTimer.

Comment on lines +104 to +106
private getDeviceInfo(): string {
return navigator.userAgent;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential SSR issue with navigator.userAgent.

Direct access to navigator will throw a ReferenceError during server-side rendering since navigator is a browser-only global. Consider adding a platform check or providing a fallback.

🛠️ Proposed fix with SSR guard
+import { Injectable, PLATFORM_ID, Inject } from "@angular/core";
+import { isPlatformBrowser } from "@angular/common";
 import { Observable } from "rxjs";
 import { ApiService } from "./api.service";

 // ... interfaces remain the same ...

 `@Injectable`({
   providedIn: "root",
 })
 export class AuthApiService {
-  constructor(private readonly api: ApiService) {}
+  private isBrowser: boolean;
+
+  constructor(
+    private readonly api: ApiService,
+    `@Inject`(PLATFORM_ID) platformId: Object,
+  ) {
+    this.isBrowser = isPlatformBrowser(platformId);
+  }

   // ... other methods remain the same ...

   private getDeviceInfo(): string {
-    return navigator.userAgent;
+    return this.isBrowser ? navigator.userAgent : 'SSR';
   }
 }
🤖 Prompt for AI Agents
In `@src/app/services/auth-api.service.ts` around lines 104 - 106, The
getDeviceInfo method directly accesses navigator.userAgent which will throw
during SSR; update getDeviceInfo to guard access (e.g., check typeof navigator
!== 'undefined' or typeof window !== 'undefined' or use globalThis) and return a
safe fallback string (like empty string or 'server') when navigator is not
present so server rendering won't error; locate and modify the getDeviceInfo()
function to implement this guard and fallback.

@maximgorbatyuk
maximgorbatyuk deleted the auth0-removal branch January 27, 2026 04:11
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.

1 participant