Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
104 changes: 104 additions & 0 deletions docs/superpowers/specs/2026-04-06-sidebar-reorganization-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Sidebar Reorganization — Admin Hub + Simplified Navigation

**Date:** 2026-04-06

## Problem

The sidebar has ~20 items for admin users (7 AppSidebar + 13 AdminSidebar). Most admin items are low-frequency config tools that don't warrant permanent sidebar slots. The UserDropdown has 3 granular account links that duplicate ManageLayout's sidebar. Some manage pages are missing the ManageLayout wrapper.

## Design

### 1. Sidebar (AppSidebar) — 7 items

| Order | Label | URL | Notes |
|-------|-------|-----|-------|
| 10 | Dashboard | `/` | Existing |
| 20 | Products | `/products` | Existing |
| 30 | Orders | `/orders` | Existing |
| 40 | Pages | `/pages` | Move from Navbar to AppSidebar |
| 50 | Files | `/files` | Existing |
| 80 | Settings | `/settings/me` | Changed from `/settings/manage` to user settings |
| 85 | Admin | `/admin` | **New** — Admin role only, links to hub page |

### 2. Items removed from sidebar

| Item | New location | Reason |
|------|-------------|--------|
| Marketplace | Navbar only (keep existing entry) | Public discovery, not daily workflow |
| Account | UserDropdown only | Accessed via avatar menu |
| All AdminSidebar items (13) | Admin hub page | Low-frequency config tools |

### 3. Admin Hub Page (`/admin`)

New `IViewEndpoint` in the Admin module at `GET /admin`. Renders an Inertia page `Admin/Hub` with a grid of card links grouped into sections.

**Identity**
- Users — `/admin/users` — Manage user accounts
- Roles — `/admin/roles` — Manage roles and permissions
- OAuth Clients — `/openiddict/clients` — Manage OAuth/OIDC applications
- Tenants — `/tenants/manage` — Manage tenants and hosts

**Content**
- Pages — `/pages/manage` — Manage published pages
- Email Templates — `/email/templates` — Create and edit email templates
- Email History — `/email/history` — View sent emails
- Menus — `/settings/menus` — Configure navigation menus

**System**
- Feature Flags — `/feature-flags/manage` — Toggle features on/off
- Rate Limiting — `/rate-limiting/manage` — Configure API rate limits
- Background Jobs — `/admin/jobs` — Monitor job queues
- Audit Logs — `/audit-logs/browse` — Review activity logs
- App Settings — `/settings/manage` — Application settings

Each card: icon (SVG), title, one-line description. Entire card is a link.

### 4. UserDropdown — simplified

Replace 3 granular account links with single entry:
- **Account Settings** → `/Identity/Account/Manage`
- *(divider)*
- **Logout** (existing)

### 5. ManageLayout consistency

Ensure ALL account manage pages use `ManageLayout` wrapper. Currently missing from:
- ManageIndex (Profile)
- Email
- ChangePassword
- SetPassword
- PersonalData
- DeletePersonalData
- ExternalLogins

## Files to change

### Module ConfigureMenu changes
- **Admin** — Remove Users/Roles from AdminSidebar; add Admin hub link to AppSidebar
- **AuditLogs** — Remove 2 AdminSidebar items
- **BackgroundJobs** — Remove AdminSidebar item
- **Email** — Remove 3 AdminSidebar items
- **FeatureFlags** — Remove AdminSidebar item
- **Marketplace** — Remove AppSidebar item (keep Navbar)
- **OpenIddict** — Remove AdminSidebar item
- **Orders** — Remove Navbar item (keep AppSidebar)
- **PageBuilder** — Remove Navbar item; change AdminSidebar to AppSidebar
- **Products** — Remove Navbar items (keep AppSidebar)
- **RateLimiting** — Remove AdminSidebar item
- **Settings** — Change AppSidebar URL to `/settings/me`; remove Menus AdminSidebar item
- **Tenants** — Remove AdminSidebar item
- **Users** — Replace 3 UserDropdown items with 1; remove Account from AppSidebar

### New files
- `modules/Admin/src/SimpleModule.Admin/Pages/HubEndpoint.cs` — Admin hub view endpoint
- `modules/Admin/src/SimpleModule.Admin/Views/Hub.tsx` — Admin hub React page

### Modified files
- `modules/Admin/src/SimpleModule.Admin/Pages/index.ts` — Register Hub page
- `packages/SimpleModule.UI/components/layouts/app-layout.tsx` — Remove AdminSection component (no longer needed)
- Account manage pages — Wrap with ManageLayout

### No changes
- API endpoints unchanged
- Route constants unchanged (already updated in previous task)
- Inertia page names unchanged
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Standardize Default Page URLs

