diff --git a/web/studio/src/app/admin/organizations/page.tsx b/web/studio/src/app/admin/organizations/page.tsx new file mode 100644 index 0000000..48824e4 --- /dev/null +++ b/web/studio/src/app/admin/organizations/page.tsx @@ -0,0 +1,169 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { cmsApi, Organization } from '@/lib/api'; +import { useAuth } from '@/context/AuthContext'; +import { Plus, Building2, Globe, Calendar, ExternalLink, ShieldCheck } from 'lucide-react'; + +export default function OrganizationsPage() { + const [organizations, setOrganizations] = useState([]); + const [loading, setLoading] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [newName, setNewName] = useState(''); + const [newDomain, setNewDomain] = useState(''); + const { user } = useAuth(); + + useEffect(() => { + loadOrganizations(); + }, []); + + const loadOrganizations = async () => { + try { + const data = await cmsApi.getOrganizations(); + setOrganizations(data); + } catch (error) { + console.error('Failed to load organizations', error); + } finally { + setLoading(false); + } + }; + + const handleCreate = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await cmsApi.createOrganization(newName, newDomain || undefined); + setNewName(''); + setNewDomain(''); + setIsModalOpen(false); + loadOrganizations(); + } catch (error) { + console.error('Failed to create organization', error); + } + }; + + if (user?.role !== 'admin') { + return ( +
+
+ +
+

Access Denied

+

Only system administrators can access this page.

+
+ ); + } + + return ( +
+
+
+

Organizations

+

Manage tenants and isolated environments.

+
+ +
+ + {loading ? ( +
+ {[1, 2, 3].map(i => ( +
+ ))} +
+ ) : ( +
+ {organizations.map((org) => ( +
+
+ +
+ +
+
+ +
+
+

{org.name}

+
+ + {org.domain || 'No custom domain'} +
+
+
+ +
+
+
+ + Created: {new Date(org.created_at).toLocaleDateString()} +
+
+ {org.id.split('-')[0]}... +
+
+ +
+
+ ))} +
+ )} + + {/* Modal */} + {isModalOpen && ( +
+
+

Create New Organization

+
+
+ + setNewName(e.target.value)} + className="w-full bg-black/40 border border-white/10 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all" + placeholder="e.g. Acme Corp" + /> +
+
+ + setNewDomain(e.target.value)} + className="w-full bg-black/40 border border-white/10 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all" + placeholder="e.g. acme.com" + /> +
+
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/web/studio/src/app/admin/users/page.tsx b/web/studio/src/app/admin/users/page.tsx new file mode 100644 index 0000000..7ccb2e3 --- /dev/null +++ b/web/studio/src/app/admin/users/page.tsx @@ -0,0 +1,172 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { cmsApi, User, Organization } from '@/lib/api'; +import { useAuth } from '@/context/AuthContext'; +import { UserCog, Mail, Search, Filter, ShieldCheck } from 'lucide-react'; + +export default function UsersPage() { + const [users, setUsers] = useState([]); + const [organizations, setOrganizations] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [roleFilter, setRoleFilter] = useState(''); + const { user: currentUser } = useAuth(); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const [usersData, orgsData] = await Promise.all([ + cmsApi.getAllUsers(), + cmsApi.getOrganizations() + ]); + setUsers(usersData); + setOrganizations(orgsData); + } catch (error) { + console.error('Failed to load data', error); + } finally { + setLoading(false); + } + }; + + const handleUpdateUser = async (userId: string, role: string, orgId: string) => { + try { + await cmsApi.updateUser(userId, role, orgId); + loadData(); + } catch (error) { + console.error('Failed to update user', error); + } + }; + + const filteredUsers = users.filter(u => { + const matchesSearch = u.full_name.toLowerCase().includes(searchTerm.toLowerCase()) || + u.email.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesRole = roleFilter === '' || u.role === roleFilter; + return matchesSearch && matchesRole; + }); + + if (currentUser?.role !== 'admin') { + return ( +
+
+ +
+

Access Denied

+

Only system administrators can access this page.

+
+ ); + } + + return ( +
+
+

User Management

+

Manage global users, roles, and organization assignments.

+
+ +
+
+ + setSearchTerm(e.target.value)} + className="w-full bg-black/40 border border-white/10 rounded-lg pl-10 pr-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/50" + /> +
+
+
+ + +
+
+
+ +
+ + + + + + + + + + + {loading ? ( + [1, 2, 3, 4, 5].map(i => ( + + + + + + + )) + ) : filteredUsers.map((u) => ( + + + + + + + ))} + +
UserRoleOrganizationActions
+
+
+ {u.full_name[0]} +
+
+
{u.full_name}
+
+ {u.email} +
+
+
+
+ + + + + +
+ {!loading && filteredUsers.length === 0 && ( +
+ No users found matching your search. +
+ )} +
+
+ ); +} diff --git a/web/studio/src/components/Navbar.tsx b/web/studio/src/components/Navbar.tsx new file mode 100644 index 0000000..d2b6a98 --- /dev/null +++ b/web/studio/src/components/Navbar.tsx @@ -0,0 +1,87 @@ +'use client'; + +import Link from 'next/link'; +import { useAuth } from '@/context/AuthContext'; +import { LayoutDashboard, Building2, Users2, LogOut } from 'lucide-react'; + +export function Navbar() { + const { user, logout } = useAuth(); + + return ( + + ); +}