Compare commits

...

2 Commits

Author SHA1 Message Date
Stavros
f2c81b6a5c refactor: switch to declarative mode instead of data mode in react router 2025-05-30 18:24:34 +03:00
Stavros
34c8d16c7d fix: fix loading states in forms 2025-05-30 18:14:33 +03:00
9 changed files with 77 additions and 79 deletions

View File

@@ -1,7 +1,8 @@
import { useAppContext } from "@/context/app-context"; import { useAppContext } from "@/context/app-context";
import { LanguageSelector } from "../language/language"; import { LanguageSelector } from "../language/language";
import { Outlet } from "react-router";
export const Layout = ({ children }: { children: React.ReactNode }) => { export const Layout = () => {
const { backgroundImage } = useAppContext(); const { backgroundImage } = useAppContext();
return ( return (
@@ -14,7 +15,7 @@ export const Layout = ({ children }: { children: React.ReactNode }) => {
}} }}
> >
<LanguageSelector /> <LanguageSelector />
{children} <Outlet />
</div> </div>
); );
}; };

View File

@@ -136,7 +136,7 @@ h4 {
} }
p { p {
@apply leading-7 [&:not(:first-child)]:mt-6; @apply leading-6 [&:not(:first-child)]:mt-6;
} }
blockquote { blockquote {

View File

@@ -2,7 +2,7 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import "./index.css"; import "./index.css";
import { Layout } from "./components/layout/layout.tsx"; import { Layout } from "./components/layout/layout.tsx";
import { createBrowserRouter, RouterProvider } from "react-router"; import { BrowserRouter, Route, Routes } from "react-router";
import { LoginPage } from "./pages/login-page.tsx"; import { LoginPage } from "./pages/login-page.tsx";
import { App } from "./App.tsx"; import { App } from "./App.tsx";
import { ErrorPage } from "./pages/error-page.tsx"; import { ErrorPage } from "./pages/error-page.tsx";
@@ -17,54 +17,6 @@ import { AppContextProvider } from "./context/app-context.tsx";
import { UserContextProvider } from "./context/user-context.tsx"; import { UserContextProvider } from "./context/user-context.tsx";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <ErrorPage />,
},
{
path: "/login",
element: <LoginPage />,
errorElement: <ErrorPage />,
},
{
path: "/logout",
element: <LogoutPage />,
errorElement: <ErrorPage />,
},
{
path: "/continue",
element: <ContinuePage />,
errorElement: <ErrorPage />,
},
{
path: "/totp",
element: <TotpPage />,
errorElement: <ErrorPage />,
},
{
path: "/forgot-password",
element: <ForgotPasswordPage />,
errorElement: <ErrorPage />,
},
{
path: "/unauthorized",
element: <UnauthorizedPage />,
errorElement: <ErrorPage />,
},
{
path: "/error",
element: <ErrorPage />,
errorElement: <ErrorPage />,
},
{
path: "*",
element: <NotFoundPage />,
errorElement: <ErrorPage />,
},
]);
const queryClient = new QueryClient(); const queryClient = new QueryClient();
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
@@ -72,10 +24,25 @@ createRoot(document.getElementById("root")!).render(
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AppContextProvider> <AppContextProvider>
<UserContextProvider> <UserContextProvider>
<Layout> <BrowserRouter>
<RouterProvider router={router} /> <Routes>
<Toaster /> <Route element={<Layout />} errorElement={<ErrorPage />}>
</Layout> <Route path="/" element={<App />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/logout" element={<LogoutPage />} />
<Route path="/continue" element={<ContinuePage />} />
<Route path="/totp" element={<TotpPage />} />
<Route
path="/forgot-password"
element={<ForgotPasswordPage />}
/>
<Route path="/unauthorized" element={<UnauthorizedPage />} />
<Route path="/error" element={<ErrorPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
<Toaster />
</UserContextProvider> </UserContextProvider>
</AppContextProvider> </AppContextProvider>
</QueryClientProvider> </QueryClientProvider>

View File

@@ -12,6 +12,7 @@ import { isValidUrl } from "@/lib/utils";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { Navigate, useLocation, useNavigate } from "react-router"; import { Navigate, useLocation, useNavigate } from "react-router";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { useState } from "react";
export const ContinuePage = () => { export const ContinuePage = () => {
const { isLoggedIn } = useUserContext(); const { isLoggedIn } = useUserContext();
@@ -22,6 +23,7 @@ export const ContinuePage = () => {
const { domain, disableContinue } = useAppContext(); const { domain, disableContinue } = useAppContext();
const { search } = useLocation(); const { search } = useLocation();
const [loading, setLoading] = useState(false);
const searchParams = new URLSearchParams(search); const searchParams = new URLSearchParams(search);
const redirectURI = searchParams.get("redirect_uri"); const redirectURI = searchParams.get("redirect_uri");
@@ -34,10 +36,15 @@ export const ContinuePage = () => {
return <Navigate to="/logout" />; return <Navigate to="/logout" />;
} }
if (disableContinue) { const handleRedirect = () => {
setLoading(true);
window.location.href = DOMPurify.sanitize(redirectURI); window.location.href = DOMPurify.sanitize(redirectURI);
} }
if (disableContinue) {
handleRedirect();
}
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -63,14 +70,13 @@ export const ContinuePage = () => {
</CardHeader> </CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2"> <CardFooter className="flex flex-col items-stretch gap-2">
<Button <Button
onClick={() => onClick={handleRedirect}
(window.location.href = DOMPurify.sanitize(redirectURI)) loading={loading}
}
variant="destructive" variant="destructive"
> >
{t("continueTitle")} {t("continueTitle")}
</Button> </Button>
<Button onClick={() => navigate("/logout")} variant="outline"> <Button onClick={() => navigate("/logout")} variant="outline" disabled={loading}>
{t("cancelTitle")} {t("cancelTitle")}
</Button> </Button>
</CardFooter> </CardFooter>
@@ -97,14 +103,13 @@ export const ContinuePage = () => {
</CardHeader> </CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2"> <CardFooter className="flex flex-col items-stretch gap-2">
<Button <Button
onClick={() => onClick={handleRedirect}
(window.location.href = DOMPurify.sanitize(redirectURI)) loading={loading}
}
variant="warning" variant="warning"
> >
{t("continueTitle")} {t("continueTitle")}
</Button> </Button>
<Button onClick={() => navigate("/logout")} variant="outline"> <Button onClick={() => navigate("/logout")} variant="outline" disabled={loading}>
{t("cancelTitle")} {t("cancelTitle")}
</Button> </Button>
</CardFooter> </CardFooter>
@@ -120,9 +125,8 @@ export const ContinuePage = () => {
</CardHeader> </CardHeader>
<CardFooter className="flex flex-col items-stretch"> <CardFooter className="flex flex-col items-stretch">
<Button <Button
onClick={() => onClick={handleRedirect}
(window.location.href = DOMPurify.sanitize(redirectURI)) loading={loading}
}
> >
{t("continueTitle")} {t("continueTitle")}
</Button> </Button>

View File

@@ -126,6 +126,8 @@ export const LoginPage = () => {
icon={<GoogleIcon />} icon={<GoogleIcon />}
className="w-full" className="w-full"
onClick={() => oauthMutation.mutate("google")} onClick={() => oauthMutation.mutate("google")}
loading={oauthMutation.isPending && oauthMutation.variables === "google"}
disabled={oauthMutation.isPending || loginMutation.isPending}
/> />
)} )}
{configuredProviders.includes("github") && ( {configuredProviders.includes("github") && (
@@ -134,6 +136,8 @@ export const LoginPage = () => {
icon={<GithubIcon />} icon={<GithubIcon />}
className="w-full" className="w-full"
onClick={() => oauthMutation.mutate("github")} onClick={() => oauthMutation.mutate("github")}
loading={oauthMutation.isPending && oauthMutation.variables === "github"}
disabled={oauthMutation.isPending || loginMutation.isPending}
/> />
)} )}
{configuredProviders.includes("generic") && ( {configuredProviders.includes("generic") && (
@@ -142,6 +146,8 @@ export const LoginPage = () => {
icon={<GenericIcon />} icon={<GenericIcon />}
className="w-full" className="w-full"
onClick={() => oauthMutation.mutate("generic")} onClick={() => oauthMutation.mutate("generic")}
loading={oauthMutation.isPending && oauthMutation.variables === "generic"}
disabled={oauthMutation.isPending || loginMutation.isPending}
/> />
)} )}
</div> </div>
@@ -152,7 +158,7 @@ export const LoginPage = () => {
{userAuthConfigured && ( {userAuthConfigured && (
<LoginForm <LoginForm
onSubmit={(values) => loginMutation.mutate(values)} onSubmit={(values) => loginMutation.mutate(values)}
loading={loginMutation.isPending} loading={loginMutation.isPending || oauthMutation.isPending}
/> />
)} )}
{configuredProviders.length == 0 && ( {configuredProviders.length == 0 && (

View File

@@ -6,12 +6,19 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
export const NotFoundPage = () => { export const NotFoundPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const handleRedirect = () => {
setLoading(true);
navigate("/");
};
return ( return (
<Card className="min-w-xs sm:min-w-sm"> <Card className="min-w-xs sm:min-w-sm">
@@ -20,7 +27,7 @@ export const NotFoundPage = () => {
<CardDescription>{t("notFoundSubtitle")}</CardDescription> <CardDescription>{t("notFoundSubtitle")}</CardDescription>
</CardHeader> </CardHeader>
<CardFooter className="flex flex-col items-stretch"> <CardFooter className="flex flex-col items-stretch">
<Button onClick={() => navigate("/")}>{t("notFoundButton")}</Button> <Button onClick={handleRedirect} loading={loading}>{t("notFoundButton")}</Button>
</CardFooter> </CardFooter>
</Card> </Card>
); );

View File

@@ -8,18 +8,24 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { useUserContext } from "@/context/user-context";
import { TotpSchema } from "@/schemas/totp-schema"; import { TotpSchema } from "@/schemas/totp-schema";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import axios from "axios"; import axios from "axios";
import { useId } from "react"; import { useId } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router"; import { Navigate, useLocation } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
export const TotpPage = () => { export const TotpPage = () => {
const { totpPending } = useUserContext();
if (!totpPending) {
return <Navigate to="/" />;
}
const { t } = useTranslation(); const { t } = useTranslation();
const { search } = useLocation(); const { search } = useLocation();
const navigate = useNavigate();
const formId = useId(); const formId = useId();
const searchParams = new URLSearchParams(search); const searchParams = new URLSearchParams(search);
@@ -34,7 +40,7 @@ export const TotpPage = () => {
}); });
setTimeout(() => { setTimeout(() => {
navigate( window.location.replace(
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`, `/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
); );
}, 500); }, 500);

View File

@@ -6,6 +6,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { useState } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { Navigate, useLocation, useNavigate } from "react-router"; import { Navigate, useLocation, useNavigate } from "react-router";
@@ -23,6 +24,12 @@ export const UnauthorizedPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const handleRedirect = () => {
setLoading(true);
navigate("/login");
};
let i18nKey = "unauthorizedLoginSubtitle"; let i18nKey = "unauthorizedLoginSubtitle";
@@ -53,7 +60,7 @@ export const UnauthorizedPage = () => {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardFooter className="flex flex-col items-stretch"> <CardFooter className="flex flex-col items-stretch">
<Button onClick={() => navigate("/login")}> <Button onClick={handleRedirect} loading={loading}>
{t("unauthorizedButton")} {t("unauthorizedButton")}
</Button> </Button>
</CardFooter> </CardFooter>

View File

@@ -255,16 +255,16 @@ func ParseUser(user string) (types.User, error) {
// Check if the user has a totp secret // Check if the user has a totp secret
if len(userSplit) == 2 { if len(userSplit) == 2 {
return types.User{ return types.User{
Username: userSplit[0], Username: strings.TrimSpace(userSplit[0]),
Password: userSplit[1], Password: strings.TrimSpace(userSplit[1]),
}, nil }, nil
} }
// Return the user struct // Return the user struct
return types.User{ return types.User{
Username: userSplit[0], Username: strings.TrimSpace(userSplit[0]),
Password: userSplit[1], Password: strings.TrimSpace(userSplit[1]),
TotpSecret: userSplit[2], TotpSecret: strings.TrimSpace(userSplit[2]),
}, nil }, nil
} }