204 lines
7.0 KiB
TypeScript
204 lines
7.0 KiB
TypeScript
import { readProfiles } from "../../stores/userinfo.ts";
|
|
import { useNavigate } from "@solidjs/router";
|
|
import { createSignal, For, Match, Show, Switch } from "solid-js";
|
|
import Cookie from "universal-cookie";
|
|
|
|
export default function Login() {
|
|
const [title, setTitle] = createSignal("Sign in");
|
|
const [subtitle, setSubtitle] = createSignal("Via your Goatpass account");
|
|
|
|
const [error, setError] = createSignal<null | string>(null);
|
|
const [loading, setLoading] = createSignal(false);
|
|
|
|
const [factor, setFactor] = createSignal<number>();
|
|
const [factors, setFactors] = createSignal<any[]>([]);
|
|
const [challenge, setChallenge] = createSignal<any>();
|
|
const [stage, setStage] = createSignal("starting");
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const handlers: { [id: string]: any } = {
|
|
"starting": async (evt: SubmitEvent) => {
|
|
evt.preventDefault();
|
|
|
|
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement));
|
|
if (!data.id) return;
|
|
|
|
setLoading(true);
|
|
const res = await fetch("/api/auth", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (res.status !== 200) {
|
|
setError(await res.text());
|
|
} else {
|
|
const data = await res.json();
|
|
setTitle(`Welcome, ${data["display_name"]}`);
|
|
setSubtitle("Before continue, we need verify that's you");
|
|
setFactors(data["factors"]);
|
|
setChallenge(data["challenge"]);
|
|
setError(null);
|
|
setStage("choosing");
|
|
}
|
|
setLoading(false);
|
|
},
|
|
"choosing": async (evt: SubmitEvent) => {
|
|
evt.preventDefault();
|
|
|
|
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement));
|
|
if (!data.factor) return;
|
|
|
|
setLoading(true);
|
|
const res = await fetch(`/api/auth/factors/${data.id}`, {
|
|
method: "POST"
|
|
});
|
|
if (res.status !== 200 && res.status !== 204) {
|
|
setError(await res.text());
|
|
} else {
|
|
setTitle(`Enter the code`);
|
|
setSubtitle(res.status === 204 ? "Enter your credentials" : "Code has been sent to your inbox");
|
|
setError(null);
|
|
setFactor(parseInt(data.factor as string));
|
|
setStage("verifying");
|
|
}
|
|
setLoading(false);
|
|
},
|
|
"verifying": async (evt: SubmitEvent) => {
|
|
evt.preventDefault();
|
|
|
|
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement));
|
|
if (!data.credentials) return;
|
|
|
|
setLoading(true);
|
|
const res = await fetch(`/api/auth`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
challenge_id: challenge().id,
|
|
factor_id: factor(),
|
|
secret: data.credentials
|
|
})
|
|
});
|
|
if (res.status !== 200) {
|
|
setError(await res.text());
|
|
} else {
|
|
const data = await res.json();
|
|
if (data["is_finished"]) {
|
|
await grantToken(data["session"]["grant_token"]);
|
|
await readProfiles();
|
|
navigate("/");
|
|
} else {
|
|
setError(null);
|
|
setStage("choosing");
|
|
setTitle("Continue verifying");
|
|
setSubtitle("You passed one check, but that's not enough.");
|
|
}
|
|
}
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
async function grantToken(tk: string) {
|
|
const res = await fetch("/api/auth/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
code: tk,
|
|
grant_type: "authorization_code"
|
|
})
|
|
});
|
|
if (res.status !== 200) {
|
|
const err = await res.text();
|
|
setError(err);
|
|
throw new Error(err);
|
|
} else {
|
|
const data = await res.json();
|
|
new Cookie().set("access_token", data["access_token"], { path: "/" });
|
|
new Cookie().set("refresh_token", data["refresh_token"], { path: "/" });
|
|
setError(null);
|
|
}
|
|
}
|
|
|
|
function getFactorName(factor: any) {
|
|
switch (factor.type) {
|
|
case 0:
|
|
return "Password Verification";
|
|
default:
|
|
return "Unknown";
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div class="w-full h-full flex justify-center items-center">
|
|
<div class="card w-[480px] max-w-screen shadow-xl">
|
|
<div class="card-body">
|
|
<div id="header" class="text-center mb-5">
|
|
<h1 class="text-xl font-bold">{title()}</h1>
|
|
<p>{subtitle()}</p>
|
|
</div>
|
|
|
|
<Show when={error()}>
|
|
<div id="alerts" class="mt-1">
|
|
<div role="alert" class="alert alert-error">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
|
viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span class="capitalize">{error()}</span>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
|
|
<form id="form" onSubmit={(e) => handlers[stage()](e)}>
|
|
<Switch>
|
|
<Match when={stage() === "starting"}>
|
|
<label class="form-control w-full">
|
|
<div class="label">
|
|
<span class="label-text">Account ID</span>
|
|
</div>
|
|
<input name="id" type="text" placeholder="Type here" class="input input-bordered w-full" />
|
|
<div class="label">
|
|
<span class="label-text-alt">Your username, email or phone number.</span>
|
|
</div>
|
|
</label>
|
|
</Match>
|
|
<Match when={stage() === "choosing"}>
|
|
<div class="join join-vertical w-full">
|
|
<For each={factors()}>
|
|
{item =>
|
|
<input class="join-item btn" type="radio" name="factor"
|
|
value={item.id}
|
|
aria-label={getFactorName(item)}
|
|
/>
|
|
}
|
|
</For>
|
|
</div>
|
|
<p class="text-center text-sm mt-2">Choose a way to verify that's you</p>
|
|
</Match>
|
|
<Match when={stage() === "verifying"}>
|
|
<label class="form-control w-full">
|
|
<div class="label">
|
|
<span class="label-text">Credentials</span>
|
|
</div>
|
|
<input name="credentials" type="password" placeholder="Type here"
|
|
class="input input-bordered w-full" />
|
|
<div class="label">
|
|
<span class="label-text-alt">Password or one time password.</span>
|
|
</div>
|
|
</label>
|
|
</Match>
|
|
</Switch>
|
|
|
|
<button type="submit" class="btn btn-primary btn-block mt-3" disabled={loading()}>
|
|
<Show when={loading()} fallback={"Next"}>
|
|
<span class="loading loading-spinner"></span>
|
|
</Show>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |