feat: enhance organization management and user authentication flow

This commit is contained in:
MOHAN 2026-07-06 15:37:19 +05:30
parent bdea5a4a96
commit 9ed4d6c429
6 changed files with 87 additions and 21 deletions

View File

@ -2,6 +2,8 @@
Windows desktop client for the Metatron.Drive private file workspace. Windows desktop client for the Metatron.Drive private file workspace.
Users sign in once, can hold multiple approved organization memberships, submit additional join-code requests, and switch the active organization locally.
## Stack ## Stack
- Electron - Electron

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { api } from './lib/api.js' import { api } from './lib/api.js'
import Splash from './components/Splash.jsx' import Splash from './components/Splash.jsx'
import AuthScreen from './components/AuthScreen.jsx' import AuthScreen from './components/AuthScreen.jsx'
@ -7,6 +7,16 @@ import DriveApp from './components/DriveApp.jsx'
export default function App() { export default function App() {
const [status, setStatus] = useState('loading') const [status, setStatus] = useState('loading')
const [user, setUser] = useState(null) const [user, setUser] = useState(null)
const [organizationId, setOrganizationId] = useState(() => localStorage.getItem('metatron.drive-organization'))
const applyProfile = useCallback((profile) => {
const approved = (profile.memberships || []).filter((item) => item.status === 'APPROVED' && item.organization?.status === 'ACTIVE')
const selected = approved.find((item) => item.organization.id === organizationId) || approved[0]
if (!selected) throw new Error('No approved organization membership is available.')
localStorage.setItem('metatron.drive-organization', selected.organization.id)
setOrganizationId(selected.organization.id)
setUser(profile)
}, [organizationId])
useEffect(() => { useEffect(() => {
let active = true let active = true
@ -16,14 +26,14 @@ export default function App() {
if (!token) return setStatus('signedOut') if (!token) return setStatus('signedOut')
try { try {
const profile = await api.me() const profile = await api.me()
if (active) { setUser(profile); setStatus('signedIn') } if (active) { applyProfile(profile); setStatus('signedIn') }
} catch { } catch {
await window.desktop.session.clear() await window.desktop.session.clear()
if (active) setStatus('signedOut') if (active) setStatus('signedOut')
} }
}) })
return () => { active = false } return () => { active = false }
}, []) }, [applyProfile])
async function logout() { async function logout() {
await window.desktop.session.clear() await window.desktop.session.clear()
@ -31,7 +41,17 @@ export default function App() {
setStatus('signedOut') setStatus('signedOut')
} }
async function refreshUser() {
const profile = await api.me()
applyProfile(profile)
}
function selectOrganization(id) {
localStorage.setItem('metatron.drive-organization', id)
setOrganizationId(id)
}
if (status === 'loading') return <Splash /> if (status === 'loading') return <Splash />
if (status === 'signedOut') return <AuthScreen onAuthenticated={(profile) => { setUser(profile); setStatus('signedIn') }} /> if (status === 'signedOut') return <AuthScreen onAuthenticated={(profile) => { applyProfile(profile); setStatus('signedIn') }} />
return <DriveApp user={user} onLogout={logout} /> return <DriveApp key={organizationId} user={user} organizationId={organizationId} onSelectOrganization={selectOrganization} onRefreshUser={refreshUser} onLogout={logout} />
} }

View File

