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
1 change: 1 addition & 0 deletions apps/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"test:ui": "vitest --ui"
},
"dependencies": {
"@object-ui/auth": "workspace:*",
"@object-ui/components": "workspace:*",
"@object-ui/core": "workspace:*",
"@object-ui/data-objectstack": "workspace:*",
Expand Down
46 changes: 39 additions & 7 deletions apps/console/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SchemaRendererProvider } from '@object-ui/react';
import { ObjectStackAdapter } from './dataSource';
import type { ConnectionState } from './dataSource';
import appConfig from '../objectstack.shared';
import { AuthProvider, AuthGuard, useAuth } from '@object-ui/auth';

// Components
import { ConsoleLayout } from './components/ConsoleLayout';
Expand All @@ -19,12 +20,25 @@ import { PageView } from './components/PageView';
import { ReportView } from './components/ReportView';
import { ExpressionProvider } from './context/ExpressionProvider';

// Auth Pages
import { LoginPage } from './pages/LoginPage';
import { RegisterPage } from './pages/RegisterPage';
import { ForgotPasswordPage } from './pages/ForgotPasswordPage';

// System Admin Pages
import { UserManagementPage } from './pages/system/UserManagementPage';
import { OrgManagementPage } from './pages/system/OrgManagementPage';
import { RoleManagementPage } from './pages/system/RoleManagementPage';
import { AuditLogPage } from './pages/system/AuditLogPage';
import { ProfilePage } from './pages/system/ProfilePage';

import { useParams } from 'react-router-dom';
import { ThemeProvider } from './components/theme-provider';

