User login

This commit is contained in:
2024-01-28 00:05:19 +08:00
parent d484b6b973
commit 4f33b9e0f6
49 changed files with 2303 additions and 10 deletions

5
view/src/.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"singleQuote": false
}

View File

@@ -0,0 +1,197 @@
:root {
--bs-body-font-family: "IBM Plex Serif", "Noto Serif SC", sans-serif !important;
}
html,
body {
font-family: var(--bs-body-font-family);
}
/* ibm-plex-serif-100 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: normal;
font-weight: 100;
src: url("./ibm-plex-serif-v19-latin-100.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-100italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: italic;
font-weight: 100;
src: url("./ibm-plex-serif-v19-latin-100italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-200 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: normal;
font-weight: 200;
src: url("./ibm-plex-serif-v19-latin-200.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-200italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: italic;
font-weight: 200;
src: url("./ibm-plex-serif-v19-latin-200italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-300 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: normal;
font-weight: 300;
src: url("./ibm-plex-serif-v19-latin-300.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-300italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: italic;
font-weight: 300;
src: url("./ibm-plex-serif-v19-latin-300italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: normal;
font-weight: 400;
src: url("./ibm-plex-serif-v19-latin-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: italic;
font-weight: 400;
src: url("./ibm-plex-serif-v19-latin-italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-500 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: normal;
font-weight: 500;
src: url("./ibm-plex-serif-v19-latin-500.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-500italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: italic;
font-weight: 500;
src: url("./ibm-plex-serif-v19-latin-500italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-600 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: normal;
font-weight: 600;
src: url("./ibm-plex-serif-v19-latin-600.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-600italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: italic;
font-weight: 600;
src: url("./ibm-plex-serif-v19-latin-600italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-700 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: normal;
font-weight: 700;
src: url("./ibm-plex-serif-v19-latin-700.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* ibm-plex-serif-700italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "IBM Plex Serif";
font-style: italic;
font-weight: 700;
src: url("./ibm-plex-serif-v19-latin-700italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-serif-sc-200 - chinese-simplified */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "Noto Serif SC";
font-style: normal;
font-weight: 200;
src: url("./noto-serif-sc-v22-chinese-simplified-200.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-serif-sc-300 - chinese-simplified */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "Noto Serif SC";
font-style: normal;
font-weight: 300;
src: url("./noto-serif-sc-v22-chinese-simplified-300.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-serif-sc-regular - chinese-simplified */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "Noto Serif SC";
font-style: normal;
font-weight: 400;
src: url("./noto-serif-sc-v22-chinese-simplified-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-serif-sc-500 - chinese-simplified */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "Noto Serif SC";
font-style: normal;
font-weight: 500;
src: url("./noto-serif-sc-v22-chinese-simplified-500.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-serif-sc-600 - chinese-simplified */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "Noto Serif SC";
font-style: normal;
font-weight: 600;
src: url("./noto-serif-sc-v22-chinese-simplified-600.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-serif-sc-700 - chinese-simplified */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "Noto Serif SC";
font-style: normal;
font-weight: 700;
src: url("./noto-serif-sc-v22-chinese-simplified-700.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-serif-sc-900 - chinese-simplified */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "Noto Serif SC";
font-style: normal;
font-weight: 900;
src: url("./noto-serif-sc-v22-chinese-simplified-900.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

9
view/src/index.css Normal file
View File

@@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html, body {
padding: 0;
margin: 0;
height: 100vh;
}

19
view/src/index.tsx Normal file
View File

@@ -0,0 +1,19 @@
/* @refresh reload */
import { render } from "solid-js/web";
import "./index.css";
import "./assets/fonts/fonts.css";
import { Route, Router } from "@solidjs/router";
import RootLayout from "./layouts/RootLayout.tsx";
import Dashboard from "./pages/dashboard.tsx";
import Login from "./pages/auth/login.tsx";
const root = document.getElementById("root");
render(() => (
<Router root={RootLayout}>
<Route path="/" component={Dashboard} />
<Route path="/auth/login" component={Login} />
</Router>
), root!);

View File

@@ -0,0 +1,11 @@
import Navbar from "./shared/Navbar.tsx";
export default function RootLayout(props: any) {
return (
<div>
<Navbar />
<main class="h-[calc(100vh-68px)]">{props.children}</main>
</div>
);
}

View File

@@ -0,0 +1,64 @@
import { For } from "solid-js";
interface MenuItem {
label: string;
href: string;
}
export default function Navbar() {
const nav: MenuItem[] = [{ label: "Dashboard", href: "/" }];
return (
<div class="navbar bg-base-100 shadow-md px-5">
<div class="navbar-start">
<div class="dropdown">
<div tabIndex={0} role="button" class="btn btn-ghost lg:hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h8m-8 6h16"
/>
</svg>
</div>
<ul
tabIndex={0}
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
>
<For each={nav}>
{(item) => (
<li>
<a href={item.href}>{item.label}</a>
</li>
)}
</For>
</ul>
</div>
<a href="/" class="btn btn-ghost text-xl">
Goatpass
</a>
</div>
<div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<For each={nav}>
{(item) => (
<li>
<a href={item.href}>{item.label}</a>
</li>
)}
</For>
</ul>
</div>
<div class="navbar-end pe-5">
<a href="/auth/login" class="btn btn-sm btn-primary">Login</a>
</div>
</div>
);
}

View File

@@ -0,0 +1,202 @@
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"]);
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>
);
}

View File

@@ -0,0 +1,8 @@
export default function Dashboard() {
return (
<div class="container mx-auto pt-12">
<h1 class="text-2xl font-bold">Welcome, undefined</h1>
<p>What's a nice day!</p>
</div>
)
}

1
view/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />