Email notification

This commit is contained in:
2024-01-29 16:11:59 +08:00
parent 20119cb177
commit 3c58cb8f0a
18 changed files with 280 additions and 127 deletions

View File

@@ -5,5 +5,4 @@
html, body {
padding: 0;
margin: 0;
height: 100vh;
}

View File

@@ -9,14 +9,20 @@ import { lazy } from "solid-js";
import { Route, Router } from "@solidjs/router";
import RootLayout from "./layouts/RootLayout.tsx";
import { UserinfoProvider } from "./stores/userinfo.tsx";
import { WellKnownProvider } from "./stores/wellKnown.tsx";
const root = document.getElementById("root");
render(() => (
<Router root={RootLayout}>
<Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
<Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} />
<Route path="/auth/register" component={lazy(() => import("./pages/auth/register.tsx"))} />
<Route path="/users/me/confirm" component={lazy(() => import("./pages/users/confirm.tsx"))} />
</Router>
<WellKnownProvider>
<UserinfoProvider>
<Router root={RootLayout}>
<Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
<Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} />
<Route path="/auth/register" component={lazy(() => import("./pages/auth/register.tsx"))} />
<Route path="/users/me/confirm" component={lazy(() => import("./pages/users/confirm.tsx"))} />
</Router>
</UserinfoProvider>
</WellKnownProvider>
), root!);

View File

