feat(customers): customer portal (#354) on top of feature-flags reorg
Implements the recurring-customer login surface from the-luap/picpeak#354 plugged into the maintainer's new feature-flag infrastructure (PR #443) instead of a parallel toggle. * New `customerPortal` feature flag (foundation flag for the not-yet-built calendar/quotes/bills/messaging customer surfaces). Defaults FALSE on fresh installs, TRUE on existing installs (events > 0) via migration 095 so live customer accounts don't disappear mid-deployment. * Foundation schema: customer_accounts, customer_invitations, event_customer_assignments, customer_password_resets, plus RBAC permissions customers.view / .create / .delete granted to super_admin + admin system roles. * Backend: /api/admin/customers (invite, list, search, assign, deactivate, reset password) + /api/customer/auth/* + /api/customer/* (login, dashboard, accept-invite, reset). Customer JWT bypass minted via /api/customer/events/:slug/access-token so existing gallery middleware stays untouched. * Frontend: /customer/* route tree gated by RequireFeature flag customerPortal, with login / dashboard / accept-invite / reset pages and a customer-side sidebar layout. /admin/customers and /admin/customers/:id gated identically. * Settings → Features grows a "Customers" section with a Customer portal card. The maintainer's Features tab stays the single source of truth — no parallel Advanced features tab. * CustomerAccountPicker on event create/edit forms hides itself when the flag is off; backend ignores customer_account_ids in that case instead of erroring the whole event save. Translations: en + de hand-translated. nl/pt/ru fall through to en — flagged here as needing native review. Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
@@ -22,9 +22,23 @@ import {
|
||||
AnalyticsPage,
|
||||
SettingsPage,
|
||||
UserManagementPage,
|
||||
CustomerManagementPage,
|
||||
CustomerDetailPage,
|
||||
WebhookDeliveriesPage
|
||||
} from './pages/admin';
|
||||
import { AcceptInvitePage } from './pages/public/AcceptInvitePage';
|
||||
import {
|
||||
CustomerLoginPage,
|
||||
CustomerDashboardPage,
|
||||
CustomerAcceptInvitePage,
|
||||
CustomerLayout,
|
||||
CustomerProfilePage,
|
||||
CustomerCalendarPage,
|
||||
CustomerQuotesPage,
|
||||
CustomerBillsPage,
|
||||
CustomerResetPasswordPage,
|
||||
} from './pages/customer';
|
||||
import { CustomerAuthProvider } from './contexts/CustomerAuthContext';
|
||||
import { AdminLayout, AdminAuthWrapper } from './components/admin';
|
||||
import { RequireFeature } from './components/admin/RequireFeature';
|
||||
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon, RobotsMetaTags, CMSContentBlock } from './components/common';
|
||||
@@ -132,6 +146,13 @@ function App() {
|
||||
<Route element={<RequireFeature flag="userManagement" />}>
|
||||
<Route path="users" element={<UserManagementPage />} />
|
||||
</Route>
|
||||
{/* Customer accounts (#354) — admin-side management.
|
||||
Hidden from sidebar + redirected away when the
|
||||
customerPortal flag is off. */}
|
||||
<Route element={<RequireFeature flag="customerPortal" />}>
|
||||
<Route path="customers" element={<CustomerManagementPage />} />
|
||||
<Route path="customers/:id" element={<CustomerDetailPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="webhooks/:id/deliveries" element={<WebhookDeliveriesPage />} />
|
||||
@@ -153,6 +174,38 @@ function App() {
|
||||
{/* Public invitation acceptance page */}
|
||||
<Route path="/invite/:token" element={<AcceptInvitePage />} />
|
||||
|
||||
{/* Customer surface (#354). Strictly separate provider /
|
||||
cookie / API surface from /admin/*. Gated by the
|
||||
customerPortal feature flag — when off, all
|
||||
/customer/* URLs redirect to /admin/dashboard. */}
|
||||
<Route element={<RequireFeature flag="customerPortal" fallback="/admin/login" />}>
|
||||
<Route path="/customer/*" element={
|
||||
<CustomerAuthProvider>
|
||||
<Routes>
|
||||
{/* Public surfaces: login, accept-invite, reset —
|
||||
no CustomerLayout (their own branded shells). */}
|
||||
<Route path="login" element={<CustomerLoginPage />} />
|
||||
<Route path="invite/:token" element={<CustomerAcceptInvitePage />} />
|
||||
<Route path="reset-password/:token" element={<CustomerResetPasswordPage />} />
|
||||
|
||||
{/* Authenticated surfaces share the sidebar layout
|
||||
(Outlet pattern, mirrors AdminLayout). The
|
||||
CustomerLayout itself enforces auth — bouncing
|
||||
unauthenticated visitors to /customer/login. */}
|
||||
<Route element={<CustomerLayout />}>
|
||||
<Route path="dashboard" element={<CustomerDashboardPage />} />
|
||||
<Route path="calendar" element={<CustomerCalendarPage />} />
|
||||
<Route path="quotes" element={<CustomerQuotesPage />} />
|
||||
<Route path="bills" element={<CustomerBillsPage />} />
|
||||
<Route path="profile" element={<CustomerProfilePage />} />
|
||||
</Route>
|
||||
|
||||
<Route index element={<Navigate to="/customer/dashboard" replace />} />
|
||||
</Routes>
|
||||
</CustomerAuthProvider>
|
||||
} />
|
||||
</Route>
|
||||
|
||||
{/* Public legal pages */}
|
||||
<Route path="/impressum" element={<LegalPage />} />
|
||||
<Route path="/datenschutz" element={<LegalPage />} />
|
||||
|
||||
Reference in New Issue
Block a user