From ed97021c19e205c3a2d04e9c755d9dfe4404d708 Mon Sep 17 00:00:00 2001 From: Stavros Date: Thu, 11 Jun 2026 18:18:21 +0300 Subject: [PATCH 1/4] chore: merge oidc-authorize branch --- frontend/src/components/language/language.tsx | 36 - frontend/src/components/layout/layout.tsx | 8 +- .../quick-actions/quick-actions.tsx | 208 +++ .../components/theme-toggle/theme-toggle.tsx | 40 - frontend/src/components/ui/scroll-area.tsx | 56 + frontend/src/lib/hooks/login-for.ts | 17 + frontend/src/lib/hooks/oidc.ts | 76 -- frontend/src/lib/hooks/redirect-uri.ts | 4 +- frontend/src/lib/hooks/screen-params.ts | 40 + frontend/src/lib/i18n/locales/en-US.json | 195 +-- frontend/src/lib/i18n/locales/en.json | 195 +-- frontend/src/main.tsx | 5 +- frontend/src/pages/authorize-page.tsx | 72 +- frontend/src/pages/continue-page.tsx | 21 +- frontend/src/pages/forgot-password-page.tsx | 11 +- frontend/src/pages/login-page.tsx | 75 +- frontend/src/pages/logout-page.tsx | 13 +- frontend/src/pages/totp-page.tsx | 30 +- frontend/src/schemas/oidc-schemas.ts | 5 - frontend/vite.config.ts | 5 + go.mod | 1 + go.sum | 2 + internal/bootstrap/router_bootstrap.go | 2 +- internal/controller/controller.go | 10 +- internal/controller/oauth_controller.go | 14 +- internal/controller/oidc_controller.go | 245 +++- internal/controller/oidc_controller_test.go | 1139 ++++++++--------- internal/controller/proxy_controller.go | 1 + internal/controller/proxy_controller_test.go | 25 +- internal/middleware/ui_middleware.go | 2 +- internal/service/auth_service.go | 29 +- internal/service/oidc_service.go | 68 +- 32 files changed, 1475 insertions(+), 1175 deletions(-) delete mode 100644 frontend/src/components/language/language.tsx create mode 100644 frontend/src/components/quick-actions/quick-actions.tsx delete mode 100644 frontend/src/components/theme-toggle/theme-toggle.tsx create mode 100644 frontend/src/components/ui/scroll-area.tsx create mode 100644 frontend/src/lib/hooks/login-for.ts delete mode 100644 frontend/src/lib/hooks/oidc.ts create mode 100644 frontend/src/lib/hooks/screen-params.ts delete mode 100644 frontend/src/schemas/oidc-schemas.ts diff --git a/frontend/src/components/language/language.tsx b/frontend/src/components/language/language.tsx deleted file mode 100644 index 3f0bf57a..00000000 --- a/frontend/src/components/language/language.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { languages, SupportedLanguage } from "@/lib/i18n/locales"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "../ui/select"; -import { useState } from "react"; -import i18n from "@/lib/i18n/i18n"; - -export const LanguageSelector = () => { - const [language, setLanguage] = useState( - i18n.language as SupportedLanguage, - ); - - const handleSelect = (option: string) => { - setLanguage(option as SupportedLanguage); - i18n.changeLanguage(option as SupportedLanguage); - }; - - return ( - - ); -}; diff --git a/frontend/src/components/layout/layout.tsx b/frontend/src/components/layout/layout.tsx index d59aadf3..e129092e 100644 --- a/frontend/src/components/layout/layout.tsx +++ b/frontend/src/components/layout/layout.tsx @@ -1,9 +1,8 @@ import { useAppContext } from "@/context/app-context"; -import { LanguageSelector } from "../language/language"; import { Outlet } from "react-router"; import { useCallback, useEffect, useState } from "react"; import { DomainWarning } from "../domain-warning/domain-warning"; -import { ThemeToggle } from "../theme-toggle/theme-toggle"; +import { QuickActions } from "../quick-actions/quick-actions"; const BaseLayout = ({ children }: { children: React.ReactNode }) => { const { ui } = useAppContext(); @@ -21,9 +20,8 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => { backgroundPosition: "center", }} > -
- - +
+
{children}
diff --git a/frontend/src/components/quick-actions/quick-actions.tsx b/frontend/src/components/quick-actions/quick-actions.tsx new file mode 100644 index 00000000..1287ec6b --- /dev/null +++ b/frontend/src/components/quick-actions/quick-actions.tsx @@ -0,0 +1,208 @@ +import { languages, SupportedLanguage } from "@/lib/i18n/locales"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { useState } from "react"; +import i18n from "@/lib/i18n/i18n"; +import { useUserContext } from "@/context/user-context"; +import { ScrollArea } from "../ui/scroll-area"; +import { useTheme } from "../providers/theme-provider"; +import { + Check, + DoorOpenIcon, + Languages, + Monitor, + Moon, + Palette, + Settings, + Sun, +} from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { useLocation } from "react-router"; +import { useRef } from "react"; +import { + useScreenParams, + recompileScreenParams, +} from "@/lib/hooks/screen-params"; +import { useMutation } from "@tanstack/react-query"; +import axios from "axios"; +import { toast } from "sonner"; +import { useEffect } from "react"; + +function Avatar({ initial }: { initial: string }) { + return ( + + + + {initial} + + + ); +} + +export const QuickActions = () => { + const { auth } = useUserContext(); + const { theme, setTheme } = useTheme(); + const { t } = useTranslation(); + const { search } = useLocation(); + + const [language, setLanguage] = useState( + i18n.language as SupportedLanguage, + ); + + const redirectTimer = useRef(null); + const searchParams = new URLSearchParams(search); + const screenParams = useScreenParams(searchParams); + const compiledParams = recompileScreenParams(screenParams); + + const logoutMutation = useMutation({ + mutationFn: () => axios.post("/api/user/logout"), + mutationKey: ["logout"], + onSuccess: () => { + toast.success(t("logoutSuccessTitle"), { + description: t("logoutSuccessSubtitle"), + }); + + redirectTimer.current = window.setTimeout(() => { + window.location.replace(`/login${compiledParams}`); + }, 500); + }, + onError: () => { + toast.error(t("logoutFailTitle"), { + description: t("logoutFailSubtitle"), + }); + }, + }); + + useEffect(() => { + return () => { + if (redirectTimer.current) { + clearTimeout(redirectTimer.current); + } + }; + }, [redirectTimer]); + + const initial = auth.authenticated + ? (auth.name[0] || "U").toUpperCase() + : null; + + const handleSelect = (option: string) => { + setLanguage(option as SupportedLanguage); + i18n.changeLanguage(option as SupportedLanguage); + }; + + const themes = [ + { key: "light", label: t("quickActionsThemeLight"), icon: Sun }, + { key: "dark", label: t("quickActionsThemeDark"), icon: Moon }, + { key: "system", label: t("quickActionsThemeSystem"), icon: Monitor }, + ] as const; + + return ( + + + + + + + {auth.authenticated && ( + <> + +
+ {initial} +
+
+ + {auth.name} + + + {auth.email} + +
+
+ + + + )} + + + + + {t("quickActionsLanguage")} + + + + + {Object.entries(languages).map(([key, value]) => ( + handleSelect(key)} + > + {value} + {language === key && } + + ))} + + + + + + + + + {t("quickActionsTheme")} + + + + {themes.map(({ key, label, icon: Icon }) => ( + setTheme(key)}> + + + {label} + + {theme === key && } + + ))} + + + + + {auth.authenticated && ( + <> + + logoutMutation.mutate()} + className="text-destructive" + > + + {t("quickActionsLogout")} + + + )} +
+
+ ); +}; diff --git a/frontend/src/components/theme-toggle/theme-toggle.tsx b/frontend/src/components/theme-toggle/theme-toggle.tsx deleted file mode 100644 index c0791cfb..00000000 --- a/frontend/src/components/theme-toggle/theme-toggle.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Moon, Sun } from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { useTheme } from "@/components/providers/theme-provider"; - -export function ThemeToggle() { - const { setTheme } = useTheme(); - - return ( - - - - - - setTheme("light")}> - Light - - setTheme("dark")}> - Dark - - setTheme("system")}> - System - - - - ); -} diff --git a/frontend/src/components/ui/scroll-area.tsx b/frontend/src/components/ui/scroll-area.tsx new file mode 100644 index 00000000..e38a492f --- /dev/null +++ b/frontend/src/components/ui/scroll-area.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { ScrollArea as ScrollAreaPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/frontend/src/lib/hooks/login-for.ts b/frontend/src/lib/hooks/login-for.ts new file mode 100644 index 00000000..8cf11579 --- /dev/null +++ b/frontend/src/lib/hooks/login-for.ts @@ -0,0 +1,17 @@ +type UseLoginForProps = { + login_for?: "oidc" | "app"; + compiledParams: string; +}; + +export const useLoginFor = (props: UseLoginForProps): string => { + const { login_for, compiledParams } = props; + + switch (login_for) { + case "oidc": + return "/oidc/authorize" + compiledParams; + case "app": + return "/continue" + compiledParams; + default: + return "/logout"; + } +}; diff --git a/frontend/src/lib/hooks/oidc.ts b/frontend/src/lib/hooks/oidc.ts deleted file mode 100644 index 1341e8c2..00000000 --- a/frontend/src/lib/hooks/oidc.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { z } from "zod"; - -export const oidcParamsSchema = z.object({ - scope: z.string().min(1), - response_type: z.string().min(1), - client_id: z.string().min(1), - redirect_uri: z.string().min(1), - state: z.string().optional(), - nonce: z.string().optional(), - code_challenge: z.string().optional(), - code_challenge_method: z.string().optional(), -}); - -function b64urlDecode(s: string): string { - const base64 = s.replace(/-/g, "+").replace(/_/g, "/"); - return atob(base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=")); -} - -function decodeRequestObject(jwt: string): Record { - try { - // Must have exactly 3 parts: header, payload, signature - const parts = jwt.split("."); - if (parts.length !== 3) return {}; - - // Header must specify "alg": "none" and signature must be empty string - const header = JSON.parse(b64urlDecode(parts[0])); - if (!header || typeof header !== "object" || header.alg !== "none" || parts[2] !== "") return {}; - - const payload = JSON.parse(b64urlDecode(parts[1])); - if (!payload || typeof payload !== "object" || Array.isArray(payload)) return {}; - const result: Record = {}; - for (const [k, v] of Object.entries(payload)) { - if (typeof v === "string") result[k] = v; - } - return result; - } catch { - return {}; - } -} - -export const useOIDCParams = ( - params: URLSearchParams, -): { - values: z.infer; - issues: string[]; - isOidc: boolean; - compiled: string; -} => { - const obj = Object.fromEntries(params.entries()); - - // RFC 9101 / OIDC Core 6.1: if `request` param present, decode JWT payload - // and merge claims over top-level params (JWT claims take precedence) - const requestJwt = params.get("request"); - if (requestJwt) { - const claims = decodeRequestObject(requestJwt); - Object.assign(obj, claims); - } - - const parsed = oidcParamsSchema.safeParse(obj); - - if (parsed.success) { - return { - values: parsed.data, - issues: [], - isOidc: true, - compiled: new URLSearchParams(parsed.data).toString(), - }; - } - - return { - issues: parsed.error.issues.map((issue) => issue.path.toString()), - values: {} as z.infer, - isOidc: false, - compiled: "", - }; -}; diff --git a/frontend/src/lib/hooks/redirect-uri.ts b/frontend/src/lib/hooks/redirect-uri.ts index 5211178a..aeeae0c5 100644 --- a/frontend/src/lib/hooks/redirect-uri.ts +++ b/frontend/src/lib/hooks/redirect-uri.ts @@ -7,7 +7,7 @@ type IuseRedirectUri = { }; export const useRedirectUri = ( - redirect_uri: string | null, + redirect_uri: string | undefined, cookieDomain: string, ): IuseRedirectUri => { let isValid = false; @@ -15,7 +15,7 @@ export const useRedirectUri = ( let isAllowedProto = false; let isHttpsDowngrade = false; - if (!redirect_uri) { + if (redirect_uri === undefined) { return { valid: isValid, trusted: isTrusted, diff --git a/frontend/src/lib/hooks/screen-params.ts b/frontend/src/lib/hooks/screen-params.ts new file mode 100644 index 00000000..921fa4b6 --- /dev/null +++ b/frontend/src/lib/hooks/screen-params.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +type ScreenParams = { + login_for?: "oidc" | "app"; + redirect_uri?: string; + oidc_ticket?: string; + oidc_scope?: string; + oidc_name?: string; +}; + +const zodScreenParams = z.object({ + login_for: z.enum(["oidc", "app"]).optional(), + redirect_uri: z.string().optional(), + oidc_ticket: z.string().optional(), + oidc_scope: z.string().optional(), + oidc_name: z.string().optional(), +}); + +export function useScreenParams(params: URLSearchParams): ScreenParams { + const paramsObj = Object.fromEntries(params.entries()); + const parsed = zodScreenParams.safeParse(paramsObj); + if (!parsed.success) { + return {}; + } + return parsed.data; +} + +export function recompileScreenParams(params: ScreenParams): string { + const p = new URLSearchParams( + Object.fromEntries( + Object.entries(params).filter(([, v]) => v !== undefined), + ) as Record, + ).toString(); + + if (p.length > 0) { + return "?" + p; + } + + return ""; +} diff --git a/frontend/src/lib/i18n/locales/en-US.json b/frontend/src/lib/i18n/locales/en-US.json index a71696e2..dbe05c1a 100644 --- a/frontend/src/lib/i18n/locales/en-US.json +++ b/frontend/src/lib/i18n/locales/en-US.json @@ -1,96 +1,103 @@ { - "loginTitle": "Welcome back, login with", - "loginTitleSimple": "Welcome back, please login", - "loginDivider": "Or", - "loginUsername": "Username", - "loginPassword": "Password", - "loginSubmit": "Login", - "loginFailTitle": "Failed to log in", - "loginFailSubtitle": "Please check your username and password", - "loginFailRateLimit": "You failed to login too many times. Please try again later", - "loginSuccessTitle": "Logged in", - "loginSuccessSubtitle": "Welcome back!", - "loginOauthFailTitle": "An error occurred", - "loginOauthFailSubtitle": "Failed to get OAuth URL", - "loginOauthSuccessTitle": "Redirecting", - "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", - "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", - "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", - "loginOauthAutoRedirectButton": "Redirect now", - "continueTitle": "Continue", - "continueRedirectingTitle": "Redirecting...", - "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueRedirectManually": "Redirect me manually", - "continueInsecureRedirectTitle": "Insecure redirect", - "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueUntrustedRedirectTitle": "Untrusted redirect", - "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", - "logoutFailTitle": "Failed to log out", - "logoutFailSubtitle": "Please try again", - "logoutSuccessTitle": "Logged out", - "logoutSuccessSubtitle": "You have been logged out", - "logoutTitle": "Logout", - "logoutUsernameSubtitle": "You are currently logged in as {{username}}. Click the button below to logout.", - "logoutOauthSubtitle": "You are currently logged in as {{username}} using the {{provider}} OAuth provider. Click the button below to logout.", - "notFoundTitle": "Page not found", - "notFoundSubtitle": "The page you are looking for does not exist.", - "notFoundButton": "Go home", - "totpFailTitle": "Failed to verify code", - "totpFailSubtitle": "Please check your code and try again", - "totpSuccessTitle": "Verified", - "totpSuccessSubtitle": "Redirecting to your app", - "totpTitle": "Enter your TOTP code", - "totpSubtitle": "Please enter the code from your authenticator app.", - "unauthorizedTitle": "Unauthorized", - "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", - "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", - "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", - "unauthorizedButton": "Try again", - "cancelTitle": "Cancel", - "forgotPasswordTitle": "Forgot your password?", - "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", - "errorTitle": "An error occurred", - "errorSubtitleInfo": "The following error occurred while processing your request:", - "errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.", - "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", - "fieldRequired": "This field is required", - "invalidInput": "Invalid input", - "domainWarningTitle": "Invalid Domain", - "domainWarningSubtitle": "You are accessing this instance from an incorrect domain. If you proceed, you may encounter issues with authentication.", - "domainWarningCurrent": "Current:", - "domainWarningExpected": "Expected:", - "ignoreTitle": "Ignore", - "goToCorrectDomainTitle": "Go to correct domain", - "authorizeTitle": "Authorize", - "authorizeCardTitle": "Continue to {{app}}?", - "authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.", - "authorizeSubtitleOAuth": "Would you like to continue to this app?", - "authorizeLoadingTitle": "Loading...", - "authorizeLoadingSubtitle": "Please wait while we load the client information.", - "authorizeSuccessTitle": "Authorized", - "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", - "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", - "authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", - "openidScopeName": "OpenID Connect", - "openidScopeDescription": "Allows the app to access your OpenID Connect information.", - "emailScopeName": "Email", - "emailScopeDescription": "Allows the app to access your email address.", - "profileScopeName": "Profile", - "profileScopeDescription": "Allows the app to access your profile information.", - "groupsScopeName": "Groups", - "groupsScopeDescription": "Allows the app to access your group information.", - "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 {{deviceName}}. Click the button below to logout." + "loginTitle": "Welcome back, login with", + "loginTitleSimple": "Welcome back, please login", + "loginDivider": "Or", + "loginUsername": "Username", + "loginPassword": "Password", + "loginSubmit": "Login", + "loginFailTitle": "Failed to log in", + "loginFailSubtitle": "Please check your username and password", + "loginFailRateLimit": "You failed to login too many times. Please try again later", + "loginSuccessTitle": "Logged in", + "loginSuccessSubtitle": "Welcome back!", + "loginOauthFailTitle": "An error occurred", + "loginOauthFailSubtitle": "Failed to get OAuth URL", + "loginOauthSuccessTitle": "Redirecting", + "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", + "continueRedirectingTitle": "Redirecting...", + "continueRedirectingSubtitle": "You should be redirected to the app soon", + "continueRedirectManually": "Redirect me manually", + "continueInsecureRedirectTitle": "Insecure redirect", + "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", + "logoutFailTitle": "Failed to log out", + "logoutFailSubtitle": "Please try again", + "logoutSuccessTitle": "Logged out", + "logoutSuccessSubtitle": "You have been logged out", + "logoutTitle": "Logout", + "logoutUsernameSubtitle": "You are currently logged in as {{username}}. Click the button below to logout.", + "logoutOauthSubtitle": "You are currently logged in as {{username}} using the {{provider}} OAuth provider. Click the button below to logout.", + "notFoundTitle": "Page not found", + "notFoundSubtitle": "The page you are looking for does not exist.", + "notFoundButton": "Go home", + "totpFailTitle": "Failed to verify code", + "totpFailSubtitle": "Please check your code and try again", + "totpSuccessTitle": "Verified", + "totpSuccessSubtitle": "Redirecting to your app", + "totpTitle": "Enter your TOTP code", + "totpSubtitle": "Please enter the code from your authenticator app.", + "unauthorizedTitle": "Unauthorized", + "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", + "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", + "unauthorizedButton": "Try again", + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?", + "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", + "errorTitle": "An error occurred", + "errorSubtitleInfo": "The following error occurred while processing your request:", + "errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.", + "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", + "fieldRequired": "This field is required", + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "You are accessing this instance from an incorrect domain. If you proceed, you may encounter issues with authentication.", + "domainWarningCurrent": "Current:", + "domainWarningExpected": "Expected:", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain", + "authorizeTitle": "Authorize", + "authorizeCardTitle": "Continue to {{app}}?", + "authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.", + "authorizeSubtitleOAuth": "Would you like to continue to this app?", + "authorizeLoadingTitle": "Loading...", + "authorizeLoadingSubtitle": "Please wait while we load the client information.", + "authorizeSuccessTitle": "Authorized", + "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", + "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", + "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.", + "openidScopeName": "OpenID Connect", + "openidScopeDescription": "Allows the app to access your OpenID Connect information.", + "emailScopeName": "Email", + "emailScopeDescription": "Allows the app to access your email address.", + "profileScopeName": "Profile", + "profileScopeDescription": "Allows the app to access your profile information.", + "groupsScopeName": "Groups", + "groupsScopeDescription": "Allows the app to access your group information.", + "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 {{deviceName}}. Click the button below to logout.", + "quickActionsLanguage": "Language", + "quickActionsTheme": "Theme", + "quickActionsThemeLight": "Light", + "quickActionsThemeDark": "Dark", + "quickActionsThemeSystem": "System", + "quickActionsLogout": "Logout", + "quickActionsTitle": "Quick Actions" } diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index a71696e2..dbe05c1a 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -1,96 +1,103 @@ { - "loginTitle": "Welcome back, login with", - "loginTitleSimple": "Welcome back, please login", - "loginDivider": "Or", - "loginUsername": "Username", - "loginPassword": "Password", - "loginSubmit": "Login", - "loginFailTitle": "Failed to log in", - "loginFailSubtitle": "Please check your username and password", - "loginFailRateLimit": "You failed to login too many times. Please try again later", - "loginSuccessTitle": "Logged in", - "loginSuccessSubtitle": "Welcome back!", - "loginOauthFailTitle": "An error occurred", - "loginOauthFailSubtitle": "Failed to get OAuth URL", - "loginOauthSuccessTitle": "Redirecting", - "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", - "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", - "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", - "loginOauthAutoRedirectButton": "Redirect now", - "continueTitle": "Continue", - "continueRedirectingTitle": "Redirecting...", - "continueRedirectingSubtitle": "You should be redirected to the app soon", - "continueRedirectManually": "Redirect me manually", - "continueInsecureRedirectTitle": "Insecure redirect", - "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", - "continueUntrustedRedirectTitle": "Untrusted redirect", - "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", - "logoutFailTitle": "Failed to log out", - "logoutFailSubtitle": "Please try again", - "logoutSuccessTitle": "Logged out", - "logoutSuccessSubtitle": "You have been logged out", - "logoutTitle": "Logout", - "logoutUsernameSubtitle": "You are currently logged in as {{username}}. Click the button below to logout.", - "logoutOauthSubtitle": "You are currently logged in as {{username}} using the {{provider}} OAuth provider. Click the button below to logout.", - "notFoundTitle": "Page not found", - "notFoundSubtitle": "The page you are looking for does not exist.", - "notFoundButton": "Go home", - "totpFailTitle": "Failed to verify code", - "totpFailSubtitle": "Please check your code and try again", - "totpSuccessTitle": "Verified", - "totpSuccessSubtitle": "Redirecting to your app", - "totpTitle": "Enter your TOTP code", - "totpSubtitle": "Please enter the code from your authenticator app.", - "unauthorizedTitle": "Unauthorized", - "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", - "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", - "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", - "unauthorizedButton": "Try again", - "cancelTitle": "Cancel", - "forgotPasswordTitle": "Forgot your password?", - "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", - "errorTitle": "An error occurred", - "errorSubtitleInfo": "The following error occurred while processing your request:", - "errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.", - "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", - "fieldRequired": "This field is required", - "invalidInput": "Invalid input", - "domainWarningTitle": "Invalid Domain", - "domainWarningSubtitle": "You are accessing this instance from an incorrect domain. If you proceed, you may encounter issues with authentication.", - "domainWarningCurrent": "Current:", - "domainWarningExpected": "Expected:", - "ignoreTitle": "Ignore", - "goToCorrectDomainTitle": "Go to correct domain", - "authorizeTitle": "Authorize", - "authorizeCardTitle": "Continue to {{app}}?", - "authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.", - "authorizeSubtitleOAuth": "Would you like to continue to this app?", - "authorizeLoadingTitle": "Loading...", - "authorizeLoadingSubtitle": "Please wait while we load the client information.", - "authorizeSuccessTitle": "Authorized", - "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", - "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", - "authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}", - "openidScopeName": "OpenID Connect", - "openidScopeDescription": "Allows the app to access your OpenID Connect information.", - "emailScopeName": "Email", - "emailScopeDescription": "Allows the app to access your email address.", - "profileScopeName": "Profile", - "profileScopeDescription": "Allows the app to access your profile information.", - "groupsScopeName": "Groups", - "groupsScopeDescription": "Allows the app to access your group information.", - "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 {{deviceName}}. Click the button below to logout." + "loginTitle": "Welcome back, login with", + "loginTitleSimple": "Welcome back, please login", + "loginDivider": "Or", + "loginUsername": "Username", + "loginPassword": "Password", + "loginSubmit": "Login", + "loginFailTitle": "Failed to log in", + "loginFailSubtitle": "Please check your username and password", + "loginFailRateLimit": "You failed to login too many times. Please try again later", + "loginSuccessTitle": "Logged in", + "loginSuccessSubtitle": "Welcome back!", + "loginOauthFailTitle": "An error occurred", + "loginOauthFailSubtitle": "Failed to get OAuth URL", + "loginOauthSuccessTitle": "Redirecting", + "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", + "loginOauthAutoRedirectTitle": "OAuth Auto Redirect", + "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", + "loginOauthAutoRedirectButton": "Redirect now", + "continueTitle": "Continue", + "continueRedirectingTitle": "Redirecting...", + "continueRedirectingSubtitle": "You should be redirected to the app soon", + "continueRedirectManually": "Redirect me manually", + "continueInsecureRedirectTitle": "Insecure redirect", + "continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?", + "continueUntrustedRedirectTitle": "Untrusted redirect", + "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", + "logoutFailTitle": "Failed to log out", + "logoutFailSubtitle": "Please try again", + "logoutSuccessTitle": "Logged out", + "logoutSuccessSubtitle": "You have been logged out", + "logoutTitle": "Logout", + "logoutUsernameSubtitle": "You are currently logged in as {{username}}. Click the button below to logout.", + "logoutOauthSubtitle": "You are currently logged in as {{username}} using the {{provider}} OAuth provider. Click the button below to logout.", + "notFoundTitle": "Page not found", + "notFoundSubtitle": "The page you are looking for does not exist.", + "notFoundButton": "Go home", + "totpFailTitle": "Failed to verify code", + "totpFailSubtitle": "Please check your code and try again", + "totpSuccessTitle": "Verified", + "totpSuccessSubtitle": "Redirecting to your app", + "totpTitle": "Enter your TOTP code", + "totpSubtitle": "Please enter the code from your authenticator app.", + "unauthorizedTitle": "Unauthorized", + "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", + "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", + "unauthorizedButton": "Try again", + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?", + "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", + "errorTitle": "An error occurred", + "errorSubtitleInfo": "The following error occurred while processing your request:", + "errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.", + "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", + "fieldRequired": "This field is required", + "invalidInput": "Invalid input", + "domainWarningTitle": "Invalid Domain", + "domainWarningSubtitle": "You are accessing this instance from an incorrect domain. If you proceed, you may encounter issues with authentication.", + "domainWarningCurrent": "Current:", + "domainWarningExpected": "Expected:", + "ignoreTitle": "Ignore", + "goToCorrectDomainTitle": "Go to correct domain", + "authorizeTitle": "Authorize", + "authorizeCardTitle": "Continue to {{app}}?", + "authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.", + "authorizeSubtitleOAuth": "Would you like to continue to this app?", + "authorizeLoadingTitle": "Loading...", + "authorizeLoadingSubtitle": "Please wait while we load the client information.", + "authorizeSuccessTitle": "Authorized", + "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", + "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", + "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.", + "openidScopeName": "OpenID Connect", + "openidScopeDescription": "Allows the app to access your OpenID Connect information.", + "emailScopeName": "Email", + "emailScopeDescription": "Allows the app to access your email address.", + "profileScopeName": "Profile", + "profileScopeDescription": "Allows the app to access your profile information.", + "groupsScopeName": "Groups", + "groupsScopeDescription": "Allows the app to access your group information.", + "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 {{deviceName}}. Click the button below to logout.", + "quickActionsLanguage": "Language", + "quickActionsTheme": "Theme", + "quickActionsThemeLight": "Light", + "quickActionsThemeDark": "Dark", + "quickActionsThemeSystem": "System", + "quickActionsLogout": "Logout", + "quickActionsTitle": "Quick Actions" } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 29b3e475..4af686d5 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -35,7 +35,10 @@ createRoot(document.getElementById("root")!).render( } errorElement={}> } /> } /> - } /> + } + /> } /> } /> } /> diff --git a/frontend/src/pages/authorize-page.tsx b/frontend/src/pages/authorize-page.tsx index 91f8f9c9..3251c774 100644 --- a/frontend/src/pages/authorize-page.tsx +++ b/frontend/src/pages/authorize-page.tsx @@ -1,5 +1,5 @@ import { useUserContext } from "@/context/user-context"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation } from "@tanstack/react-query"; import { Navigate, useNavigate } from "react-router"; import { useLocation } from "react-router"; import { @@ -10,11 +10,9 @@ import { CardFooter, CardContent, } from "@/components/ui/card"; -import { getOidcClientInfoSchema } from "@/schemas/oidc-schemas"; import { Button } from "@/components/ui/button"; import axios from "axios"; import { toast } from "sonner"; -import { useOIDCParams } from "@/lib/hooks/oidc"; import { useTranslation } from "react-i18next"; import { TFunction } from "i18next"; import { Mail, MapPin, Phone, Shield, User, Users } from "lucide-react"; @@ -23,6 +21,10 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { + recompileScreenParams, + useScreenParams, +} from "@/lib/hooks/screen-params"; type Scope = { id: string; @@ -84,27 +86,17 @@ export const AuthorizePage = () => { const scopeMap = createScopeMap(t); const searchParams = new URLSearchParams(search); - const oidcParams = useOIDCParams(searchParams); - - const getClientInfo = useQuery({ - queryKey: ["client", oidcParams.values.client_id], - queryFn: async () => { - const res = await fetch( - `/api/oidc/clients/${encodeURIComponent(oidcParams.values.client_id)}`, - ); - const data = await getOidcClientInfoSchema.parseAsync(await res.json()); - return data; - }, - enabled: oidcParams.isOidc, - }); + const screenParams = useScreenParams(searchParams); + const isOidc = screenParams.login_for === "oidc"; + const compiledParams = recompileScreenParams(screenParams); const authorizeMutation = useMutation({ mutationFn: () => { - return axios.post("/api/oidc/authorize", { - ...oidcParams.values, + return axios.post("/api/oidc/authorize-complete", { + ticket: screenParams.oidc_ticket, }); }, - mutationKey: ["authorize", oidcParams.values.client_id], + mutationKey: ["authorize", screenParams.oidc_ticket], onSuccess: (data) => { toast.info(t("authorizeSuccessTitle"), { description: t("authorizeSuccessSubtitle"), @@ -118,56 +110,36 @@ export const AuthorizePage = () => { }, }); - if (oidcParams.issues.length > 0) { + if ( + !isOidc || + screenParams.oidc_ticket === undefined || + screenParams.oidc_scope === undefined + ) { return ( ); } if (!auth.authenticated) { - return ; - } - - if (getClientInfo.isLoading) { - return ( - - - - {t("authorizeLoadingTitle")} - - - - {t("authorizeLoadingSubtitle")} - - - ); - } - - if (getClientInfo.isError) { - return ( - - ); + return ; } const scopes = - oidcParams.values.scope.split(" ").filter((s) => s.trim() !== "") || []; + screenParams.oidc_scope.split(" ").filter((s) => s.trim() !== "") || []; return (
- {getClientInfo.data?.name.slice(0, 1) || "U"} + {screenParams.oidc_name ? screenParams.oidc_name.slice(0, 1) : "U"}
{t("authorizeCardTitle", { - app: getClientInfo.data?.name || "Unknown", + app: screenParams.oidc_name || "Unknown", })} @@ -206,7 +178,7 @@ export const AuthorizePage = () => { {t("authorizeTitle")}