Skip to content

Commit 9e1a094

Browse files
committed
feat(debug): add preview-only topbar role switcher for fast role-based UI testing
1 parent 3cb5034 commit 9e1a094

4 files changed

Lines changed: 120 additions & 0 deletions

File tree

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,38 @@ export class AuthService {
137137
sessionStorage.setItem(DEBUG_SESSION_FLAG_STORAGE_KEY, '1');
138138
}
139139

140+
setDebugSessionRole(role: AppRole): void {
141+
const currentSession = this.authSessionSignal();
142+
if (!currentSession?.isDebugSession || !this.isDebugAuthAllowed() || !this.hasDebugSessionFlag()) {
143+
return;
144+
}
145+
146+
const nowUtcMs = Date.now();
147+
const roleName = this.resolveDebugRoleName(role);
148+
const debugClaims = {
149+
sub: 'debug-user',
150+
name: `${roleName} (Preview)`,
151+
preferred_username: `debug.${role.toLowerCase()}`,
152+
email: `debug.${role.toLowerCase()}@local.test`,
153+
role: [role]
154+
};
155+
156+
const session: AuthSession = {
157+
...currentSession,
158+
accessToken: this.createUnsignedJwt(debugClaims),
159+
idToken: this.createUnsignedJwt({
160+
sub: debugClaims.sub,
161+
name: debugClaims.name,
162+
email: debugClaims.email
163+
}),
164+
scope: this.appEnvironment.auth.scopes.join(' '),
165+
expiresAtUtcMs: nowUtcMs + 8 * 60 * 60 * 1000,
166+
isDebugSession: true
167+
};
168+
169+
this.setSession(session);
170+
}
171+
140172
hasRole(role: string): boolean {
141173
return this.userRoles().includes(role);
142174
}
@@ -406,4 +438,17 @@ export class AuthService {
406438
private hasDebugSessionFlag(): boolean {
407439
return sessionStorage.getItem(DEBUG_SESSION_FLAG_STORAGE_KEY) === '1';
408440
}
441+
442+
private resolveDebugRoleName(role: AppRole): string {
443+
switch (role) {
444+
case AppRole.Administrator:
445+
return 'Debug Admin';
446+
case AppRole.ProjectManager:
447+
return 'Debug Manager';
448+
case AppRole.User:
449+
return 'Debug User';
450+
default:
451+
return 'Debug User';
452+
}
453+
}
409454
}

src/app/core/layout/component/app-topbar/app-topbar.component.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@
1515

1616
<div class="layout-topbar-actions">
1717
<div class="layout-config-menu">
18+
<button
19+
*ngIf="isPreviewMode"
20+
type="button"
21+
class="layout-topbar-preview-role"
22+
(click)="previewRoleMenu.toggle($event)">
23+
<i class="pi pi-sliders-h"></i>
24+
<span>{{ previewRoleButtonLabel }}</span>
25+
<i class="pi pi-angle-down"></i>
26+
</button>
1827
<button type="button" class="layout-topbar-action" (click)="toggleDarkMode()">
1928
<i [ngClass]="{ 'pi ': true, 'pi-sun': layoutService.isDarkTheme(), 'pi-moon': !layoutService.isDarkTheme() }"></i>
2029
</button>
@@ -68,6 +77,7 @@
6877
</div>
6978
</ng-template>
7079
</p-menu>
80+
<p-menu #previewRoleMenu [popup]="true" [model]="previewRoleMenuItems" [appendTo]="'body'" styleClass="topbar-preview-role-menu"></p-menu>
7181
<p-overlayPanel #notificationsPanel styleClass="topbar-notifications-panel" [showCloseIcon]="true" [appendTo]="'body'" (onShow)="markNotificationsAsRead()">
7282
<div class="topbar-notifications">
7383
<div class="topbar-notifications__header">

src/app/core/layout/component/app-topbar/app-topbar.component.scss

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,34 @@
184184
display: flex;
185185
gap: 1rem;
186186
}
187+
188+
.layout-topbar-preview-role {
189+
border: 1px solid color-mix(in srgb, var(--orange-500) 40%, var(--surface-border));
190+
border-radius: 999px;
191+
background: color-mix(in srgb, var(--orange-100) 55%, transparent);
192+
color: var(--orange-700);
193+
display: inline-flex;
194+
align-items: center;
195+
gap: 0.35rem;
196+
padding: 0.28rem 0.6rem;
197+
font-size: 0.75rem;
198+
font-weight: 600;
199+
cursor: pointer;
200+
transition: background-color .2s, border-color .2s;
201+
202+
&:hover {
203+
background: color-mix(in srgb, var(--orange-100) 72%, transparent);
204+
border-color: color-mix(in srgb, var(--orange-500) 62%, var(--surface-border));
205+
}
206+
207+
i {
208+
font-size: 0.78rem;
209+
}
210+
211+
span {
212+
white-space: nowrap;
213+
}
214+
}
187215
}
188216

189217
:host ::ng-deep .topbar-account-menu {
@@ -244,6 +272,10 @@
244272
width: min(24rem, calc(100vw - 2rem));
245273
}
246274

275+
:host ::ng-deep .topbar-preview-role-menu {
276+
width: 14rem;
277+
}
278+
247279
:host ::ng-deep .topbar-notifications-panel .p-overlaypanel-content {
248280
padding-top: 1rem;
249281
}
@@ -405,6 +437,10 @@
405437
margin-left: 0.5rem;
406438
}
407439

440+
.layout-topbar-preview-role {
441+
display: none;
442+
}
443+
408444
.layout-topbar-menu-content {
409445
flex-direction: row;
410446
}

src/app/core/layout/component/app-topbar/app-topbar.component.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ import { AppRole } from '../../../../core/auth/models/app-role.model';
2222
})
2323
export class AppTopbarComponent {
2424
private readonly notificationsService = inject(ActivityNotificationService);
25+
readonly previewRoleMenuItems: MenuItem[] = [
26+
{
27+
label: 'Administrator',
28+
icon: 'pi pi-shield',
29+
command: () => this.switchPreviewRole(AppRole.Administrator)
30+
},
31+
{
32+
label: 'Project Manager',
33+
icon: 'pi pi-briefcase',
34+
command: () => this.switchPreviewRole(AppRole.ProjectManager)
35+
},
36+
{
37+
label: 'User',
38+
icon: 'pi pi-user',
39+
command: () => this.switchPreviewRole(AppRole.User)
40+
}
41+
];
2542

2643
readonly accountMenuItems: MenuItem[] = [
2744
{
@@ -79,6 +96,14 @@ export class AppTopbarComponent {
7996
this.authService.logout();
8097
}
8198

99+
switchPreviewRole(role: AppRole): void {
100+
if (!this.isPreviewMode) {
101+
return;
102+
}
103+
104+
this.authService.setDebugSessionRole(role);
105+
}
106+
82107
get homeRoute(): string {
83108
return this.authService.isAuthenticated() ? this.preferencesService.getDefaultHomeRoutePath() : '/';
84109
}
@@ -176,4 +201,8 @@ export class AppTopbarComponent {
176201
}
177202
}
178203

204+
get previewRoleButtonLabel(): string {
205+
return `Preview: ${this.currentRoleLabel}`;
206+
}
207+
179208
}

0 commit comments

Comments
 (0)