@ -2,7 +2,7 @@ import { useState } from 'react'
import { ArrowLeft, Cloud, Eye, EyeOff, LockKeyhole, Mail, Phone, User, UserRound } from 'lucide-react' import { ArrowLeft, Cloud, Eye, EyeOff, LockKeyhole, Mail, Phone, User, UserRound } from 'lucide-react'
import { api, errorMessage } from '../lib/api.js' import { api, errorMessage } from '../lib/api.js'
const emptyRegister = { name: '', username: '', email: '', phone: '', password: '' } const emptyRegister = { organizationCode: '', name: '', username: '', email: '', phone: '', password: '' }
export default function AuthScreen({ onAuthenticated }) { export default function AuthScreen({ onAuthenticated }) {
const [mode, setMode] = useState('login') const [mode, setMode] = useState('login')
@ -11,12 +11,14 @@ export default function AuthScreen({ onAuthenticated }) {
const [hidden, setHidden] = useState(true) const [hidden, setHidden] = useState(true)
const [busy, setBusy] = useState(false) const [busy, setBusy] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
const [notice, setNotice] = useState('')
async function submitLogin(event) { async function submitLogin(event) {
event.preventDefault() event.preventDefault()
if (login.username.trim().length < 3 || !login.password) return setError('Enter your username and password.') if (login.username.trim().length < 3 || !login.password) return setError('Enter your username and password.')
setBusy(true) setBusy(true)
setError('') setError('')
setNotice('')
try { try {
const result = await api.login(login.username.trim(), login.password) const result = await api.login(login.username.trim(), login.password)
await window.desktop.session.write(result.token) await window.desktop.session.write(result.token)
@ -31,6 +33,7 @@ export default function AuthScreen({ onAuthenticated }) {
async function submitRegister(event) { async function submitRegister(event) {
event.preventDefault() event.preventDefault()
const username = register.username.trim().toLowerCase() const username = register.username.trim().toLowerCase()
if (register.organizationCode.trim().length < 4) return setError('Enter your organization code.')
if (register.name.trim().length < 2) return setError('Enter your full name.') if (register.name.trim().length < 2) return setError('Enter your full name.')
if (!/^[a-z0-9._-]{3,30}$/.test(username)) return setError('Enter a valid username.') if (!/^[a-z0-9._-]{3,30}$/.test(username)) return setError('Enter a valid username.')
if (!/^.+@.+\..+$/.test(register.email.trim())) return setError('Enter a valid email address.') if (!/^.+@.+\..+$/.test(register.email.trim())) return setError('Enter a valid email address.')
@ -40,6 +43,7 @@ export default function AuthScreen({ onAuthenticated }) {
setError('') setError('')
try { try {
const result = await api.register({ const result = await api.register({
organizationCode: register.organizationCode.trim().toUpperCase(),
name: register.name.trim(), name: register.name.trim(),
username, username,
email: register.email.trim().toLowerCase(), email: register.email.trim().toLowerCase(),
@ -48,8 +52,8 @@ export default function AuthScreen({ onAuthenticated }) {
gender: 'Prefer not to say', gender: 'Prefer not to say',
password: register.password, password: register.password,
}) })
await window.desktop.session.write(result.token) setNotice(result.message)
onAuthenticated(result.user) setRegister(emptyRegister)
} catch (requestError) { } catch (requestError) {
setError(errorMessage(requestError)) setError(errorMessage(requestError))
} finally { } finally {
@ -85,7 +89,7 @@ export default function AuthScreen({ onAuthenticated }) {
<Field icon={UserRound} label="Username" value={login.username} onChange={(value) => setLogin({ ...login, username: value })} autoFocus /> <Field icon={UserRound} label="Username" value={login.username} onChange={(value) => setLogin({ ...login, username: value })} autoFocus />
<PasswordField value={login.password} onChange={(value) => setLogin({ ...login, password: value })} hidden={hidden} setHidden={setHidden} /> <PasswordField value={login.password} onChange={(value) => setLogin({ ...login, password: value })} hidden={hidden} setHidden={setHidden} />
</div> </div>
{error && <ErrorBox message={error} />} {error && <ErrorBox message={error} />}{notice && <NoticeBox message={notice} />}
<button className="btn-primary mt-6 w-full" disabled={busy}>{busy ? <Spinner /> : 'Sign in'}</button> <button className="btn-primary mt-6 w-full" disabled={busy}>{busy ? <Spinner /> : 'Sign in'}</button>
<button type="button" className="mt-5 w-full text-sm font-bold text-blue-600 hover:text-blue-700" onClick={() => { setError(''); setMode('register') }}>New to the family? Create an account</button> <button type="button" className="mt-5 w-full text-sm font-bold text-blue-600 hover:text-blue-700" onClick={() => { setError(''); setMode('register') }}>New to the family? Create an account</button>
</form> </form>
@ -95,13 +99,14 @@ export default function AuthScreen({ onAuthenticated }) {
<h2 className="text-3xl font-black text-slate-900">Join the digital family</h2> <h2 className="text-3xl font-black text-slate-900">Join the digital family</h2>
<p className="mt-2 text-slate-500">We create your private Google Drive folder automatically.</p> <p className="mt-2 text-slate-500">We create your private Google Drive folder automatically.</p>
<div className="mt-7 grid grid-cols-2 gap-4"> <div className="mt-7 grid grid-cols-2 gap-4">
<div className="col-span-2"><Field icon={User} label="Full name" value={register.name} onChange={(value) => setRegister({ ...register, name: value })} autoFocus /></div> <div className="col-span-2"><Field icon={Cloud} label="Organization code" value={register.organizationCode} onChange={(value) => setRegister({ ...register, organizationCode: value.toUpperCase() })} autoFocus /></div>
<div className="col-span-2"><Field icon={User} label="Full name" value={register.name} onChange={(value) => setRegister({ ...register, name: value })} /></div>
<Field icon={UserRound} label="Username" value={register.username} onChange={(value) => setRegister({ ...register, username: value })} /> <Field icon={UserRound} label="Username" value={register.username} onChange={(value) => setRegister({ ...register, username: value })} />
<Field icon={Mail} type="email" label="Email address" value={register.email} onChange={(value) => setRegister({ ...register, email: value })} /> <Field icon={Mail} type="email" label="Email address" value={register.email} onChange={(value) => setRegister({ ...register, email: value })} />
<div className="col-span-2"><Field icon={Phone} label="Phone number (optional)" value={register.phone} onChange={(value) => setRegister({ ...register, phone: value })} /></div> <div className="col-span-2"><Field icon={Phone} label="Phone number (optional)" value={register.phone} onChange={(value) => setRegister({ ...register, phone: value })} /></div>
<div className="col-span-2"><PasswordField value={register.password} onChange={(value) => setRegister({ ...register, password: value })} hidden={hidden} setHidden={setHidden} /></div> <div className="col-span-2"><PasswordField value={register.password} onChange={(value) => setRegister({ ...register, password: value })} hidden={hidden} setHidden={setHidden} /></div>
</div> </div>
{error && <ErrorBox message={error} />} {error && <ErrorBox message={error} />}{notice && <NoticeBox message={notice} />}
<button className="btn-primary mt-6 w-full" disabled={busy}>{busy ? <Spinner /> : 'Create Metatron.Drive'}</button> <button className="btn-primary mt-6 w-full" disabled={busy}>{busy ? <Spinner /> : 'Create Metatron.Drive'}</button>
</form> </form>
)} )}
@ -122,6 +127,10 @@ function ErrorBox({ message }) {
return <div className="mt-5 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm font-medium text-red-700">{message}</div> return <div className="mt-5 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm font-medium text-red-700">{message}</div>
} }
function NoticeBox({ message }) {
return <div className="mt-5 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-700">{message}</div>
}
function Spinner() { function Spinner() {
return <span className="h-5 w-5 animate-spin rounded-full border-2 border-white/30 border-t-white" /> return <span className="h-5 w-5 animate-spin rounded-full border-2 border-white/30 border-t-white" />
} }

View File

@ -11,7 +11,7 @@ import { useUploads } from '../hooks/useUploads.js'
import Modal from './Modal.jsx' import Modal from './Modal.jsx'
import UploadHistory from './UploadHistory.jsx' import UploadHistory from './UploadHistory.jsx'
export default function DriveApp({ user, onLogout }) { export default function DriveApp({ user, organizationId, onSelectOrganization, onRefreshUser, onLogout }) {
const [path, setPath] = useState([]) const [path, setPath] = useState([])
const [folders, setFolders] = useState([]) const [folders, setFolders] = useState([])
const [files, setFiles] = useState([]) const [files, setFiles] = useState([])
@ -23,6 +23,8 @@ export default function DriveApp({ user, onLogout }) {
const [toast, setToast] = useState(null) const [toast, setToast] = useState(null)
const inputRef = useRef(null) const inputRef = useRef(null)
const folderId = path.at(-1)?.id || null const folderId = path.at(-1)?.id || null
const memberships = (user.memberships || []).filter((item) => item.status === 'APPROVED' && item.organization?.status === 'ACTIVE')
const selectedMembership = memberships.find((item) => item.organization.id === organizationId) || memberships[0]
const load = useCallback(async () => { const load = useCallback(async () => {
setLoading(true) setLoading(true)
@ -51,7 +53,7 @@ export default function DriveApp({ user, onLogout }) {
const onUploadComplete = useCallback((destinationId) => { const onUploadComplete = useCallback((destinationId) => {
if ((destinationId || null) === folderId) load() if ((destinationId || null) === folderId) load()
}, [folderId, load]) }, [folderId, load])
const uploadState = useUploads(onUploadComplete) const uploadState = useUploads(onUploadComplete, organizationId)
async function createFolder(name) { async function createFolder(name) {
try { try {
@ -110,13 +112,13 @@ export default function DriveApp({ user, onLogout }) {
return ( return (
<main className="flex h-screen bg-[#f6f8fc]"> <main className="flex h-screen bg-[#f6f8fc]">
<Sidebar user={user} view={view} setView={setView} activeUploads={uploadState.active.length} onProfile={() => setModal('profile')} /> <Sidebar user={user} memberships={memberships} organizationId={organizationId} onSelectOrganization={onSelectOrganization} view={view} setView={setView} activeUploads={uploadState.active.length} onJoin={() => setModal('join')} onProfile={() => setModal('profile')} />
<section className="flex min-w-0 flex-1 flex-col"> <section className="flex min-w-0 flex-1 flex-col">
<header className="flex h-[82px] shrink-0 items-center gap-5 border-b border-slate-200 bg-white px-7"> <header className="flex h-[82px] shrink-0 items-center gap-5 border-b border-slate-200 bg-white px-7">
{view === 'drive' && path.length > 0 && <button className="icon-btn" onClick={() => setPath((current) => current.slice(0, -1))}><ArrowLeft size={21} /></button>} {view === 'drive' && path.length > 0 && <button className="icon-btn" onClick={() => setPath((current) => current.slice(0, -1))}><ArrowLeft size={21} /></button>}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h1 className="truncate text-xl font-black text-slate-900">{view === 'history' ? 'Upload history' : path.at(-1)?.name || 'Metatron.Drive'}</h1> <h1 className="truncate text-xl font-black text-slate-900">{view === 'history' ? 'Upload history' : path.at(-1)?.name || selectedMembership?.organization.name || 'Metatron.Drive'}</h1>
<p className="mt-0.5 text-xs text-slate-500">{view === 'history' ? 'Track every desktop upload' : `Hello, ${user.name.split(' ')[0]}`}</p> <p className="mt-0.5 text-xs text-slate-500">{view === 'history' ? `Uploads for ${selectedMembership?.organization.name}` : `Hello, ${user.name.split(' ')[0]} · ${selectedMembership?.organization.code}`}</p>
</div> </div>
{view === 'drive' && <> {view === 'drive' && <>
<label className="relative w-72"><Search className="absolute left-3.5 top-2.5 text-slate-400" size={18} /><input className="field py-2 pl-10" placeholder="Search this folder" value={search} onChange={(event) => setSearch(event.target.value)} /></label> <label className="relative w-72"><Search className="absolute left-3.5 top-2.5 text-slate-400" size={18} /><input className="field py-2 pl-10" placeholder="Search this folder" value={search} onChange={(event) => setSearch(event.target.value)} /></label>
@ -155,19 +157,23 @@ export default function DriveApp({ user, onLogout }) {
{uploadState.active.length > 0 && <ActiveUploadBar items={uploadState.active} onOpen={() => setView('history')} onCancel={uploadState.cancel} />} {uploadState.active.length > 0 && <ActiveUploadBar items={uploadState.active} onOpen={() => setView('history')} onCancel={uploadState.cancel} />}
{toast && <Toast toast={toast} />} {toast && <Toast toast={toast} />}
{modal === 'folder' && <NewFolderModal onClose={() => setModal(null)} onCreate={createFolder} />} {modal === 'folder' && <NewFolderModal onClose={() => setModal(null)} onCreate={createFolder} />}
{modal === 'join' && <JoinOrganizationModal onClose={() => setModal(null)} onJoined={async (message) => { await onRefreshUser(); setModal(null); setToast({ type: 'success', message }) }} />}
{modal === 'profile' && <ProfileModal user={user} onClose={() => setModal(null)} onLogout={onLogout} onDeleted={onLogout} setToast={setToast} />} {modal === 'profile' && <ProfileModal user={user} onClose={() => setModal(null)} onLogout={onLogout} onDeleted={onLogout} setToast={setToast} />}
{modal?.type === 'preview' && <PreviewModal item={modal.item} onClose={() => setModal(null)} onDownload={() => downloadFile(modal.item)} />} {modal?.type === 'preview' && <PreviewModal item={modal.item} onClose={() => setModal(null)} onDownload={() => downloadFile(modal.item)} />}
</main> </main>
) )
} }
function Sidebar({ user, view, setView, activeUploads, onProfile }) { function Sidebar({ user, memberships, organizationId, onSelectOrganization, view, setView, activeUploads, onJoin, onProfile }) {
return <aside className="flex w-64 shrink-0 flex-col border-r border-slate-200 bg-white px-4 py-5"> return <aside className="flex w-64 shrink-0 flex-col border-r border-slate-200 bg-white px-4 py-5">
<div className="flex items-center gap-3 px-3 text-lg font-black"><span className="flex h-10 w-10 items-center justify-center rounded-2xl bg-blue-600 text-white"><Cloud size={23} fill="currentColor" /></span>Metatron.Drive</div> <div className="flex items-center gap-3 px-3 text-lg font-black"><span className="flex h-10 w-10 items-center justify-center rounded-2xl bg-blue-600 text-white"><Cloud size={23} fill="currentColor" /></span>Metatron.Drive</div>
<nav className="mt-9 space-y-1"> <nav className="mt-9 space-y-1">
<NavButton active={view === 'drive'} icon={HardDrive} label="My Drive" onClick={() => setView('drive')} /> <NavButton active={view === 'drive'} icon={HardDrive} label="My Drive" onClick={() => setView('drive')} />
<NavButton active={view === 'history'} icon={History} label="Upload history" badge={activeUploads || null} onClick={() => setView('history')} /> <NavButton active={view === 'history'} icon={History} label="Upload history" badge={activeUploads || null} onClick={() => setView('history')} />
</nav> </nav>
<label className="mt-7 px-3 text-[10px] font-black uppercase tracking-wider text-slate-400">Organization</label>
<select className="field mt-2 py-2 text-sm font-bold" value={organizationId || ''} onChange={(event) => onSelectOrganization(event.target.value)}>{memberships.map((item) => <option key={item.organization.id} value={item.organization.id}>{item.organization.name}</option>)}</select>
<button className="mt-2 px-3 text-left text-xs font-bold text-blue-600" onClick={onJoin}>+ Join another organization</button>
<div className="mt-8 rounded-2xl bg-[#101d42] p-4 text-white"> <div className="mt-8 rounded-2xl bg-[#101d42] p-4 text-white">
<ShieldCheck className="text-emerald-300" size={23} /> <ShieldCheck className="text-emerald-300" size={23} />
<p className="mt-3 text-sm font-bold">Private workspace</p> <p className="mt-3 text-sm font-bold">Private workspace</p>
@ -250,6 +256,25 @@ function NewFolderModal({ onClose, onCreate }) {
return <Modal title="New folder" onClose={onClose}><form onSubmit={(event) => { event.preventDefault(); if (name.trim()) onCreate(name.trim()) }}><label className="text-sm font-bold text-slate-600">Folder name</label><input className="field mt-2" value={name} onChange={(event) => setName(event.target.value)} autoFocus maxLength={100} /><div className="mt-6 flex justify-end gap-3"><button type="button" className="btn-secondary" onClick={onClose}>Cancel</button><button className="btn-primary py-2.5" disabled={!name.trim()}><FolderPlus size={18} /> Create folder</button></div></form></Modal> return <Modal title="New folder" onClose={onClose}><form onSubmit={(event) => { event.preventDefault(); if (name.trim()) onCreate(name.trim()) }}><label className="text-sm font-bold text-slate-600">Folder name</label><input className="field mt-2" value={name} onChange={(event) => setName(event.target.value)} autoFocus maxLength={100} /><div className="mt-6 flex justify-end gap-3"><button type="button" className="btn-secondary" onClick={onClose}>Cancel</button><button className="btn-primary py-2.5" disabled={!name.trim()}><FolderPlus size={18} /> Create folder</button></div></form></Modal>
} }
function JoinOrganizationModal({ onClose, onJoined }) {
const [code, setCode] = useState('')
const [organization, setOrganization] = useState(null)
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
async function find(event) {
event.preventDefault(); setBusy(true); setError('')
try { setOrganization(await api.lookupOrganization(code.trim().toUpperCase())) }
catch (requestError) { setError(errorMessage(requestError)); setOrganization(null) }
finally { setBusy(false) }
}
async function join() {
setBusy(true); setError('')
try { const result = await api.joinOrganization(code.trim().toUpperCase()); await onJoined(result.message) }
catch (requestError) { setError(errorMessage(requestError)); setBusy(false) }
}
return <Modal title="Join an organization" onClose={onClose}><form onSubmit={find}><label className="text-sm font-bold text-slate-600">Organization code</label><input className="field mt-2 uppercase" value={code} onChange={(event) => { setCode(event.target.value.toUpperCase()); setOrganization(null) }} autoFocus /><button className="btn-secondary mt-3 w-full" disabled={busy || code.trim().length < 4}>{busy ? 'Checking…' : 'Find organization'}</button></form>{organization && <div className="mt-5 rounded-xl border border-blue-200 bg-blue-50 p-4"><p className="font-black text-slate-800">{organization.name}</p><p className="mt-1 text-xs text-slate-500">Code: {organization.code}</p><button className="btn-primary mt-4 w-full" disabled={busy} onClick={join}>Request to join</button></div>}{error && <p className="mt-4 text-sm font-bold text-red-600">{error}</p>}</Modal>
}
function ProfileModal({ user, onClose, onLogout, onDeleted, setToast }) { function ProfileModal({ user, onClose, onLogout, onDeleted, setToast }) {
const [deleting, setDeleting] = useState(false) const [deleting, setDeleting] = useState(false)
const [password, setPassword] = useState('') const [password, setPassword] = useState('')

View File

@ -12,7 +12,7 @@ function readHistory() {
} }
} }
export function useUploads(onComplete) { export function useUploads(onComplete, organizationId) {
const [uploads, setUploadsState] = useState(readHistory) const [uploads, setUploadsState] = useState(readHistory)
const fileRefs = useRef(new Map()) const fileRefs = useRef(new Map())
const abortRefs = useRef(new Map()) const abortRefs = useRef(new Map())
@ -63,6 +63,7 @@ export function useUploads(onComplete) {
size: file.size, size: file.size,
type: file.type || 'application/octet-stream', type: file.type || 'application/octet-stream',
folderId: folderId || null, folderId: folderId || null,
organizationId,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
status: 'queued', status: 'queued',
progress: 0, progress: 0,
@ -72,7 +73,7 @@ export function useUploads(onComplete) {
setUploads((current) => [entry, ...current]) setUploads((current) => [entry, ...current])
run(entry, file) run(entry, file)
} }
}, [run, setUploads]) }, [organizationId, run, setUploads])
const cancel = useCallback((id) => abortRefs.current.get(id)?.abort(), []) const cancel = useCallback((id) => abortRefs.current.get(id)?.abort(), [])
@ -97,6 +98,7 @@ export function useUploads(onComplete) {
setUploads((current) => current.filter((item) => !finalStatuses.has(item.status))) setUploads((current) => current.filter((item) => !finalStatuses.has(item.status)))
}, [setUploads]) }, [setUploads])
const active = useMemo(() => uploads.filter((item) => !finalStatuses.has(item.status)), [uploads]) const visibleUploads = useMemo(() => uploads.filter((item) => !item.organizationId || item.organizationId === organizationId), [organizationId, uploads])
return { uploads, active, enqueue, cancel, retry, remove, clearFinished } const active = useMemo(() => visibleUploads.filter((item) => !finalStatuses.has(item.status)), [visibleUploads])
return { uploads: visibleUploads, active, enqueue, cancel, retry, remove, clearFinished }
} }

View File

@ -10,6 +10,8 @@ const client = axios.create({
client.interceptors.request.use(async (config) => { client.interceptors.request.use(async (config) => {
const token = await window.desktop.session.read() const token = await window.desktop.session.read()
if (token) config.headers.Authorization = `Bearer ${token}` if (token) config.headers.Authorization = `Bearer ${token}`
const organizationId = localStorage.getItem('metatron.drive-organization')
if (organizationId) config.headers['X-Organization-Id'] = organizationId
return config return config
}) })
@ -24,6 +26,12 @@ export const api = {
async register(input) { async register(input) {
return (await client.post('/auth/register', input)).data return (await client.post('/auth/register', input)).data
}, },
async joinOrganization(organizationCode) {
return (await client.post('/auth/join', { organizationCode })).data
},
async lookupOrganization(code) {
return (await client.get(`/organizations/lookup/${encodeURIComponent(code)}`)).data.organization
},
async me() { async me() {
return (await client.get('/auth/me')).data.user return (await client.get('/auth/me')).data.user
}, },