- {{ showToken() ? developerToken() : maskedToken() }}
-
diff --git a/apps/lfx-one/src/app/modules/settings/account-settings/account-settings.component.ts b/apps/lfx-one/src/app/modules/settings/account-settings/account-settings.component.ts
index 4ddeb43ff..5623145c2 100644
--- a/apps/lfx-one/src/app/modules/settings/account-settings/account-settings.component.ts
+++ b/apps/lfx-one/src/app/modules/settings/account-settings/account-settings.component.ts
@@ -8,12 +8,14 @@ import { AbstractControl, FormBuilder, FormControl, FormGroup, ReactiveFormsModu
import { BadgeComponent } from '@components/badge/badge.component';
import { ButtonComponent } from '@components/button/button.component';
import { InputTextComponent } from '@components/input-text/input-text.component';
+import { TokenRevealDialogComponent } from '@components/token-reveal-dialog/token-reveal-dialog.component';
import { markFormControlsAsTouched } from '@lfx-one/shared';
import { useResendCooldown } from '@shared/utils/resend-cooldown';
import { ChangePasswordRequest, EmailManagementData, PasswordStrength, UserEmail } from '@lfx-one/shared/interfaces';
import { UserService } from '@services/user.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
+import { DialogService, DynamicDialogModule } from 'primeng/dynamicdialog';
import { ToastModule } from 'primeng/toast';
import { TooltipModule } from 'primeng/tooltip';
import { HttpErrorResponse } from '@angular/common/http';
@@ -22,8 +24,18 @@ import { BehaviorSubject, catchError, finalize, of, switchMap, take } from 'rxjs
@Component({
selector: 'lfx-account-settings',
host: { class: 'block' },
- imports: [NgClass, ReactiveFormsModule, BadgeComponent, ButtonComponent, InputTextComponent, ConfirmDialogModule, ToastModule, TooltipModule],
- providers: [ConfirmationService, MessageService],
+ imports: [
+ NgClass,
+ ReactiveFormsModule,
+ BadgeComponent,
+ ButtonComponent,
+ InputTextComponent,
+ ConfirmDialogModule,
+ ToastModule,
+ TooltipModule,
+ DynamicDialogModule,
+ ],
+ providers: [ConfirmationService, MessageService, DialogService],
templateUrl: './account-settings.component.html',
})
export class AccountSettingsComponent {
@@ -31,6 +43,7 @@ export class AccountSettingsComponent {
private readonly userService = inject(UserService);
private readonly confirmationService = inject(ConfirmationService);
private readonly messageService = inject(MessageService);
+ private readonly dialogService = inject(DialogService);
private readonly platformId = inject(PLATFORM_ID);
private readonly destroyRef = inject(DestroyRef);
@@ -92,16 +105,16 @@ export class AccountSettingsComponent {
// DEVELOPER SETTINGS
// ══════════════════════════════════════════
+ // v2 OIDC session token (audience PCC_AUTH0_AUDIENCE)
public developerToken = signal('');
- public showToken = signal(false);
+ // v1 API Gateway token (audience api-gw.*) — empty when the server did not return one
+ public developerV1Token = signal('');
public loadingToken = signal(true);
- public tokenCopied = signal(false);
+ // Tracks which token's Copy button most recently succeeded, so only that button shows "Copied!"
+ public tokenCopied = signal<'v2' | 'v1' | null>(null);
- public maskedToken = computed(() => {
- const token = this.developerToken();
- if (!token || token.length <= 8) return token;
- return `${token.slice(0, 4)}${'*'.repeat(11)}${token.slice(-4)}`;
- });
+ public maskedToken = computed(() => this.maskTokenValue(this.developerToken()));
+ public maskedV1Token = computed(() => this.maskTokenValue(this.developerV1Token()));
// ══════════════════════════════════════════
// PASSWORD
@@ -384,19 +397,33 @@ export class AccountSettingsComponent {
// DEVELOPER SETTINGS PUBLIC METHODS
// ══════════════════════════════════════════
- public toggleTokenVisibility(): void {
- this.showToken.set(!this.showToken());
+ public openTokenPopup(title: string, token: string): void {
+ if (!token) return;
+ this.dialogService.open(TokenRevealDialogComponent, {
+ header: title,
+ width: '40rem',
+ modal: true,
+ draggable: false,
+ resizable: false,
+ dismissableMask: true,
+ style: { maxWidth: '90vw' },
+ data: { token },
+ });
}
- public copyToken(): void {
- const token = this.developerToken();
+ public copyToken(token: string, kind: 'v2' | 'v1'): void {
if (!token || !isPlatformBrowser(this.platformId)) return;
- navigator.clipboard.writeText(token).then(() => {
- this.tokenCopied.set(true);
- this.messageService.add({ severity: 'success', summary: 'Copied', detail: 'Token copied to clipboard' });
- setTimeout(() => this.tokenCopied.set(false), 2000);
- });
+ navigator.clipboard
+ .writeText(token)
+ .then(() => {
+ this.tokenCopied.set(kind);
+ this.messageService.add({ severity: 'success', summary: 'Copied', detail: 'Token copied to clipboard' });
+ setTimeout(() => this.tokenCopied.set(null), 2000);
+ })
+ .catch(() => {
+ this.messageService.add({ severity: 'error', summary: 'Copy Failed', detail: 'Failed to copy token to clipboard. Please try again.' });
+ });
}
// ══════════════════════════════════════════
@@ -424,11 +451,24 @@ export class AccountSettingsComponent {
.getDeveloperTokenInfo()
.pipe(finalize(() => this.loadingToken.set(false)))
.subscribe({
- next: (info) => this.developerToken.set(info.token),
- error: () => this.developerToken.set(''),
+ next: (info) => {
+ // Guard the shape at runtime: a non-string (e.g. null on a transient error path) resets
+ // to empty rather than leaking a raw value through maskTokenValue.
+ this.developerToken.set(typeof info.token === 'string' ? info.token : '');
+ this.developerV1Token.set(typeof info.v1Token === 'string' ? info.v1Token : '');
+ },
+ error: () => {
+ this.developerToken.set('');
+ this.developerV1Token.set('');
+ },
});
}
+ private maskTokenValue(token: string): string {
+ if (!token || token.length <= 8) return token;
+ return `${token.slice(0, 4)}${'*'.repeat(11)}${token.slice(-4)}`;
+ }
+
private calculatePasswordStrength(password: string): PasswordStrength {
const requirements = {
minLength: password.length >= 8,
diff --git a/apps/lfx-one/src/app/shared/components/header/header.component.ts b/apps/lfx-one/src/app/shared/components/header/header.component.ts
index 3d208ce0e..b175960ff 100644
--- a/apps/lfx-one/src/app/shared/components/header/header.component.ts
+++ b/apps/lfx-one/src/app/shared/components/header/header.component.ts
@@ -58,7 +58,8 @@ export class HeaderComponent {
{
label: 'Developer Settings',
icon: 'fa-light fa-cog',
- routerLink: '/profile/developer',
+ routerLink: '/settings',
+ fragment: 'developer-settings',
},
{
separator: true,
diff --git a/apps/lfx-one/src/app/shared/components/token-reveal-dialog/token-reveal-dialog.component.html b/apps/lfx-one/src/app/shared/components/token-reveal-dialog/token-reveal-dialog.component.html
new file mode 100644
index 000000000..bebb14f61
--- /dev/null
+++ b/apps/lfx-one/src/app/shared/components/token-reveal-dialog/token-reveal-dialog.component.html
@@ -0,0 +1,19 @@
+
+
+
+
+
Copy your full token below. Keep it secure and never share it publicly.
+
+ {{ token }}
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/lfx-one/src/app/shared/components/token-reveal-dialog/token-reveal-dialog.component.ts b/apps/lfx-one/src/app/shared/components/token-reveal-dialog/token-reveal-dialog.component.ts
new file mode 100644
index 000000000..190e21498
--- /dev/null
+++ b/apps/lfx-one/src/app/shared/components/token-reveal-dialog/token-reveal-dialog.component.ts
@@ -0,0 +1,59 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Clipboard } from '@angular/cdk/clipboard';
+import { Component, inject } from '@angular/core';
+import { ButtonComponent } from '@components/button/button.component';
+import { MessageService } from 'primeng/api';
+import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
+
+/**
+ * Reveal popup for a developer API token. Opened via DialogService.open() with the full
+ * token passed as dialog data; displays it in a wrapped, vertically-scrollable box with
+ * Copy and Close. The dialog title is set by the opener via the DialogService config header.
+ */
+@Component({
+ selector: 'lfx-token-reveal-dialog',
+ imports: [ButtonComponent],
+ templateUrl: './token-reveal-dialog.component.html',
+})
+export class TokenRevealDialogComponent {
+ private readonly dialogRef = inject(DynamicDialogRef);
+ private readonly dialogConfig = inject(DynamicDialogConfig);
+ private readonly clipboard = inject(Clipboard);
+ private readonly messageService = inject(MessageService);
+
+ // Guard against a missing/non-string token in the dialog data so we never render the literal
+ // string "undefined"; the single in-app opener always passes a string, this is defense in depth.
+ public readonly token = typeof this.dialogConfig.data?.token === 'string' ? this.dialogConfig.data.token : '';
+
+ public copy(): void {
+ if (!this.token) {
+ this.messageService.add({
+ severity: 'warn',
+ summary: 'No Token',
+ detail: 'No API token available to copy.',
+ });
+ return;
+ }
+
+ const success = this.clipboard.copy(this.token);
+ if (success) {
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Copied',
+ detail: 'API token copied to clipboard successfully.',
+ });
+ } else {
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Copy Failed',
+ detail: 'Failed to copy token to clipboard. Please try again.',
+ });
+ }
+ }
+
+ public close(): void {
+ this.dialogRef.close();
+ }
+}
diff --git a/apps/lfx-one/src/app/shared/services/user.service.ts b/apps/lfx-one/src/app/shared/services/user.service.ts
index b4f35d2fa..fb0647af1 100644
--- a/apps/lfx-one/src/app/shared/services/user.service.ts
+++ b/apps/lfx-one/src/app/shared/services/user.service.ts
@@ -12,6 +12,7 @@ import {
CombinedProfile,
ClaimAliasRequest,
CreateUserPermissionRequest,
+ DeveloperTokenInfo,
EmailManagementData,
EnrichedIdentity,
Impersonator,
@@ -167,8 +168,8 @@ export class UserService {
/**
* Get developer token information
*/
- public getDeveloperTokenInfo(): Observable<{ token: string; type: string }> {
- return this.http.get<{ token: string; type: string }>('/api/profile/developer').pipe(take(1));
+ public getDeveloperTokenInfo(): Observable
{
+ return this.http.get('/api/profile/developer').pipe(take(1));
}
/**
diff --git a/apps/lfx-one/src/server/controllers/profile.controller.ts b/apps/lfx-one/src/server/controllers/profile.controller.ts
index d2042b1e0..85cb28022 100644
--- a/apps/lfx-one/src/server/controllers/profile.controller.ts
+++ b/apps/lfx-one/src/server/controllers/profile.controller.ts
@@ -16,6 +16,7 @@ import {
CdpWorkExperienceRequest,
ClaimAliasRequest,
CombinedProfile,
+ DeveloperTokenInfo,
EmailManagementData,
EnrichedIdentity,
IdentityDisplayState,
@@ -756,15 +757,22 @@ export class ProfileController {
return next(validationError);
}
- // Return token information
- const tokenInfo = {
+ // The v1 API Gateway token (audience api-gw.*) is minted by the auth middleware via
+ // refresh-token exchange and stored on req.apiGatewayToken. Surface it alongside the v2
+ // token so users migrating off the ID dashboard can keep calling v1 APIs. Only include the
+ // key when present so the UI can hide the v1 row when the exchange produced nothing.
+ const v1Token = req.apiGatewayToken;
+
+ const tokenInfo: DeveloperTokenInfo = {
token: bearerToken,
type: 'Bearer',
+ ...(v1Token ? { v1Token } : {}),
};
logger.success(req, 'get_developer_token_info', startTime, {
user_id: userId,
token_length: bearerToken.length,
+ has_v1_token: Boolean(v1Token),
});
// Set cache headers to prevent caching of sensitive bearer tokens
diff --git a/packages/shared/src/interfaces/profile.interface.ts b/packages/shared/src/interfaces/profile.interface.ts
index cff5c8037..2322ddc18 100644
--- a/packages/shared/src/interfaces/profile.interface.ts
+++ b/packages/shared/src/interfaces/profile.interface.ts
@@ -1,6 +1,20 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT
+/**
+ * Developer settings token info returned by GET /api/profile/developer.
+ *
+ * `token` is the user's v2 OIDC session token (audience PCC_AUTH0_AUDIENCE).
+ * `v1Token` is the user-scoped v1 API Gateway token (audience api-gw.*), minted via
+ * refresh-token exchange in the auth middleware and surfaced for users still calling v1 APIs.
+ * It is omitted when unavailable (no refresh token / exchange failed) so the UI can hide the row.
+ */
+export interface DeveloperTokenInfo {
+ token: string;
+ type: string;
+ v1Token?: string;
+}
+
/**
* Profile tab configuration for the profile layout navigation
*/