feat: proper redirect handling

This commit is contained in:
2025-05-29 14:17:09 +02:00
parent 8364a8e9ec
commit 807d7538a0
10 changed files with 106 additions and 53 deletions

View File

@ -5,7 +5,7 @@ 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 OAuthAuthorizePage from "./pages/Authorize";
import AuthLayout from "./layout/AuthLayout";
const router = createBrowserRouter([

View File

@ -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

View File

@ -3,12 +3,14 @@ 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 location = useLocation();
const oauth = useOAuthContext();
const handleAccountSelect = useCallback(
@ -52,7 +54,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">

View 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;

View File

@ -28,19 +28,28 @@ const AuthLayout = () => {
const isAuthPage = useMemo(() => {
const allowedPaths = ["/login", "/register", "/authorize"];
if (!allowedPaths.some((p) => location.pathname.startsWith(p))) {
return false;
}
return true;
return allowedPaths.some((p) => location.pathname.startsWith(p));
}, [location.pathname]);
const loading = useMemo(() => {
if (isAuthPage) {
return connecting;
}
return (!hasAuthenticated || !hasLoadedAccounts) || loadingAccounts || authenticating || connecting;
}, [isAuthPage, hasAuthenticated, hasLoadedAccounts, loadingAccounts, authenticating, connecting]);
return (
!hasAuthenticated ||
!hasLoadedAccounts ||
loadingAccounts ||
authenticating ||
connecting
);
}, [
isAuthPage,
hasAuthenticated,
hasLoadedAccounts,
loadingAccounts,
authenticating,
connecting,
]);
useEffect(() => {
connect();
@ -59,10 +68,11 @@ const AuthLayout = () => {
}, [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 (

View File

@ -1,12 +1,13 @@
import { 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";
const AgreementPage: FC = () => {
const profile = useAuth((state) => state.profile);
const promptAccountSelection = useAuth((state) => state.deleteActiveAccount);
return (
<div
@ -22,16 +23,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 +47,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
</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{" "}

View File

@ -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, useLocation, useNavigate } from "react-router-dom";
import { Link, useLocation } from "react-router-dom";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@ -34,7 +34,6 @@ export default function LoginPage() {
const repo = useAccountRepo();
const location = useLocation();
const navigate = useNavigate();
const updateActiveAccount = useAuth((state) => state.updateActiveAccount);
@ -68,10 +67,6 @@ export default function LoginPage() {
oauth.selectSession(response.access);
await updateActiveAccount(account);
if (!location.state?.from) {
navigate("/");
}
} catch (err: any) {
console.log(err);
setError(
@ -82,7 +77,7 @@ export default function LoginPage() {
setLoading(false);
}
},
[repo, reset, oauth, updateActiveAccount, location.state?.from, navigate]
[repo, reset, oauth, updateActiveAccount]
);
return (
@ -173,7 +168,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>

View File

@ -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>

View File

@ -22,7 +22,10 @@ export interface IAuthState {
loadAccounts: () => Promise<void>;
updateActiveAccount: (account: LocalAccount) => Promise<void>;
deleteActiveAccount: () => Promise<void>;
authenticate: () => Promise<void>;
requireSignIn: () => void;
signOut: () => void;
}
@ -79,23 +82,30 @@ 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 });
}
}
set({
activeAccount: account,
accounts,
loadingAccounts: false,
hasLoadedAccounts: true,
});
},
deleteActiveAccount: async () => {
resetActiveAccount();
set({ activeAccount: null });
get().loadAccounts();
},
authenticate: async () => {
const { authenticating } = get();
if (authenticating) return;