refactor: rework backend to frontend context

This commit is contained in:
Stavros
2026-05-10 19:17:22 +03:00
parent 25017a76c9
commit 32595e351d
16 changed files with 326 additions and 162 deletions
+2 -2
View File
@@ -2,9 +2,9 @@ import { Navigate } from "react-router";
import { useUserContext } from "./context/user-context";
export const App = () => {
const { isLoggedIn } = useUserContext();
const { auth } = useUserContext();
if (isLoggedIn) {
if (auth.authenticated) {
return <Navigate to="/logout" replace />;
}
+11 -7
View File
@@ -6,17 +6,17 @@ import { DomainWarning } from "../domain-warning/domain-warning";
import { ThemeToggle } from "../theme-toggle/theme-toggle";
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
const { backgroundImage, title } = useAppContext();
const { ui } = useAppContext();
useEffect(() => {
document.title = title;
}, [title]);
document.title = ui.title;
}, [ui.title]);
return (
<div
className="flex flex-col justify-center items-center min-h-svh px-4"
style={{
backgroundImage: `url(${backgroundImage})`,
backgroundImage: `url(${ui.backgroundImage})`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
@@ -31,7 +31,7 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => {
};
export const Layout = () => {
const { appUrl, warningsEnabled } = useAppContext();
const { app, ui } = useAppContext();
const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => {
return window.sessionStorage.getItem("ignoreDomainWarning") === "true";
});
@@ -42,11 +42,15 @@ export const Layout = () => {
setIgnoreDomainWarning(true);
}, [setIgnoreDomainWarning]);
if (!ignoreDomainWarning && warningsEnabled && appUrl !== currentUrl) {
if (
!ignoreDomainWarning &&
ui.warningsEnabled &&
!app.trustedDomains.includes(currentUrl)
) {
return (
<BaseLayout>
<DomainWarning
appUrl={appUrl}
appUrl={app.appUrl}
currentUrl={currentUrl}
onClick={() => handleIgnore()}
/>
+2 -2
View File
@@ -77,7 +77,7 @@ const createScopeMap = (t: TFunction<"translation", undefined>): Scope[] => {
};
export const AuthorizePage = () => {
const { isLoggedIn } = useUserContext();
const { auth } = useUserContext();
const { search } = useLocation();
const { t } = useTranslation();
const navigate = useNavigate();
@@ -127,7 +127,7 @@ export const AuthorizePage = () => {
);
}
if (!isLoggedIn) {
if (!auth.authenticated) {
return <Navigate to={`/login?${oidcParams.compiled}`} replace />;
}
+9 -8
View File
@@ -14,8 +14,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useRedirectUri } from "@/lib/hooks/redirect-uri";
export const ContinuePage = () => {
const { cookieDomain, warningsEnabled } = useAppContext();
const { isLoggedIn } = useUserContext();
const { app, ui } = useAppContext();
const { auth } = useUserContext();
const { search } = useLocation();
const { t } = useTranslation();
const navigate = useNavigate();
@@ -29,17 +29,18 @@ export const ContinuePage = () => {
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
redirectUri,
cookieDomain,
app.cookieDomain,
);
const urlHref = url?.href;
const hasValidRedirect = valid && allowedProto;
const showUntrustedWarning = hasValidRedirect && !trusted && warningsEnabled;
const showUntrustedWarning =
hasValidRedirect && !trusted && ui.warningsEnabled;
const showInsecureWarning =
hasValidRedirect && httpsDowngrade && warningsEnabled;
hasValidRedirect && httpsDowngrade && ui.warningsEnabled;
const shouldAutoRedirect =
isLoggedIn &&
auth.authenticated &&
hasValidRedirect &&
!showUntrustedWarning &&
!showInsecureWarning;
@@ -77,7 +78,7 @@ export const ContinuePage = () => {
};
}, [shouldAutoRedirect, redirectToTarget]);
if (!isLoggedIn) {
if (!auth.authenticated) {
return (
<Navigate
to={`/login${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
@@ -104,7 +105,7 @@ export const ContinuePage = () => {
components={{
code: <code />,
}}
values={{ cookieDomain }}
values={{ cookieDomain: app.cookieDomain }}
shouldUnescape={true}
/>
</CardDescription>
+3 -3
View File
@@ -13,7 +13,7 @@ import Markdown from "react-markdown";
import { useLocation } from "react-router";
export const ForgotPasswordPage = () => {
const { forgotPasswordMessage } = useAppContext();
const { ui } = useAppContext();
const { t } = useTranslation();
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
@@ -26,8 +26,8 @@ export const ForgotPasswordPage = () => {
<CardContent>
<CardDescription>
<Markdown>
{forgotPasswordMessage !== ""
? forgotPasswordMessage
{ui.forgotPasswordMessage !== ""
? ui.forgotPasswordMessage
: t("forgotPasswordMessage")}
</Markdown>
</CardDescription>
+17 -17
View File
@@ -36,13 +36,13 @@ const iconMap: Record<string, React.ReactNode> = {
};
export const LoginPage = () => {
const { isLoggedIn, tailscaleNodeName } = useUserContext();
const { providers, title, oauthAutoRedirect } = useAppContext();
const { auth, tailscale } = useUserContext();
const { ui, oauth, auth: cauth } = useAppContext();
const { search } = useLocation();
const { t } = useTranslation();
const [showRedirectButton, setShowRedirectButton] = useState(false);
const [useTailscale, setUseTailscale] = useState(tailscaleNodeName !== "");
const [useTailscale, setUseTailscale] = useState(tailscale.nodeName !== "");
const hasAutoRedirectedRef = useRef(false);
@@ -56,15 +56,15 @@ export const LoginPage = () => {
const oidcParams = useOIDCParams(searchParams);
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
providers.find((provider) => provider.id === oauthAutoRedirect) !==
cauth.providers.find((provider) => provider.id === oauth.autoRedirect) !==
undefined && redirectUri !== undefined,
);
const oauthProviders = providers.filter(
const oauthProviders = cauth.providers.filter(
(provider) => provider.id !== "local" && provider.id !== "ldap",
);
const userAuthConfigured =
providers.find(
cauth.providers.find(
(provider) => provider.id === "local" || provider.id === "ldap",
) !== undefined;
@@ -177,19 +177,19 @@ export const LoginPage = () => {
useEffect(() => {
if (
!isLoggedIn &&
!auth.authenticated &&
isOauthAutoRedirect &&
!hasAutoRedirectedRef.current &&
redirectUri !== undefined
) {
hasAutoRedirectedRef.current = true;
oauthMutate(oauthAutoRedirect);
oauthMutate(oauth.autoRedirect);
}
}, [
isLoggedIn,
auth.authenticated,
oauthMutate,
hasAutoRedirectedRef,
oauthAutoRedirect,
oauth.autoRedirect,
isOauthAutoRedirect,
redirectUri,
]);
@@ -206,11 +206,11 @@ export const LoginPage = () => {
};
}, [redirectTimer, redirectButtonTimer]);
if (isLoggedIn && oidcParams.isOidc) {
if (auth.authenticated && oidcParams.isOidc) {
return <Navigate to={`/authorize?${oidcParams.compiled}`} replace />;
}
if (isLoggedIn && redirectUri !== undefined) {
if (auth.authenticated && redirectUri !== undefined) {
return (
<Navigate
to={`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
@@ -219,7 +219,7 @@ export const LoginPage = () => {
);
}
if (isLoggedIn) {
if (auth.authenticated) {
return <Navigate to="/logout" replace />;
}
@@ -272,7 +272,7 @@ export const LoginPage = () => {
credentials?
</div>
<div className="text-muted-foreground text-sm">
Machine Name: <code>{tailscaleNodeName}</code>
Machine Name: <code>{tailscale.nodeName}</code>
</div>
</CardContent>
<CardFooter className="flex flex-col items-stretch gap-3">
@@ -299,8 +299,8 @@ export const LoginPage = () => {
return (
<Card>
<CardHeader className="gap-1.5">
<CardTitle className="text-center text-xl">{title}</CardTitle>
{providers.length > 0 && (
<CardTitle className="text-center text-xl">{ui.title}</CardTitle>
{cauth.providers.length > 0 && (
<CardDescription className="text-center">
{oauthProviders.length !== 0
? t("loginTitle")
@@ -338,7 +338,7 @@ export const LoginPage = () => {
})()}
/>
)}
{providers.length == 0 && (
{cauth.providers.length == 0 && (
<pre className="break-normal! text-sm text-red-600">
{t("failedToFetchProvidersTitle")}
</pre>
+65 -31
View File
@@ -13,9 +13,11 @@ import { useEffect, useRef } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Navigate } from "react-router";
import { toast } from "sonner";
import { type UseMutationResult } from "@tanstack/react-query";
import { type AxiosResponse } from "axios";
export const LogoutPage = () => {
const { provider, username, isLoggedIn, email, oauthName } = useUserContext();
const { auth, oauth, tailscale } = useUserContext();
const { t } = useTranslation();
const redirectTimer = useRef<number | null>(null);
@@ -47,42 +49,74 @@ export const LogoutPage = () => {
};
}, [redirectTimer]);
if (!isLoggedIn) {
if (!auth.authenticated) {
return <Navigate to="/login" replace />;
}
if (oauth.active) {
return (
<LogoutLayout logoutMutation={logoutMutation}>
<Trans
i18nKey="logoutOauthSubtitle"
t={t}
components={{
code: <code />,
}}
values={{
username: auth.email,
provider: oauth.displayName,
}}
shouldUnescape={true}
/>
</LogoutLayout>
);
}
if (auth.providerId === "tailscale") {
return (
<LogoutLayout logoutMutation={logoutMutation}>
You are currently logged in with the Tailscale integration identified by
the <code>{tailscale.nodeName}</code> node. Click the button below to
log out.
</LogoutLayout>
);
}
return (
<LogoutLayout logoutMutation={logoutMutation}>
<Trans
i18nKey="logoutUsernameSubtitle"
t={t}
components={{
code: <code />,
}}
values={{
username: auth.username,
}}
shouldUnescape={true}
/>
</LogoutLayout>
);
};
interface LogoutLayoutProps {
children: React.ReactNode;
logoutMutation: UseMutationResult<
//eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-empty-object-type
AxiosResponse<any, any, {}>,
Error,
void,
unknown
>;
}
function LogoutLayout({ children, logoutMutation }: LogoutLayoutProps) {
const { t } = useTranslation();
return (
<Card>
<CardHeader className="gap-1.5">
<CardTitle className="text-xl">{t("logoutTitle")}</CardTitle>
<CardDescription>
{provider !== "local" && provider !== "ldap" ? (
<Trans
i18nKey="logoutOauthSubtitle"
t={t}
components={{
code: <code />,
}}
values={{
username: email,
provider: oauthName,
}}
shouldUnescape={true}
/>
) : (
<Trans
i18nKey="logoutUsernameSubtitle"
t={t}
components={{
code: <code />,
}}
values={{
username,
}}
shouldUnescape={true}
/>
)}
</CardDescription>
<CardDescription>{children}</CardDescription>
</CardHeader>
<CardFooter>
<Button
@@ -96,4 +130,4 @@ export const LogoutPage = () => {
</CardFooter>
</Card>
);
};
}
+2 -2
View File
@@ -19,7 +19,7 @@ import { toast } from "sonner";
import { useOIDCParams } from "@/lib/hooks/oidc";
export const TotpPage = () => {
const { totpPending } = useUserContext();
const { totp } = useUserContext();
const { t } = useTranslation();
const { search } = useLocation();
const formId = useId();
@@ -64,7 +64,7 @@ export const TotpPage = () => {
};
}, [redirectTimer]);
if (!totpPending) {
if (!totp.pending) {
return <Navigate to="/" replace />;
}
+21 -4
View File
@@ -6,15 +6,32 @@ export const providerSchema = z.object({
oauth: z.boolean(),
});
export const appContextSchema = z.object({
const authSchema = z.object({
providers: z.array(providerSchema),
});
const oauthSchema = z.object({
autoRedirect: z.string(),
});
const uiSchema = z.object({
title: z.string(),
appUrl: z.string(),
cookieDomain: z.string(),
forgotPasswordMessage: z.string(),
backgroundImage: z.string(),
oauthAutoRedirect: z.string(),
warningsEnabled: z.boolean(),
});
const appSchema = z.object({
appUrl: z.string(),
cookieDomain: z.string(),
trustedDomains: z.array(z.string()),
});
export const appContextSchema = z.object({
auth: authSchema,
oauth: oauthSchema,
ui: uiSchema,
app: appSchema,
});
export type AppContextSchema = z.infer<typeof appContextSchema>;
+23 -7
View File
@@ -1,15 +1,31 @@
import { z } from "zod";
export const userContextSchema = z.object({
isLoggedIn: z.boolean(),
const authSchema = z.object({
authenticated: z.boolean(),
username: z.string(),
name: z.string(),
email: z.string(),
provider: z.string(),
oauth: z.boolean(),
totpPending: z.boolean(),
oauthName: z.string(),
tailscaleNodeName: z.string(),
providerId: z.string(),
});
const oauthSchema = z.object({
active: z.boolean(),
displayName: z.string(),
});
const totpSchema = z.object({
pending: z.boolean(),
});
const tailscaleSchema = z.object({
nodeName: z.string(),
});
export const userContextSchema = z.object({
auth: authSchema,
oauth: oauthSchema,
totp: totpSchema,
tailscale: tailscaleSchema,
});
export type UserContextSchema = z.infer<typeof userContextSchema>;