'use client'; import { useState } from 'react'; import { useQuery } from 'convex/react'; import { KeyRound, Loader2, UserRound } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@spoon/backend/convex/_generated/api.js'; import { Button, Card, CardContent, CardHeader, CardTitle, Input, } from '@spoon/ui'; // Mirrors the worker's linuxUsername (runtime/box-user.ts) so the identity // line shows the actual passwd entry. const linuxUsername = (spoonUsername: string): string => { const cleaned = spoonUsername .toLowerCase() .replaceAll(/[^a-z0-9_-]/g, '-') .slice(0, 32); if (!cleaned) return 'spoon'; return /^[a-z_]/.test(cleaned) ? cleaned : `u${cleaned}`.slice(0, 32); }; /** * The box's Linux account: identity (user@host) and the user-settable * password. Password is write-only — the UI only ever knows whether one is * set. Saving stores it (encrypted) and applies it to a running box live; * clearing reverts sudo to passwordless. */ export const BoxUserCard = ({ username }: { username: string }) => { const passwordState = useQuery(api.boxSettings.hasMine, {}); const hasPassword = passwordState?.hasPassword ?? false; const user = linuxUsername(username || 'spoon'); const [password, setPassword] = useState(''); const [confirm, setConfirm] = useState(''); const [saving, setSaving] = useState(false); const [pendingRestart, setPendingRestart] = useState(false); const tooShort = password.length > 0 && password.length < 8; const mismatch = confirm.length > 0 && confirm !== password; const canSave = !saving && password.length >= 8 && password.length <= 128 && !mismatch; const submit = (next: string | null) => { setSaving(true); void (async () => { try { const response = await fetch('/api/box/set-password', { method: 'POST', body: JSON.stringify({ password: next }), }); if (!response.ok) throw new Error(await response.text()); const data = (await response.json()) as { hasPassword: boolean; applied: boolean; }; setPassword(''); setConfirm(''); setPendingRestart(!data.applied); toast.success( next === null ? 'Password removed.' : 'Password updated.', ); } catch (error) { console.error(error); toast.error('Could not update the box password.'); } finally { setSaving(false); } })(); }; return ( Box user

Identity

{user}@{user}-box

Password

{hasPassword ? 'Set' : 'Not set'}

Without a password, sudo works without prompting. Once you set one, sudo asks for it — like a normal machine.

{ event.preventDefault(); if (canSave) submit(password); }} >
setPassword(event.target.value)} className='w-56' />
setConfirm(event.target.value)} className='w-56' />
{hasPassword ? ( ) : null}
{tooShort ? (

Password must be at least 8 characters.

) : null} {mismatch ? (

Passwords do not match.

) : null} {pendingRestart ? (

Saved — your machine is not running, so it takes effect the next time it starts.

) : null}
); };