Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 />;
}

Expand Down
18 changes: 11 additions & 7 deletions frontend/src/components/layout/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}}
Expand All @@ -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";
});
Expand All @@ -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()}
/>
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/lib/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,17 @@
"profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information.",
"backToLoginButton": "Back to login"
"backToLoginButton": "Back to login",
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleSubmit": "Continue with Tailscale",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout."
}
10 changes: 9 additions & 1 deletion frontend/src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,13 @@
"phoneScopeName": "Phone",
"phoneScopeDescription": "Allows the app to access your phone number.",
"addressScopeName": "Address",
"addressScopeDescription": "Allows the app to access your address."
"addressScopeDescription": "Allows the app to access your address.",
"loginTailscaleTitle": "Continue with Tailscale",
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
"loginTailscaleDeviceName": "Device name:",
"loginTailscaleSubmit": "Continue with Tailscale",
"loginTailscaleOtherMethod": "Login with another method",
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout."
}
4 changes: 2 additions & 2 deletions frontend/src/pages/authorize-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -127,7 +127,7 @@ export const AuthorizePage = () => {
);
}

if (!isLoggedIn) {
if (!auth.authenticated) {
return <Navigate to={`/login?${oidcParams.compiled}`} replace />;
}

Expand Down
17 changes: 9 additions & 8 deletions frontend/src/pages/continue-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
Expand Down Expand Up @@ -77,7 +78,7 @@ export const ContinuePage = () => {
};
}, [shouldAutoRedirect, redirectToTarget]);

if (!isLoggedIn) {
if (!auth.authenticated) {
return (
<Navigate
to={`/login${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
Expand All @@ -104,7 +105,7 @@ export const ContinuePage = () => {
components={{
code: <code />,
}}
values={{ cookieDomain }}
values={{ cookieDomain: app.cookieDomain }}
shouldUnescape={true}
/>
</CardDescription>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/pages/forgot-password-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -26,8 +26,8 @@ export const ForgotPasswordPage = () => {
<CardContent>
<CardDescription>
<Markdown>
{forgotPasswordMessage !== ""
? forgotPasswordMessage
{ui.forgotPasswordMessage !== ""
? ui.forgotPasswordMessage
: t("forgotPasswordMessage")}
</Markdown>
</CardDescription>
Expand Down
92 changes: 81 additions & 11 deletions frontend/src/pages/login-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,17 @@ const iconMap: Record<string, React.ReactNode> = {
};

export const LoginPage = () => {
const { isLoggedIn } = useUserContext();
const { providers, title, oauthAutoRedirect } = useAppContext();
const { auth, tailscale } = useUserContext();
const {
ui,
oauth,
auth: { providers },
} = useAppContext();
const { search } = useLocation();
const { t } = useTranslation();

const [showRedirectButton, setShowRedirectButton] = useState(false);
const [useTailscale, setUseTailscale] = useState(tailscale.nodeName !== "");

const hasAutoRedirectedRef = useRef(false);

Expand All @@ -55,7 +60,7 @@ export const LoginPage = () => {
const oidcParams = useOIDCParams(searchParams);

const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
providers.find((provider) => provider.id === oauthAutoRedirect) !==
providers.find((provider) => provider.id === oauth.autoRedirect) !==
undefined && redirectUri !== undefined,
);

Expand Down Expand Up @@ -148,21 +153,47 @@ export const LoginPage = () => {
},
});

const { mutate: tailscaleMutate, isPending: tailscaleIsPending } =
useMutation({
mutationFn: () => axios.post("/api/user/tailscale"),
mutationKey: ["tailscale"],
onSuccess: () => {
toast.success(t("loginSuccessTitle"), {
description: t("loginTailscaleSuccess"),
});

redirectTimer.current = window.setTimeout(() => {
if (oidcParams.isOidc) {
window.location.replace(`/authorize?${oidcParams.compiled}`);
return;
}
window.location.replace(
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
);
}, 500);
},
onError: () => {
toast.error(t("loginFailTitle"), {
description: t("loginTailscaleFail"),
});
},
});

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,
]);
Expand All @@ -179,11 +210,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)}` : ""}`}
Expand All @@ -192,7 +223,7 @@ export const LoginPage = () => {
);
}

if (isLoggedIn) {
if (auth.authenticated) {
return <Navigate to="/logout" replace />;
}

Expand Down Expand Up @@ -228,10 +259,49 @@ export const LoginPage = () => {
</Card>
);
}

if (useTailscale) {
return (
<Card>
<CardHeader className="gap-3">
<TailscaleIcon className="mx-auto h-8 w-8" />
<CardTitle className="text-center text-xl">
{t("loginTailscaleTitle")}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="text-muted-foreground text-sm">
{t("loginTailscaleDescription")}
</div>
<div className="text-muted-foreground text-sm">
{t("loginTailscaleDeviceName")} <code>{tailscale.nodeName}</code>
</div>
</CardContent>
<CardFooter className="flex flex-col items-stretch gap-3">
<Button
className="w-full"
onClick={() => tailscaleMutate()}
loading={tailscaleIsPending}
>
{t("loginTailscaleSubmit")}
</Button>
<Button
className="w-full"
variant="outline"
onClick={() => setUseTailscale(false)}
disabled={tailscaleIsPending}
>
{t("loginTailscaleOtherMethod")}
</Button>
</CardFooter>
</Card>
);
}

return (
<Card>
<CardHeader className="gap-1.5">
<CardTitle className="text-center text-xl">{title}</CardTitle>
<CardTitle className="text-center text-xl">{ui.title}</CardTitle>
{providers.length > 0 && (
<CardDescription className="text-center">
{oauthProviders.length !== 0
Expand Down
Loading