Login

This commit is contained in:
2024-02-02 00:53:22 +08:00
parent 434773976f
commit 19e1775476
15 changed files with 236 additions and 165 deletions

View File

@ -19,13 +19,8 @@ render(() => (
<UserinfoProvider>
<Router root={RootLayout}>
<Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
<Route path="/security" component={lazy(() => import("./pages/security.tsx"))} />
<Route path="/personalise" component={lazy(() => import("./pages/personalise.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="/auth/oauth/connect" component={lazy(() => import("./pages/auth/connect.tsx"))} />
<Route path="/auth/oauth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} />
<Route path="/users/me/confirm" component={lazy(() => import("./pages/users/confirm.tsx"))} />
<Route path="/auth" component={lazy(() => import("./pages/auth/callout.tsx"))} />
<Route path="/auth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} />
</Router>
</UserinfoProvider>
</WellKnownProvider>

View File

@ -21,7 +21,7 @@ export default function RootLayout(props: any) {
}, [ready, userinfo]);
function keepGate(path: string, e?: BeforeLeaveEventArgs) {
const whitelist = ["/auth/login", "/auth/register", "/users/me/confirm"];
const whitelist = ["/auth", "/auth/callback"];
if (!userinfo?.isLoggedIn && !whitelist.includes(path)) {
if (!e?.defaultPrevented) e?.preventDefault();

View File

@ -74,7 +74,7 @@ export default function Navbar() {
</ul>
</div>
<a href="/" class="btn btn-ghost text-xl">
{wellKnown?.name ?? "Goatpass"}
{wellKnown?.name ?? "Interactive"}
</a>
</div>
<div class="navbar-center hidden lg:flex">
@ -109,7 +109,7 @@ export default function Navbar() {
<button type="button" class="btn btn-sm btn-ghost" onClick={() => logout()}>Logout</button>
</Match>
<Match when={!userinfo?.isLoggedIn}>
<a href="/auth/login" class="btn btn-sm btn-primary">Login</a>
<a href="/auth" class="btn btn-sm btn-primary">Login</a>
</Match>
</Switch>
</div>

View File

@ -0,0 +1,64 @@
import { createSignal, Show } from "solid-js";
import { readProfiles } from "../../stores/userinfo.tsx";
import { useNavigate } from "@solidjs/router";
import Cookie from "universal-cookie";
export default function AuthCallback() {
const [error, setError] = createSignal<string | null>(null);
const [status, setStatus] = createSignal("Communicating with Goatpass...");
const navigate = useNavigate();
async function callback() {
const res = await fetch(`/api/auth/callback${location.search}`);
if (res.status !== 200) {
setError(await res.text());
} else {
const data = await res.json();
new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined });
new Cookie().set("refresh_token", data["refresh_token"], { path: "/", maxAge: undefined });
setStatus("Pulling your personal data...");
await readProfiles();
setStatus("Redirecting...")
setTimeout(() => navigate("/"), 1850)
}
}
callback();
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">Authenticate</h1>
<p>Via your Goatpass account</p>
</div>
<div class="pt-16 text-center">
<div class="text-center">
<div>
<span class="loading loading-lg loading-bars"></span>
</div>
<span>{status()}</span>
</div>
</div>
<Show when={error()} fallback={<div class="mt-16"></div>}>
<div id="alerts" class="mt-16">
<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>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,55 @@
import { createSignal, Show } from "solid-js";
export default function AuthCallout() {
const [error, setError] = createSignal<string | null>(null);
const [status, setStatus] = createSignal("Communicating with Goatpass...");
async function communicate() {
const res = await fetch(`/api/auth${location.search}`);
if (res.status !== 200) {
setError(await res.text());
} else {
const data = await res.json();
setStatus("Got you! Now redirecting...");
window.open(data["target"], "_self");
}
}
communicate();
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">Authenticate</h1>
<p>Via your Goatpass account</p>
</div>
<div class="pt-16 text-center">
<div class="text-center">
<div>
<span class="loading loading-lg loading-bars"></span>
</div>
<span>{status()}</span>
</div>
</div>
<Show when={error()} fallback={<div class="mt-16"></div>}>
<div id="alerts" class="mt-16">
<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>
</div>
</div>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { getAtk, readProfiles, useUserinfo } from "../stores/userinfo.tsx";
import { createSignal, For, Show } from "solid-js";
import { useUserinfo } from "../stores/userinfo.tsx";
import { createSignal, Show } from "solid-js";
export default function DashboardPage() {
const userinfo = useUserinfo();
@ -18,19 +18,6 @@ export default function DashboardPage() {
}
}
async function readNotification(item: any) {
const res = await fetch(`/api/notifications/${item.id}/read`, {
method: "PUT",
headers: { Authorization: `Bearer ${getAtk()}` }
});
if (res.status !== 200) {
setError(await res.text());
} else {
await readProfiles();
setError(null);
}
}
return (
<div class="max-w-[720px] mx-auto px-5 pt-12">
<div id="greeting" class="px-5">
@ -39,19 +26,6 @@ export default function DashboardPage() {
</div>
<div id="alerts">
<Show when={!userinfo?.meta?.confirmed_at}>
<div role="alert" class="alert alert-warning mt-5">
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div>
<span>Your account isn't confirmed yet. Please check your inbox and confirm your account.</span> <br />
<span>Otherwise your account will be deactivate after 48 hours.</span>
</div>
</div>
</Show>
<Show when={error()}>
<div role="alert" class="alert alert-error mt-5">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
@ -64,45 +38,6 @@ export default function DashboardPage() {
</Show>
</div>
<div class="card shadow-xl mt-5">
<div class="card-body">
<h2 class="card-title">Notifications</h2>
<div class="bg-base-200 mt-3 mx-[-32px]">
<Show when={userinfo?.meta?.notifications?.length <= 0}>
<table class="table">
<tbody>
<tr>
<td class="px-[32px]">You're done! There are no notifications unread for you.</td>
</tr>
</tbody>
</table>
</Show>
<Show when={userinfo?.meta?.notifications?.length > 0}>
<table class="table">
<tbody>
<For each={userinfo?.meta?.notifications}>
{item =>
<tr>
<td class="px-[32px]">
<h2 class="font-bold">{item.subject}</h2>
<p>{item.content}</p>
<div class="flex gap-2">
<Show when={item.is_important}>
<span class="font-bold">Important</span>
</Show>
<a class="link" onClick={() => readNotification(item)}>Mark as read</a>
</div>
</td>
</tr>
}
</For>
</tbody>
</table>
</Show>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,95 +1,91 @@
import Cookie from "universal-cookie";
import { createContext, useContext } from "solid-js";
import { createStore } from "solid-js/store";
import {createContext, useContext} from "solid-js";
import {createStore} from "solid-js/store";
export interface Userinfo {
isLoggedIn: boolean,
displayName: string,
profiles: any,
meta: any
isLoggedIn: boolean,
displayName: string,
profiles: any,
}
const UserinfoContext = createContext<Userinfo>();
const defaultUserinfo: Userinfo = {
isLoggedIn: false,
displayName: "Citizen",
profiles: null,
meta: null
isLoggedIn: false,
displayName: "Citizen",
profiles: null,
};
const [userinfo, setUserinfo] = createStore<Userinfo>(structuredClone(defaultUserinfo));
export function getAtk(): string {
return new Cookie().get("access_token");
return new Cookie().get("access_token");
}
export async function refreshAtk() {
const rtk = new Cookie().get("refresh_token");
const rtk = new Cookie().get("refresh_token");
const res = await fetch("/api/auth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
refresh_token: rtk,
grant_type: "refresh_token"
})
});
if (res.status !== 200) {
console.error(await res.text())
} else {
const data = await res.json();
new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined });
new Cookie().set("refresh_token", data["refresh_token"], { path: "/", maxAge: undefined });
}
const res = await fetch("/api/auth/refresh", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
refresh_token: rtk,
})
});
if (res.status !== 200) {
console.error(await res.text())
} else {
const data = await res.json();
new Cookie().set("access_token", data["access_token"], {path: "/", maxAge: undefined});
new Cookie().set("refresh_token", data["refresh_token"], {path: "/", maxAge: undefined});
}
}
function checkLoggedIn(): boolean {
return new Cookie().get("access_token");
return new Cookie().get("access_token");
}
export async function readProfiles(recovering = true) {
if (!checkLoggedIn()) return;
if (!checkLoggedIn()) return;
const res = await fetch("/api/users/me", {
headers: { "Authorization": `Bearer ${getAtk()}` }
});
const res = await fetch("/api/users/me", {
headers: {"Authorization": `Bearer ${getAtk()}`}
});
if (res.status !== 200) {
if (recovering) {
// Auto retry after refresh access token
await refreshAtk();
return await readProfiles(false);
} else {
clearUserinfo();
window.location.reload();
if (res.status !== 200) {
if (recovering) {
// Auto retry after refresh access token
await refreshAtk();
return await readProfiles(false);
} else {
clearUserinfo();
window.location.reload();
}
}
}
const data = await res.json();
const data = await res.json();
setUserinfo({
isLoggedIn: true,
displayName: data["nick"],
profiles: null,
meta: data
});
setUserinfo({
isLoggedIn: true,
displayName: data["name"],
profiles: data,
});
}
export function clearUserinfo() {
new Cookie().remove("access_token", { path: "/", maxAge: undefined });
new Cookie().remove("refresh_token", { path: "/", maxAge: undefined });
setUserinfo(defaultUserinfo);
new Cookie().remove("access_token", {path: "/", maxAge: undefined});
new Cookie().remove("refresh_token", {path: "/", maxAge: undefined});
setUserinfo(defaultUserinfo);
}
export function UserinfoProvider(props: any) {
return (
<UserinfoContext.Provider value={userinfo}>
{props.children}
</UserinfoContext.Provider>
);
return (
<UserinfoContext.Provider value={userinfo}>
{props.children}
</UserinfoContext.Provider>
);
}
export function useUserinfo() {
return useContext(UserinfoContext);
return useContext(UserinfoContext);
}

View File

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