Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/lfx-one/public/images/logo_lf_white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions apps/lfx-one/public/images/logo_lfx_white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions apps/lfx-one/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { Routes } from '@angular/router';

import { PlaceholderPageComponent } from './shared/components/placeholder-page/placeholder-page.component';
import { authGuard } from './shared/guards/auth.guard';

export const routes: Routes = [
Expand Down Expand Up @@ -47,6 +48,21 @@ export const routes: Routes = [
path: 'profile',
loadChildren: () => import('./modules/profile/profile.routes').then((m) => m.PROFILE_ROUTES),
},
// Me lens — new pages
{ path: 'me/actions', component: PlaceholderPageComponent, data: { title: 'My Actions' } },
{ path: 'me/events', component: PlaceholderPageComponent, data: { title: 'My Events' } },
{ path: 'me/training', component: PlaceholderPageComponent, data: { title: 'Trainings & Certifications' } },
{ path: 'me/badges', component: PlaceholderPageComponent, data: { title: 'Badges' } },
{ path: 'me/easycla', component: PlaceholderPageComponent, data: { title: 'EasyCLA' } },
{ path: 'me/transactions', component: PlaceholderPageComponent, data: { title: 'Transactions' } },
// Foundation lens — new pages
{ path: 'foundation/projects', component: PlaceholderPageComponent, data: { title: 'Projects' } },
{ path: 'foundation/events', component: PlaceholderPageComponent, data: { title: 'Events' } },
// Org lens — module
{
path: 'org',
loadChildren: () => import('./modules/org-lens/org-lens.routes').then((m) => m.ORG_LENS_ROUTES),
},
Comment on lines +62 to +65
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -p 'org-lens.routes.ts' apps/lfx-one/src/app/modules/org-lens | while read -r file; do
  echo "== $file =="
  sed -n '1,220p' "$file"
done

echo
rg -nP --type=ts 'canActivate|canMatch|orgUserType|employee|admin-edit|conglomerate-admin' \
  apps/lfx-one/src/app/modules/org-lens \
  apps/lfx-one/src/app/shared \
  apps/lfx-one/src/app/layouts

Repository: linuxfoundation/lfx-v2-ui

Length of output: 7444


Add route-level guards to admin-only org child paths.

The ORG_LENS_ROUTES lack canActivate guards on sensitive routes. Access control is currently implemented at the component level (orgUserType computed properties in membership, code, groups, and profile components), which only hides UI features but does not prevent the route from activating. Routes such as membership, groups, and profile are directly reachable by any authenticated user without role validation at the routing layer.

Move role checks to route guards using canActivate to prevent unauthorized access before the component loads.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/lfx-one/src/app/app.routes.ts` around lines 62 - 65, The ORG_LENS_ROUTES
currently rely on component-level role checks (membership, code, groups,
profile) so add a route guard to enforce admin-only access at the routing layer:
implement an OrgAdminGuard (implementing CanActivate) that checks the current
user's org role via your auth/user service and returns true only for org admins,
register it in the org-lens module providers, and add canActivate:
[OrgAdminGuard] to the sensitive child route entries inside ORG_LENS_ROUTES
(membership, groups, profile, code or any admin-only paths) so unauthorized
users are blocked before the component loads.

],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<!-- SPDX-License-Identifier: MIT -->

@if (showDevToolbar()) {
<div class="fixed top-0 left-0 right-0 z-50 bg-gray-900 text-white px-4 py-2" data-testid="dev-tools-bar">
<div class="fixed top-0 left-0 right-0 z-[100] bg-gray-900 text-white px-4 py-2" data-testid="dev-tools-bar">
<div class="flex items-center gap-4 max-w-full">
<!-- Persona Selector (left side) -->
<div class="flex items-center gap-2" data-testid="dev-tools-bar-persona">
Expand Down Expand Up @@ -38,8 +38,27 @@
</div>
}

<!-- Organization Selector (only visible on board dashboard) -->
@if (showOrganizationSelector() && isOnBoardDashboard()) {
<!-- Org User Type Selector (visible in org lens) -->
@if (showOrgUserTypeSelector()) {
<div class="flex items-center gap-2" data-testid="dev-tools-bar-org-user-type">
<label for="org-user-type-select" class="text-sm">User Type:</label>
<lfx-select
[form]="form"
control="orgUserType"
[options]="orgUserTypeOptions"
optionLabel="label"
optionValue="value"
[filter]="false"
placeholder="Select user type"
[showClear]="false"
styleClass="min-w-[200px] dev-tools-select"
inputId="org-user-type-select"
data-testid="dev-tools-bar-org-user-type-select" />
</div>
}

<!-- Organization Selector (visible in org lens or board dashboard) -->
@if (showOrgSelector()) {
<div class="flex items-center gap-2" data-testid="dev-tools-bar-organization">
<label for="organization-select" class="text-sm">Organization:</label>
<lfx-select
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { Component, inject, Signal } from '@angular/core';
import { Component, computed, inject, Signal } from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { NavigationEnd, Router } from '@angular/router';
import { ButtonComponent } from '@components/button/button.component';
import { SelectButtonComponent } from '@components/select-button/select-button.component';
import { SelectComponent } from '@components/select/select.component';
import { ACCOUNTS, PERSONA_OPTIONS } from '@lfx-one/shared/constants';
import { Account, isBoardScopedPersona, PersonaType } from '@lfx-one/shared/interfaces';
import { ACCOUNTS, ORG_USER_TYPE_OPTIONS, PERSONA_OPTIONS } from '@lfx-one/shared/constants';
import { Account, isBoardScopedPersona, OrgUserType, PersonaType } from '@lfx-one/shared/interfaces';
import { AccountContextService } from '@services/account-context.service';
import { AppService } from '@services/app.service';
import { CookieRegistryService } from '@services/cookie-registry.service';
import { FeatureFlagService } from '@services/feature-flag.service';
import { PersonaService } from '@services/persona.service';
Expand All @@ -27,20 +28,31 @@ export class DevToolbarComponent {
private readonly personaService = inject(PersonaService);
protected readonly projectContextService = inject(ProjectContextService);
private readonly accountContextService = inject(AccountContextService);
private readonly appService = inject(AppService);
private readonly cookieRegistry = inject(CookieRegistryService);
private readonly featureFlagService = inject(FeatureFlagService);
private readonly router = inject(Router);

// Feature flags
protected readonly showDevToolbar = this.featureFlagService.getBooleanFlag('dev-toolbar', true);
// Feature flags — showDevToolbar from AppService (single source of truth for layout offsets)
protected readonly showDevToolbar = this.appService.showDevToolbar;
protected readonly showOrganizationSelector = this.featureFlagService.getBooleanFlag('organization-selector', true);

// Active lens
protected readonly activeLens = this.appService.activeLens;
protected readonly showOrgSelector = computed(() => this.showOrganizationSelector() && (this.activeLens() === 'org' || this.isOnBoardDashboard()));

// Organization selector options
protected readonly availableAccounts = ACCOUNTS;

// Persona options for SelectButton
protected readonly personaOptions = PERSONA_OPTIONS;

// Org user type options
protected readonly orgUserTypeOptions = ORG_USER_TYPE_OPTIONS;

// Show org user type selector only in org lens
protected readonly showOrgUserTypeSelector = computed(() => this.activeLens() === 'org');

// Board member project override — delegates to centralized isBoardScoped signal
protected readonly isBoardMember = this.personaService.isBoardScoped;

Expand All @@ -55,6 +67,7 @@ export class DevToolbarComponent {
persona: new FormControl<PersonaType>(this.personaService.currentPersona(), [Validators.required]),
selectedAccountId: new FormControl<string>(this.accountContextService.selectedAccount().accountId),
selectedProjectUid: new FormControl<string>(this.projectContextService.selectedFoundation()?.uid || ''),
orgUserType: new FormControl<OrgUserType>(this.appService.orgUserType()),
});

// Subscribe to persona changes
Expand Down Expand Up @@ -105,6 +118,14 @@ export class DevToolbarComponent {
this.projectContextService.setFoundation(project);
}
});

// Subscribe to org user type changes
this.form
.get('orgUserType')
?.valueChanges.pipe(takeUntilDestroyed())
.subscribe((value: OrgUserType) => {
this.appService.setOrgUserType(value);
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@
<!-- Dev Toolbar (fixed at top) -->
<lfx-dev-toolbar></lfx-dev-toolbar>

<div class="flex min-h-screen" [ngClass]="{ 'pt-12': showDevToolbar() }">
<!-- Sidebar - Desktop -->
<div class="hidden lg:block w-64 flex-shrink-0 fixed left-0 bottom-0" [ngClass]="{ 'top-12': showDevToolbar(), 'top-0': !showDevToolbar() }">
<lfx-sidebar [items]="sidebarItems()" [footerItems]="sidebarFooterItems()" [showProjectSelector]="true"></lfx-sidebar>
<div class="flex min-h-screen" [ngClass]="{ 'pt-[48px]': showDevToolbar() }">
<!-- Sidebar - Desktop: lens switcher (72px) + nav panel (280px) = 352px total -->
<div class="hidden lg:flex flex-row flex-shrink-0 fixed left-0 bottom-0 w-[344px] z-[60]" [ngClass]="{ 'top-[48px]': showDevToolbar(), 'top-0': !showDevToolbar() }">
<!-- Lens Switcher Column (72px) -->
Comment on lines +8 to +10
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

The width math in the comment doesn’t match the actual Tailwind widths: the lens switcher column is w-[64px] and the wrapper is w-[344px], but the comment says 72px + 280px = 352px. This makes it harder to keep layout-aligned (and is likely related to the project selector panel using left: 352px). Please update the comment (and any dependent positioning) to the actual values.

Suggested change
<!-- Sidebar - Desktop: lens switcher (72px) + nav panel (280px) = 352px total -->
<div class="hidden lg:flex flex-row flex-shrink-0 fixed left-0 bottom-0 w-[344px] z-[60]" [ngClass]="{ 'top-[48px]': showDevToolbar(), 'top-0': !showDevToolbar() }">
<!-- Lens Switcher Column (72px) -->
<!-- Sidebar - Desktop: lens switcher (64px) + nav panel (280px) = 344px total -->
<div class="hidden lg:flex flex-row flex-shrink-0 fixed left-0 bottom-0 w-[344px] z-[60]" [ngClass]="{ 'top-[48px]': showDevToolbar(), 'top-0': !showDevToolbar() }">
<!-- Lens Switcher Column (64px) -->

Copilot uses AI. Check for mistakes.
<div class="w-[64px] flex-shrink-0" data-testid="lens-switcher-column">
<lfx-lens-switcher></lfx-lens-switcher>
</div>
<!-- Nav Panel (280px) -->
<div class="w-[280px] flex-shrink-0">
<lfx-sidebar [items]="sidebarItems()" [footerItems]="[]" [showMeSelector]="activeLens() === 'me'" [showProjectSelector]="activeLens() === 'foundation'" [showOrgSelector]="activeLens() === 'org'" [grayed]="false"></lfx-sidebar>
</div>
</div>

<!-- Sidebar - Mobile Drawer -->
Expand All @@ -27,13 +34,37 @@
</a>
</div>
</ng-template>
<lfx-sidebar [items]="sidebarItems()" [footerItems]="sidebarFooterItems()" [showProjectSelector]="true" [mobile]="true"></lfx-sidebar>
<!-- Mobile: lens switcher row + nav -->
<div class="flex flex-col h-full">
<div class="flex flex-row gap-2 px-3 py-3 border-b border-gray-100">
<lfx-lens-switcher></lfx-lens-switcher>
</div>
<div class="flex-1 min-h-0">
<lfx-sidebar
[items]="sidebarItems()"
[footerItems]="[]"
[showMeSelector]="activeLens() === 'me'"
[showProjectSelector]="activeLens() === 'foundation'"
[showOrgSelector]="activeLens() === 'org'"
[grayed]="false"
[mobile]="true"></lfx-sidebar>
</div>
</div>
</p-drawer>

<!-- Project Selector Backdrop (z-[50]: visual dim only; close is handled by document:click in project-selector) -->
@if (projectSelectorOpen()) {
<div
class="fixed inset-0 z-[50] bg-black/30 cursor-pointer"
[ngClass]="{ 'top-[48px]': showDevToolbar(), 'top-0': !showDevToolbar() }"
(click)="closeProjectSelector()"
data-testid="project-selector-backdrop"></div>
}

<!-- Main Content Area -->
<main class="flex-1 min-w-0 transition-all duration-300 lg:ml-64 flex flex-col" data-testid="main-content">
<main class="flex-1 min-w-0 lg:ml-[344px] flex flex-col" data-testid="main-content">
<!-- Mobile Menu Button -->
<div class="lg:hidden sticky z-30 bg-white border-b border-gray-200 px-5 py-3" [ngClass]="{ 'top-12': showDevToolbar(), 'top-0': !showDevToolbar() }">
<div class="lg:hidden sticky z-30 bg-white border-b border-gray-200 px-5 py-3" [ngClass]="{ 'top-[48px]': showDevToolbar(), 'top-0': !showDevToolbar() }">
<button
type="button"
class="flex items-center justify-center w-10 h-10 rounded-lg hover:bg-gray-100 transition-colors"
Expand All @@ -43,7 +74,7 @@
<i class="fa-light fa-bars text-gray-600 text-xl"></i>
</button>
</div>
<div class="flex-grow px-5 md:px-8 py-6">
<div class="flex-grow px-10 py-6">
<router-outlet />
</div>
<!-- Footer Component -->
Expand Down
Loading
Loading