export function AppContent() {
const [dataSource, setDataSource] = useState<ObjectStackAdapter | null>(null);
const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');
const { user } = useAuth();

// App Selection
const navigate = useNavigate();
Expand Down Expand Up @@ -132,7 +146,9 @@ export function AppContent() {
);

// Expression context for dynamic visibility/disabled/hidden expressions
const expressionUser = { name: 'John Doe', email: 'admin@example.com', role: 'admin' };
const expressionUser = user
? { name: user.name, email: user.email, role: user.role ?? 'user' }
: { name: 'Anonymous', email: '', role: 'guest' };

return (
<ExpressionProvider user={expressionUser} app={activeApp} data={{}}>
Expand Down Expand Up @@ -191,6 +207,13 @@ export function AppContent() {
<Route path="page/:pageName" element={
<PageView />
} />

{/* System Administration Routes */}
<Route path="system/users" element={<UserManagementPage />} />
<Route path="system/organizations" element={<OrgManagementPage />} />
<Route path="system/roles" element={<RoleManagementPage />} />
<Route path="system/audit-log" element={<AuditLogPage />} />
Comment on lines +212 to +215
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

System admin routes are mounted without any admin role guard, so any authenticated user can navigate to /apps/:appName/system/users|organizations|roles. The pages only hide the “Add …” button, which doesn’t meet the “admin-gated” intent and is a security gap. Wrap the admin-only routes (users/orgs/roles, possibly audit-log) in an AuthGuard requiredRoles={["admin"]} (or a permissions-based guard) and provide an explicit access-denied fallback.

Suggested change
<Route path="system/users" element={<UserManagementPage />} />
<Route path="system/organizations" element={<OrgManagementPage />} />
<Route path="system/roles" element={<RoleManagementPage />} />
<Route path="system/audit-log" element={<AuditLogPage />} />
<Route
path="system/users"
element={
<AuthGuard
requiredRoles={["admin"]}
fallback={
<Empty>
<EmptyTitle>Access denied</EmptyTitle>
</Empty>
}
>
<UserManagementPage />
</AuthGuard>
}
/>
<Route
path="system/organizations"
element={
<AuthGuard
requiredRoles={["admin"]}
fallback={
<Empty>
<EmptyTitle>Access denied</EmptyTitle>
</Empty>
}
>
<OrgManagementPage />
</AuthGuard>
}
/>
<Route
path="system/roles"
element={
<AuthGuard
requiredRoles={["admin"]}
fallback={
<Empty>
<EmptyTitle>Access denied</EmptyTitle>
</Empty>
}
>
<RoleManagementPage />
</AuthGuard>
}
/>
<Route
path="system/audit-log"
element={
<AuthGuard
requiredRoles={["admin"]}
fallback={
<Empty>
<EmptyTitle>Access denied</EmptyTitle>
</Empty>
}
>
<AuditLogPage />
</AuthGuard>
}
/>

Copilot uses AI. Check for mistakes.
<Route path="system/profile" element={<ProfilePage />} />
</Routes>
</ErrorBoundary>

Expand Down Expand Up @@ -268,12 +291,21 @@ function RootRedirect() {
export function App() {
return (
<ThemeProvider defaultTheme="system" storageKey="object-ui-theme">
<BrowserRouter basename="/">
<Routes>
<Route path="/apps/:appName/*" element={<AppContent />} />
<Route path="/" element={<RootRedirect />} />
</Routes>
</BrowserRouter>
<AuthProvider authUrl="/api/auth">
<BrowserRouter basename="/">
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route path="/apps/:appName/*" element={
<AuthGuard fallback={<Navigate to="/login" />} loadingFallback={<LoadingScreen />}>
<AppContent />
</AuthGuard>
} />
<Route path="/" element={<RootRedirect />} />
</Routes>
</BrowserRouter>
</AuthProvider>
</ThemeProvider>
);
}
27 changes: 18 additions & 9 deletions apps/console/src/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
} from 'lucide-react';
import appConfig from '../../objectstack.shared';
import { useExpressionContext, evaluateVisibility } from '../context/ExpressionProvider';
import { useAuth, getUserInitials } from '@object-ui/auth';

/**
* Resolve a Lucide icon component by name string.
Expand Down Expand Up @@ -75,6 +76,7 @@ function getIcon(name?: string): React.ComponentType<any> {

export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: string, onAppChange: (name: string) => void }) {
const { isMobile } = useSidebar();
const { user, signOut } = useAuth();

const apps = appConfig.apps || [];
// Filter out inactive apps
Expand Down Expand Up @@ -165,12 +167,14 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src="/avatars/user.jpg" alt="User" />
<AvatarFallback className="rounded-lg bg-primary text-primary-foreground">JD</AvatarFallback>
<AvatarImage src={user?.image ?? '/avatars/user.jpg'} alt={user?.name ?? 'User'} />
<AvatarFallback className="rounded-lg bg-primary text-primary-foreground">
{getUserInitials(user)}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">John Doe</span>
<span className="truncate text-xs text-muted-foreground">admin@example.com</span>
<span className="truncate font-semibold">{user?.name ?? 'User'}</span>
<span className="truncate text-xs text-muted-foreground">{user?.email ?? ''}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
Expand All @@ -184,12 +188,14 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src="/avatars/user.jpg" alt="User" />
<AvatarFallback className="rounded-lg bg-primary text-primary-foreground">JD</AvatarFallback>
<AvatarImage src={user?.image ?? '/avatars/user.jpg'} alt={user?.name ?? 'User'} />
<AvatarFallback className="rounded-lg bg-primary text-primary-foreground">
{getUserInitials(user)}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">John Doe</span>
<span className="truncate text-xs text-muted-foreground">admin@example.com</span>
<span className="truncate font-semibold">{user?.name ?? 'User'}</span>
<span className="truncate text-xs text-muted-foreground">{user?.email ?? ''}</span>
</div>
</div>
</DropdownMenuLabel>
Expand All @@ -201,7 +207,10 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive focus:text-destructive">
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => signOut()}
>
<LogOut className="mr-2 h-4 w-4" />
Log out
</DropdownMenuItem>
Expand Down
13 changes: 13 additions & 0 deletions apps/console/src/pages/ForgotPasswordPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Forgot Password Page for ObjectStack Console
*/

import { ForgotPasswordForm } from '@object-ui/auth';

export function ForgotPasswordPage() {
return (
<div className="flex min-h-screen items-center justify-center bg-background">
<ForgotPasswordForm loginUrl="/login" />
</div>
);
}
20 changes: 20 additions & 0 deletions apps/console/src/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Login Page for ObjectStack Console
*/

import { useNavigate } from 'react-router-dom';
import { LoginForm } from '@object-ui/auth';

export function LoginPage() {
const navigate = useNavigate();

return (
<div className="flex min-h-screen items-center justify-center bg-background">
<LoginForm
onSuccess={() => navigate('/')}
registerUrl="/register"
forgotPasswordUrl="/forgot-password"
/>
</div>
);
}
19 changes: 19 additions & 0 deletions apps/console/src/pages/RegisterPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Register Page for ObjectStack Console
*/

import { useNavigate } from 'react-router-dom';
import { RegisterForm } from '@object-ui/auth';

export function RegisterPage() {
const navigate = useNavigate();

return (
<div className="flex min-h-screen items-center justify-center bg-background">
<RegisterForm
onSuccess={() => navigate('/')}
loginUrl="/login"
/>
</div>
);
}
45 changes: 45 additions & 0 deletions apps/console/src/pages/system/AuditLogPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Audit Log Page
*
* Read-only grid displaying system audit logs.
* Shows user actions, resources, timestamps, and details.
*/

import { systemObjects } from './systemObjects';

const auditObject = systemObjects.find((o) => o.name === 'sys_audit_log')!;

export function AuditLogPage() {
return (
<div className="flex flex-col gap-6 p-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">Audit Log</h1>
<p className="text-muted-foreground">View system activity and user actions</p>
</div>

<div className="rounded-md border">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
{auditObject.views[0].columns.map((col) => {
const field = auditObject.fields.find((f) => f.name === col);
return (
<th key={col} className="h-10 px-4 text-left font-medium text-muted-foreground">
{field?.label ?? col}
</th>
);
})}
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-4 text-muted-foreground" colSpan={auditObject.views[0].columns.length}>
Connect to ObjectStack server to load audit logs. In production, this page uses plugin-grid in read-only mode.
</td>
</tr>
</tbody>
</table>
</div>
</div>
);
}
59 changes: 59 additions & 0 deletions apps/console/src/pages/system/OrgManagementPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Organization Management Page
*
* Displays a list of organizations with member management.
* Reuses the plugin-grid for data display.
*/

import { useAuth } from '@object-ui/auth';
import { systemObjects } from './systemObjects';

const orgObject = systemObjects.find((o) => o.name === 'sys_org')!;

export function OrgManagementPage() {
const { user: currentUser } = useAuth();
const isAdmin = currentUser?.role === 'admin';

return (
<div className="flex flex-col gap-6 p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Organization Management</h1>
<p className="text-muted-foreground">Manage organizations and their members</p>
</div>
{isAdmin && (
<button
type="button"
className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
Add Organization
</button>
)}
</div>

<div className="rounded-md border">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
{orgObject.views[0].columns.map((col) => {
const field = orgObject.fields.find((f) => f.name === col);
return (
<th key={col} className="h-10 px-4 text-left font-medium text-muted-foreground">
{field?.label ?? col}
</th>
);
})}
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-4 text-muted-foreground" colSpan={orgObject.views[0].columns.length}>
Connect to ObjectStack server to load organizations. In production, this page uses plugin-grid for full CRUD functionality.
</td>
</tr>
</tbody>
</table>
</div>
</div>
);
}
Loading
Loading