Skip to content

Commit 736cfea

Browse files
guguclaude
andauthored
permissions: skip /permissions/available fetch on unauthenticated pages (#1808)
UsersService is provided in root and eagerly creates an httpResource for /permissions/available, which auto-fires on app bootstrap before any auth check, leaking a 401 request from /login and other unauthenticated pages. Gate the request function on a new isAuthenticated signal in AuthService — seeded from the existing localStorage 'token_expiration' and flipped via setAuthenticated() at the login, session-restoration, and logout transitions already managed by AppComponent. Mirrors the connection-id gating used by the other httpResource-backed services. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6927ca0 commit 736cfea

4 files changed

Lines changed: 26 additions & 3 deletions

File tree

frontend/src/app/app.component.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { provideHttpClient } from '@angular/common/http';
2-
import { ChangeDetectorRef } from '@angular/core';
2+
import { ChangeDetectorRef, signal } from '@angular/core';
33
import { ComponentFixture, TestBed } from '@angular/core/testing';
44
import { MatDialogModule } from '@angular/material/dialog';
55
import { MatMenuModule } from '@angular/material/menu';
@@ -44,9 +44,12 @@ describe('AppComponent', () => {
4444
const authCast = new Subject<any>();
4545
const userCast = new Subject<any>();
4646

47+
const isAuthenticatedSignal = signal(false);
4748
const mockAuthService = {
4849
cast: authCast,
4950
logOutUser: vi.fn().mockReturnValue(of(true)),
51+
isAuthenticated: isAuthenticatedSignal.asReadonly(),
52+
setAuthenticated: (value: boolean) => isAuthenticatedSignal.set(value),
5053
};
5154

5255
const mockUserService = {

frontend/src/app/app.component.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export class AppComponent {
215215
localStorage.setItem('token_expiration', expirationTime.toISOString());
216216
expirationToken = expirationTime.toISOString();
217217
}
218+
this._auth.setAuthenticated(true);
218219

219220
this.router.navigate(['/connections-list']);
220221

@@ -236,6 +237,7 @@ export class AppComponent {
236237
const expirationInterval = differenceInMilliseconds(expirationTime, currantTime);
237238
console.log('expirationInterval', expirationInterval);
238239
if (expirationInterval > 0) {
240+
this._auth.setAuthenticated(true);
239241
console.log('App component, session restoration');
240242
this.initializeUserSession();
241243

@@ -355,6 +357,7 @@ export class AppComponent {
355357
this._user.setIsDemo(false);
356358
this.currentUser = null;
357359
localStorage.removeItem('token_expiration');
360+
this._auth.setAuthenticated(false);
358361
this.router.navigate(['/registration']);
359362
});
360363
}
@@ -375,6 +378,7 @@ export class AppComponent {
375378
this._auth.logOutUser().subscribe(() => {
376379
this.setUserLoggedIn(null);
377380
localStorage.removeItem('token_expiration');
381+
this._auth.setAuthenticated(false);
378382

379383
if (this.isSaas) {
380384
if (!isTokenExpired) window.location.href = 'https://rocketadmin.com/';

frontend/src/app/services/auth.service.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { HttpClient } from '@angular/common/http';
2-
import { Injectable } from '@angular/core';
2+
import { Injectable, signal } from '@angular/core';
33
import * as Sentry from '@sentry/angular';
44
import { BehaviorSubject, EMPTY } from 'rxjs';
55
import { catchError, map } from 'rxjs/operators';
@@ -16,12 +16,19 @@ export class AuthService {
1616
private auth = new BehaviorSubject<any>('');
1717
public cast = this.auth.asObservable();
1818

19+
private _isAuthenticated = signal<boolean>(AuthService._hasValidSessionToken());
20+
public readonly isAuthenticated = this._isAuthenticated.asReadonly();
21+
1922
constructor(
2023
private _http: HttpClient,
2124
private _notifications: NotificationsService,
2225
private _configuration: ConfigurationService,
2326
) {}
2427

28+
setAuthenticated(value: boolean): void {
29+
this._isAuthenticated.set(value);
30+
}
31+
2532
signUpUser(userData: NewAuthUser) {
2633
const config = this._configuration.getConfig();
2734
return this._http.post<any>(config.saasURL + '/saas/user/register', userData).pipe(
@@ -328,4 +335,11 @@ export class AuthService {
328335
}),
329336
);
330337
}
338+
339+
private static _hasValidSessionToken(): boolean {
340+
const exp = localStorage.getItem('token_expiration');
341+
if (!exp) return false;
342+
const expiration = new Date(exp);
343+
return !isNaN(expiration.getTime()) && expiration.getTime() > Date.now();
344+
}
331345
}

frontend/src/app/services/users.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { PolicyAction, PolicyActionGroup } from 'src/app/lib/cedar-policy-items'
55
import { groupNameForAction, PERMISSION_GROUP_ORDER } from 'src/app/lib/permission-display';
66
import { GroupUser, Permissions, UserGroup, UserGroupInfo } from 'src/app/models/user';
77
import { ApiService } from './api.service';
8+
import { AuthService } from './auth.service';
89
import { NotificationsService } from './notifications.service';
910

1011
export type GroupUpdateEvent =
@@ -21,6 +22,7 @@ export type GroupUpdateEvent =
2122
})
2223
export class UsersService {
2324
private _api = inject(ApiService);
25+
private _auth = inject(AuthService);
2426
private _http = inject(HttpClient);
2527
private _notifications = inject(NotificationsService);
2628

@@ -48,7 +50,7 @@ export class UsersService {
4850

4951
private _availablePermissionsResource: HttpResourceRef<{ actions: PolicyAction[] } | undefined> = this._api.resource<{
5052
actions: PolicyAction[];
51-
}>(() => '/permissions/available');
53+
}>(() => (this._auth.isAuthenticated() ? '/permissions/available' : undefined));
5254

5355
public readonly availablePermissions = computed<PolicyAction[]>(
5456
() => this._availablePermissionsResource.value()?.actions ?? [],

0 commit comments

Comments
 (0)