fix: support platform admin environment login

This commit is contained in:
MOHAN 2026-07-06 23:30:51 +05:30
parent 266789daf5
commit c242ea1b43
2 changed files with 74 additions and 7 deletions

View File

@ -8,6 +8,7 @@ import { adminApi, messageFrom } from './api.js'
const tokenKey = 'metatron-org-admin-token'
const organizationKey = 'metatron-org-admin-organization'
const sessionModeKey = 'metatron-admin-session-mode'
function adminMemberships(user) {
return (user?.memberships || []).filter((item) => item.status === 'APPROVED' && ['OWNER', 'ADMIN'].includes(item.role) && item.organization?.status === 'ACTIVE')
@ -16,36 +17,51 @@ function adminMemberships(user) {
export default function App() {
const [status, setStatus] = useState(() => sessionStorage.getItem(tokenKey) ? 'loading' : 'signedOut')
const [user, setUser] = useState(null)
const [platformAdmin, setPlatformAdmin] = 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))
sessionStorage.setItem(sessionModeKey, 'organization')
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')
}, [])
const acceptPlatformSession = useCallback((result) => {
sessionStorage.setItem(tokenKey, result.token || sessionStorage.getItem(tokenKey))
sessionStorage.setItem(sessionModeKey, 'platform')
setPlatformAdmin(result.admin)
setStatus('platform')
}, [])
useEffect(() => {
if (!sessionStorage.getItem(tokenKey)) return
adminApi.me().then((nextUser) => acceptSession({ user: nextUser })).catch((requestError) => {
const platform = sessionStorage.getItem(sessionModeKey) === 'platform'
const request = platform
? adminApi.platformMe().then((admin) => acceptPlatformSession({ admin }))
: adminApi.me().then((nextUser) => acceptSession({ user: nextUser }))
request.catch((requestError) => {
sessionStorage.removeItem(tokenKey)
setError(messageFrom(requestError))
setStatus('signedOut')
})
}, [acceptSession])
}, [acceptPlatformSession, acceptSession])
function logout() {
sessionStorage.removeItem(tokenKey)
sessionStorage.removeItem(organizationKey)
sessionStorage.removeItem(sessionModeKey)
setUser(null)
setStatus('signedOut')
}
if (status === 'loading') return <LoadingScreen />
if (status === 'signedOut') return <AuthScreen onAuthenticated={acceptSession} initialError={error} />
if (status === 'signedOut') return <AuthScreen onAuthenticated={acceptSession} onPlatformAuthenticated={acceptPlatformSession} initialError={error} />
if (status === 'platform') return <PlatformDashboard admin={platformAdmin} onLogout={logout} />
return <Dashboard user={user} onLogout={logout} />
}
@ -53,7 +69,7 @@ function LoadingScreen() {
return <main className="flex min-h-screen items-center justify-center bg-[#101d42]"><LoaderCircle className="animate-spin text-emerald-300" size={38} /></main>
}
function AuthScreen({ onAuthenticated, initialError }) {
function AuthScreen({ onAuthenticated, onPlatformAuthenticated, 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' })
@ -62,8 +78,15 @@ function AuthScreen({ onAuthenticated, initialError }) {
async function submitLogin(event) {
event.preventDefault(); setBusy(true); setError('')
try { onAuthenticated(await adminApi.login(login.username.trim(), login.password)) }
catch (requestError) { setError(messageFrom(requestError)) }
try {
onAuthenticated(await adminApi.login(login.username.trim(), login.password))
} catch (requestError) {
if (requestError?.response?.status !== 401) setError(messageFrom(requestError))
else {
try { onPlatformAuthenticated(await adminApi.platformLogin(login.username.trim(), login.password)) }
catch (platformError) { setError(messageFrom(platformError)) }
}
}
finally { setBusy(false) }
}
@ -85,7 +108,7 @@ function AuthScreen({ onAuthenticated, initialError }) {
<section className="scrollbar flex max-h-screen items-center justify-center overflow-y-auto bg-[#f5f7fb] p-8">
{mode === 'login' ? <form className="w-full max-w-md" onSubmit={submitLogin}>
<span className="mb-7 flex h-14 w-14 items-center justify-center rounded-2xl bg-blue-600 text-white"><ShieldCheck size={29} /></span>
<h2 className="text-3xl font-black text-slate-900">Organization admin</h2><p className="mt-2 text-slate-500">Sign in with your global Metatron.Drive account.</p>
<h2 className="text-3xl font-black text-slate-900">Administrator sign in</h2><p className="mt-2 text-slate-500">Organization accounts and the backend super-admin credentials are both supported.</p>
<div className="mt-8 space-y-4"><Input label="Username" value={login.username} set={(value) => setLogin({ ...login, username: value })} /><Input label="Password" type="password" value={login.password} set={(value) => setLogin({ ...login, password: value })} /></div>
{error && <ErrorBox text={error} />}
<button className="btn-primary mt-6 w-full py-3" disabled={busy}>{busy ? <LoaderCircle className="animate-spin" size={20} /> : <><KeyRound size={18} /> Sign in</>}</button>
@ -101,6 +124,45 @@ function AuthScreen({ onAuthenticated, initialError }) {
</main>
}
function PlatformDashboard({ admin, onLogout }) {
const [stats, setStats] = useState(null)
const [users, setUsers] = useState([])
const [pagination, setPagination] = useState({ page: 1, pages: 1, total: 0 })
const [searchInput, setSearchInput] = useState('')
const [search, setSearch] = useState('')
const [filter, setFilter] = useState('all')
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const load = useCallback(async (page = 1) => {
setLoading(true); setError('')
try {
const [nextStats, result] = await Promise.all([
adminApi.platformStats(),
adminApi.platformUsers({ page, limit: 25, search, status: filter }),
])
setStats(nextStats); setUsers(result.users); setPagination(result.pagination)
} catch (requestError) { setError(messageFrom(requestError)) }
finally { setLoading(false) }
}, [filter, search])
useEffect(() => { const timer = setTimeout(() => load(1), 0); return () => clearTimeout(timer) }, [load])
async function toggle(user) {
try { await adminApi.platformUpdateUser(user.id, { isSuspended: !user.isSuspended }); await load(pagination.page) }
catch (requestError) { setError(messageFrom(requestError)) }
}
return <main className="min-h-screen bg-[#f5f7fb]">
<aside className="fixed inset-y-0 left-0 flex w-72 flex-col bg-[#101d42] p-5 text-white"><div className="flex items-center gap-3 px-2 text-lg font-black"><span className="flex h-10 w-10 items-center justify-center rounded-2xl bg-blue-600"><Cloud fill="currentColor" size={22} /></span>Metatron.Drive</div><div className="mt-10 rounded-xl bg-white/10 p-4"><ShieldCheck className="text-emerald-300" size={22} /><p className="mt-3 text-sm font-black">Platform super admin</p><p className="mt-1 text-xs text-white/45">Backend-controlled access</p></div><div className="mt-auto rounded-2xl border border-white/10 p-3"><p className="text-xs text-white/40">Signed in as</p><p className="mt-1 text-sm font-bold">{admin?.username}</p><button className="mt-3 flex items-center gap-2 text-xs font-bold text-white/60" onClick={onLogout}><LogOut size={15} /> Sign out</button></div></aside>
<section className="ml-72 min-h-screen"><header className="flex h-20 items-center justify-between border-b bg-white px-8"><div><h1 className="text-xl font-black">Platform administration</h1><p className="text-xs text-slate-500">Global users and system totals</p></div><button className="btn-secondary" onClick={() => load(pagination.page)}><RefreshCw size={17} /> Refresh</button></header><div className="p-8"><div className="grid grid-cols-5 gap-4">{[{ label: 'Users', value: stats?.users }, { label: 'Active', value: stats?.activeUsers }, { label: 'Suspended', value: stats?.suspendedUsers }, { label: 'Files', value: stats?.files }, { label: 'Folders', value: stats?.folders }].map((item) => <div className="card p-5" key={item.label}><p className="text-2xl font-black">{item.value ?? '—'}</p><p className="mt-1 text-xs font-bold text-slate-500">{item.label}</p></div>)}</div>
<section className="card mt-7 overflow-hidden"><div className="flex items-center justify-between gap-4 border-b p-5"><div><h2 className="font-black">Global users</h2><p className="text-xs text-slate-500">{pagination.total} registered accounts</p></div><div className="flex gap-3"><form onSubmit={(event) => { event.preventDefault(); setSearch(searchInput.trim()) }}><input className="field w-72 py-2" placeholder="Search users" value={searchInput} onChange={(event) => setSearchInput(event.target.value)} /></form><select className="field w-36 py-2" value={filter} onChange={(event) => setFilter(event.target.value)}><option value="all">All users</option><option value="active">Active</option><option value="suspended">Suspended</option></select></div></div>
{error ? <ErrorState message={error} retry={() => load(pagination.page)} /> : loading ? <LoadingRows /> : <div className="overflow-x-auto"><table className="w-full text-left"><thead className="bg-slate-50 text-[11px] uppercase text-slate-400"><tr><th className="px-5 py-3">User</th><th className="px-5 py-3">Contact</th><th className="px-5 py-3">Content</th><th className="px-5 py-3">Status</th><th className="px-5 py-3 text-right">Action</th></tr></thead><tbody className="divide-y">{users.map((user) => <tr key={user.id}><td className="px-5 py-4"><p className="font-bold">{user.name}</p><p className="text-xs text-slate-400">@{user.username}</p></td><td className="px-5 py-4 text-sm"><p>{user.email}</p><p className="text-xs text-slate-400">{user.phone}</p></td><td className="px-5 py-4 text-xs text-slate-500">{user.counts?.folders || 0} folders · {user.counts?.files || 0} files</td><td className="px-5 py-4"><Status value={user.isSuspended ? 'SUSPENDED' : 'APPROVED'} /></td><td className="px-5 py-4 text-right"><button className={`icon-btn ${user.isSuspended ? 'text-emerald-600' : 'text-amber-600'}`} onClick={() => toggle(user)} title={user.isSuspended ? 'Restore' : 'Suspend'}>{user.isSuspended ? <UserCheck size={18} /> : <Ban size={18} />}</button></td></tr>)}</tbody></table></div>}
<div className="flex items-center justify-between border-t px-5 py-4 text-sm"><p>Page {pagination.page} of {pagination.pages}</p><div className="flex gap-2"><button className="icon-btn border" disabled={pagination.page <= 1} onClick={() => load(pagination.page - 1)}><ChevronLeft size={18} /></button><button className="icon-btn border" disabled={pagination.page >= pagination.pages} onClick={() => load(pagination.page + 1)}><ChevronRight size={18} /></button></div></div>
</section></div></section>
</main>
}
function Dashboard({ user, onLogout }) {
const organizations = adminMemberships(user)
const [organizationId, setOrganizationId] = useState(() => sessionStorage.getItem(organizationKey) || organizations[0].organization.id)

View File

@ -20,6 +20,11 @@ export function messageFrom(error) {
export const adminApi = {
async login(username, password) { return (await client.post('/auth/login', { username, password })).data },
async me() { return (await client.get('/auth/me')).data.user },
async platformLogin(username, password) { return (await client.post('/admin/login', { username, password })).data },
async platformMe() { return (await client.get('/admin/me')).data.admin },
async platformStats() { return (await client.get('/admin/stats')).data.stats },
async platformUsers(params) { return (await client.get('/admin/users', { params })).data },
async platformUpdateUser(id, input) { return (await client.patch(`/admin/users/${id}`, input)).data.user },
async registerOrganization(input) { return (await client.post('/organizations/register', input)).data },
async currentOrganization() { return (await client.get('/organizations/current')).data },
async drive() { return (await client.get('/organizations/drive')).data.connection },