mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
* feat(branding): white-label — public /branding, admin write, shell theming app_settings branding.* keys (product name, logo/favicon URLs, accent color, docs/support links). Public GET /branding so login and shell theme pre-auth; admin writes via PUT /settings. useBranding() applies document.title, accent CSS var and favicon; BrandWordmark replaces hardcoded wordmarks; admin Platform settings gain a Branding form. MCP containr_branding_get + settings_update. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(invites): team invite links — admin create/revoke, public accept Admins mint single-use invite links (sha256-hashed token at rest, optional email binding, configurable TTL). Public endpoints let the accept page validate the link and register — working even while public signup is disabled. - migration + sqlc queries/model for user_invites - POST /admin/invites (returns one-time token+url), GET list, DELETE revoke - GET /auth/invites/:token public validation, POST /auth/accept-invite - CLI containr invites list|create|revoke; MCP invites tools - /auth/accept-invite page (expired/used/invalid states, bound-email lock) - Admin page Team invites section: create, copy link, revoke - OpenAPI + generated types + CLI guide Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(admin): impersonate user — 15-min token, audit-logged POST /admin/users/:id/impersonate mints a short-lived bearer token (impersonated_by claim) for support/debugging. Admin accounts cannot be impersonated; every mint is audit-logged with actor + target. - CLI: containr admin impersonate <user-id> (works via CONTAINR_TOKEN) - MCP: containr_admin_impersonate - Admin UI: per-user Impersonate action → token modal with usage hint - OpenAPI + generated types Verified live: token resolves to target, admin target 403, audit row. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(git): GitHub App manifest self-provisioning + app-level webhook Admins create the instance GitHub App in-browser instead of hand- setting GITHUB_APP_* env vars: - POST /admin/git/github-app/manifest builds the manifest + URL; the frontend auto-submits it to github.com - /git/github-app/callback page exchanges the code via POST /admin/git/github-app/convert; credentials persist in app_settings (precedence over env) - GET /admin/git/github-app reports configured/source - POST /api/git/github-app/webhook — single app-level push receiver, HMAC-verified, dispatches via extracted dispatchPushToServices - install-url + installation-token paths resolve settings→env - CLI: containr admin github-app status|manifest|convert; MCP tools - Git providers UI shows "Set up GitHub App" for admins when unconfigured Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
182 lines
6.9 KiB
TypeScript
182 lines
6.9 KiB
TypeScript
import { Link, Navigate, Outlet, Route, Routes, useLocation } from 'react-router-dom';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { Compass, ShieldCheck } from 'lucide-react';
|
|
import { PlatformShell } from './layout/PlatformShell';
|
|
import { ProjectsPage } from '@/features/projects/pages/ProjectsPage';
|
|
import { ProjectWorkspacePage } from '@/features/workspace/pages/ProjectWorkspacePage';
|
|
import { ServiceDetailPage } from '@/features/service/pages/ServiceDetailPage';
|
|
import { ServiceMetricsDashboard } from '@/features/service/pages/ServiceMetricsDashboard';
|
|
import { BuildsPage } from '@/features/builds/pages/BuildsPage';
|
|
import { TemplatesPage } from '@/features/templates/pages/TemplatesPage';
|
|
import { AuditLogsPage } from '@/features/audit/pages/AuditLogsPage';
|
|
import { WebhooksPage } from '@/features/webhooks/pages/WebhooksPage';
|
|
import { OperationsPage } from '@/features/operations/pages/OperationsPage';
|
|
import { AdminPage } from '@/features/admin/pages/AdminPage';
|
|
import { LandingPage } from '@/features/landing/LandingPage';
|
|
import { useBranding } from '@/lib/use-branding';
|
|
import {
|
|
DocsPage,
|
|
PeoplePage,
|
|
SettingsPage,
|
|
UsagePage,
|
|
DatabasesPage,
|
|
HighAvailabilityPage,
|
|
SecurityPage,
|
|
ComponentShowcase,
|
|
GitHubAppCallbackPage,
|
|
} from '@/features/secondary/pages';
|
|
import { AcceptInvitePage, SignInPage, SignUpPage } from '@/features/auth/pages';
|
|
import { useAuthSession } from '@/lib/use-auth-session';
|
|
import { useDemoMode } from '@/lib/demo-mode';
|
|
import { getCurrentUserProfile } from '@/lib/api-client';
|
|
import { ErrorBoundary, LoadingState } from '@/shared/components';
|
|
|
|
function AuthRequired() {
|
|
const location = useLocation();
|
|
const isDemoMode = useDemoMode();
|
|
const sessionQuery = useAuthSession({ enabled: !isDemoMode });
|
|
|
|
if (isDemoMode) {
|
|
return <Outlet />;
|
|
}
|
|
|
|
if (sessionQuery.isPending) {
|
|
return (
|
|
<div className="min-h-screen bg-[var(--bg-void)]">
|
|
<LoadingState message="Checking session..." className="h-screen" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!sessionQuery.data) {
|
|
const redirect = `${location.pathname}${location.search}`;
|
|
return <Navigate to={`/auth/sign-in?redirect=${encodeURIComponent(redirect)}`} replace />;
|
|
}
|
|
|
|
return <Outlet />;
|
|
}
|
|
|
|
function AdminRequired() {
|
|
const location = useLocation();
|
|
const isDemoMode = useDemoMode();
|
|
const sessionQuery = useAuthSession({ enabled: !isDemoMode });
|
|
const profileQuery = useQuery({
|
|
queryKey: ['current-profile'],
|
|
queryFn: getCurrentUserProfile,
|
|
enabled: !isDemoMode && Boolean(sessionQuery.data),
|
|
retry: false,
|
|
});
|
|
|
|
if (isDemoMode) {
|
|
return <Outlet />;
|
|
}
|
|
|
|
if (sessionQuery.isPending || (sessionQuery.data && profileQuery.isPending)) {
|
|
return (
|
|
<div className="min-h-screen bg-[var(--bg-void)]">
|
|
<LoadingState message="Checking admin access..." className="h-screen" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!sessionQuery.data) {
|
|
const redirect = `${location.pathname}${location.search}`;
|
|
return <Navigate to={`/auth/sign-in?redirect=${encodeURIComponent(redirect)}`} replace />;
|
|
}
|
|
|
|
if (!profileQuery.data?.isAdmin) {
|
|
return (
|
|
<div className="flex min-h-screen flex-col items-center justify-center gap-4 bg-[var(--bg-void)] px-6 text-center">
|
|
<ShieldCheck size={32} className="text-[var(--error)]" />
|
|
<div>
|
|
<h1 className="text-lg font-semibold text-[var(--text-primary)]">Admin access required</h1>
|
|
<p className="mt-1 max-w-sm text-sm text-[var(--text-secondary)]">
|
|
This area is restricted to the platform administrator.
|
|
</p>
|
|
</div>
|
|
<Link
|
|
to="/"
|
|
className="rounded-[var(--radius-md)] border border-[var(--border-default)] px-4 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
>
|
|
Back to home
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <Outlet />;
|
|
}
|
|
|
|
function NotFoundPage() {
|
|
return (
|
|
<div className="flex min-h-[60vh] flex-col items-center justify-center gap-4 px-6 text-center">
|
|
<Compass size={32} className="text-[var(--text-tertiary)]" />
|
|
<div>
|
|
<h1 className="text-lg font-semibold text-[var(--text-primary)]">Page not found</h1>
|
|
<p className="mt-1 max-w-sm text-sm text-[var(--text-secondary)]">
|
|
The page you are looking for does not exist or was moved.
|
|
</p>
|
|
</div>
|
|
<Link
|
|
to="/"
|
|
className="rounded-[var(--radius-md)] border border-[var(--border-default)] px-4 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
>
|
|
Back to home
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
useBranding();
|
|
return (
|
|
<ErrorBoundary>
|
|
<Routes>
|
|
<Route path="/" element={<LandingPage />} />
|
|
<Route path="/auth/sign-in" element={<SignInPage />} />
|
|
<Route path="/auth/sign-up" element={<SignUpPage />} />
|
|
<Route path="/auth/accept-invite" element={<AcceptInvitePage />} />
|
|
|
|
{/* Public browsing — approved projects, templates, docs. */}
|
|
{/* Project workspace renders outside the shell — fullscreen canvas, no nav chrome */}
|
|
<Route path="/projects/:projectId" element={<ProjectWorkspacePage />} />
|
|
<Route path="/projects/:projectId/services/:serviceId" element={<ServiceDetailPage />} />
|
|
|
|
<Route element={<PlatformShell />}>
|
|
<Route path="/projects" element={<ProjectsPage />} />
|
|
<Route path="/templates" element={<TemplatesPage />} />
|
|
<Route path="/databases" element={<DatabasesPage />} />
|
|
<Route path="/ha" element={<HighAvailabilityPage />} />
|
|
<Route path="/operations" element={<OperationsPage />} />
|
|
<Route path="/security" element={<SecurityPage />} />
|
|
<Route path="/usage" element={<UsagePage />} />
|
|
<Route path="/docs" element={<DocsPage />} />
|
|
|
|
{/* Authenticated areas */}
|
|
<Route element={<AuthRequired />}>
|
|
<Route path="/builds" element={<BuildsPage />} />
|
|
<Route path="/people" element={<PeoplePage />} />
|
|
<Route path="/settings" element={<SettingsPage />} />
|
|
<Route path="/git/github-app/callback" element={<GitHubAppCallbackPage />} />
|
|
<Route path="/settings/audit-logs" element={<AuditLogsPage />} />
|
|
<Route path="/settings/webhooks" element={<WebhooksPage />} />
|
|
<Route path="/showcase" element={<ComponentShowcase />} />
|
|
</Route>
|
|
|
|
{/* Platform owner only */}
|
|
<Route element={<AdminRequired />}>
|
|
<Route path="/admin" element={<AdminPage />} />
|
|
</Route>
|
|
|
|
<Route path="*" element={<NotFoundPage />} />
|
|
</Route>
|
|
|
|
{/* Full-bleed design demo — renders its own chrome, no app shell */}
|
|
<Route element={<AuthRequired />}>
|
|
<Route path="/metrics-demo" element={<ServiceMetricsDashboard />} />
|
|
</Route>
|
|
</Routes>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|