Compare commits
12 Commits
aa152a4127
...
4b3a814d7e
Author | SHA1 | Date | |
---|---|---|---|
4b3a814d7e | |||
dd5c59afa8 | |||
0723a48ab0 | |||
ffefee930a | |||
a7ddd3d1ff | |||
21cedeabbd | |||
807d7538a0 | |||
8364a8e9ec | |||
56755ac531 | |||
e4d83e75a0 | |||
3dd91cf238 | |||
03d6730151 |
@ -198,7 +198,7 @@ func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
Nonce: nonce,
|
||||
Roles: []string{"user", "admin"},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "https://cb5f-2a00-10-5b00-c801-e955-5c68-63d0-b777.ngrok-free.app",
|
||||
Issuer: h.cfg.Jwt.Issuer,
|
||||
// TODO: use dedicated API id that is in local DB and bind to user there
|
||||
Subject: user.ID.String(),
|
||||
Audience: jwt.ClaimStrings{clientId},
|
||||
|
16
web/package-lock.json
generated
16
web/package-lock.json
generated
@ -17,6 +17,7 @@
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-jwt": "^1.3.0",
|
||||
"react-router": "^7.6.0",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"zustand": "^5.0.5"
|
||||
@ -4353,6 +4354,21 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-jwt": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-jwt/-/react-jwt-1.3.0.tgz",
|
||||
"integrity": "sha512-aC+X6q8pi63zoO7A060/4mfF5jM6Ay+4YyY4QgdD8dDOqp89sPcg0IhWEHyPACnVETMjBWzmxMPgIPosQNeYyw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "^2.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
|
@ -19,6 +19,7 @@
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-jwt": "^1.3.0",
|
||||
"react-router": "^7.6.0",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"zustand": "^5.0.5"
|
||||
|
@ -4,8 +4,8 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import IndexPage from "./pages/Index";
|
||||
import LoginPage from "./pages/Login";
|
||||
import RegisterPage from "./pages/Register";
|
||||
import AgreementPage from "./pages/Agreement";
|
||||
import OAuthAuthorizePage from "./pages/OAuthAuthorize";
|
||||
import AuthorizePage from "./pages/Authorize";
|
||||
import AuthenticatePage from "./pages/Authenticate";
|
||||
import AuthLayout from "./layout/AuthLayout";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
@ -14,10 +14,10 @@ const router = createBrowserRouter([
|
||||
element: <AuthLayout />,
|
||||
children: [
|
||||
{ index: true, element: <IndexPage /> },
|
||||
{ path: "agreement", element: <AgreementPage /> },
|
||||
{ path: "authorize", element: <AuthorizePage /> },
|
||||
{ path: "login", element: <LoginPage /> },
|
||||
{ path: "register", element: <RegisterPage /> },
|
||||
{ path: "authorize", element: <OAuthAuthorizePage /> },
|
||||
{ path: "authenticate", element: <AuthenticatePage /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
@ -4,65 +4,100 @@ import { useAuth } from "@/store/auth";
|
||||
import Axios, { type AxiosResponse } from "axios";
|
||||
import { refreshTokenApi } from "./refresh";
|
||||
|
||||
import { isExpired } from "react-jwt";
|
||||
|
||||
export const axios = Axios.create({
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
let isRefreshing = false;
|
||||
let refreshQueue: ((token: string | null) => void)[] = [];
|
||||
|
||||
const waitForTokenRefresh = () => {
|
||||
return new Promise<string | null>((resolve) => {
|
||||
refreshQueue.push((token: string | null) => resolve(token));
|
||||
});
|
||||
};
|
||||
|
||||
const processRefreshQueue = async (token: string | null) => {
|
||||
refreshQueue.forEach((resolve) => resolve(token));
|
||||
refreshQueue = [];
|
||||
};
|
||||
|
||||
const refreshToken = async (
|
||||
accountId: string,
|
||||
refreshToken: string
|
||||
): Promise<{ access: string; refresh: string }> => {
|
||||
const db = useDbStore.getState().db;
|
||||
const loadAccounts = useAuth.getState().loadAccounts;
|
||||
const requireSignIn = useAuth.getState().requireSignIn;
|
||||
|
||||
if (!db) {
|
||||
console.log("No database connection available.");
|
||||
return Promise.reject("No database connection available.");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await refreshTokenApi(refreshToken);
|
||||
|
||||
await updateAccountTokens(db, {
|
||||
accountId: accountId,
|
||||
access: response.access,
|
||||
refresh: response.refresh,
|
||||
});
|
||||
|
||||
processRefreshQueue(response.access);
|
||||
|
||||
return { access: response.access, refresh: response.refresh };
|
||||
} catch (err) {
|
||||
console.error("Token refresh failed:", err);
|
||||
await deleteAccount(db, accountId);
|
||||
requireSignIn?.();
|
||||
processRefreshQueue(null);
|
||||
throw err;
|
||||
} finally {
|
||||
localStorage.removeItem("refreshing");
|
||||
loadAccounts?.();
|
||||
isRefreshing = false;
|
||||
}
|
||||
};
|
||||
|
||||
axios.interceptors.request.use(
|
||||
(request) => {
|
||||
async (request) => {
|
||||
const account = useAuth.getState().activeAccount;
|
||||
if (account?.access) {
|
||||
request.headers["Authorization"] = `Bearer ${account.access}`;
|
||||
let token: string | null = account?.access ?? null;
|
||||
|
||||
if (!token || !isExpired(token)) {
|
||||
request.headers["Authorization"] = `Bearer ${token}`;
|
||||
return request;
|
||||
}
|
||||
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
try {
|
||||
const { access } = await refreshToken(
|
||||
account!.accountId,
|
||||
account!.refresh
|
||||
);
|
||||
token = access;
|
||||
} catch (err) {
|
||||
console.error("Token refresh failed:", err);
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
token = await waitForTokenRefresh();
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw new Error("No token available");
|
||||
}
|
||||
|
||||
request.headers["Authorization"] = `Bearer ${token}`;
|
||||
return request;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.response.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
const account = useAuth.getState().activeAccount;
|
||||
if (!account?.refresh) {
|
||||
return Promise.reject(new Error("Unauthorized. No refresh token"));
|
||||
}
|
||||
|
||||
const db = useDbStore.getState().db;
|
||||
if (!db) {
|
||||
return Promise.reject(new Error("No database connection"));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await refreshTokenApi(account.refresh);
|
||||
|
||||
updateAccountTokens(db, {
|
||||
accountId: account.accountId,
|
||||
access: response.access,
|
||||
refresh: response.refresh,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("token refresh failed:", err);
|
||||
await deleteAccount(db, account.accountId);
|
||||
const loadAccounts = useAuth.getState().loadAccounts;
|
||||
loadAccounts?.();
|
||||
const requireSignIn = useAuth.getState().requireSignIn;
|
||||
requireSignIn?.();
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
export const handleApiError = async (response: AxiosResponse) => {
|
||||
|
@ -9,7 +9,7 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
variant?: "contained" | "outlined" | "text";
|
||||
variant?: "contained" | "outlined" | "text" | "icon";
|
||||
}
|
||||
|
||||
export const Button: FC<ButtonProps> = ({
|
||||
@ -22,11 +22,13 @@ export const Button: FC<ButtonProps> = ({
|
||||
const appearance = useMemo(() => {
|
||||
switch (variant) {
|
||||
case "contained":
|
||||
return "bg-blue-600 text-white hover:bg-blue-700";
|
||||
return "px-4 py-2 bg-blue-600 text-white hover:bg-blue-700";
|
||||
case "outlined":
|
||||
return "border border-blue-600 text-blue-600 hover:text-blue-700 font-medium";
|
||||
return "px-4 py-2 border border-blue-600 text-blue-600 hover:text-blue-700 font-medium";
|
||||
case "text":
|
||||
return "text-blue-600 hover:text-blue-700 font-medium";
|
||||
return "py-2 px-4 text-blue-600 hover:text-blue-700 font-medium";
|
||||
case "icon":
|
||||
return "py-0 px-0 text-gray-400 hover:text-gray-600";
|
||||
}
|
||||
|
||||
return "";
|
||||
@ -34,7 +36,7 @@ export const Button: FC<ButtonProps> = ({
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`cursor-pointer py-2 px-4 rounded-md transition-colors ${appearance} ${
|
||||
className={`${appearance} cursor-pointer rounded-md transition-colors ${
|
||||
className || ""
|
||||
}${
|
||||
loading
|
||||
|
@ -1,22 +1,20 @@
|
||||
import { useOAuthContext } from "@/context/oauth";
|
||||
import { type LocalAccount } from "@/repository/account";
|
||||
import { useAuth } from "@/store/auth";
|
||||
import { CirclePlus, User } from "lucide-react";
|
||||
import { useCallback, type FC } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
|
||||
const AccountList: FC = () => {
|
||||
const accounts = useAuth((state) => state.accounts);
|
||||
const updateActiveAccount = useAuth((state) => state.updateActiveAccount);
|
||||
|
||||
const oauth = useOAuthContext();
|
||||
const location = useLocation();
|
||||
|
||||
const handleAccountSelect = useCallback(
|
||||
(account: LocalAccount) => {
|
||||
oauth.selectSession(account.access);
|
||||
updateActiveAccount(account);
|
||||
},
|
||||
[oauth, updateActiveAccount]
|
||||
[updateActiveAccount]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -52,7 +50,7 @@ const AccountList: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Link to="/login">
|
||||
<Link to="/login" state={location.state}>
|
||||
<div className="flex flex-row items-center p-4 border-gray-200 dark:border-gray-700/65 border-b border-r-0 border-l-0 select-none cursor-pointer hover:bg-gray-50/50 dark:hover:bg-gray-800/10 transition-colors mb-0">
|
||||
<div>
|
||||
<div className="rounded-full p-2 text-gray-900 dark:text-gray-200 mr-3">
|
||||
|
30
web/src/feature/Avatar/index.tsx
Normal file
30
web/src/feature/Avatar/index.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { useAuth } from "@/store/auth";
|
||||
import { User } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
|
||||
export interface AvatarProps {
|
||||
iconSize?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Avatar: FC<AvatarProps> = ({ iconSize = 32, className }) => {
|
||||
const profile = useAuth((state) => state.profile);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`overflow-hidden bg-gray-100 rounded-full ring ring-gray-400 dark:ring dark:ring-gray-500 flex items-center justify-center ${className}`}
|
||||
>
|
||||
{profile?.profile_picture ? (
|
||||
<img
|
||||
src={profile?.profile_picture?.toString()}
|
||||
className="w-full h-full flex-1 object-cover"
|
||||
alt="profile"
|
||||
/>
|
||||
) : (
|
||||
<User size={iconSize} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Avatar;
|
@ -1,22 +1,43 @@
|
||||
import { useDbStore } from "@/store/db";
|
||||
import { useAuth } from "@/store/auth";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Navigate,
|
||||
Outlet,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useSearchParams,
|
||||
} from "react-router-dom";
|
||||
import BackgroundLayout from "./BackgroundLayout";
|
||||
import { useOAuthContext } from "@/context/oauth";
|
||||
|
||||
const AuthLayout = () => {
|
||||
const { connecting, db, connect } = useDbStore();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const {
|
||||
active,
|
||||
setActive,
|
||||
setClientID,
|
||||
setRedirectURI,
|
||||
setScope,
|
||||
setState,
|
||||
setNonce,
|
||||
} = useOAuthContext();
|
||||
|
||||
const dbConnected = useMemo(() => !!db, [db]);
|
||||
|
||||
const loadingAccounts = useAuth((state) => state.loadingAccounts);
|
||||
const loadAccounts = useAuth((state) => state.loadAccounts);
|
||||
const hasLoadedAccounts = useAuth((state) => state.hasLoadedAccounts);
|
||||
|
||||
const activeAccount = useAuth((state) => state.activeAccount);
|
||||
|
||||
const hasAccounts = useAuth((state) => state.accounts.length > 0);
|
||||
|
||||
const fetchingProfile = useAuth((state) => state.authenticating);
|
||||
const fetchProfile = useAuth((state) => state.authenticate);
|
||||
const authenticating = useAuth((state) => state.authenticating);
|
||||
const authenticate = useAuth((state) => state.authenticate);
|
||||
const hasAuthenticated = useAuth((state) => state.hasAuthenticated);
|
||||
|
||||
const signInRequired = useAuth((state) => state.signInRequired);
|
||||
|
||||
@ -24,28 +45,54 @@ const AuthLayout = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isAuthPage = useMemo(() => {
|
||||
const allowedPaths = ["/login", "/register", "/authorize"];
|
||||
if (!allowedPaths.some((p) => location.pathname.startsWith(p))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
const allowedPaths = ["/login", "/register", "/authenticate"];
|
||||
return allowedPaths.some((p) => location.pathname.startsWith(p));
|
||||
}, [location.pathname]);
|
||||
|
||||
console.log({
|
||||
isAuthPage,
|
||||
loadingAccounts,
|
||||
fetchingProfile,
|
||||
connecting,
|
||||
dbConnected,
|
||||
});
|
||||
|
||||
const loading = useMemo(() => {
|
||||
if (isAuthPage) {
|
||||
return connecting;
|
||||
}
|
||||
return loadingAccounts || fetchingProfile || connecting;
|
||||
}, [connecting, fetchingProfile, isAuthPage, loadingAccounts]);
|
||||
return (
|
||||
!hasAuthenticated ||
|
||||
!hasLoadedAccounts ||
|
||||
loadingAccounts ||
|
||||
authenticating ||
|
||||
connecting
|
||||
);
|
||||
}, [
|
||||
isAuthPage,
|
||||
hasAuthenticated,
|
||||
hasLoadedAccounts,
|
||||
loadingAccounts,
|
||||
authenticating,
|
||||
connecting,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
console.log(
|
||||
"setting search params:",
|
||||
Object.fromEntries(searchParams.entries())
|
||||
);
|
||||
setActive(true);
|
||||
setClientID(searchParams.get("client_id") ?? "");
|
||||
setRedirectURI(searchParams.get("redirect_uri") ?? "");
|
||||
const scope = searchParams.get("scope") ?? "";
|
||||
setScope(scope.split(" ").filter((s) => s.length > 0));
|
||||
setState(searchParams.get("state") ?? "");
|
||||
setNonce(searchParams.get("nonce") ?? "");
|
||||
}
|
||||
}, [
|
||||
active,
|
||||
searchParams,
|
||||
setActive,
|
||||
setClientID,
|
||||
setNonce,
|
||||
setRedirectURI,
|
||||
setScope,
|
||||
setState,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
connect();
|
||||
@ -59,20 +106,21 @@ const AuthLayout = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (dbConnected && !loadingAccounts && activeAccount) {
|
||||
fetchProfile();
|
||||
authenticate();
|
||||
}
|
||||
}, [activeAccount, dbConnected, fetchProfile, loadingAccounts]);
|
||||
}, [activeAccount, dbConnected, authenticate, loadingAccounts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!signInRequired && location.state?.from) {
|
||||
navigate(location.state.from, { state: {} });
|
||||
if (!signInRequired && isAuthPage) {
|
||||
const to = location.state?.from ?? "/";
|
||||
navigate(to, { state: { reset: true } });
|
||||
}
|
||||
}, [location.state, location.state?.from, navigate, signInRequired]);
|
||||
}, [isAuthPage, location.state?.from, navigate, signInRequired]);
|
||||
|
||||
if (signInRequired && !isAuthPage) {
|
||||
return (
|
||||
<Navigate
|
||||
to={hasAccounts ? "/authorize" : "/login"}
|
||||
to={hasAccounts ? "/authenticate" : "/login"}
|
||||
state={{ from: location.pathname }}
|
||||
/>
|
||||
);
|
||||
|
@ -1,39 +1,8 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useOAuthContext } from "@/context/oauth";
|
||||
import AccountList from "@/feature/AccountList";
|
||||
import { useEffect, type FC } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
const OAuthAuthorizePage: FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const {
|
||||
setActive,
|
||||
setClientID,
|
||||
setRedirectURI,
|
||||
setScope,
|
||||
setState,
|
||||
setNonce,
|
||||
} = useOAuthContext();
|
||||
|
||||
useEffect(() => {
|
||||
setActive(true);
|
||||
setClientID(searchParams.get("client_id") ?? "");
|
||||
setRedirectURI(searchParams.get("redirect_uri") ?? "");
|
||||
const scope = searchParams.get("scope") ?? "";
|
||||
setScope(scope.split(" ").filter((s) => s.length > 0));
|
||||
setState(searchParams.get("state") ?? "");
|
||||
setNonce(searchParams.get("nonce") ?? "");
|
||||
}, [
|
||||
searchParams,
|
||||
setActive,
|
||||
setClientID,
|
||||
setNonce,
|
||||
setRedirectURI,
|
||||
setScope,
|
||||
setState,
|
||||
]);
|
||||
import type { FC } from "react";
|
||||
|
||||
const AuthenticatePage: FC = () => {
|
||||
return (
|
||||
<div className="relative z-10 flex items-center justify-center min-h-screen">
|
||||
<Card className="sm:w-[700px] sm:min-w-[700px] sm:max-w-96 sm:min-h-auto p-3 min-h-screen w-full min-w-full shadow-lg bg-white/65 dark:bg-black/65 backdrop-blur-md">
|
||||
@ -66,4 +35,4 @@ const OAuthAuthorizePage: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuthAuthorizePage;
|
||||
export default AuthenticatePage;
|
@ -1,13 +1,25 @@
|
||||
import { type FC } from "react";
|
||||
import { useCallback, type FC } from "react";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { ArrowLeftRight } from "lucide-react";
|
||||
import { ArrowLeftRight, ChevronDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Avatar from "@/feature/Avatar";
|
||||
import { useAuth } from "@/store/auth";
|
||||
import { useOAuthContext } from "@/context/oauth";
|
||||
|
||||
const AuthorizePage: FC = () => {
|
||||
const promptAccountSelection = useAuth((state) => state.deleteActiveAccount);
|
||||
const activeAccount = useAuth((state) => state.activeAccount);
|
||||
|
||||
const oauth = useOAuthContext();
|
||||
|
||||
const AgreementPage: FC = () => {
|
||||
const profile = useAuth((state) => state.profile);
|
||||
|
||||
const handleAgree = useCallback(() => {
|
||||
if (!activeAccount) return;
|
||||
oauth.selectSession(activeAccount.access);
|
||||
}, [activeAccount, oauth]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(/dark-overlay.jpg)]`}
|
||||
@ -22,16 +34,8 @@ const AgreementPage: FC = () => {
|
||||
className="w-16 h-16 mb-4 mt-2 sm:mt-6"
|
||||
/> */}
|
||||
<div className="flex flex-row items-center gap-4 mt-2 mb-4 sm:mt-6">
|
||||
<div className="w-12 h-12 overflow-hidden bg-gray-100 rounded-full ring ring-gray-400 dark:ring dark:ring-gray-500">
|
||||
{/* <User size={32} /> */}
|
||||
<img
|
||||
src={profile?.profile_picture?.toString()}
|
||||
className="w-full h-full flex-1 object-cover"
|
||||
alt="profile"
|
||||
/>
|
||||
</div>
|
||||
<Avatar iconSize={32} className="w-12 h-12" />
|
||||
<div className="text-gray-400 dark:text-gray-600">
|
||||
{/* <Activity /> */}
|
||||
<ArrowLeftRight />
|
||||
</div>
|
||||
<div className="p-2 rounded-full bg-gray-900 ring ring-gray-400 dark:ring dark:ring-gray-500">
|
||||
@ -54,16 +58,17 @@ const AgreementPage: FC = () => {
|
||||
wants to access your Home Account
|
||||
</h2>
|
||||
<div className="flex flex-row items-center justify-center mb-6 gap-2">
|
||||
<div className="w-10 h-10 overflow-hidden bg-gray-100 rounded-full ring ring-gray-400 dark:ring dark:ring-gray-500">
|
||||
<img
|
||||
src="http://192.168.178.69:9000/guard-storage/profile_eff00028-2d9e-458d-8944-677855edc147_1748099702417601900.jpg"
|
||||
className="w-full h-full flex-1"
|
||||
alt="profile"
|
||||
/>
|
||||
</div>
|
||||
<Avatar iconSize={28} className="w-9 h-9" />
|
||||
<p className="text-sm text-gray-500 dark:text-gray-500">
|
||||
qwer.009771@gmail.com
|
||||
{profile?.email}
|
||||
</p>
|
||||
<Button
|
||||
variant="icon"
|
||||
className="px-0 py-0"
|
||||
onClick={promptAccountSelection}
|
||||
>
|
||||
<ChevronDown />
|
||||
</Button>
|
||||
</div>
|
||||
<h4 className="text-base mb-3 text-gray-400 dark:text-gray-500 text-left">
|
||||
This will allow{" "}
|
||||
@ -107,7 +112,7 @@ const AgreementPage: FC = () => {
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<Button variant="text">Cancel</Button>
|
||||
|
||||
<Button>Allow</Button>
|
||||
<Button onClick={handleAgree}>Allow</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
@ -117,4 +122,4 @@ const AgreementPage: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AgreementPage;
|
||||
export default AuthorizePage;
|
@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Mail, Lock } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
@ -9,7 +9,6 @@ import { useForm, type SubmitHandler } from "react-hook-form";
|
||||
import { useCallback, useState } from "react";
|
||||
import { loginApi } from "@/api/login";
|
||||
import { useAccountRepo } from "@/repository/account";
|
||||
import { useOAuthContext } from "@/context/oauth";
|
||||
import { useAuth } from "@/store/auth";
|
||||
|
||||
interface LoginForm {
|
||||
@ -29,10 +28,10 @@ export default function LoginPage() {
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
|
||||
const oauth = useOAuthContext();
|
||||
|
||||
const repo = useAccountRepo();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const updateActiveAccount = useAuth((state) => state.updateActiveAccount);
|
||||
|
||||
const onSubmit: SubmitHandler<LoginForm> = useCallback(
|
||||
@ -63,8 +62,7 @@ export default function LoginPage() {
|
||||
setSuccess("You have successfully logged in");
|
||||
reset();
|
||||
|
||||
oauth.selectSession(response.access);
|
||||
updateActiveAccount(account);
|
||||
await updateActiveAccount(account);
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
setError(
|
||||
@ -75,7 +73,7 @@ export default function LoginPage() {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[oauth, repo, reset, updateActiveAccount]
|
||||
[repo, reset, updateActiveAccount]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -166,7 +164,11 @@ export default function LoginPage() {
|
||||
</Button>
|
||||
<div className="text-sm text-center text-gray-600">
|
||||
Don't have an account?{" "}
|
||||
<Link to="/register" className="text-blue-600 hover:underline">
|
||||
<Link
|
||||
to="/register"
|
||||
state={location.state}
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Mail, Lock, User, Phone } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
@ -23,6 +23,8 @@ export default function RegisterPage() {
|
||||
formState: { errors },
|
||||
} = useForm<RegisterForm>();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
@ -238,7 +240,11 @@ export default function RegisterPage() {
|
||||
</Button>
|
||||
<div className="text-sm text-center text-gray-600">
|
||||
Already have an account?{" "}
|
||||
<Link to="/login" className="text-blue-600 hover:underline">
|
||||
<Link
|
||||
to="/login"
|
||||
state={location.state}
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -17,9 +17,15 @@ export interface IAuthState {
|
||||
accounts: LocalAccount[];
|
||||
signInRequired: boolean;
|
||||
|
||||
hasAuthenticated: boolean;
|
||||
hasLoadedAccounts: boolean;
|
||||
|
||||
loadAccounts: () => Promise<void>;
|
||||
updateActiveAccount: (account: LocalAccount) => Promise<void>;
|
||||
deleteActiveAccount: () => Promise<void>;
|
||||
|
||||
authenticate: () => Promise<void>;
|
||||
|
||||
requireSignIn: () => void;
|
||||
signOut: () => void;
|
||||
}
|
||||
@ -47,6 +53,9 @@ export const useAuth = create<IAuthState>((set, get) => ({
|
||||
loadingAccounts: false,
|
||||
signInRequired: false,
|
||||
|
||||
hasAuthenticated: false,
|
||||
hasLoadedAccounts: false,
|
||||
|
||||
updateActiveAccount: async (account) => {
|
||||
set({ activeAccount: account });
|
||||
|
||||
@ -73,16 +82,28 @@ export const useAuth = create<IAuthState>((set, get) => ({
|
||||
|
||||
if (!active) {
|
||||
set({ signInRequired: true });
|
||||
} else {
|
||||
const account = accounts.find((acc) => acc.accountId === active);
|
||||
|
||||
if (!account) {
|
||||
resetActiveAccount();
|
||||
set({ signInRequired: true });
|
||||
} else {
|
||||
set({ activeAccount: account });
|
||||
}
|
||||
}
|
||||
|
||||
const account = accounts.find((acc) => acc.accountId === active);
|
||||
set({
|
||||
accounts,
|
||||
loadingAccounts: false,
|
||||
hasLoadedAccounts: true,
|
||||
});
|
||||
},
|
||||
|
||||
if (!account) {
|
||||
resetActiveAccount();
|
||||
set({ signInRequired: true });
|
||||
}
|
||||
|
||||
set({ activeAccount: account, accounts, loadingAccounts: false });
|
||||
deleteActiveAccount: async () => {
|
||||
resetActiveAccount();
|
||||
set({ activeAccount: null });
|
||||
get().loadAccounts();
|
||||
},
|
||||
|
||||
authenticate: async () => {
|
||||
@ -114,7 +135,7 @@ export const useAuth = create<IAuthState>((set, get) => ({
|
||||
// TODO: set error
|
||||
console.log(err);
|
||||
} finally {
|
||||
set({ authenticating: false });
|
||||
set({ authenticating: false, hasAuthenticated: true });
|
||||
}
|
||||
},
|
||||
|
||||
|
Reference in New Issue
Block a user