mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 14:15:50 +00:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			v4.0.1-bet
			...
			feat/domai
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 95f8a95fd3 | ||
|   | c80c37ba69 | ||
|   | 0c18a64466 | ||
|   | 57dd8a6d81 | ||
|   | 4ef5eef167 | ||
|   | 3873bb279c | ||
|   | d9fda75d41 | ||
|   | 4093a91e12 | 
| @@ -95,7 +95,6 @@ func init() { | |||||||
| 		{"generic-user-url", "", "Generic OAuth user info URL."}, | 		{"generic-user-url", "", "Generic OAuth user info URL."}, | ||||||
| 		{"generic-name", "Generic", "Generic OAuth provider name."}, | 		{"generic-name", "Generic", "Generic OAuth provider name."}, | ||||||
| 		{"generic-skip-ssl", false, "Skip SSL verification for the generic OAuth provider."}, | 		{"generic-skip-ssl", false, "Skip SSL verification for the generic OAuth provider."}, | ||||||
| 		{"disable-continue", false, "Disable continue screen and redirect to app directly."}, |  | ||||||
| 		{"oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth."}, | 		{"oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth."}, | ||||||
| 		{"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"}, | 		{"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"}, | ||||||
| 		{"session-expiry", 86400, "Session (cookie) expiration time in seconds."}, | 		{"session-expiry", 86400, "Session (cookie) expiration time in seconds."}, | ||||||
|   | |||||||
| @@ -14,7 +14,6 @@ | |||||||
|         "axios": "^1.11.0", |         "axios": "^1.11.0", | ||||||
|         "class-variance-authority": "^0.7.1", |         "class-variance-authority": "^0.7.1", | ||||||
|         "clsx": "^2.1.1", |         "clsx": "^2.1.1", | ||||||
|         "dompurify": "^3.2.6", |  | ||||||
|         "i18next": "^25.4.2", |         "i18next": "^25.4.2", | ||||||
|         "i18next-browser-languagedetector": "^8.2.0", |         "i18next-browser-languagedetector": "^8.2.0", | ||||||
|         "i18next-resources-to-backend": "^1.2.1", |         "i18next-resources-to-backend": "^1.2.1", | ||||||
| @@ -364,8 +363,6 @@ | |||||||
|  |  | ||||||
|     "@types/react-dom": ["@types/react-dom@19.1.8", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ=="], |     "@types/react-dom": ["@types/react-dom@19.1.8", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ=="], | ||||||
|  |  | ||||||
|     "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], |  | ||||||
|  |  | ||||||
|     "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], |     "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.41.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/type-utils": "8.41.0", "@typescript-eslint/utils": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.41.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw=="], |     "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.41.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/type-utils": "8.41.0", "@typescript-eslint/utils": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.41.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw=="], | ||||||
| @@ -476,8 +473,6 @@ | |||||||
|  |  | ||||||
|     "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], |     "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], | ||||||
|  |  | ||||||
|     "dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="], |  | ||||||
|  |  | ||||||
|     "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], |     "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], | ||||||
|  |  | ||||||
|     "electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="], |     "electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="], | ||||||
|   | |||||||
| @@ -20,7 +20,6 @@ | |||||||
|     "axios": "^1.11.0", |     "axios": "^1.11.0", | ||||||
|     "class-variance-authority": "^0.7.1", |     "class-variance-authority": "^0.7.1", | ||||||
|     "clsx": "^2.1.1", |     "clsx": "^2.1.1", | ||||||
|     "dompurify": "^3.2.6", |  | ||||||
|     "i18next": "^25.4.2", |     "i18next": "^25.4.2", | ||||||
|     "i18next-browser-languagedetector": "^8.2.0", |     "i18next-browser-languagedetector": "^8.2.0", | ||||||
|     "i18next-resources-to-backend": "^1.2.1", |     "i18next-resources-to-backend": "^1.2.1", | ||||||
|   | |||||||
| @@ -5,8 +5,8 @@ export const App = () => { | |||||||
|   const { isLoggedIn } = useUserContext(); |   const { isLoggedIn } = useUserContext(); | ||||||
|  |  | ||||||
|   if (isLoggedIn) { |   if (isLoggedIn) { | ||||||
|     return <Navigate to="/logout" />; |     return <Navigate to="/logout" replace />; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return <Navigate to="/login" />; |   return <Navigate to="/login" replace />; | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										56
									
								
								frontend/src/components/domain-warning/domain-warning.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								frontend/src/components/domain-warning/domain-warning.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | import { | ||||||
|  |   Card, | ||||||
|  |   CardDescription, | ||||||
|  |   CardFooter, | ||||||
|  |   CardHeader, | ||||||
|  |   CardTitle, | ||||||
|  | } from "../ui/card"; | ||||||
|  | import { Button } from "../ui/button"; | ||||||
|  | import { Trans, useTranslation } from "react-i18next"; | ||||||
|  | import { useLocation } from "react-router"; | ||||||
|  |  | ||||||
|  | interface Props { | ||||||
|  |   onClick: () => void; | ||||||
|  |   appUrl: string; | ||||||
|  |   currentUrl: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const DomainWarning = (props: Props) => { | ||||||
|  |   const { onClick, appUrl, currentUrl } = props; | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const { search } = useLocation(); | ||||||
|  |  | ||||||
|  |   const searchParams = new URLSearchParams(search); | ||||||
|  |   const redirectUri = searchParams.get("redirect_uri"); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm"> | ||||||
|  |       <CardHeader> | ||||||
|  |         <CardTitle className="text-3xl">{t("domainWarningTitle")}</CardTitle> | ||||||
|  |         <CardDescription> | ||||||
|  |           <Trans | ||||||
|  |             t={t} | ||||||
|  |             i18nKey="domainWarningSubtitle" | ||||||
|  |             values={{ appUrl, currentUrl }} | ||||||
|  |             components={{ code: <code /> }} | ||||||
|  |           /> | ||||||
|  |         </CardDescription> | ||||||
|  |       </CardHeader> | ||||||
|  |       <CardFooter className="flex flex-col items-stretch gap-2"> | ||||||
|  |         <Button onClick={onClick} variant="warning"> | ||||||
|  |           {t("ignoreTitle")} | ||||||
|  |         </Button> | ||||||
|  |         <Button | ||||||
|  |           onClick={() => | ||||||
|  |             window.location.assign( | ||||||
|  |               `${appUrl}/login?redirect_uri=${encodeURIComponent(redirectUri || "")}`, | ||||||
|  |             ) | ||||||
|  |           } | ||||||
|  |           variant="outline" | ||||||
|  |         > | ||||||
|  |           {t("goToCorrectDomainTitle")} | ||||||
|  |         </Button> | ||||||
|  |       </CardFooter> | ||||||
|  |     </Card> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -1,8 +1,10 @@ | |||||||
| 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"; | import { Outlet } from "react-router"; | ||||||
|  | import { useCallback, useState } from "react"; | ||||||
|  | import { DomainWarning } from "../domain-warning/domain-warning"; | ||||||
|  |  | ||||||
| export const Layout = () => { | const BaseLayout = ({ children }: { children: React.ReactNode }) => { | ||||||
|   const { backgroundImage } = useAppContext(); |   const { backgroundImage } = useAppContext(); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
| @@ -15,7 +17,38 @@ export const Layout = () => { | |||||||
|       }} |       }} | ||||||
|     > |     > | ||||||
|       <LanguageSelector /> |       <LanguageSelector /> | ||||||
|       <Outlet /> |       {children} | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const Layout = () => { | ||||||
|  |   const { appUrl } = useAppContext(); | ||||||
|  |   const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => { | ||||||
|  |     return window.sessionStorage.getItem("ignoreDomainWarning") === "true"; | ||||||
|  |   }); | ||||||
|  |   const currentUrl = window.location.origin; | ||||||
|  |  | ||||||
|  |   const handleIgnore = useCallback(() => { | ||||||
|  |     window.sessionStorage.setItem("ignoreDomainWarning", "true"); | ||||||
|  |     setIgnoreDomainWarning(true); | ||||||
|  |   }, [setIgnoreDomainWarning]); | ||||||
|  |  | ||||||
|  |   if (!ignoreDomainWarning && appUrl !== currentUrl) { | ||||||
|  |     return ( | ||||||
|  |       <BaseLayout> | ||||||
|  |         <DomainWarning | ||||||
|  |           appUrl={appUrl} | ||||||
|  |           currentUrl={currentUrl} | ||||||
|  |           onClick={() => handleIgnore()} | ||||||
|  |         /> | ||||||
|  |       </BaseLayout> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <BaseLayout> | ||||||
|  |       <Outlet /> | ||||||
|  |     </BaseLayout> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ const buttonVariants = cva( | |||||||
|           "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", |           "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", | ||||||
|         link: "text-primary underline-offset-4 hover:underline", |         link: "text-primary underline-offset-4 hover:underline", | ||||||
|         warning: |         warning: | ||||||
|           "bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40 dark:bg-amber-600", |           "bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40", | ||||||
|       }, |       }, | ||||||
|       size: { |       size: { | ||||||
|         default: "h-9 px-4 py-2 has-[>svg]:px-3", |         default: "h-9 px-4 py-2 has-[>svg]:px-3", | ||||||
|   | |||||||
| @@ -156,7 +156,7 @@ ul { | |||||||
| } | } | ||||||
|  |  | ||||||
| code { | code { | ||||||
|   @apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold; |   @apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold break-all; | ||||||
| } | } | ||||||
|  |  | ||||||
| .lead { | .lead { | ||||||
|   | |||||||
| @@ -14,14 +14,14 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", | ||||||
|  |     "continueTitle": "Continue", | ||||||
|     "continueRedirectingTitle": "Redirecting...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{rootDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -44,8 +44,6 @@ | |||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
| @@ -53,5 +51,9 @@ | |||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", |     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", | ||||||
|     "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", |     "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", | ||||||
|     "fieldRequired": "This field is required", |     "fieldRequired": "This field is required", | ||||||
|     "invalidInput": "Invalid input" |     "invalidInput": "Invalid input", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -14,14 +14,14 @@ | |||||||
|     "loginOauthFailSubtitle": "Failed to get OAuth URL", |     "loginOauthFailSubtitle": "Failed to get OAuth URL", | ||||||
|     "loginOauthSuccessTitle": "Redirecting", |     "loginOauthSuccessTitle": "Redirecting", | ||||||
|     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", |     "loginOauthSuccessSubtitle": "Redirecting to your OAuth provider", | ||||||
|  |     "continueTitle": "Continue", | ||||||
|     "continueRedirectingTitle": "Redirecting...", |     "continueRedirectingTitle": "Redirecting...", | ||||||
|     "continueRedirectingSubtitle": "You should be redirected to the app soon", |     "continueRedirectingSubtitle": "You should be redirected to the app soon", | ||||||
|     "continueInvalidRedirectTitle": "Invalid redirect", |     "continueRedirectManually": "Redirect me manually", | ||||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", |  | ||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueTitle": "Continue", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueSubtitle": "Click the button to continue to your app.", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{rootDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
| @@ -44,8 +44,6 @@ | |||||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", |     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |  | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", |  | ||||||
|     "cancelTitle": "Cancel", |     "cancelTitle": "Cancel", | ||||||
|     "forgotPasswordTitle": "Forgot your password?", |     "forgotPasswordTitle": "Forgot your password?", | ||||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", |     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||||
| @@ -53,5 +51,9 @@ | |||||||
|     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", |     "errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.", | ||||||
|     "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", |     "forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.", | ||||||
|     "fieldRequired": "This field is required", |     "fieldRequired": "This field is required", | ||||||
|     "invalidInput": "Invalid input" |     "invalidInput": "Invalid input", | ||||||
|  |     "domainWarningTitle": "Invalid Domain", | ||||||
|  |     "domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.", | ||||||
|  |     "ignoreTitle": "Ignore", | ||||||
|  |     "goToCorrectDomainTitle": "Go to correct domain" | ||||||
| } | } | ||||||
| @@ -11,60 +11,101 @@ import { useUserContext } from "@/context/user-context"; | |||||||
| import { isValidUrl } from "@/lib/utils"; | 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 { useEffect, useState } from "react"; | ||||||
| import { useState } from "react"; |  | ||||||
|  |  | ||||||
| export const ContinuePage = () => { | export const ContinuePage = () => { | ||||||
|  |   const { rootDomain } = useAppContext(); | ||||||
|   const { isLoggedIn } = useUserContext(); |   const { isLoggedIn } = useUserContext(); | ||||||
|  |  | ||||||
|   if (!isLoggedIn) { |  | ||||||
|     return <Navigate to="/login" />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const { domain, disableContinue } = useAppContext(); |  | ||||||
|   const { search } = useLocation(); |   const { search } = useLocation(); | ||||||
|   const [loading, setLoading] = useState(false); |  | ||||||
|  |  | ||||||
|   const searchParams = new URLSearchParams(search); |  | ||||||
|   const redirectURI = searchParams.get("redirect_uri"); |  | ||||||
|  |  | ||||||
|   if (!redirectURI) { |  | ||||||
|     return <Navigate to="/logout" />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (!isValidUrl(DOMPurify.sanitize(redirectURI))) { |  | ||||||
|     return <Navigate to="/logout" />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const handleRedirect = () => { |  | ||||||
|     setLoading(true); |  | ||||||
|     window.location.href = DOMPurify.sanitize(redirectURI); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (disableContinue) { |  | ||||||
|     handleRedirect(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|  |  | ||||||
|   const url = new URL(redirectURI); |   const [loading, setLoading] = useState(false); | ||||||
|  |   const [showRedirectButton, setShowRedirectButton] = useState(false); | ||||||
|  |  | ||||||
|   if (!(url.hostname == domain) && !url.hostname.endsWith(`.${domain}`)) { |   const searchParams = new URLSearchParams(search); | ||||||
|  |   const redirectUri = searchParams.get("redirect_uri"); | ||||||
|  |  | ||||||
|  |   const isValidRedirectUri = | ||||||
|  |     redirectUri !== null ? isValidUrl(redirectUri) : false; | ||||||
|  |   const redirectUriObj = isValidRedirectUri | ||||||
|  |     ? new URL(redirectUri as string) | ||||||
|  |     : null; | ||||||
|  |   const isTrustedRedirectUri = | ||||||
|  |     redirectUriObj !== null | ||||||
|  |       ? redirectUriObj.hostname === rootDomain || | ||||||
|  |         redirectUriObj.hostname.endsWith(`.${rootDomain}`) | ||||||
|  |       : false; | ||||||
|  |   const isAllowedRedirectProto = | ||||||
|  |     redirectUriObj !== null | ||||||
|  |       ? redirectUriObj.protocol === "https:" || | ||||||
|  |         redirectUriObj.protocol === "http:" | ||||||
|  |       : false; | ||||||
|  |   const isHttpsDowngrade = | ||||||
|  |     redirectUriObj !== null | ||||||
|  |       ? redirectUriObj.protocol === "http:" && | ||||||
|  |         window.location.protocol === "https:" | ||||||
|  |       : false; | ||||||
|  |  | ||||||
|  |   const handleRedirect = () => { | ||||||
|  |     setLoading(true); | ||||||
|  |     window.location.assign(redirectUriObj!.toString()); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if ( | ||||||
|  |       !isLoggedIn || | ||||||
|  |       !isValidRedirectUri || | ||||||
|  |       !isTrustedRedirectUri || | ||||||
|  |       !isAllowedRedirectProto || | ||||||
|  |       isHttpsDowngrade | ||||||
|  |     ) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const auto = setTimeout(() => { | ||||||
|  |       handleRedirect(); | ||||||
|  |     }, 100); | ||||||
|  |  | ||||||
|  |     const reveal = setTimeout(() => { | ||||||
|  |       setLoading(false); | ||||||
|  |       setShowRedirectButton(true); | ||||||
|  |     }, 1000); | ||||||
|  |  | ||||||
|  |     return () => { | ||||||
|  |       clearTimeout(auto); | ||||||
|  |       clearTimeout(reveal); | ||||||
|  |     }; | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   if (!isLoggedIn) { | ||||||
|     return ( |     return ( | ||||||
|       <Card className="min-w-xs sm:min-w-sm"> |       <Navigate | ||||||
|  |         to={`/login?redirect_uri=${encodeURIComponent(redirectUri || "")}`} | ||||||
|  |         replace | ||||||
|  |       /> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!isValidRedirectUri || !isAllowedRedirectProto) { | ||||||
|  |     return <Navigate to="/logout" replace />; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!isTrustedRedirectUri) { | ||||||
|  |     return ( | ||||||
|  |       <Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm"> | ||||||
|         <CardHeader> |         <CardHeader> | ||||||
|           <CardTitle className="text-3xl"> |           <CardTitle className="text-3xl"> | ||||||
|             {t("untrustedRedirectTitle")} |             {t("continueUntrustedRedirectTitle")} | ||||||
|           </CardTitle> |           </CardTitle> | ||||||
|           <CardDescription> |           <CardDescription> | ||||||
|             <Trans |             <Trans | ||||||
|               i18nKey="untrustedRedirectSubtitle" |               i18nKey="continueUntrustedRedirectSubtitle" | ||||||
|               t={t} |               t={t} | ||||||
|               components={{ |               components={{ | ||||||
|                 code: <code />, |                 code: <code />, | ||||||
|               }} |               }} | ||||||
|               values={{ domain }} |               values={{ rootDomain }} | ||||||
|             /> |             /> | ||||||
|           </CardDescription> |           </CardDescription> | ||||||
|         </CardHeader> |         </CardHeader> | ||||||
| @@ -76,7 +117,11 @@ export const ContinuePage = () => { | |||||||
|           > |           > | ||||||
|             {t("continueTitle")} |             {t("continueTitle")} | ||||||
|           </Button> |           </Button> | ||||||
|           <Button onClick={() => navigate("/logout")} variant="outline" disabled={loading}> |           <Button | ||||||
|  |             onClick={() => navigate("/logout")} | ||||||
|  |             variant="outline" | ||||||
|  |             disabled={loading} | ||||||
|  |           > | ||||||
|             {t("cancelTitle")} |             {t("cancelTitle")} | ||||||
|           </Button> |           </Button> | ||||||
|         </CardFooter> |         </CardFooter> | ||||||
| @@ -84,9 +129,9 @@ export const ContinuePage = () => { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (url.protocol === "http:" && window.location.protocol === "https:") { |   if (isHttpsDowngrade) { | ||||||
|     return ( |     return ( | ||||||
|       <Card className="min-w-xs sm:min-w-sm"> |       <Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm"> | ||||||
|         <CardHeader> |         <CardHeader> | ||||||
|           <CardTitle className="text-3xl"> |           <CardTitle className="text-3xl"> | ||||||
|             {t("continueInsecureRedirectTitle")} |             {t("continueInsecureRedirectTitle")} | ||||||
| @@ -102,14 +147,14 @@ export const ContinuePage = () => { | |||||||
|           </CardDescription> |           </CardDescription> | ||||||
|         </CardHeader> |         </CardHeader> | ||||||
|         <CardFooter className="flex flex-col items-stretch gap-2"> |         <CardFooter className="flex flex-col items-stretch gap-2"> | ||||||
|           <Button |           <Button onClick={handleRedirect} loading={loading} variant="warning"> | ||||||
|             onClick={handleRedirect} |  | ||||||
|             loading={loading} |  | ||||||
|             variant="warning" |  | ||||||
|           > |  | ||||||
|             {t("continueTitle")} |             {t("continueTitle")} | ||||||
|           </Button> |           </Button> | ||||||
|           <Button onClick={() => navigate("/logout")} variant="outline" disabled={loading}> |           <Button | ||||||
|  |             onClick={() => navigate("/logout")} | ||||||
|  |             variant="outline" | ||||||
|  |             disabled={loading} | ||||||
|  |           > | ||||||
|             {t("cancelTitle")} |             {t("cancelTitle")} | ||||||
|           </Button> |           </Button> | ||||||
|         </CardFooter> |         </CardFooter> | ||||||
| @@ -120,17 +165,18 @@ export const ContinuePage = () => { | |||||||
|   return ( |   return ( | ||||||
|     <Card className="min-w-xs sm:min-w-sm"> |     <Card className="min-w-xs sm:min-w-sm"> | ||||||
|       <CardHeader> |       <CardHeader> | ||||||
|         <CardTitle className="text-3xl">{t("continueTitle")}</CardTitle> |         <CardTitle className="text-3xl"> | ||||||
|         <CardDescription>{t("continueSubtitle")}</CardDescription> |           {t("continueRedirectingTitle")} | ||||||
|  |         </CardTitle> | ||||||
|  |         <CardDescription>{t("continueRedirectingSubtitle")}</CardDescription> | ||||||
|       </CardHeader> |       </CardHeader> | ||||||
|       <CardFooter className="flex flex-col items-stretch"> |       {showRedirectButton && ( | ||||||
|         <Button |         <CardFooter className="flex flex-col items-stretch"> | ||||||
|           onClick={handleRedirect} |           <Button onClick={handleRedirect}> | ||||||
|           loading={loading} |             {t("continueRedirectManually")} | ||||||
|         > |           </Button> | ||||||
|           {t("continueTitle")} |         </CardFooter> | ||||||
|         </Button> |       )} | ||||||
|       </CardFooter> |  | ||||||
|     </Card> |     </Card> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -17,23 +17,21 @@ import { useIsMounted } from "@/lib/hooks/use-is-mounted"; | |||||||
| import { LoginSchema } from "@/schemas/login-schema"; | import { LoginSchema } from "@/schemas/login-schema"; | ||||||
| import { useMutation } from "@tanstack/react-query"; | import { useMutation } from "@tanstack/react-query"; | ||||||
| import axios, { AxiosError } from "axios"; | import axios, { AxiosError } from "axios"; | ||||||
| import { useEffect } from "react"; | import { useEffect, useRef } from "react"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import { Navigate, useLocation } from "react-router"; | import { Navigate, useLocation } from "react-router"; | ||||||
| import { toast } from "sonner"; | import { toast } from "sonner"; | ||||||
|  |  | ||||||
| export const LoginPage = () => { | export const LoginPage = () => { | ||||||
|   const { isLoggedIn } = useUserContext(); |   const { isLoggedIn } = useUserContext(); | ||||||
|  |   const { configuredProviders, title, oauthAutoRedirect, genericName } = | ||||||
|   if (isLoggedIn) { |     useAppContext(); | ||||||
|     return <Navigate to="/logout" />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const { configuredProviders, title, oauthAutoRedirect, genericName } = useAppContext(); |  | ||||||
|   const { search } = useLocation(); |   const { search } = useLocation(); | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const isMounted = useIsMounted(); |   const isMounted = useIsMounted(); | ||||||
|  |  | ||||||
|  |   const redirectTimer = useRef<number | null>(null); | ||||||
|  |  | ||||||
|   const searchParams = new URLSearchParams(search); |   const searchParams = new URLSearchParams(search); | ||||||
|   const redirectUri = searchParams.get("redirect_uri"); |   const redirectUri = searchParams.get("redirect_uri"); | ||||||
|  |  | ||||||
| @@ -53,8 +51,8 @@ export const LoginPage = () => { | |||||||
|         description: t("loginOauthSuccessSubtitle"), |         description: t("loginOauthSuccessSubtitle"), | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       setTimeout(() => { |       redirectTimer.current = window.setTimeout(() => { | ||||||
|         window.location.href = data.data.url; |         window.location.replace(data.data.url); | ||||||
|       }, 500); |       }, 500); | ||||||
|     }, |     }, | ||||||
|     onError: () => { |     onError: () => { | ||||||
| @@ -79,7 +77,7 @@ export const LoginPage = () => { | |||||||
|         description: t("loginSuccessSubtitle"), |         description: t("loginSuccessSubtitle"), | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       setTimeout(() => { |       redirectTimer.current = window.setTimeout(() => { | ||||||
|         window.location.replace( |         window.location.replace( | ||||||
|           `/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`, |           `/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`, | ||||||
|         ); |         ); | ||||||
| @@ -100,6 +98,7 @@ export const LoginPage = () => { | |||||||
|       if ( |       if ( | ||||||
|         oauthConfigured && |         oauthConfigured && | ||||||
|         configuredProviders.includes(oauthAutoRedirect) && |         configuredProviders.includes(oauthAutoRedirect) && | ||||||
|  |         !isLoggedIn && | ||||||
|         redirectUri |         redirectUri | ||||||
|       ) { |       ) { | ||||||
|         oauthMutation.mutate(oauthAutoRedirect); |         oauthMutation.mutate(oauthAutoRedirect); | ||||||
| @@ -107,6 +106,26 @@ export const LoginPage = () => { | |||||||
|     } |     } | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|  |   useEffect( | ||||||
|  |     () => () => { | ||||||
|  |       if (redirectTimer.current) clearTimeout(redirectTimer.current); | ||||||
|  |     }, | ||||||
|  |     [], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   if (isLoggedIn && redirectUri) { | ||||||
|  |     return ( | ||||||
|  |       <Navigate | ||||||
|  |         to={`/continue?redirect_uri=${encodeURIComponent(redirectUri)}`} | ||||||
|  |         replace | ||||||
|  |       /> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (isLoggedIn) { | ||||||
|  |     return <Navigate to="/logout" replace />; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Card className="min-w-xs sm:min-w-sm"> |     <Card className="min-w-xs sm:min-w-sm"> | ||||||
|       <CardHeader> |       <CardHeader> | ||||||
| @@ -126,7 +145,10 @@ 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"} |                 loading={ | ||||||
|  |                   oauthMutation.isPending && | ||||||
|  |                   oauthMutation.variables === "google" | ||||||
|  |                 } | ||||||
|                 disabled={oauthMutation.isPending || loginMutation.isPending} |                 disabled={oauthMutation.isPending || loginMutation.isPending} | ||||||
|               /> |               /> | ||||||
|             )} |             )} | ||||||
| @@ -136,7 +158,10 @@ 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"} |                 loading={ | ||||||
|  |                   oauthMutation.isPending && | ||||||
|  |                   oauthMutation.variables === "github" | ||||||
|  |                 } | ||||||
|                 disabled={oauthMutation.isPending || loginMutation.isPending} |                 disabled={oauthMutation.isPending || loginMutation.isPending} | ||||||
|               /> |               /> | ||||||
|             )} |             )} | ||||||
| @@ -146,7 +171,10 @@ 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"} |                 loading={ | ||||||
|  |                   oauthMutation.isPending && | ||||||
|  |                   oauthMutation.variables === "generic" | ||||||
|  |                 } | ||||||
|                 disabled={oauthMutation.isPending || loginMutation.isPending} |                 disabled={oauthMutation.isPending || loginMutation.isPending} | ||||||
|               /> |               /> | ||||||
|             )} |             )} | ||||||
|   | |||||||
| @@ -11,20 +11,18 @@ import { useUserContext } from "@/context/user-context"; | |||||||
| import { capitalize } from "@/lib/utils"; | import { capitalize } from "@/lib/utils"; | ||||||
| import { useMutation } from "@tanstack/react-query"; | import { useMutation } from "@tanstack/react-query"; | ||||||
| import axios from "axios"; | import axios from "axios"; | ||||||
|  | import { useEffect, useRef } from "react"; | ||||||
| import { Trans, useTranslation } from "react-i18next"; | import { Trans, useTranslation } from "react-i18next"; | ||||||
| import { Navigate } from "react-router"; | import { Navigate } from "react-router"; | ||||||
| import { toast } from "sonner"; | import { toast } from "sonner"; | ||||||
|  |  | ||||||
| export const LogoutPage = () => { | export const LogoutPage = () => { | ||||||
|   const { provider, username, isLoggedIn, email } = useUserContext(); |   const { provider, username, isLoggedIn, email } = useUserContext(); | ||||||
|  |  | ||||||
|   if (!isLoggedIn) { |  | ||||||
|     return <Navigate to="/login" />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const { genericName } = useAppContext(); |   const { genericName } = useAppContext(); | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |  | ||||||
|  |   const redirectTimer = useRef<number | null>(null); | ||||||
|  |  | ||||||
|   const logoutMutation = useMutation({ |   const logoutMutation = useMutation({ | ||||||
|     mutationFn: () => axios.post("/api/user/logout"), |     mutationFn: () => axios.post("/api/user/logout"), | ||||||
|     mutationKey: ["logout"], |     mutationKey: ["logout"], | ||||||
| @@ -33,8 +31,8 @@ export const LogoutPage = () => { | |||||||
|         description: t("logoutSuccessSubtitle"), |         description: t("logoutSuccessSubtitle"), | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       setTimeout(async () => { |       redirectTimer.current = window.setTimeout(() => { | ||||||
|         window.location.replace("/login"); |         window.location.assign("/login"); | ||||||
|       }, 500); |       }, 500); | ||||||
|     }, |     }, | ||||||
|     onError: () => { |     onError: () => { | ||||||
| @@ -44,6 +42,17 @@ export const LogoutPage = () => { | |||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   useEffect( | ||||||
|  |     () => () => { | ||||||
|  |       if (redirectTimer.current) clearTimeout(redirectTimer.current); | ||||||
|  |     }, | ||||||
|  |     [], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   if (!isLoggedIn) { | ||||||
|  |     return <Navigate to="/login" replace />; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Card className="min-w-xs sm:min-w-sm"> |     <Card className="min-w-xs sm:min-w-sm"> | ||||||
|       <CardHeader> |       <CardHeader> | ||||||
|   | |||||||
| @@ -12,22 +12,19 @@ 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 { useEffect, useId, useRef } from "react"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import { Navigate, useLocation } 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(); |   const { totpPending } = useUserContext(); | ||||||
|  |  | ||||||
|   if (!totpPending) { |  | ||||||
|     return <Navigate to="/" />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { search } = useLocation(); |   const { search } = useLocation(); | ||||||
|   const formId = useId(); |   const formId = useId(); | ||||||
|  |  | ||||||
|  |   const redirectTimer = useRef<number | null>(null); | ||||||
|  |  | ||||||
|   const searchParams = new URLSearchParams(search); |   const searchParams = new URLSearchParams(search); | ||||||
|   const redirectUri = searchParams.get("redirect_uri"); |   const redirectUri = searchParams.get("redirect_uri"); | ||||||
|  |  | ||||||
| @@ -39,7 +36,7 @@ export const TotpPage = () => { | |||||||
|         description: t("totpSuccessSubtitle"), |         description: t("totpSuccessSubtitle"), | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       setTimeout(() => { |       redirectTimer.current = window.setTimeout(() => { | ||||||
|         window.location.replace( |         window.location.replace( | ||||||
|           `/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`, |           `/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`, | ||||||
|         ); |         ); | ||||||
| @@ -52,6 +49,17 @@ export const TotpPage = () => { | |||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   useEffect( | ||||||
|  |     () => () => { | ||||||
|  |       if (redirectTimer.current) clearTimeout(redirectTimer.current); | ||||||
|  |     }, | ||||||
|  |     [], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   if (!totpPending) { | ||||||
|  |     return <Navigate to="/" replace />; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Card className="min-w-xs sm:min-w-sm"> |     <Card className="min-w-xs sm:min-w-sm"> | ||||||
|       <CardHeader> |       <CardHeader> | ||||||
|   | |||||||
| @@ -12,6 +12,10 @@ import { Navigate, useLocation, useNavigate } from "react-router"; | |||||||
|  |  | ||||||
| export const UnauthorizedPage = () => { | export const UnauthorizedPage = () => { | ||||||
|   const { search } = useLocation(); |   const { search } = useLocation(); | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const navigate = useNavigate(); | ||||||
|  |  | ||||||
|  |   const [loading, setLoading] = useState(false); | ||||||
|  |  | ||||||
|   const searchParams = new URLSearchParams(search); |   const searchParams = new URLSearchParams(search); | ||||||
|   const username = searchParams.get("username"); |   const username = searchParams.get("username"); | ||||||
| @@ -19,19 +23,15 @@ export const UnauthorizedPage = () => { | |||||||
|   const groupErr = searchParams.get("groupErr"); |   const groupErr = searchParams.get("groupErr"); | ||||||
|   const ip = searchParams.get("ip"); |   const ip = searchParams.get("ip"); | ||||||
|  |  | ||||||
|   if (!username && !ip) { |  | ||||||
|     return <Navigate to="/" />; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const navigate = useNavigate(); |  | ||||||
|   const [loading, setLoading] = useState(false); |  | ||||||
|  |  | ||||||
|   const handleRedirect = () => { |   const handleRedirect = () => { | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     navigate("/login"); |     navigate("/login"); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   if (!username && !ip) { | ||||||
|  |     return <Navigate to="/" />; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   let i18nKey = "unauthorizedLoginSubtitle"; |   let i18nKey = "unauthorizedLoginSubtitle"; | ||||||
|  |  | ||||||
|   if (resource) { |   if (resource) { | ||||||
|   | |||||||
| @@ -2,10 +2,10 @@ import { z } from "zod"; | |||||||
|  |  | ||||||
| export const appContextSchema = z.object({ | export const appContextSchema = z.object({ | ||||||
|   configuredProviders: z.array(z.string()), |   configuredProviders: z.array(z.string()), | ||||||
|   disableContinue: z.boolean(), |  | ||||||
|   title: z.string(), |   title: z.string(), | ||||||
|   genericName: z.string(), |   genericName: z.string(), | ||||||
|   domain: z.string(), |   appUrl: z.string(), | ||||||
|  |   rootDomain: z.string(), | ||||||
|   forgotPasswordMessage: z.string(), |   forgotPasswordMessage: z.string(), | ||||||
|   oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]), |   oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]), | ||||||
|   backgroundImage: z.string(), |   backgroundImage: z.string(), | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package bootstrap | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"tinyauth/internal/config" | 	"tinyauth/internal/config" | ||||||
| 	"tinyauth/internal/controller" | 	"tinyauth/internal/controller" | ||||||
| @@ -44,15 +45,16 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Get domain | 	// Get root domain | ||||||
| 	domain, err := utils.GetUpperDomain(app.Config.AppURL) | 	rootDomain, err := utils.GetRootDomain(app.Config.AppURL) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Cookie names | 	// Cookie names | ||||||
| 	cookieId := utils.GenerateIdentifier(strings.Split(domain, ".")[0]) | 	appUrl, _ := url.Parse(app.Config.AppURL) // Already validated | ||||||
|  | 	cookieId := utils.GenerateIdentifier(appUrl.Hostname()) | ||||||
| 	sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId) | 	sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId) | ||||||
| 	csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId) | 	csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId) | ||||||
| 	redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId) | 	redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId) | ||||||
| @@ -63,7 +65,7 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 		OauthWhitelist:    app.Config.OAuthWhitelist, | 		OauthWhitelist:    app.Config.OAuthWhitelist, | ||||||
| 		SessionExpiry:     app.Config.SessionExpiry, | 		SessionExpiry:     app.Config.SessionExpiry, | ||||||
| 		SecureCookie:      app.Config.SecureCookie, | 		SecureCookie:      app.Config.SecureCookie, | ||||||
| 		Domain:            domain, | 		RootDomain:        rootDomain, | ||||||
| 		LoginTimeout:      app.Config.LoginTimeout, | 		LoginTimeout:      app.Config.LoginTimeout, | ||||||
| 		LoginMaxRetries:   app.Config.LoginMaxRetries, | 		LoginMaxRetries:   app.Config.LoginMaxRetries, | ||||||
| 		SessionCookieName: sessionCookieName, | 		SessionCookieName: sessionCookieName, | ||||||
| @@ -153,7 +155,7 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 	var middlewares []Middleware | 	var middlewares []Middleware | ||||||
|  |  | ||||||
| 	contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{ | 	contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{ | ||||||
| 		Domain: domain, | 		RootDomain: rootDomain, | ||||||
| 	}, authService, oauthBrokerService) | 	}, authService, oauthBrokerService) | ||||||
|  |  | ||||||
| 	uiMiddleware := middleware.NewUIMiddleware() | 	uiMiddleware := middleware.NewUIMiddleware() | ||||||
| @@ -177,10 +179,10 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 	// Create controllers | 	// Create controllers | ||||||
| 	contextController := controller.NewContextController(controller.ContextControllerConfig{ | 	contextController := controller.NewContextController(controller.ContextControllerConfig{ | ||||||
| 		ConfiguredProviders:   configuredProviders, | 		ConfiguredProviders:   configuredProviders, | ||||||
| 		DisableContinue:       app.Config.DisableContinue, |  | ||||||
| 		Title:                 app.Config.Title, | 		Title:                 app.Config.Title, | ||||||
| 		GenericName:           app.Config.GenericName, | 		GenericName:           app.Config.GenericName, | ||||||
| 		Domain:                domain, | 		AppURL:                app.Config.AppURL, | ||||||
|  | 		RootDomain:            rootDomain, | ||||||
| 		ForgotPasswordMessage: app.Config.ForgotPasswordMessage, | 		ForgotPasswordMessage: app.Config.ForgotPasswordMessage, | ||||||
| 		BackgroundImage:       app.Config.BackgroundImage, | 		BackgroundImage:       app.Config.BackgroundImage, | ||||||
| 		OAuthAutoRedirect:     app.Config.OAuthAutoRedirect, | 		OAuthAutoRedirect:     app.Config.OAuthAutoRedirect, | ||||||
| @@ -191,7 +193,7 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 		SecureCookie:       app.Config.SecureCookie, | 		SecureCookie:       app.Config.SecureCookie, | ||||||
| 		CSRFCookieName:     csrfCookieName, | 		CSRFCookieName:     csrfCookieName, | ||||||
| 		RedirectCookieName: redirectCookieName, | 		RedirectCookieName: redirectCookieName, | ||||||
| 		Domain:             domain, | 		RootDomain:         rootDomain, | ||||||
| 	}, apiRouter, authService, oauthBrokerService) | 	}, apiRouter, authService, oauthBrokerService) | ||||||
|  |  | ||||||
| 	proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ | 	proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ | ||||||
| @@ -199,7 +201,7 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 	}, apiRouter, dockerService, authService) | 	}, apiRouter, dockerService, authService) | ||||||
|  |  | ||||||
| 	userController := controller.NewUserController(controller.UserControllerConfig{ | 	userController := controller.NewUserController(controller.UserControllerConfig{ | ||||||
| 		Domain: domain, | 		RootDomain: rootDomain, | ||||||
| 	}, apiRouter, authService) | 	}, apiRouter, authService) | ||||||
|  |  | ||||||
| 	resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{ | 	resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{ | ||||||
|   | |||||||
| @@ -36,7 +36,6 @@ type Config struct { | |||||||
| 	GenericUserURL          string `mapstructure:"generic-user-url"` | 	GenericUserURL          string `mapstructure:"generic-user-url"` | ||||||
| 	GenericName             string `mapstructure:"generic-name"` | 	GenericName             string `mapstructure:"generic-name"` | ||||||
| 	GenericSkipSSL          bool   `mapstructure:"generic-skip-ssl"` | 	GenericSkipSSL          bool   `mapstructure:"generic-skip-ssl"` | ||||||
| 	DisableContinue         bool   `mapstructure:"disable-continue"` |  | ||||||
| 	OAuthWhitelist          string `mapstructure:"oauth-whitelist"` | 	OAuthWhitelist          string `mapstructure:"oauth-whitelist"` | ||||||
| 	OAuthAutoRedirect       string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` | 	OAuthAutoRedirect       string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` | ||||||
| 	SessionExpiry           int    `mapstructure:"session-expiry"` | 	SessionExpiry           int    `mapstructure:"session-expiry"` | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| package controller | package controller | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
| 	"tinyauth/internal/utils" | 	"tinyauth/internal/utils" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| @@ -15,7 +17,7 @@ type UserContextResponse struct { | |||||||
| 	Name        string `json:"name"` | 	Name        string `json:"name"` | ||||||
| 	Email       string `json:"email"` | 	Email       string `json:"email"` | ||||||
| 	Provider    string `json:"provider"` | 	Provider    string `json:"provider"` | ||||||
| 	Oauth       bool   `json:"oauth"` | 	OAuth       bool   `json:"oauth"` | ||||||
| 	TotpPending bool   `json:"totpPending"` | 	TotpPending bool   `json:"totpPending"` | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -23,10 +25,10 @@ type AppContextResponse struct { | |||||||
| 	Status                int      `json:"status"` | 	Status                int      `json:"status"` | ||||||
| 	Message               string   `json:"message"` | 	Message               string   `json:"message"` | ||||||
| 	ConfiguredProviders   []string `json:"configuredProviders"` | 	ConfiguredProviders   []string `json:"configuredProviders"` | ||||||
| 	DisableContinue       bool     `json:"disableContinue"` |  | ||||||
| 	Title                 string   `json:"title"` | 	Title                 string   `json:"title"` | ||||||
| 	GenericName           string   `json:"genericName"` | 	GenericName           string   `json:"genericName"` | ||||||
| 	Domain                string   `json:"domain"` | 	AppURL                string   `json:"appUrl"` | ||||||
|  | 	RootDomain            string   `json:"rootDomain"` | ||||||
| 	ForgotPasswordMessage string   `json:"forgotPasswordMessage"` | 	ForgotPasswordMessage string   `json:"forgotPasswordMessage"` | ||||||
| 	BackgroundImage       string   `json:"backgroundImage"` | 	BackgroundImage       string   `json:"backgroundImage"` | ||||||
| 	OAuthAutoRedirect     string   `json:"oauthAutoRedirect"` | 	OAuthAutoRedirect     string   `json:"oauthAutoRedirect"` | ||||||
| @@ -34,10 +36,10 @@ type AppContextResponse struct { | |||||||
|  |  | ||||||
| type ContextControllerConfig struct { | type ContextControllerConfig struct { | ||||||
| 	ConfiguredProviders   []string | 	ConfiguredProviders   []string | ||||||
| 	DisableContinue       bool |  | ||||||
| 	Title                 string | 	Title                 string | ||||||
| 	GenericName           string | 	GenericName           string | ||||||
| 	Domain                string | 	AppURL                string | ||||||
|  | 	RootDomain            string | ||||||
| 	ForgotPasswordMessage string | 	ForgotPasswordMessage string | ||||||
| 	BackgroundImage       string | 	BackgroundImage       string | ||||||
| 	OAuthAutoRedirect     string | 	OAuthAutoRedirect     string | ||||||
| @@ -72,7 +74,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) { | |||||||
| 		Name:        context.Name, | 		Name:        context.Name, | ||||||
| 		Email:       context.Email, | 		Email:       context.Email, | ||||||
| 		Provider:    context.Provider, | 		Provider:    context.Provider, | ||||||
| 		Oauth:       context.OAuth, | 		OAuth:       context.OAuth, | ||||||
| 		TotpPending: context.TotpPending, | 		TotpPending: context.TotpPending, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -89,14 +91,16 @@ func (controller *ContextController) userContextHandler(c *gin.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (controller *ContextController) appContextHandler(c *gin.Context) { | func (controller *ContextController) appContextHandler(c *gin.Context) { | ||||||
|  | 	appUrl, _ := url.Parse(controller.Config.AppURL) // no need to check error, validated on startup | ||||||
|  |  | ||||||
| 	c.JSON(200, AppContextResponse{ | 	c.JSON(200, AppContextResponse{ | ||||||
| 		Status:                200, | 		Status:                200, | ||||||
| 		Message:               "Success", | 		Message:               "Success", | ||||||
| 		ConfiguredProviders:   controller.Config.ConfiguredProviders, | 		ConfiguredProviders:   controller.Config.ConfiguredProviders, | ||||||
| 		DisableContinue:       controller.Config.DisableContinue, |  | ||||||
| 		Title:                 controller.Config.Title, | 		Title:                 controller.Config.Title, | ||||||
| 		GenericName:           controller.Config.GenericName, | 		GenericName:           controller.Config.GenericName, | ||||||
| 		Domain:                controller.Config.Domain, | 		AppURL:                fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host), | ||||||
|  | 		RootDomain:            controller.Config.RootDomain, | ||||||
| 		ForgotPasswordMessage: controller.Config.ForgotPasswordMessage, | 		ForgotPasswordMessage: controller.Config.ForgotPasswordMessage, | ||||||
| 		BackgroundImage:       controller.Config.BackgroundImage, | 		BackgroundImage:       controller.Config.BackgroundImage, | ||||||
| 		OAuthAutoRedirect:     controller.Config.OAuthAutoRedirect, | 		OAuthAutoRedirect:     controller.Config.OAuthAutoRedirect, | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ type OAuthControllerConfig struct { | |||||||
| 	RedirectCookieName string | 	RedirectCookieName string | ||||||
| 	SecureCookie       bool | 	SecureCookie       bool | ||||||
| 	AppURL             string | 	AppURL             string | ||||||
| 	Domain             string | 	RootDomain         string | ||||||
| } | } | ||||||
|  |  | ||||||
| type OAuthController struct { | type OAuthController struct { | ||||||
| @@ -74,13 +74,13 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) { | |||||||
|  |  | ||||||
| 	state := service.GenerateState() | 	state := service.GenerateState() | ||||||
| 	authURL := service.GetAuthURL(state) | 	authURL := service.GetAuthURL(state) | ||||||
| 	c.SetCookie(controller.Config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true) | 	c.SetCookie(controller.Config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.RootDomain), controller.Config.SecureCookie, true) | ||||||
|  |  | ||||||
| 	redirectURI := c.Query("redirect_uri") | 	redirectURI := c.Query("redirect_uri") | ||||||
|  |  | ||||||
| 	if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.Config.Domain) { | 	if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.Config.RootDomain) { | ||||||
| 		log.Debug().Msg("Setting redirect URI cookie") | 		log.Debug().Msg("Setting redirect URI cookie") | ||||||
| 		c.SetCookie(controller.Config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true) | 		c.SetCookie(controller.Config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.Config.RootDomain), controller.Config.SecureCookie, true) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.JSON(200, gin.H{ | 	c.JSON(200, gin.H{ | ||||||
| @@ -112,7 +112,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.SetCookie(controller.Config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true) | 	c.SetCookie(controller.Config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.RootDomain), controller.Config.SecureCookie, true) | ||||||
|  |  | ||||||
| 	code := c.Query("code") | 	code := c.Query("code") | ||||||
| 	service, exists := controller.Broker.GetService(req.Provider) | 	service, exists := controller.Broker.GetService(req.Provider) | ||||||
| @@ -189,7 +189,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { | |||||||
|  |  | ||||||
| 	redirectURI, err := c.Cookie(controller.Config.RedirectCookieName) | 	redirectURI, err := c.Cookie(controller.Config.RedirectCookieName) | ||||||
|  |  | ||||||
| 	if err != nil || !utils.IsRedirectSafe(redirectURI, controller.Config.Domain) { | 	if err != nil || !utils.IsRedirectSafe(redirectURI, controller.Config.RootDomain) { | ||||||
| 		log.Debug().Msg("No redirect URI cookie found, redirecting to app root") | 		log.Debug().Msg("No redirect URI cookie found, redirecting to app root") | ||||||
| 		c.Redirect(http.StatusTemporaryRedirect, controller.Config.AppURL) | 		c.Redirect(http.StatusTemporaryRedirect, controller.Config.AppURL) | ||||||
| 		return | 		return | ||||||
| @@ -205,6 +205,6 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.SetCookie(controller.Config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.Domain), controller.Config.SecureCookie, true) | 	c.SetCookie(controller.Config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.Config.RootDomain), controller.Config.SecureCookie, true) | ||||||
| 	c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.Config.AppURL, queries.Encode())) | 	c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.Config.AppURL, queries.Encode())) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ type TotpRequest struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| type UserControllerConfig struct { | type UserControllerConfig struct { | ||||||
| 	Domain string | 	RootDomain string | ||||||
| } | } | ||||||
|  |  | ||||||
| type UserController struct { | type UserController struct { | ||||||
| @@ -115,7 +115,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { | |||||||
| 			err := controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ | 			err := controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ | ||||||
| 				Username:    user.Username, | 				Username:    user.Username, | ||||||
| 				Name:        utils.Capitalize(req.Username), | 				Name:        utils.Capitalize(req.Username), | ||||||
| 				Email:       fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.Domain), | 				Email:       fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.RootDomain), | ||||||
| 				Provider:    "username", | 				Provider:    "username", | ||||||
| 				TotpPending: true, | 				TotpPending: true, | ||||||
| 			}) | 			}) | ||||||
| @@ -141,7 +141,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { | |||||||
| 	err = controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ | 	err = controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ | ||||||
| 		Username: req.Username, | 		Username: req.Username, | ||||||
| 		Name:     utils.Capitalize(req.Username), | 		Name:     utils.Capitalize(req.Username), | ||||||
| 		Email:    fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.Domain), | 		Email:    fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.Config.RootDomain), | ||||||
| 		Provider: "username", | 		Provider: "username", | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| @@ -246,7 +246,7 @@ func (controller *UserController) totpHandler(c *gin.Context) { | |||||||
| 	err = controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ | 	err = controller.Auth.CreateSessionCookie(c, &config.SessionCookie{ | ||||||
| 		Username: user.Username, | 		Username: user.Username, | ||||||
| 		Name:     utils.Capitalize(user.Username), | 		Name:     utils.Capitalize(user.Username), | ||||||
| 		Email:    fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.Config.Domain), | 		Email:    fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.Config.RootDomain), | ||||||
| 		Provider: "username", | 		Provider: "username", | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type ContextMiddlewareConfig struct { | type ContextMiddlewareConfig struct { | ||||||
| 	Domain string | 	RootDomain string | ||||||
| } | } | ||||||
|  |  | ||||||
| type ContextMiddleware struct { | type ContextMiddleware struct { | ||||||
| @@ -134,7 +134,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { | |||||||
| 			c.Set("context", &config.UserContext{ | 			c.Set("context", &config.UserContext{ | ||||||
| 				Username:    user.Username, | 				Username:    user.Username, | ||||||
| 				Name:        utils.Capitalize(user.Username), | 				Name:        utils.Capitalize(user.Username), | ||||||
| 				Email:       fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.Config.Domain), | 				Email:       fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.Config.RootDomain), | ||||||
| 				Provider:    "basic", | 				Provider:    "basic", | ||||||
| 				IsLoggedIn:  true, | 				IsLoggedIn:  true, | ||||||
| 				TotpEnabled: user.TotpSecret != "", | 				TotpEnabled: user.TotpSecret != "", | ||||||
| @@ -146,7 +146,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { | |||||||
| 			c.Set("context", &config.UserContext{ | 			c.Set("context", &config.UserContext{ | ||||||
| 				Username:   basic.Username, | 				Username:   basic.Username, | ||||||
| 				Name:       utils.Capitalize(basic.Username), | 				Name:       utils.Capitalize(basic.Username), | ||||||
| 				Email:      fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.Config.Domain), | 				Email:      fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.Config.RootDomain), | ||||||
| 				Provider:   "basic", | 				Provider:   "basic", | ||||||
| 				IsLoggedIn: true, | 				IsLoggedIn: true, | ||||||
| 			}) | 			}) | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ type AuthServiceConfig struct { | |||||||
| 	OauthWhitelist    string | 	OauthWhitelist    string | ||||||
| 	SessionExpiry     int | 	SessionExpiry     int | ||||||
| 	SecureCookie      bool | 	SecureCookie      bool | ||||||
| 	Domain            string | 	RootDomain        string | ||||||
| 	LoginTimeout      int | 	LoginTimeout      int | ||||||
| 	LoginMaxRetries   int | 	LoginMaxRetries   int | ||||||
| 	SessionCookieName string | 	SessionCookieName string | ||||||
| @@ -216,7 +216,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.SetCookie(auth.Config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true) | 	c.SetCookie(auth.Config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.Config.RootDomain), auth.Config.SecureCookie, true) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| @@ -234,7 +234,7 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error { | |||||||
| 		return res.Error | 		return res.Error | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true) | 	c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.RootDomain), auth.Config.SecureCookie, true) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,8 +12,8 @@ import ( | |||||||
| 	"github.com/rs/zerolog" | 	"github.com/rs/zerolog" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Get upper domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) | // Get root domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) | ||||||
| func GetUpperDomain(appUrl string) (string, error) { | func GetRootDomain(appUrl string) (string, error) { | ||||||
| 	appUrlParsed, err := url.Parse(appUrl) | 	appUrlParsed, err := url.Parse(appUrl) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| @@ -88,7 +88,7 @@ func IsRedirectSafe(redirectURL string, domain string) bool { | |||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	upper, err := GetUpperDomain(redirectURL) | 	upper, err := GetRootDomain(redirectURL) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false | 		return false | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user