@@ -1,11 +1,12 @@
import Navbar from "./shared/Navbar.tsx";
import { readProfiles, UserinfoProvider } from "../stores/userinfo.tsx";
import { readProfiles } from "../stores/userinfo.tsx";
import { createSignal, Show } from "solid-js";
import { readWellKnown } from "../stores/wellKnown.tsx";
export default function RootLayout(props: any) {
const [ready, setReady] = createSignal(false);
readProfiles().then(() => setReady(true));
Promise.all([readWellKnown(), readProfiles()]).then(() => setReady(true));
return (
<Show when={ready()} fallback={
@@ -15,11 +16,8 @@ export default function RootLayout(props: any) {
</div>
</div>
}>
<UserinfoProvider>
<Navbar />
<main class="h-[calc(100vh-68px)]">{props.children}</main>
</UserinfoProvider>
<Navbar />
<main class="h-[calc(100vh-68px)] mt-[68px]">{props.children}</main>
</Show>
);
}

View File

@@ -1,6 +1,7 @@
import { For, Match, Switch } from "solid-js";
import { clearUserinfo, useUserinfo } from "../../stores/userinfo.tsx";
import { useNavigate } from "@solidjs/router";
import { useWellKnown } from "../../stores/wellKnown.tsx";
interface MenuItem {
label: string;
@@ -10,6 +11,7 @@ interface MenuItem {
export default function Navbar() {
const nav: MenuItem[] = [{ label: "Dashboard", href: "/" }];
const wellKnown = useWellKnown();
const userinfo = useUserinfo();
const navigate = useNavigate();
@@ -19,7 +21,7 @@ export default function Navbar() {
}
return (
<div class="navbar bg-base-100 shadow-md px-5">
<div class="navbar bg-base-100 shadow-md px-5 z-10 fixed top-0">
<div class="navbar-start">
<div class="dropdown">
<div tabIndex={0} role="button" class="btn btn-ghost lg:hidden">
@@ -52,7 +54,7 @@ export default function Navbar() {
</ul>
</div>
<a href="/" class="btn btn-ghost text-xl">
Goatpass
{wellKnown?.name ?? "Goatpass"}
</a>
</div>
<div class="navbar-center hidden lg:flex">

View File

@@ -50,7 +50,7 @@ export default function LoginPage() {
if (!data.factor) return;
setLoading(true);
const res = await fetch(`/api/auth/factors/${data.id}`, {
const res = await fetch(`/api/auth/factors/${data.factor}`, {
method: "POST"
});
if (res.status !== 200 && res.status !== 204) {
@@ -93,6 +93,7 @@ export default function LoginPage() {
setStage("choosing");
setTitle("Continue verifying");
setSubtitle("You passed one check, but that's not enough.");
setChallenge(data["challenge"]);
}
}
setLoading(false);
@@ -120,10 +121,17 @@ export default function LoginPage() {
}
}
function getFactorAvailable(factor: any) {
const blacklist: number[] = challenge()?.blacklist_factors ?? [];
return blacklist.includes(factor.id);
}
function getFactorName(factor: any) {
switch (factor.type) {
case 0:
return "Password Verification";
case 1:
return "Email Verification Code";
default:
return "Unknown";
}
@@ -171,6 +179,7 @@ export default function LoginPage() {
{item =>
<input class="join-item btn" type="radio" name="factor"
value={item.id}
disabled={getFactorAvailable(item)}
aria-label={getFactorName(item)}
/>
}

View File

@@ -1,4 +1,5 @@
import { createSignal, Show } from "solid-js";
import { useWellKnown } from "../../stores/wellKnown.tsx";
export default function RegisterPage() {
const [title, setTitle] = createSignal("Create an account");
@@ -8,6 +9,8 @@ export default function RegisterPage() {
const [loading, setLoading] = createSignal(false);
const [done, setDone] = createSignal(false);
const metadata = useWellKnown();
async function submit(evt: SubmitEvent) {
evt.preventDefault();
@@ -23,6 +26,7 @@ export default function RegisterPage() {
if (res.status !== 200) {
setError(await res.text());
} else {
setError(null);
setTitle("Congratulations!");
setSubtitle("Your account has been created and activation email has sent to your inbox!");
setDone(true);
@@ -32,83 +36,100 @@ export default function RegisterPage() {
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>
<div>
<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>
<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>
</div>
</Show>
</Show>
<Show when={!done()}>
<form id="form" onSubmit={submit}>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Username</span>
<span class="label-text-alt font-bold">Cannot be modify</span>
</div>
<input name="name" type="text" placeholder="Type here" class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">Lowercase alphabet and numbers only, maximum 16 characters</span>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Nickname</span>
</div>
<input name="nick" type="text" placeholder="Type here" class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">Maximum length is 24 characters</span>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Email Address</span>
</div>
<input name="email" type="email" placeholder="Type here" class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">Do not accept address with plus sign</span>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Password</span>
</div>
<input name="password" type="password" placeholder="Type here" class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">Must be secure</span>
</div>
</label>
<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 when={!done()}>
<form id="form" onSubmit={submit}>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Username</span>
<span class="label-text-alt font-bold">Cannot be modify</span>
</div>
<input name="name" type="text" placeholder="Type here" class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">Lowercase alphabet and numbers only, maximum 16 characters</span>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Nickname</span>
</div>
<input name="nick" type="text" placeholder="Type here" class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">Maximum length is 24 characters</span>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Email Address</span>
</div>
<input name="email" type="email" placeholder="Type here" class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">Do not accept address with plus sign</span>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Password</span>
</div>
<input name="password" type="password" placeholder="Type here"
class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">Must be secure</span>
</div>
</label>
<Show when={!metadata?.open_registration}>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Magic Token</span>
</div>
<input name="magic_token" type="password" placeholder="Type here"
class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">
This server enabled invitation only, so you need a magic token.
</span>
</div>
</label>
</Show>
</button>
</form>
</Show>
<Show when={done()}>
<div class="py-12 text-center">
<h2 class="text-lg font-bold">What's next?</h2>
<span>
<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>
</Show>
<Show when={done()}>
<div class="py-12 text-center">
<h2 class="text-lg font-bold">What's next?</h2>
<span>
<a href="/auth/login" class="link">Go login</a>{" "}
then you can take part in the entire smartsheep community.
then you can take part in the entire smartsheep community.
</span>
</div>
</Show>
</div>
</Show>
</div>
</div>
<div class="text-sm text-center mt-3">

View File

@@ -1,5 +1,6 @@
import { createSignal, Show } from "solid-js";
import { useSearchParams } from "@solidjs/router";
import { useNavigate, useSearchParams } from "@solidjs/router";
import { readProfiles } from "../../stores/userinfo.tsx";
export default function ConfirmRegistrationPage() {
const [error, setError] = createSignal<string | null>(null);
@@ -7,6 +8,8 @@ export default function ConfirmRegistrationPage() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
async function doConfirm() {
if (!searchParams["tk"]) {
setError("Bad Request: Code was not exists");
@@ -23,6 +26,8 @@ export default function ConfirmRegistrationPage() {
setError(await res.text());
} else {
setStatus("Confirmed. Redirecting to dashboard...");
await readProfiles();
navigate("/");
}
}

View File

@@ -0,0 +1,23 @@
import { createContext, useContext } from "solid-js";
import { createStore } from "solid-js/store";
const WellKnownContext = createContext<any>();
const [wellKnown, setWellKnown] = createStore<any>(null);
export async function readWellKnown() {
const res = await fetch("/.well-known")
setWellKnown(await res.json())
}
export function WellKnownProvider(props: any) {
return (
<WellKnownContext.Provider value={wellKnown}>
{props.children}
</WellKnownContext.Provider>
);
}
export function useWellKnown() {
return useContext(WellKnownContext);
}

View File

@@ -6,7 +6,8 @@ export default defineConfig({
plugins: [devtools({ autoname: true }), solid()],
server: {
proxy: {
"/api": "http://localhost:8444"
"/api": "http://localhost:8444",
"/.well-known": "http://localhost:8444"
}
}
});