// The Users page: your own account, where it is signed in, and the accounts on this host.
// Hub-only, like Machines: accounts are each machine's own, and a hub reaches a node with
// a token rather than a login, so a node's accounts are managed on that node's console.
import { FormEvent, useCallback, useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { api, FreshGuest, Guest, Me, Session, User } from './api'
import { Btn, Loading, askYes, toast, useEscape } from './ui'
import { fmtLeft } from './guest'
export default function Users({ me }: { me: Me }) {
return (
)
}
const GUEST_MINUTES = [15, 30, 60, 120, 480] as const
// A login to hand somebody you are showing the console to: an ordinary account with a clock
// on it, which expires on its own and can be ended here at any moment. Making one is the only
// time its password is readable, so it is shown until dismissed rather than in a toast.
function Guests() {
const [list, setList] = useState(null)
const [minutes, setMinutes] = useState(30)
const [fresh, setFresh] = useState(null)
const [busy, setBusy] = useState(false)
const load = useCallback(async () => { try { setList(await api.guests()) } catch { setList([]) } }, [])
useEffect(() => { load() }, [load])
// The clock is the point of the thing, so it keeps up without a reload.
useEffect(() => { const t = setInterval(load, 30000); return () => clearInterval(t) }, [load])
const create = async () => {
setBusy(true)
try { setFresh(await api.createGuest(minutes)); load() }
catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') }
finally { setBusy(false) }
}
const revoke = async (g: Guest) => {
if (!await askYes(`End the guest login ${g.name}? Whoever has it is signed out at once, everywhere.`, 'Revoke')) return
try { await api.revokeGuest(g.name); toast(`${g.name} revoked`); if (fresh?.name === g.name) setFresh(null); load() }
catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') }
}
return (
<>
Guest logins
temporary, and can be revoked
{list === null && }
{list?.length === 0 &&
None. One lets somebody in for a while without giving them an account.
}
{list?.map((g) => {
const left = new Date(g.expires).getTime() - Date.now()
return (
{g.name}{left > 0 ? `expires in ${fmtLeft(left)}` : 'expiring'} · {g.sessions > 0 ? `${g.sessions} signed in` : g.used ? 'signed in earlier' : 'not used yet'}{g.issued_by ? ` · by ${g.issued_by}` : ''} revoke(g)}>Revoke
)
})}
{fresh && setFresh(null)} />}
Create guest loginA guest can do everything you can, on every machine — including the terminal — until the time is up or you revoke it. Give one out accordingly.
>
)
}
// The credentials, once. Copy takes the whole invitation, because that is what gets pasted
// into a message.
function FreshCard({ g, onClose }: { g: FreshGuest; onClose: () => void }) {
const invite = `${location.origin}\nuser: ${g.name}\npassword: ${g.password}\nexpires ${new Date(g.expires).toLocaleTimeString()}`
const copy = async (text: string, what: string) => {
try { await navigator.clipboard.writeText(text); toast(`${what} copied`) }
catch { toast('select and copy it by hand', 'err') }
}
return (