'use client'; import { useState, useEffect } from 'react'; import { cmsApi, Organization, getImageUrl } from '@/lib/api'; import { useAuth } from '@/context/AuthContext'; import Image from 'next/image'; import { Plus, Building2, Globe, Calendar, ExternalLink, ShieldCheck, Palette, Upload, Save, X, Fingerprint, Key, Settings2 } 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(''); // Admin User States const [adminFullName, setAdminFullName] = useState(''); const [adminEmail, setAdminEmail] = useState(''); const [adminPassword, setAdminPassword] = useState(''); // Branding States const [isBrandingModalOpen, setIsBrandingModalOpen] = useState(false); const [selectedOrg, setSelectedOrg] = useState(null); const [primaryColor, setPrimaryColor] = useState('#3B82F6'); const [secondaryColor, setSecondaryColor] = useState('#8B5CF6'); const [isSavingBranding, setIsSavingBranding] = useState(false); const [uploadingLogo, setUploadingLogo] = useState(false); // SSO States const [isSSOModalOpen, setIsSSOModalOpen] = useState(false); const [issuerUrl, setIssuerUrl] = useState(''); const [clientId, setClientId] = useState(''); const [clientSecret, setClientSecret] = useState(''); const [ssoEnabled, setSsoEnabled] = useState(false); const [isSavingSSO, setIsSavingSSO] = useState(false); 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.provisionOrganization({ org_name: newName, org_domain: newDomain || undefined, admin_full_name: adminFullName, admin_email: adminEmail, admin_password: adminPassword }); setNewName(''); setNewDomain(''); setAdminFullName(''); setAdminEmail(''); setAdminPassword(''); setIsModalOpen(false); loadOrganizations(); } catch (error) { console.error('Failed to create organization', error); alert('Failed to provision organization. Please ensure the email is unique.'); } }; const openBranding = (org: Organization) => { setSelectedOrg(org); setPrimaryColor(org.primary_color || '#3B82F6'); setSecondaryColor(org.secondary_color || '#8B5CF6'); setIsBrandingModalOpen(true); }; const handleLogoUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !selectedOrg) return; setUploadingLogo(true); try { const resp = await cmsApi.uploadOrganizationLogo(selectedOrg.id, file); setSelectedOrg({ ...selectedOrg, logo_url: resp.url }); // Update in list setOrganizations(orgs => orgs.map(o => o.id === selectedOrg.id ? { ...o, logo_url: resp.url } : o)); } catch (error) { console.error('Failed to upload logo', error); alert('Failed to upload logo. Please try again.'); } finally { setUploadingLogo(false); } }; const handleBrandingSave = async () => { if (!selectedOrg) return; setIsSavingBranding(true); try { await cmsApi.updateOrganizationBranding(selectedOrg.id, { primary_color: primaryColor, secondary_color: secondaryColor }); // Update in list setOrganizations(orgs => orgs.map(o => o.id === selectedOrg.id ? { ...o, primary_color: primaryColor, secondary_color: secondaryColor } : o)); setIsBrandingModalOpen(false); } catch (error) { console.error('Failed to update branding', error); alert('Failed to update branding. Please try again.'); } finally { setIsSavingBranding(false); } }; const openSSOConfig = async (org: Organization) => { setSelectedOrg(org); setIsSSOModalOpen(true); // Temporarily set org in localStorage for API calls localStorage.setItem('studio_selected_org_id', org.id); try { const config = await cmsApi.getSSOConfig(); if (config) { setIssuerUrl(config.issuer_url); setClientId(config.client_id); setClientSecret(config.client_secret); setSsoEnabled(config.enabled); } else { setIssuerUrl(''); setClientId(''); setClientSecret(''); setSsoEnabled(false); } } catch (error) { console.error('Failed to load SSO config', error); } }; const handleSSOSave = async () => { if (!selectedOrg) return; setIsSavingSSO(true); try { await cmsApi.updateSSOConfig({ issuer_url: issuerUrl, client_id: clientId, client_secret: clientSecret, enabled: ssoEnabled }); setIsSSOModalOpen(false); alert('SSO configuration saved successfully!'); } catch (error) { console.error('Failed to save SSO config', error); alert('Failed to save SSO config. Please ensure all fields are correct.'); } finally { setIsSavingSSO(false); } }; 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.logo_url ? ( {org.name} ) : ( )}

{org.name}

{org.domain || 'No custom domain'}
Created: {new Date(org.created_at).toLocaleDateString()}
{org.id.split('-')[0]}...
))}
)} {/* Create Organization 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 font-mono text-sm" placeholder="e.g. acme.com" />

Initial Administrator

setAdminFullName(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. John Doe" />
setAdminEmail(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="admin@acme.com" />
setAdminPassword(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="••••••••" />
)} {/* Branding Management Modal */} {isBrandingModalOpen && selectedOrg && (

Branding Management

{selectedOrg.name}

{/* Logo Upload */}
{selectedOrg.logo_url ? ( Preview ) : ( )}

PNG, JPG or SVG. Max 2MB.

{/* Colors */}
setPrimaryColor(e.target.value)} className="w-10 h-10 rounded cursor-pointer bg-transparent border-none" /> setPrimaryColor(e.target.value)} className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-sm font-mono" />
setSecondaryColor(e.target.value)} className="w-10 h-10 rounded cursor-pointer bg-transparent border-none" /> setSecondaryColor(e.target.value)} className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-sm font-mono" />
{/* Live Preview */}
{/* Mock Experience Header */}
{selectedOrg.logo_url ? ( Logo ) :
}
{/* Mock Experience Content */}
GET STARTED

This is a real-time preview of how the brand identity will apply to the student's learning experience.

)} {/* SSO Configuration Modal */} {isSSOModalOpen && selectedOrg && (

Single Sign-On (OIDC)

{selectedOrg.name}

Enable OIDC SSO

Allow users to log in via your identity provider.

setIssuerUrl(e.target.value)} className="w-full bg-black/40 border border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm" placeholder="https://accounts.google.com or https://okta.com/..." />
setClientId(e.target.value)} className="w-full bg-black/40 border border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm" placeholder="Your OIDC Client ID" />
setClientSecret(e.target.value)} className="w-full bg-black/40 border border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm" placeholder="••••••••••••••••" />
CONFIGURATION STEPS

1. Register OpenCCB as an application in your Identity Provider (Okta, Google, Azure AD).
2. Set the Redirect URI to: http://localhost:3001/auth/sso/callback
3. Copy the Issuer URL, Client ID, and Client Secret here.

)}
); }