Skip to content

Commit 1166a15

Browse files
authored
feat: tailscale integration (#847)
1 parent c855f9b commit 1166a15

30 files changed

Lines changed: 1233 additions & 358 deletions

frontend/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { Navigate } from "react-router";
22
import { useUserContext } from "./context/user-context";
33

44
export const App = () => {
5-
const { isLoggedIn } = useUserContext();
5+
const { auth } = useUserContext();
66

7-
if (isLoggedIn) {
7+
if (auth.authenticated) {
88
return <Navigate to="/logout" replace />;
99
}
1010

frontend/src/components/layout/layout.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@ import { DomainWarning } from "../domain-warning/domain-warning";
66
import { ThemeToggle } from "../theme-toggle/theme-toggle";
77

88
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
9-
const { backgroundImage, title } = useAppContext();
9+
const { ui } = useAppContext();
1010

1111
useEffect(() => {
12-
document.title = title;
13-
}, [title]);
12+
document.title = ui.title;
13+
}, [ui.title]);
1414

1515
return (
1616
<div
1717
className="flex flex-col justify-center items-center min-h-svh px-4"
1818
style={{
19-
backgroundImage: `url(${backgroundImage})`,
19+
backgroundImage: `url(${ui.backgroundImage})`,
2020
backgroundSize: "cover",
2121
backgroundPosition: "center",
2222
}}
@@ -31,7 +31,7 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => {
3131
};
3232

3333
export const Layout = () => {
34-
const { appUrl, warningsEnabled } = useAppContext();
34+
const { app, ui } = useAppContext();
3535
const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => {
3636
return window.sessionStorage.getItem("ignoreDomainWarning") === "true";
3737
});
@@ -42,11 +42,15 @@ export const Layout = () => {
4242
setIgnoreDomainWarning(true);
4343
}, [setIgnoreDomainWarning]);
4444

45-
if (!ignoreDomainWarning && warningsEnabled && appUrl !== currentUrl) {
45+
if (
46+
!ignoreDomainWarning &&
47+
ui.warningsEnabled &&
48+
!app.trustedDomains.includes(currentUrl)
49+
) {
4650
return (
4751
<BaseLayout>
4852
<DomainWarning
49-
appUrl={appUrl}
53+
appUrl={app.appUrl}
5054
currentUrl={currentUrl}
5155
onClick={() => handleIgnore()}
5256
/>

frontend/src/lib/i18n/locales/en-US.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,17 @@
8080
"profileScopeDescription": "Allows the app to access your profile information.",
8181
"groupsScopeName": "Groups",
8282
"groupsScopeDescription": "Allows the app to access your group information.",
83-
"backToLoginButton": "Back to login"
83+
"backToLoginButton": "Back to login",
84+
"phoneScopeName": "Phone",
85+
"phoneScopeDescription": "Allows the app to access your phone number.",
86+
"addressScopeName": "Address",
87+
"addressScopeDescription": "Allows the app to access your address.",
88+
"loginTailscaleTitle": "Continue with Tailscale",
89+
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
90+
"loginTailscaleDeviceName": "Device name:",
91+
"loginTailscaleSubmit": "Continue with Tailscale",
92+
"loginTailscaleOtherMethod": "Login with another method",
93+
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
94+
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
95+
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout."
8496
}

frontend/src/lib/i18n/locales/en.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,13 @@
8484
"phoneScopeName": "Phone",
8585
"phoneScopeDescription": "Allows the app to access your phone number.",
8686
"addressScopeName": "Address",
87-
"addressScopeDescription": "Allows the app to access your address."
87+
"addressScopeDescription": "Allows the app to access your address.",
88+
"loginTailscaleTitle": "Continue with Tailscale",
89+
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
90+
"loginTailscaleDeviceName": "Device name:",
91+
"loginTailscaleSubmit": "Continue with Tailscale",
92+
"loginTailscaleOtherMethod": "Login with another method",
93+
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
94+
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
95+
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout."
8896
}

frontend/src/pages/authorize-page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const createScopeMap = (t: TFunction<"translation", undefined>): Scope[] => {
7777
};
7878

7979
export const AuthorizePage = () => {
80-
const { isLoggedIn } = useUserContext();
80+
const { auth } = useUserContext();
8181
const { search } = useLocation();
8282
const { t } = useTranslation();
8383
const navigate = useNavigate();
@@ -127,7 +127,7 @@ export const AuthorizePage = () => {
127127
);
128128
}
129129

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

frontend/src/pages/continue-page.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
1414
import { useRedirectUri } from "@/lib/hooks/redirect-uri";
1515

1616
export const ContinuePage = () => {
17-
const { cookieDomain, warningsEnabled } = useAppContext();
18-
const { isLoggedIn } = useUserContext();
17+
const { app, ui } = useAppContext();
18+
const { auth } = useUserContext();
1919
const { search } = useLocation();
2020
const { t } = useTranslation();
2121
const navigate = useNavigate();
@@ -29,17 +29,18 @@ export const ContinuePage = () => {
2929

3030
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
3131
redirectUri,
32-
cookieDomain,
32+
app.cookieDomain,
3333
);
3434

3535
const urlHref = url?.href;
3636

3737
const hasValidRedirect = valid && allowedProto;
38-
const showUntrustedWarning = hasValidRedirect && !trusted && warningsEnabled;
38+
const showUntrustedWarning =
39+
hasValidRedirect && !trusted && ui.warningsEnabled;
3940
const showInsecureWarning =
40-
hasValidRedirect && httpsDowngrade && warningsEnabled;
41+
hasValidRedirect && httpsDowngrade && ui.warningsEnabled;
4142
const shouldAutoRedirect =
42-
isLoggedIn &&
43+
auth.authenticated &&
4344
hasValidRedirect &&
4445
!showUntrustedWarning &&
4546
!showInsecureWarning;
@@ -77,7 +78,7 @@ export const ContinuePage = () => {
7778
};
7879
}, [shouldAutoRedirect, redirectToTarget]);
7980

80-
if (!isLoggedIn) {
81+
if (!auth.authenticated) {
8182
return (
8283
<Navigate
8384
to={`/login${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
@@ -104,7 +105,7 @@ export const ContinuePage = () => {
104105
components={{
105106
code: <code />,
106107
}}
107-
values={{ cookieDomain }}
108+
values={{ cookieDomain: app.cookieDomain }}
108109
shouldUnescape={true}
109110
/>
110111
</CardDescription>

frontend/src/pages/forgot-password-page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Markdown from "react-markdown";
1313
import { useLocation } from "react-router";
1414

1515
export const ForgotPasswordPage = () => {
16-
const { forgotPasswordMessage } = useAppContext();
16+
const { ui } = useAppContext();
1717
const { t } = useTranslation();
1818
const { search } = useLocation();
1919
const searchParams = new URLSearchParams(search);
@@ -26,8 +26,8 @@ export const ForgotPasswordPage = () => {
2626
<CardContent>
2727
<CardDescription>
2828
<Markdown>
29-
{forgotPasswordMessage !== ""
30-
? forgotPasswordMessage
29+
{ui.forgotPasswordMessage !== ""
30+
? ui.forgotPasswordMessage
3131
: t("forgotPasswordMessage")}
3232
</Markdown>
3333
</CardDescription>

frontend/src/pages/login-page.tsx

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,17 @@ const iconMap: Record<string, React.ReactNode> = {
3636
};
3737

3838
export const LoginPage = () => {
39-
const { isLoggedIn } = useUserContext();
40-
const { providers, title, oauthAutoRedirect } = useAppContext();
39+
const { auth, tailscale } = useUserContext();
40+
const {
41+
ui,
42+
oauth,
43+
auth: { providers },
44+
} = useAppContext();
4145
const { search } = useLocation();
4246
const { t } = useTranslation();
4347

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

4651
const hasAutoRedirectedRef = useRef(false);
4752

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

5762
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
58-
providers.find((provider) => provider.id === oauthAutoRedirect) !==
63+
providers.find((provider) => provider.id === oauth.autoRedirect) !==
5964
undefined && redirectUri !== undefined,
6065
);
6166

@@ -148,21 +153,47 @@ export const LoginPage = () => {
148153
},
149154
});
150155

156+
const { mutate: tailscaleMutate, isPending: tailscaleIsPending } =
157+
useMutation({
158+
mutationFn: () => axios.post("/api/user/tailscale"),
159+
mutationKey: ["tailscale"],
160+
onSuccess: () => {
161+
toast.success(t("loginSuccessTitle"), {
162+
description: t("loginTailscaleSuccess"),
163+
});
164+
165+
redirectTimer.current = window.setTimeout(() => {
166+
if (oidcParams.isOidc) {
167+
window.location.replace(`/authorize?${oidcParams.compiled}`);
168+
return;
169+
}
170+
window.location.replace(
171+
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
172+
);
173+
}, 500);
174+
},
175+
onError: () => {
176+
toast.error(t("loginFailTitle"), {
177+
description: t("loginTailscaleFail"),
178+
});
179+
},
180+
});
181+
151182
useEffect(() => {
152183
if (
153-
!isLoggedIn &&
184+
!auth.authenticated &&
154185
isOauthAutoRedirect &&
155186
!hasAutoRedirectedRef.current &&
156187
redirectUri !== undefined
157188
) {
158189
hasAutoRedirectedRef.current = true;
159-
oauthMutate(oauthAutoRedirect);
190+
oauthMutate(oauth.autoRedirect);
160191
}
161192
}, [
162-
isLoggedIn,
193+
auth.authenticated,
163194
oauthMutate,
164195
hasAutoRedirectedRef,
165-
oauthAutoRedirect,
196+
oauth.autoRedirect,
166197
isOauthAutoRedirect,
167198
redirectUri,
168199
]);
@@ -179,11 +210,11 @@ export const LoginPage = () => {
179210
};
180211
}, [redirectTimer, redirectButtonTimer]);
181212

182-
if (isLoggedIn && oidcParams.isOidc) {
213+
if (auth.authenticated && oidcParams.isOidc) {
183214
return <Navigate to={`/authorize?${oidcParams.compiled}`} replace />;
184215
}
185216

186-
if (isLoggedIn && redirectUri !== undefined) {
217+
if (auth.authenticated && redirectUri !== undefined) {
187218
return (
188219
<Navigate
189220
to={`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
@@ -192,7 +223,7 @@ export const LoginPage = () => {
192223
);
193224
}
194225

195-
if (isLoggedIn) {
226+
if (auth.authenticated) {
196227
return <Navigate to="/logout" replace />;
197228
}
198229

@@ -228,10 +259,49 @@ export const LoginPage = () => {
228259
</Card>
229260
);
230261
}
262+
263+
if (useTailscale) {
264+
return (
265+
<Card>
266+
<CardHeader className="gap-3">
267+
<TailscaleIcon className="mx-auto h-8 w-8" />
268+
<CardTitle className="text-center text-xl">
269+
{t("loginTailscaleTitle")}
270+
</CardTitle>
271+
</CardHeader>
272+
<CardContent className="flex flex-col gap-4">
273+
<div className="text-muted-foreground text-sm">
274+
{t("loginTailscaleDescription")}
275+
</div>
276+
<div className="text-muted-foreground text-sm">
277+
{t("loginTailscaleDeviceName")} <code>{tailscale.nodeName}</code>
278+
</div>
279+
</CardContent>
280+
<CardFooter className="flex flex-col items-stretch gap-3">
281+
<Button
282+
className="w-full"
283+
onClick={() => tailscaleMutate()}
284+
loading={tailscaleIsPending}
285+
>
286+
{t("loginTailscaleSubmit")}
287+
</Button>
288+
<Button
289+
className="w-full"
290+
variant="outline"
291+
onClick={() => setUseTailscale(false)}
292+
disabled={tailscaleIsPending}
293+
>
294+
{t("loginTailscaleOtherMethod")}
295+
</Button>
296+
</CardFooter>
297+
</Card>
298+
);
299+
}
300+
231301
return (
232302
<Card>
233303
<CardHeader className="gap-1.5">
234-
<CardTitle className="text-center text-xl">{title}</CardTitle>
304+
<CardTitle className="text-center text-xl">{ui.title}</CardTitle>
235305
{providers.length > 0 && (
236306
<CardDescription className="text-center">
237307
{oauthProviders.length !== 0

0 commit comments

Comments
 (0)