From 266789daf5b08ca4e334ef6b7c639974126c578f Mon Sep 17 00:00:00 2001 From: MOHAN Date: Mon, 6 Jul 2026 15:37:14 +0530 Subject: [PATCH] Refactor admin API and user authentication flow - Updated API endpoints for user login and organization registration. - Enhanced session management by introducing organization-specific tokens. - Refactored user authentication logic in App component to handle organization memberships. - Improved loading and error handling in user interface components. - Added new components for organization management and Google Drive integration. - Adjusted timeout settings for API requests to improve performance. --- README.md | 4 +- src/App.jsx | 267 +++++++++++++++++++++++++++------------------------- src/api.js | 22 +++-- 3 files changed, 155 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index 02fba7a..af595f0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Metatron.Drive Admin -Administrator frontend for Metatron.Drive user management. +Organization signup and administration portal for Metatron.Drive. Owners connect Google Drive, share their organization code, and approve or reject membership requests. ## Development @@ -15,4 +15,4 @@ npm run dev npm run build ``` -Configure the API using `VITE_API_BASE_URL`. Administrator credentials are configured only on the backend through `ADMIN_USERNAME` and `ADMIN_PASSWORD`. +Configure the API using `VITE_API_BASE_URL`. Organization administrators use their global Metatron.Drive user account. The backend-only `ADMIN_USERNAME` and `ADMIN_PASSWORD` remain reserved for the future platform-super-admin API. diff --git a/src/App.jsx b/src/App.jsx index 6039b48..9e70718 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,188 +1,199 @@ import { useCallback, useEffect, useState } from 'react' import { - Ban, CheckCircle2, ChevronLeft, ChevronRight, Cloud, Database, - Edit3, Eye, EyeOff, File, Folder, HardDrive, KeyRound, LoaderCircle, LockKeyhole, - LogOut, RefreshCw, Search, ShieldCheck, Trash2, UserCheck, Users, X, XCircle, + Ban, ChevronLeft, ChevronRight, Cloud, CloudCog, Copy, + ExternalLink, File, Folder, HardDrive, KeyRound, LoaderCircle, + LogOut, RefreshCw, Search, ShieldCheck, UserCheck, UserPlus, Users, XCircle, } from 'lucide-react' import { adminApi, messageFrom } from './api.js' -export default function App() { - const [status, setStatus] = useState(() => sessionStorage.getItem('metatron-admin-token') ? 'loading' : 'signedOut') - const [admin, setAdmin] = useState(null) +const tokenKey = 'metatron-org-admin-token' +const organizationKey = 'metatron-org-admin-organization' - useEffect(() => { - const token = sessionStorage.getItem('metatron-admin-token') - if (!token) return - adminApi.me().then((profile) => { setAdmin(profile); setStatus('signedIn') }).catch(() => { - sessionStorage.removeItem('metatron-admin-token') - setStatus('signedOut') - }) +function adminMemberships(user) { + return (user?.memberships || []).filter((item) => item.status === 'APPROVED' && ['OWNER', 'ADMIN'].includes(item.role) && item.organization?.status === 'ACTIVE') +} + +export default function App() { + const [status, setStatus] = useState(() => sessionStorage.getItem(tokenKey) ? 'loading' : 'signedOut') + const [user, setUser] = useState(null) + const [error, setError] = useState('') + + const acceptSession = useCallback((result) => { + const memberships = adminMemberships(result.user) + if (!memberships.length) throw new Error('This account is not an administrator of an active organization.') + sessionStorage.setItem(tokenKey, result.token || sessionStorage.getItem(tokenKey)) + const saved = sessionStorage.getItem(organizationKey) + if (!memberships.some((item) => item.organization.id === saved)) sessionStorage.setItem(organizationKey, memberships[0].organization.id) + setUser(result.user) + setStatus('signedIn') }, []) - function authenticated(result) { - sessionStorage.setItem('metatron-admin-token', result.token) - setAdmin(result.admin) - setStatus('signedIn') - } + useEffect(() => { + if (!sessionStorage.getItem(tokenKey)) return + adminApi.me().then((nextUser) => acceptSession({ user: nextUser })).catch((requestError) => { + sessionStorage.removeItem(tokenKey) + setError(messageFrom(requestError)) + setStatus('signedOut') + }) + }, [acceptSession]) function logout() { - sessionStorage.removeItem('metatron-admin-token') - setAdmin(null) + sessionStorage.removeItem(tokenKey) + sessionStorage.removeItem(organizationKey) + setUser(null) setStatus('signedOut') } if (status === 'loading') return - if (status === 'signedOut') return - return + if (status === 'signedOut') return + return } function LoadingScreen() { return
} -function LoginScreen({ onAuthenticated }) { - const [username, setUsername] = useState('') - const [password, setPassword] = useState('') - const [hidden, setHidden] = useState(true) +function AuthScreen({ onAuthenticated, initialError }) { + const [mode, setMode] = useState('login') + const [login, setLogin] = useState({ username: '', password: '' }) + const [form, setForm] = useState({ organizationName: '', name: '', username: '', email: '', phone: '', password: '', age: '18', gender: 'Prefer not to say' }) const [busy, setBusy] = useState(false) - const [error, setError] = useState('') + const [error, setError] = useState(initialError || '') - async function submit(event) { - event.preventDefault() - setBusy(true) - setError('') - try { onAuthenticated(await adminApi.login(username.trim(), password)) } + async function submitLogin(event) { + event.preventDefault(); setBusy(true); setError('') + try { onAuthenticated(await adminApi.login(login.username.trim(), login.password)) } catch (requestError) { setError(messageFrom(requestError)) } finally { setBusy(false) } } - return
+ async function submitSignup(event) { + event.preventDefault(); setBusy(true); setError('') + try { onAuthenticated(await adminApi.registerOrganization({ ...form, age: Number(form.age), username: form.username.trim().toLowerCase() })) } + catch (requestError) { setError(messageFrom(requestError)) } + finally { setBusy(false) } + } + + return
Metatron.Drive
-

Administrator control

Manage your digital family securely.

Review accounts, update user details, reset passwords, suspend access, and manage database records.

-

Restricted administrator access

+

Organization workspace

Your organization.
Your Drive.
Your control.

Connect Google Drive, approve members, and manage one secure digital workspace.

+

Multi-tenant organization administration

+
+
+ {mode === 'login' ?
+ +

Organization admin

Sign in with your global Metatron.Drive account.

+
setLogin({ ...login, username: value })} /> setLogin({ ...login, password: value })} />
+ {error && } + + + :
+

Create your organization

The organization is active immediately. You can connect Drive after signup.

+
setForm({ ...form, organizationName: value })} />
setForm({ ...form, name: value })} /> setForm({ ...form, username: value })} /> setForm({ ...form, email: value })} /> setForm({ ...form, phone: value })} /> setForm({ ...form, age: value })} />
setForm({ ...form, password: value })} />
+ {error && } + + + }
-
- -

