✨ Login
This commit is contained in:
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
64
pkg/view/src/pages/auth/callback.tsx
Normal file
64
pkg/view/src/pages/auth/callback.tsx
Normal 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>
|
||||
);
|
||||
}
|
55
pkg/view/src/pages/auth/callout.tsx
Normal file
55
pkg/view/src/pages/auth/callout.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user