**Date:** 2026-04-06
**Convention:** Hybrid by audience — public/user-facing modules use root URL, admin-only modules use explicit action suffix.

## Changes

### Public/user-facing: shorten to root

| Module | Before | After | Constant |
|--------|--------|-------|----------|
| Products | `/products/browse` | `/products/` | `ProductsConstants.Routes.Browse = "/"` |
| Marketplace | `/marketplace/browse` | `/marketplace/` | `MarketplaceConstants.Routes.Browse = "/"` |
| FileStorage | `/files/browse` | `/files/` | `FileStorageConstants.Routes.Browse = "/"` |

### Admin-only: add explicit action

| Module | Before | After | Constant |
|--------|--------|-------|----------|
| FeatureFlags | `/feature-flags/` | `/feature-flags/manage` | `FeatureFlagsConstants.Routes.Manage = "/manage"` |
| RateLimiting | `/rate-limiting/` | `/rate-limiting/manage` | `RateLimitingConstants.Routes.Admin = "/manage"` |
| Settings | `/settings/` | `/settings/manage` | `SettingsConstants.Routes.Views.AdminSettings = "/manage"` |

## Files to Change Per Module

### 1. Route constant (source of truth)
- `modules/{Module}/src/SimpleModule.{Module}.Contracts/{Module}Constants.cs`

### 2. Menu URLs in module class
- `modules/{Module}/src/SimpleModule.{Module}/{Module}Module.cs`
- Products: 2 menu items reference `/products/browse` (Navbar + AppSidebar)
- Marketplace: 2 menu items reference `/marketplace/browse` (Navbar + AppSidebar)
- FileStorage: 1 menu item references `/files/browse`
- FeatureFlags: 1 menu item references `/feature-flags`
- RateLimiting: 1 menu item references `/rate-limiting`
- Settings: 1 menu item references `/settings` (AppSidebar)

### 3. Auto-generated routes.ts
- `packages/SimpleModule.Client/src/routes.ts` — regenerated via `npm run generate:routes` after `dotnet build`

### 4. E2E page objects
- `tests/e2e/pages/products/browse.page.ts` — `/products/browse` -> `/products/`
- `tests/e2e/pages/marketplace/browse.page.ts` — `/marketplace/browse` -> `/marketplace/`
- `tests/e2e/pages/feature-flags/manage.page.ts` — `/feature-flags` -> `/feature-flags/manage`
- `tests/e2e/pages/rate-limiting/admin.page.ts` — `/rate-limiting` -> `/rate-limiting/manage`
- `tests/e2e/pages/settings/admin.page.ts` — `/settings` -> `/settings/manage`

### 5. E2E test specs
- `tests/e2e/tests/flows/permissions.spec.ts` — `/products/browse`
- `tests/e2e/tests/smoke/filestorage.spec.ts` — `/files/browse` (4 references)
- `tests/e2e/tests/flows/filestorage-crud.spec.ts` — `/files/browse` (4 references)