Admin sign in

Enter the administrator credentials configured on the API server.

-
- {error &&
{error}
} - -
} -function Dashboard({ admin, onLogout }) { +function Dashboard({ user, onLogout }) { + const organizations = adminMemberships(user) + const [organizationId, setOrganizationId] = useState(() => sessionStorage.getItem(organizationKey) || organizations[0].organization.id) + const organization = organizations.find((item) => item.organization.id === organizationId)?.organization || organizations[0].organization const [stats, setStats] = useState(null) - const [users, setUsers] = useState([]) + const [drive, setDrive] = useState(null) + const [rows, setRows] = useState([]) const [pagination, setPagination] = useState({ page: 1, pages: 1, total: 0 }) - const [search, setSearch] = useState('') + const [status, setStatus] = useState('ALL') const [searchInput, setSearchInput] = useState('') - const [filter, setFilter] = useState('all') + const [search, setSearch] = useState('') const [loading, setLoading] = useState(true) const [error, setError] = useState('') - const [editing, setEditing] = useState(null) - const [toast, setToast] = useState(null) + const [connectOpen, setConnectOpen] = useState(false) const load = useCallback(async (page = 1) => { - setLoading(true) - setError('') + if (!organizationId) return + setLoading(true); setError('') try { - const [nextStats, result] = await Promise.all([ - adminApi.stats(), - adminApi.users({ search, status: filter, page, limit: 25 }), - ]) - setStats(nextStats) - setUsers(result.users) - setPagination(result.pagination) - } catch (requestError) { - if (requestError?.response?.status === 401) onLogout() - else setError(messageFrom(requestError)) - } finally { setLoading(false) } - }, [filter, onLogout, search]) + const [nextStats, nextDrive, result] = await Promise.all([adminApi.stats(), adminApi.drive(), adminApi.memberships({ page, limit: 25, status, search })]) + setStats(nextStats); setDrive(nextDrive); setRows(result.memberships); setPagination(result.pagination) + } catch (requestError) { setError(messageFrom(requestError)) } + finally { setLoading(false) } + }, [search, status, organizationId]) - useEffect(() => { const timer = setTimeout(() => load(1), 0); return () => clearTimeout(timer) }, [load]) - useEffect(() => { if (!toast) return undefined; const timer = setTimeout(() => setToast(null), 3500); return () => clearTimeout(timer) }, [toast]) + useEffect(() => { sessionStorage.setItem(organizationKey, organizationId); const timer = setTimeout(() => load(1), 0); return () => clearTimeout(timer) }, [organizationId, load]) - function submitSearch(event) { event.preventDefault(); setSearch(searchInput.trim()) } - - async function saveUser(id, input) { - try { - await adminApi.updateUser(id, input) - setEditing(null) - setToast({ type: 'success', text: 'User updated successfully.' }) - load(pagination.page) - } catch (requestError) { setToast({ type: 'error', text: messageFrom(requestError) }) } + async function changeMembership(id, input) { + try { await adminApi.updateMembership(id, input); await load(pagination.page) } + catch (requestError) { setError(messageFrom(requestError)) } } - async function toggleSuspension(user) { - try { - await adminApi.updateUser(user.id, { isSuspended: !user.isSuspended }) - setToast({ type: 'success', text: user.isSuspended ? 'User access restored.' : 'User suspended.' }) - load(pagination.page) - } catch (requestError) { setToast({ type: 'error', text: messageFrom(requestError) }) } - } - - async function deleteUser(user) { - if (!window.confirm(`Delete ${user.name} from the database? Their Google Drive files will remain untouched.`)) return - try { - await adminApi.deleteUser(user.id) - setToast({ type: 'success', text: 'User database records deleted.' }) - load(Math.min(pagination.page, pagination.pages)) - } catch (requestError) { setToast({ type: 'error', text: messageFrom(requestError) }) } + function reject(row) { + const reason = window.prompt(`Why are you rejecting ${row.user.name}'s application?`) + if (reason?.trim()) changeMembership(row.id, { status: 'REJECTED', rejectionReason: reason.trim() }) } return
-
} -function Stats({ stats }) { - const cards = [ - { label: 'Total users', value: stats?.users, icon: Users, tone: 'text-blue-600 bg-blue-50' }, - { label: 'Active users', value: stats?.activeUsers, icon: UserCheck, tone: 'text-emerald-600 bg-emerald-50' }, - { label: 'Suspended', value: stats?.suspendedUsers, icon: Ban, tone: 'text-amber-600 bg-amber-50' }, - { label: 'Stored files', value: stats?.files, icon: File, tone: 'text-violet-600 bg-violet-50' }, - { label: 'Storage indexed', value: stats ? fileSize(stats.storageBytes) : '—', icon: HardDrive, tone: 'text-cyan-600 bg-cyan-50' }, - ] - return
{cards.map(({ label, value, icon: Icon, tone }) =>

{value ?? '—'}

{label}

)}
-} - -function UserTable({ users, onEdit, onToggle, onDelete }) { - return
{users.map((user) => )}
UserContactContentJoinedStatusActions
{initials(user.name)}

{user.name}

@{user.username}

{user.email}

{displayPhone(user.phone)}

{user.counts?.folders || 0}{user.counts?.files || 0}
{dateTime(user.createdAt)}{user.isSuspended ? : }{user.isSuspended ? 'Suspended' : 'Active'}
-} - -function EditUserModal({ user, onClose, onSave }) { - const [form, setForm] = useState({ name: user.name, username: user.username, email: user.email, phone: user.phone, gender: user.gender, age: String(user.age), createdAt: toLocalInput(user.createdAt), isSuspended: user.isSuspended, password: '' }) +function DriveModal({ drive, close, connected }) { + const [step, setStep] = useState('choose') + const [storageType, setStorageType] = useState(drive?.storageType || 'MY_DRIVE') + const [sharedDriveId, setSharedDriveId] = useState(drive?.sharedDriveId || '') + const [redirectUrl, setRedirectUrl] = useState('') + const [state, setState] = useState('') const [busy, setBusy] = useState(false) - async function submit(event) { event.preventDefault(); setBusy(true); const input = { ...form, age: Number(form.age), createdAt: new Date(form.createdAt).toISOString() }; if (!input.password) delete input.password; await onSave(user.id, input); setBusy(false) } - return
event.stopPropagation()}>

Edit user

Update account details and access

setForm({ ...form, name: value })} /> setForm({ ...form, username: value })} /> setForm({ ...form, email: value })} /> setForm({ ...form, phone: value })} /> setForm({ ...form, age: value })} /> setForm({ ...form, createdAt: value })} /> setForm({ ...form, password: value })} />
+ const [error, setError] = useState('') + + async function begin() { + setBusy(true); setError('') + try { const result = await adminApi.authorizationUrl(); setState(result.state); window.open(result.authorizationUrl, '_blank', 'noopener,noreferrer'); setStep('paste') } + catch (requestError) { setError(messageFrom(requestError)) } + finally { setBusy(false) } + } + async function finish(event) { + event.preventDefault(); setBusy(true); setError('') + try { await adminApi.exchangeDrive({ redirectUrlOrCode: redirectUrl, state, storageType, sharedDriveId: storageType === 'SHARED_DRIVE' ? sharedDriveId : undefined }); connected() } + catch (requestError) { setError(messageFrom(requestError)) } + finally { setBusy(false) } + } + return
event.stopPropagation()}>

Connect Google Drive

{drive &&

Currently connected to {drive.storageType === 'MY_DRIVE' ? 'My Drive' : 'a Shared Drive'}.

} + {step === 'choose' ? <>
setStorageType('MY_DRIVE')} /> setStorageType('SHARED_DRIVE')} />
{storageType === 'SHARED_DRIVE' &&
}

Testing uses the desktop OAuth client. Google will redirect to localhost; that page may not load. Copy the complete URL from the browser address bar.

{error && }
:

After approving access, copy the complete http://localhost/?code=…&state=… URL and paste it below.