+
+ My profile
+
+
+ Identity served by the BFF from the active session.
+
+
+
+
+
-
+ Display name
+
+ -
+ {{ currentUser.displayName }}
+
+
+
+
-
+ Username
+
+ -
+ {{ currentUser.username }}
+
+
+
+
-
+ Entra object id
+
+ -
+ {{ currentUser.oid }}
+
+
+
+
-
+ Tenant id
+
+ -
+ {{ currentUser.tid }}
+
+
+
+
+}
diff --git a/apps/portal-shell/src/app/pages/profile/profile.spec.ts b/apps/portal-shell/src/app/pages/profile/profile.spec.ts
new file mode 100644
index 0000000..3b12af8
--- /dev/null
+++ b/apps/portal-shell/src/app/pages/profile/profile.spec.ts
@@ -0,0 +1,61 @@
+import { provideHttpClient } from '@angular/common/http';
+import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+import { AUTH_BFF_BASE_URL, type CurrentUser } from 'feature-auth';
+import { Profile } from './profile';
+
+const BFF_BASE = 'http://bff.test/api';
+const ME_URL = `${BFF_BASE}/auth/me`;
+
+const USER: CurrentUser = {
+ oid: 'user-oid',
+ tid: 'tenant-id',
+ username: 'jane.doe@apf.example',
+ displayName: 'Jane Doe',
+};
+
+async function setup() {
+ TestBed.configureTestingModule({
+ imports: [Profile],
+ providers: [
+ provideHttpClient(),
+ provideHttpClientTesting(),
+ { provide: AUTH_BFF_BASE_URL, useValue: BFF_BASE },
+ ],
+ });
+ const fixture = TestBed.createComponent(Profile);
+ const httpCtrl = TestBed.inject(HttpTestingController);
+ // AuthService's bootstrap /me fires from its constructor on first
+ // injection — drain it so the user signal commits before the
+ // template renders.
+ httpCtrl.expectOne(ME_URL).flush(USER);
+ await Promise.resolve();
+ fixture.detectChanges();
+ await fixture.whenStable();
+ return { fixture };
+}
+
+describe('Profile', () => {
+ afterEach(() => {
+ TestBed.resetTestingModule();
+ });
+
+ it('renders the identity served by the BFF /me payload', async () => {
+ const { fixture } = await setup();
+ const root = fixture.nativeElement as HTMLElement;
+ expect(root.querySelector('[data-testid="profile-displayname"]')?.textContent?.trim()).toBe(
+ USER.displayName,
+ );
+ expect(root.querySelector('[data-testid="profile-username"]')?.textContent?.trim()).toBe(
+ USER.username,
+ );
+ expect(root.querySelector('[data-testid="profile-oid"]')?.textContent?.trim()).toBe(USER.oid);
+ expect(root.querySelector('[data-testid="profile-tid"]')?.textContent?.trim()).toBe(USER.tid);
+ });
+
+ it('renders an accessible heading', async () => {
+ const { fixture } = await setup();
+ const heading = (fixture.nativeElement as HTMLElement).querySelector('h1');
+ expect(heading?.textContent?.trim()).toBe('My profile');
+ });
+});
diff --git a/apps/portal-shell/src/app/pages/profile/profile.ts b/apps/portal-shell/src/app/pages/profile/profile.ts
new file mode 100644
index 0000000..a8c62a6
--- /dev/null
+++ b/apps/portal-shell/src/app/pages/profile/profile.ts
@@ -0,0 +1,23 @@
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { AuthService } from 'feature-auth';
+
+/**
+ * Authenticated demo page. First real consumer of `authGuard` — the
+ * guard upstream guarantees `AuthService.currentUser()` is non-null
+ * by the time the component renders, so the template can read it
+ * directly without an "anonymous" branch.
+ *
+ * v1 carries just the user identity card. The route is a deliberate
+ * stub to exercise the full auth loop end-to-end (guard → BFF /me →
+ * SPA render); the real profile content lands when the user-profile
+ * feature is in scope.
+ */
+@Component({
+ selector: 'app-profile',
+ templateUrl: './profile.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class Profile {
+ private readonly auth = inject(AuthService);
+ protected readonly user = this.auth.currentUser;
+}
diff --git a/apps/portal-shell/src/locale/messages.fr.xlf b/apps/portal-shell/src/locale/messages.fr.xlf
index a6fbc19..9d81cf3 100644
--- a/apps/portal-shell/src/locale/messages.fr.xlf
+++ b/apps/portal-shell/src/locale/messages.fr.xlf
@@ -178,6 +178,36 @@