## No changes needed
- Inertia page names (e.g., `Products/Browse`) stay the same — only URLs change
- `Pages/index.ts` files in each module — unchanged
- API endpoints — unchanged
- Integration test files in `*.Tests/` projects — no URL references found
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ PermissionRequirement requirement
)
{
// Admin role bypasses all permission checks
if (context.User.IsInRole("Admin"))
if (context.User.IsInRole(WellKnownRoles.Admin))
{
context.Succeed(requirement);
return Task.CompletedTask;
Expand Down
6 changes: 6 additions & 0 deletions framework/SimpleModule.Core/Authorization/WellKnownRoles.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace SimpleModule.Core.Authorization;

public static class WellKnownRoles
{
public const string Admin = "Admin";
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Security.Claims;
using SimpleModule.Core.Authorization;

namespace SimpleModule.Users.Extensions;
namespace SimpleModule.Core.Extensions;

public static class ClaimsPrincipalExtensions
{
Expand All @@ -12,4 +13,12 @@ public static class ClaimsPrincipalExtensions
return principal.FindFirstValue("sub")
?? principal.FindFirstValue(ClaimTypes.NameIdentifier);
}

/// <summary>
/// Returns null for Admin users (no scoping — sees all resources), or the user ID for regular users.
/// </summary>
public static string? GetScopedUserId(this ClaimsPrincipal principal)
{
return principal.IsInRole(WellKnownRoles.Admin) ? null : principal.GetUserId();
}
}
8 changes: 8 additions & 0 deletions framework/SimpleModule.Core/Menu/MenuItem.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Generic;

namespace SimpleModule.Core.Menu;

public sealed class MenuItem
Expand All @@ -14,4 +16,10 @@ public sealed class MenuItem
public MenuSection Section { get; init; } = MenuSection.Navbar;
public bool RequiresAuth { get; init; } = true;
public string? Group { get; init; }

/// <summary>
/// When set, this menu item is only visible to users who have at least one of these roles.
/// An empty list means visible to all authenticated users.
/// </summary>
public IReadOnlyList<string> Roles { get; init; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,17 @@ CancellationToken cancellationToken
{
var entityType = entity.GetType();
var metadata = MetadataCache.GetOrAdd(entityType, BuildMetadata);
var handlers = serviceProvider.GetServices(metadata.HandlerType);

object?[] handlers;
try
{
handlers = [.. serviceProvider.GetServices(metadata.HandlerType)];
}
catch (ObjectDisposedException)
{
// Service provider disposed during shutdown — skip handler dispatch
return;
}

foreach (var handler in handlers)
{
Expand Down
1 change: 0 additions & 1 deletion framework/SimpleModule.DevTools/DevToolsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ public static class DevToolsExtensions
public static IServiceCollection AddDevTools(this IServiceCollection services)
{
services.AddSingleton<LiveReloadServer>();
services.AddHostedService<ViteDevWatchService>();
return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,31 @@ public async Task InvokeAsync(HttpContext context)
}
);

// Menu items
// Menu items (filtered by user roles)
var menuRegistry = context.RequestServices.GetService<IMenuRegistry>();
if (menuRegistry is not null)
{
IReadOnlyList<MenuItem> Filter(MenuSection section)
{
var items = menuRegistry.GetItems(section);
if (!isAuthenticated)
{
return items.Where(m => !m.RequiresAuth).ToList();
}

return items
.Where(m => m.Roles.Count == 0 || m.Roles.Any(r => user.IsInRole(r)))
.ToList();
}

sharedData.Set(
"menus",
new
{
sidebar = menuRegistry.GetItems(MenuSection.AppSidebar),
adminSidebar = menuRegistry.GetItems(MenuSection.AdminSidebar),
userDropdown = menuRegistry.GetItems(MenuSection.UserDropdown),
navbar = menuRegistry.GetItems(MenuSection.Navbar),
sidebar = Filter(MenuSection.AppSidebar),
adminSidebar = Filter(MenuSection.AdminSidebar),
userDropdown = Filter(MenuSection.UserDropdown),
navbar = Filter(MenuSection.Navbar),
}
);
}
Expand Down
6 changes: 4 additions & 2 deletions framework/SimpleModule.Hosting/SimpleModuleHostExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public static WebApplicationBuilder AddSimpleModuleInfrastructure(
configure?.Invoke(options);
builder.Services.AddSingleton(options);

builder.Services.Configure<HostOptions>(o => o.ShutdownTimeout = TimeSpan.FromSeconds(5));

BridgeAspireConnectionString(builder.Configuration);
options.DatabaseProvider = ValidateDatabaseConfiguration(builder.Configuration);

Expand Down Expand Up @@ -171,8 +173,8 @@ public static async Task UseSimpleModuleInfrastructure(this WebApplication app)
var csp =
$"default-src 'none'; "
+ $"script-src 'self' 'nonce-{nonce}'; "
+ $"style-src 'self' 'unsafe-inline' fonts.googleapis.com; "
+ $"font-src 'self' fonts.gstatic.com; "
+ $"style-src 'self' 'unsafe-inline' fonts.googleapis.com rsms.me; "
+ $"font-src 'self' fonts.gstatic.com rsms.me; "
+ $"connect-src {connectSrc}; "
+ $"img-src 'self' data:; "
+ $"object-src 'none'; "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public static class AdminConstants
public static class Routes
{
// View routes
public const string Hub = "/";
public const string Roles = "/roles";
public const string RolesCreate = "/roles/create";
public const string RolesEdit = "/roles/{id}/edit";
Expand Down
22 changes: 6 additions & 16 deletions modules/Admin/src/SimpleModule.Admin/AdminModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,13 @@ public void ConfigureMenu(IMenuBuilder menus)
menus.Add(
new MenuItem
{
Label = "Users",
Url = "/admin/users",
Label = "Admin",
Url = "/admin",
Icon =
"""<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>""",
Order = 10,
Section = MenuSection.AdminSidebar,
}
);
menus.Add(
new MenuItem
{
Label = "Roles",
Url = "/admin/roles",
Icon =
"""<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>""",
Order = 11,
Section = MenuSection.AdminSidebar,
"""<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.248a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.282c-.062-.373-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.28z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>""",
Order = 85,
Section = MenuSection.AppSidebar,
Roles = ["Admin"],
}
);
}
Expand Down
Loading
Loading