Skip to content

Commit 9a78e9c

Browse files
authored
Merge pull request #421 from objectstack-ai/copilot/develop-console-auth-system
2 parents 979cda4 + 148234f commit 9a78e9c

28 files changed

Lines changed: 1902 additions & 20 deletions

apps/console/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"test:ui": "vitest --ui"
2626
},
2727
"dependencies": {
28+
"@object-ui/auth": "workspace:*",
2829
"@object-ui/components": "workspace:*",
2930
"@object-ui/core": "workspace:*",
3031
"@object-ui/data-objectstack": "workspace:*",

apps/console/src/App.tsx

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { SchemaRendererProvider } from '@object-ui/react';
66
import { ObjectStackAdapter } from './dataSource';
77
import type { ConnectionState } from './dataSource';
88
import appConfig from '../objectstack.shared';
9+
import { AuthProvider, AuthGuard, useAuth } from '@object-ui/auth';
910

1011
// Components
1112
import { ConsoleLayout } from './components/ConsoleLayout';
@@ -19,12 +20,25 @@ import { PageView } from './components/PageView';
1920
import { ReportView } from './components/ReportView';
2021
import { ExpressionProvider } from './context/ExpressionProvider';
2122

23+
// Auth Pages
24+
import { LoginPage } from './pages/LoginPage';
25+
import { RegisterPage } from './pages/RegisterPage';
26+
import { ForgotPasswordPage } from './pages/ForgotPasswordPage';
27+
28+
// System Admin Pages
29+
import { UserManagementPage } from './pages/system/UserManagementPage';
30+
import { OrgManagementPage } from './pages/system/OrgManagementPage';
31+
import { RoleManagementPage } from './pages/system/RoleManagementPage';
32+
import { AuditLogPage } from './pages/system/AuditLogPage';
33+
import { ProfilePage } from './pages/system/ProfilePage';
34+
2235
import { useParams } from 'react-router-dom';
2336
import { ThemeProvider } from './components/theme-provider';
2437

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

2943
// App Selection
3044
const navigate = useNavigate();
@@ -132,7 +146,9 @@ export function AppContent() {
132146
);
133147

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

137153
return (
138154
<ExpressionProvider user={expressionUser} app={activeApp} data={{}}>
@@ -191,6 +207,13 @@ export function AppContent() {
191207
<Route path="page/:pageName" element={
192208
<PageView />
193209
} />
210+
211+
{/* System Administration Routes */}
212+
<Route path="system/users" element={<UserManagementPage />} />
213+
<Route path="system/organizations" element={<OrgManagementPage />} />
214+
<Route path="system/roles" element={<RoleManagementPage />} />
215+
<Route path="system/audit-log" element={<AuditLogPage />} />
216+
<Route path="system/profile" element={<ProfilePage />} />
194217
</Routes>
195218
</ErrorBoundary>
196219

@@ -268,12 +291,21 @@ function RootRedirect() {
268291
export function App() {
269292
return (
270293
<ThemeProvider defaultTheme="system" storageKey="object-ui-theme">
271-
<BrowserRouter basename="/">
272-
<Routes>
273-
<Route path="/apps/:appName/*" element={<AppContent />} />
274-
<Route path="/" element={<RootRedirect />} />
275-
</Routes>
276-
</BrowserRouter>
294+
<AuthProvider authUrl="/api/auth">
295+
<BrowserRouter basename="/">
296+
<Routes>
297+
<Route path="/login" element={<LoginPage />} />
298+
<Route path="/register" element={<RegisterPage />} />
299+
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
300+
<Route path="/apps/:appName/*" element={
301+
<AuthGuard fallback={<Navigate to="/login" />} loadingFallback={<LoadingScreen />}>
302+
<AppContent />
303+
</AuthGuard>
304+
} />
305+
<Route path="/" element={<RootRedirect />} />
306+
</Routes>
307+
</BrowserRouter>
308+
</AuthProvider>
277309
</ThemeProvider>
278310
);
279311
}

apps/console/src/components/AppSidebar.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
} from 'lucide-react';
4747
import appConfig from '../../objectstack.shared';
4848
import { useExpressionContext, evaluateVisibility } from '../context/ExpressionProvider';
49+
import { useAuth, getUserInitials } from '@object-ui/auth';
4950

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

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

7981
const apps = appConfig.apps || [];
8082
// Filter out inactive apps
@@ -165,12 +167,14 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
165167
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
166168
>
167169
<Avatar className="h-8 w-8 rounded-lg">
168-
<AvatarImage src="/avatars/user.jpg" alt="User" />
169-
<AvatarFallback className="rounded-lg bg-primary text-primary-foreground">JD</AvatarFallback>
170+
<AvatarImage src={user?.image ?? '/avatars/user.jpg'} alt={user?.name ?? 'User'} />
171+
<AvatarFallback className="rounded-lg bg-primary text-primary-foreground">
172+
{getUserInitials(user)}
173+
</AvatarFallback>
170174
</Avatar>
171175
<div className="grid flex-1 text-left text-sm leading-tight">
172-
<span className="truncate font-semibold">John Doe</span>
173-
<span className="truncate text-xs text-muted-foreground">admin@example.com</span>
176+
<span className="truncate font-semibold">{user?.name ?? 'User'}</span>
177+
<span className="truncate text-xs text-muted-foreground">{user?.email ?? ''}</span>
174178
</div>
175179
<ChevronsUpDown className="ml-auto size-4" />
176180
</SidebarMenuButton>
@@ -184,12 +188,14 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
184188
<DropdownMenuLabel className="p-0 font-normal">
185189
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
186190
<Avatar className="h-8 w-8 rounded-lg">
187-
<AvatarImage src="/avatars/user.jpg" alt="User" />
188-
<AvatarFallback className="rounded-lg bg-primary text-primary-foreground">JD</AvatarFallback>
191+
<AvatarImage src={user?.image ?? '/avatars/user.jpg'} alt={user?.name ?? 'User'} />
192+
<AvatarFallback className="rounded-lg bg-primary text-primary-foreground">
193+
{getUserInitials(user)}
194+
</AvatarFallback>
189195
</Avatar>
190196
<div className="grid flex-1 text-left text-sm leading-tight">
191-
<span className="truncate font-semibold">John Doe</span>
192-
<span className="truncate text-xs text-muted-foreground">admin@example.com</span>
197+
<span className="truncate font-semibold">{user?.name ?? 'User'}</span>
198+
<span className="truncate text-xs text-muted-foreground">{user?.email ?? ''}</span>
193199
</div>
194200
</div>
195201
</DropdownMenuLabel>
@@ -201,7 +207,10 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
201207
</DropdownMenuItem>
202208
</DropdownMenuGroup>
203209
<DropdownMenuSeparator />
204-
<DropdownMenuItem className="text-destructive focus:text-destructive">
210+
<DropdownMenuItem
211+
className="text-destructive focus:text-destructive"
212+
onClick={() => signOut()}
213+
>
205214
<LogOut className="mr-2 h-4 w-4" />
206215
Log out
207216
</DropdownMenuItem>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Forgot Password Page for ObjectStack Console
3+
*/
4+
5+
import { ForgotPasswordForm } from '@object-ui/auth';
6+
7+
export function ForgotPasswordPage() {
8+
return (
9+
<div className="flex min-h-screen items-center justify-center bg-background">
10+
<ForgotPasswordForm loginUrl="/login" />
11+
</div>
12+
);
13+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Login Page for ObjectStack Console
3+
*/
4+
5+
import { useNavigate } from 'react-router-dom';
6+
import { LoginForm } from '@object-ui/auth';
7+
8+
export function LoginPage() {
9+
const navigate = useNavigate();
10+
11+
return (
12+
<div className="flex min-h-screen items-center justify-center bg-background">
13+
<LoginForm
14+
onSuccess={() => navigate('/')}
15+
registerUrl="/register"
16+
forgotPasswordUrl="/forgot-password"
17+
/>
18+
</div>
19+
);
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Register Page for ObjectStack Console
3+
*/
4+
5+
import { useNavigate } from 'react-router-dom';
6+
import { RegisterForm } from '@object-ui/auth';
7+
8+
export function RegisterPage() {
9+
const navigate = useNavigate();
10+
11+
return (
12+
<div className="flex min-h-screen items-center justify-center bg-background">
13+
<RegisterForm
14+
onSuccess={() => navigate('/')}
15+
loginUrl="/login"
16+
/>
17+
</div>
18+
);
19+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Audit Log Page
3+
*
4+
* Read-only grid displaying system audit logs.
5+
* Shows user actions, resources, timestamps, and details.
6+
*/
7+
8+
import { systemObjects } from './systemObjects';
9+
10+
const auditObject = systemObjects.find((o) => o.name === 'sys_audit_log')!;
11+
12+
export function AuditLogPage() {
13+
return (
14+
<div className="flex flex-col gap-6 p-6">
15+
<div>
16+
<h1 className="text-2xl font-bold tracking-tight">Audit Log</h1>
17+
<p className="text-muted-foreground">View system activity and user actions</p>
18+
</div>
19+
20+
<div className="rounded-md border">
21+
<table className="w-full text-sm">
22+
<thead>
23+
<tr className="border-b bg-muted/50">
24+
{auditObject.views[0].columns.map((col) => {
25+
const field = auditObject.fields.find((f) => f.name === col);
26+
return (
27+
<th key={col} className="h-10 px-4 text-left font-medium text-muted-foreground">
28+
{field?.label ?? col}
29+
</th>
30+
);
31+
})}
32+
</tr>
33+
</thead>
34+
<tbody>
35+
<tr className="border-b">
36+
<td className="p-4 text-muted-foreground" colSpan={auditObject.views[0].columns.length}>
37+
Connect to ObjectStack server to load audit logs. In production, this page uses plugin-grid in read-only mode.
38+
</td>
39+
</tr>
40+
</tbody>
41+
</table>
42+
</div>
43+
</div>
44+
);
45+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Organization Management Page
3+
*
4+
* Displays a list of organizations with member management.
5+
* Reuses the plugin-grid for data display.
6+
*/
7+
8+
import { useAuth } from '@object-ui/auth';
9+
import { systemObjects } from './systemObjects';
10+
11+
const orgObject = systemObjects.find((o) => o.name === 'sys_org')!;
12+
13+
export function OrgManagementPage() {
14+
const { user: currentUser } = useAuth();
15+
const isAdmin = currentUser?.role === 'admin';
16+
17+
return (
18+
<div className="flex flex-col gap-6 p-6">
19+
<div className="flex items-center justify-between">
20+
<div>
21+
<h1 className="text-2xl font-bold tracking-tight">Organization Management</h1>
22+
<p className="text-muted-foreground">Manage organizations and their members</p>
23+
</div>
24+
{isAdmin && (
25+
<button
26+
type="button"
27+
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"
28+
>
29+
Add Organization
30+
</button>
31+
)}
32+
</div>
33+
34+
<div className="rounded-md border">
35+
<table className="w-full text-sm">
36+
<thead>
37+
<tr className="border-b bg-muted/50">
38+
{orgObject.views[0].columns.map((col) => {
39+
const field = orgObject.fields.find((f) => f.name === col);
40+
return (
41+
<th key={col} className="h-10 px-4 text-left font-medium text-muted-foreground">
42+
{field?.label ?? col}
43+
</th>
44+
);
45+
})}
46+
</tr>
47+
</thead>
48+
<tbody>
49+
<tr className="border-b">
50+
<td className="p-4 text-muted-foreground" colSpan={orgObject.views[0].columns.length}>
51+
Connect to ObjectStack server to load organizations. In production, this page uses plugin-grid for full CRUD functionality.
52+
</td>
53+
</tr>
54+
</tbody>
55+
</table>
56+
</div>
57+
</div>
58+
);
59+
}

0 commit comments

Comments
 (0)