This commit is contained in:
+128
-168
@@ -19,188 +19,148 @@ const MIN_PASSWORD_LENGTH = 8;
|
||||
* session cookie on the host their org actually lives on.
|
||||
*/
|
||||
function orgLoginUrlForSlug(slug: string): string {
|
||||
if (typeof window === "undefined") return "/login";
|
||||
const { protocol, host } = window.location;
|
||||
const [hostname, port] = host.split(":");
|
||||
const parts = hostname.split(".");
|
||||
if (typeof window === "undefined") return "/login";
|
||||
const { protocol, host } = window.location;
|
||||
const [hostname, port] = host.split(":");
|
||||
const parts = hostname.split(".");
|
||||
|
||||
if (parts.length < 2 || parts[parts.length - 1] === "localhost") return "/login";
|
||||
if (parts.length < 2 || parts[parts.length - 1] === "localhost") return "/login";
|
||||
|
||||
const rest = parts[0] === "vantage" ? parts : parts.slice(1);
|
||||
if (rest[0] !== "vantage") return "/login";
|
||||
const rest = parts[0] === "vantage" ? parts : parts.slice(1);
|
||||
if (rest[0] !== "vantage") return "/login";
|
||||
|
||||
const newHost = [slug, ...rest].join(".") + (port ? `:${port}` : "");
|
||||
return `${protocol}
|
||||
const newHost = [slug, ...rest].join(".") + (port ? `:${port}` : "");
|
||||
return `${protocol}`;
|
||||
}
|
||||
|
||||
export default function SetupPage() {
|
||||
const [orgName, setOrgName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirm, setConfirm] = useState("");
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
const [created, setCreated] = useState<{ slug: string; loginUrl: string } | null>(null);
|
||||
const [orgName, setOrgName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirm, setConfirm] = useState("");
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
const [created, setCreated] = useState<{ slug: string; loginUrl: string } | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
auth
|
||||
.bootstrapStatus()
|
||||
.then((s) => {
|
||||
if (!s.needs_setup) window.location.href = "/login";
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
auth.bootstrapStatus()
|
||||
.then((s) => {
|
||||
if (!s.needs_setup) window.location.href = "/login";
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const { mutate: bootstrap, isPending, error } = useMutation({
|
||||
mutationFn: () => auth.bootstrap({ org_name: orgName, email, password }),
|
||||
onSuccess: (res) => {
|
||||
setCreated({ slug: res.slug, loginUrl: orgLoginUrlForSlug(res.slug) });
|
||||
},
|
||||
});
|
||||
const {
|
||||
mutate: bootstrap,
|
||||
isPending,
|
||||
error,
|
||||
} = useMutation({
|
||||
mutationFn: () => auth.bootstrap({ org_name: orgName, email, password }),
|
||||
onSuccess: (res) => {
|
||||
setCreated({ slug: res.slug, loginUrl: orgLoginUrlForSlug(res.slug) });
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (password.length < MIN_PASSWORD_LENGTH) {
|
||||
setValidationError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters.`);
|
||||
return;
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (password.length < MIN_PASSWORD_LENGTH) {
|
||||
setValidationError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters.`);
|
||||
return;
|
||||
}
|
||||
if (password !== confirm) {
|
||||
setValidationError("Passwords do not match.");
|
||||
return;
|
||||
}
|
||||
setValidationError(null);
|
||||
bootstrap();
|
||||
}
|
||||
if (password !== confirm) {
|
||||
setValidationError("Passwords do not match.");
|
||||
return;
|
||||
|
||||
const message = validationError ?? (error ? (error as Error).message : null);
|
||||
|
||||
const inputClass = "w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30";
|
||||
|
||||
if (created) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-text-primary">Organization created</h1>
|
||||
<p className="mt-1 text-sm text-text-secondary">Your owner account is ready. One more step to finish signing in.</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{created.slug} has its own address, and sign-in is kept separate per organization. Continue to your organization's sign-in page and log in with the email and password
|
||||
you just chose.
|
||||
</p>
|
||||
<code className="mt-3 block overflow-x-auto rounded bg-surface-2 px-2 py-1.5 font-mono text-xs text-text-primary">{created.loginUrl}</code>
|
||||
<a href={created.loginUrl} className="mt-5 block">
|
||||
<Button type="button" variant="primary" className="w-full justify-center">
|
||||
Go to sign in
|
||||
</Button>
|
||||
</a>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
setValidationError(null);
|
||||
bootstrap();
|
||||
}
|
||||
|
||||
|
||||
const message = validationError ?? (error ? (error as Error).message : null);
|
||||
|
||||
const inputClass =
|
||||
"w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30";
|
||||
|
||||
if (created) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-text-primary">Organization created</h1>
|
||||
<p className="mt-1 text-sm text-text-secondary">
|
||||
Your owner account is ready. One more step to finish signing in.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-text-primary">Welcome to Vantage</h1>
|
||||
<p className="mt-1 text-sm text-text-secondary">Create your organization and its owner account to get started.</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{created.slug} has its own address, and sign-in is kept separate per organization. Continue
|
||||
to your organization's sign-in page and log in with the email and password you just
|
||||
chose.
|
||||
</p>
|
||||
<code className="mt-3 block overflow-x-auto rounded bg-surface-2 px-2 py-1.5 font-mono text-xs text-text-primary">
|
||||
{created.loginUrl}
|
||||
</code>
|
||||
<a href={created.loginUrl} className="mt-5 block">
|
||||
<Button type="button" variant="primary" className="w-full justify-center">
|
||||
Go to sign in
|
||||
</Button>
|
||||
</a>
|
||||
</Card>
|
||||
<Card>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="org" className="mb-1.5 block text-sm font-medium text-text-secondary">
|
||||
Organization name
|
||||
</label>
|
||||
<input id="org" type="text" required value={orgName} onChange={(e) => setOrgName(e.target.value)} className={inputClass} />
|
||||
<p className="mt-1 text-xs text-text-tertiary">Used to derive your organization's subdomain.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="mb-1.5 block text-sm font-medium text-text-secondary">
|
||||
Owner email
|
||||
</label>
|
||||
<input id="email" type="email" required autoComplete="username" value={email} onChange={(e) => setEmail(e.target.value)} className={inputClass} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="mb-1.5 block text-sm font-medium text-text-secondary">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
minLength={MIN_PASSWORD_LENGTH}
|
||||
autoComplete="new-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-text-tertiary">At least {MIN_PASSWORD_LENGTH} characters.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirm" className="mb-1.5 block text-sm font-medium text-text-secondary">
|
||||
Confirm password
|
||||
</label>
|
||||
<input id="confirm" type="password" required autoComplete="new-password" value={confirm} onChange={(e) => setConfirm(e.target.value)} className={inputClass} />
|
||||
</div>
|
||||
|
||||
{message && <div className="rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{message}</div>}
|
||||
|
||||
<Button type="submit" variant="primary" loading={isPending} className="w-full justify-center">
|
||||
Create Organization
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-text-primary">Welcome to Vantage</h1>
|
||||
<p className="mt-1 text-sm text-text-secondary">
|
||||
Create your organization and its owner account to get started.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="org" className="mb-1.5 block text-sm font-medium text-text-secondary">
|
||||
Organization name
|
||||
</label>
|
||||
<input
|
||||
id="org"
|
||||
type="text"
|
||||
required
|
||||
value={orgName}
|
||||
onChange={(e) => setOrgName(e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-text-tertiary">
|
||||
Used to derive your organization's subdomain.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="mb-1.5 block text-sm font-medium text-text-secondary">
|
||||
Owner email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
autoComplete="username"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="mb-1.5 block text-sm font-medium text-text-secondary">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
minLength={MIN_PASSWORD_LENGTH}
|
||||
autoComplete="new-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-text-tertiary">
|
||||
At least {MIN_PASSWORD_LENGTH} characters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirm" className="mb-1.5 block text-sm font-medium text-text-secondary">
|
||||
Confirm password
|
||||
</label>
|
||||
<input
|
||||
id="confirm"
|
||||
type="password"
|
||||
required
|
||||
autoComplete="new-password"
|
||||
value={confirm}
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className="rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" variant="primary" loading={isPending} className="w-full justify-center">
|
||||
Create Organization
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user