remove the dead multi-user surface
Officer is single-user: the server owner is the only account, created once by /auth/bootstrap. Everything that existed to serve additional users was unreachable, so it is gone rather than left looking like it does something. Accounts: drop the invite / resend-invite / delete / list-users routes and the Users settings screen, the inert /auth/signup handler, and the account verification chain it fed (verify, resend-verification, VerifyScreen, the UserInvite + VerifyAdmin + VerifyRegistration templates). /auth/verify-token survives for password resets only, and now requires a reset-password token rather than accepting any signed JWT. Roles: drop the users.role column and the four-value USER_ROLES enum. The permissions table granted every role identical methods, and every role === 'Super Admin' check was permanently true. The JWT no longer carries a role claim. Sandbox: remove sidecar/sandbox.ts and its five call sites. bwrap was selected only for non-Super-Admin users, so it never ran. It was also not a usable agent jail as written — --share-net, the project root (with .env) bound read-only, and runuser dropping to the server's own uid. Rebuilding it for agent containment would be a different construction, and git history keeps this one. getHomeDir keeps its DATA_PATH meaning; the new getOwnerHomeDir resolves the owner's real login home, which is what terminals, chats and task runs use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
92de996412
commit
044aacf4d5
@@ -1,202 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { toast } from 'sonner';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/Card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useMounted } from 'hooks/useMounted';
|
||||
import { useForm } from 'hooks/useForm';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
|
||||
export const Verify = () => {
|
||||
const isMounted = useMounted();
|
||||
const navigate = useNavigate();
|
||||
const client = useClient('/api/auth');
|
||||
const { state, formRef, update } = useForm<VerifyFormState>({ email: '' });
|
||||
|
||||
const [verificationCode] = useState(() => new URL(window.location.href).searchParams.get('verificationCode') ?? '');
|
||||
const [tokenStatus, setTokenStatus] = useState<TokenStatus>('loading');
|
||||
const [flow, setFlow] = useState<'bootstrap' | 'invite' | 'verify'>('verify');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const verifyToken = async () => {
|
||||
if (!verificationCode) return;
|
||||
try {
|
||||
const data = await client.post<{ ok: boolean; email: string; flow: 'bootstrap' | 'invite' | 'verify' }>('/verify-token', { verificationCode });
|
||||
if (data.ok) {
|
||||
setTokenStatus('valid');
|
||||
setFlow(data.flow);
|
||||
requestAnimationFrame(() => update({ email: data.email }));
|
||||
} else {
|
||||
setTokenStatus('invalid');
|
||||
}
|
||||
} catch {
|
||||
setTokenStatus('invalid');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMounted) return;
|
||||
if (!verificationCode) {
|
||||
setTokenStatus('invalid');
|
||||
return;
|
||||
}
|
||||
verifyToken();
|
||||
}, [isMounted]);
|
||||
|
||||
const isValid = validateForm(state);
|
||||
|
||||
const handleSubmit = async (ev: React.FormEvent) => {
|
||||
ev.preventDefault();
|
||||
if (!isValid || isSubmitting) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
if (flow === 'bootstrap') {
|
||||
await client.post('/bootstrap', {
|
||||
token: verificationCode,
|
||||
name: state.name,
|
||||
username: state.username,
|
||||
password: state.password,
|
||||
confirmPassword: state.confirmPassword,
|
||||
});
|
||||
} else {
|
||||
await client.post('/verify', {
|
||||
verificationCode,
|
||||
name: state.name,
|
||||
username: state.username,
|
||||
password: state.password,
|
||||
confirmPassword: state.confirmPassword,
|
||||
});
|
||||
}
|
||||
toast.success('Account created. Please sign in.');
|
||||
navigate('/');
|
||||
} catch (ex) {
|
||||
const error = ex as { message?: string };
|
||||
toast.error(error.message || 'Failed to create account. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loading = tokenStatus === 'loading';
|
||||
const invalid = tokenStatus === 'invalid';
|
||||
const hideForm = loading || invalid;
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && (
|
||||
<Card className="py-12 px-24 flex flex-col gap-6">
|
||||
<div className="text-center text-duck-dark/60">Verifying...</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{invalid && (
|
||||
<Card className="py-12 px-24 flex flex-col gap-6">
|
||||
<div className="text-center">
|
||||
<div className="text-duck-dark text-2xl font-bold">Invalid Link</div>
|
||||
<div className="text-duck-dark/60">This verification link is invalid or has expired.</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card className={cn('flex flex-col gap-6', hideForm && 'hidden')}>
|
||||
<div className="text-center">
|
||||
<div className="text-duck-dark text-2xl font-bold">
|
||||
{flow === 'bootstrap' ? 'Create Your Account' : 'Accept Invitation'}
|
||||
</div>
|
||||
<div className="text-duck-dark/60">
|
||||
{flow === 'bootstrap' ? 'Set up your administrator profile' : 'Set up your profile to get started'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form ref={formRef} onSubmit={handleSubmit} className="grid gap-4">
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70">Email</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="email"
|
||||
name="email"
|
||||
disabled
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70">Name</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Your name"
|
||||
autoComplete="name"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70">Username</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="your-username"
|
||||
autoComplete="username"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<div className="grid md:flex gap-4">
|
||||
<Label className="grid gap-2 flex-1">
|
||||
<span className="text-duck-dark/70">Password</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Min 12 characters"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label className="grid gap-2 flex-1">
|
||||
<span className="text-duck-dark/70">Confirm Password</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
placeholder="Confirm your password"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!isValid || isSubmitting}
|
||||
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold text-lg transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
|
||||
>
|
||||
{isSubmitting ? 'Creating account...' : 'Create Account'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const validateForm = (state: Partial<VerifyFormState>) => {
|
||||
const { name, username, password, confirmPassword } = state;
|
||||
if (!name || !username || !password || !confirmPassword) return false;
|
||||
if (password !== confirmPassword) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
type VerifyFormState = {
|
||||
email?: string;
|
||||
name?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
};
|
||||
|
||||
type TokenStatus = 'loading' | 'valid' | 'invalid';
|
||||
@@ -1,111 +0,0 @@
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/Card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useVerifyScreen } from './useVerifyScreen';
|
||||
|
||||
export const VerifyScreen = () => {
|
||||
const { formRef, isValid, tokenStatus, flow, isSubmitting, handleSubmit } = useVerifyScreen();
|
||||
const loading = tokenStatus === 'loading';
|
||||
const invalid = tokenStatus === 'invalid';
|
||||
const hideform = loading || invalid;
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && (
|
||||
<Card className="py-12 px-24 flex flex-col gap-6">
|
||||
<div className="text-center text-duck-dark/60">Verifying...</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{invalid && (
|
||||
<Card className="py-12 px-24 flex flex-col gap-6">
|
||||
<div className="text-center">
|
||||
<div className="text-duck-dark text-2xl font-bold">Invalid Link</div>
|
||||
<div className="text-duck-dark/60">This verification link is invalid or has expired.</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card className={cn("flex flex-col gap-6", hideform && "hidden")}>
|
||||
<div className="text-center">
|
||||
<div className="text-duck-dark text-2xl font-bold">
|
||||
{flow === 'bootstrap' ? 'Create Your Account' : 'Accept Invitation'}
|
||||
</div>
|
||||
<div className="text-duck-dark/60">
|
||||
{flow === 'bootstrap' ? 'Set up your administrator profile' : 'Set up your profile to get started'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form ref={formRef} onSubmit={handleSubmit} className="grid gap-4">
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70">Email</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="email"
|
||||
name="email"
|
||||
disabled
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70">Name</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Your name"
|
||||
autoComplete="name"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70">Username</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="your-username"
|
||||
autoComplete="username"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<div className="grid md:flex gap-4">
|
||||
<Label className="grid gap-2 flex-1">
|
||||
<span className="text-duck-dark/70">Password</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Min 12 characters"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label className="grid gap-2 flex-1">
|
||||
<span className="text-duck-dark/70">Confirm Password</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
placeholder="Confirm your password"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!isValid || isSubmitting}
|
||||
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold text-lg transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
|
||||
>
|
||||
{isSubmitting ? 'Creating account...' : 'Create Account'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card >
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './VerifyScreen';
|
||||
@@ -1,104 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { toast } from 'sonner';
|
||||
import { useMounted } from 'hooks/useMounted';
|
||||
import { useForm } from 'hooks/useForm';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
|
||||
export const useVerifyScreen = () => {
|
||||
const isMounted = useMounted();
|
||||
const navigate = useNavigate();
|
||||
const client = useClient('/api/auth');
|
||||
const { state, formRef, update } = useForm<VerifyFormState>({ email: '' });
|
||||
|
||||
const [verificationCode] = useState(() => new URL(window.location.href).searchParams.get('verificationCode') ?? '');
|
||||
const [tokenStatus, setTokenStatus] = useState<TokenStatus>('loading');
|
||||
const [flow, setFlow] = useState<'bootstrap' | 'invite' | 'verify'>('bootstrap');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const verifyToken = async () => {
|
||||
if (!verificationCode) return;
|
||||
try {
|
||||
const data = await client.post<{ ok: boolean; email: string; flow: 'bootstrap' | 'invite' | 'verify' }>('/verify-token', { verificationCode });
|
||||
if (data.ok) {
|
||||
setTokenStatus('valid');
|
||||
setFlow(data.flow);
|
||||
requestAnimationFrame(() => update({ email: data.email }));
|
||||
} else {
|
||||
setTokenStatus('invalid');
|
||||
}
|
||||
} catch {
|
||||
setTokenStatus('invalid');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMounted) return;
|
||||
if (!verificationCode) {
|
||||
setTokenStatus('invalid');
|
||||
return;
|
||||
}
|
||||
verifyToken();
|
||||
}, [isMounted]);
|
||||
|
||||
const handleSubmit = async (ev: React.FormEvent) => {
|
||||
ev.preventDefault();
|
||||
if (!isValid || isSubmitting) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
if (flow === 'bootstrap') {
|
||||
await client.post('/bootstrap', {
|
||||
token: verificationCode,
|
||||
name: state.name,
|
||||
username: state.username,
|
||||
password: state.password,
|
||||
confirmPassword: state.confirmPassword,
|
||||
});
|
||||
} else {
|
||||
await client.post('/verify', {
|
||||
verificationCode,
|
||||
name: state.name,
|
||||
username: state.username,
|
||||
password: state.password,
|
||||
confirmPassword: state.confirmPassword,
|
||||
});
|
||||
}
|
||||
toast.success('Account created. Please sign in.');
|
||||
navigate('/');
|
||||
} catch (ex) {
|
||||
const error = ex as { message?: string };
|
||||
toast.error(error.message || 'Failed to create account. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isValid = flow === 'bootstrap' ? validateBootstrap(state) : validateInvite(state);
|
||||
|
||||
return { formRef, state, isValid, tokenStatus, flow, isSubmitting, handleSubmit };
|
||||
};
|
||||
|
||||
type VerifyFormState = {
|
||||
email?: string;
|
||||
name?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
};
|
||||
|
||||
type TokenStatus = 'loading' | 'valid' | 'invalid';
|
||||
|
||||
const validateBootstrap = (state: Partial<VerifyFormState>) => {
|
||||
const { name, username, password, confirmPassword } = state;
|
||||
if (!name || !username || !password || !confirmPassword) return false;
|
||||
if (password !== confirmPassword) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateInvite = (state: Partial<VerifyFormState>) => {
|
||||
const { name, username, password, confirmPassword } = state;
|
||||
if (!name || !username || !password || !confirmPassword) return false;
|
||||
if (password !== confirmPassword) return false;
|
||||
return true;
|
||||
};
|
||||
@@ -1,14 +1,6 @@
|
||||
import { AuthenticationLayout } from './Layout';
|
||||
import { LandingPage } from './LandingPage';
|
||||
import { SignoutScreen } from './Signout';
|
||||
import { VerifyScreen } from './VerifyScreen';
|
||||
import { ForgotPassword, ResetPassword } from './ForgotPassword';
|
||||
|
||||
export {
|
||||
AuthenticationLayout,
|
||||
LandingPage,
|
||||
SignoutScreen,
|
||||
VerifyScreen,
|
||||
ForgotPassword,
|
||||
ResetPassword,
|
||||
};
|
||||
export { AuthenticationLayout, LandingPage, SignoutScreen, ForgotPassword, ResetPassword };
|
||||
|
||||
Reference in New Issue
Block a user