mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-28 04:35:40 +00:00 
			
		
		
		
	wip
This commit is contained in:
		| @@ -95,7 +95,6 @@ func init() { | ||||
| 		{"generic-user-url", "", "Generic OAuth user info URL."}, | ||||
| 		{"generic-name", "Generic", "Generic OAuth provider name."}, | ||||
| 		{"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-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."}, | ||||
|   | ||||
							
								
								
									
										36
									
								
								frontend/src/components/domain-warning/domain-warning.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								frontend/src/components/domain-warning/domain-warning.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import { | ||||
|   Card, | ||||
|   CardDescription, | ||||
|   CardFooter, | ||||
|   CardHeader, | ||||
|   CardTitle, | ||||
| } from "../ui/card"; | ||||
| import { Button } from "../ui/button"; | ||||
|  | ||||
| interface Props { | ||||
|   onClick: () => void; | ||||
|   appUrl: string; | ||||
|   currentUrl: string; | ||||
| } | ||||
|  | ||||
| export const DomainWarning = (props: Props) => { | ||||
|   const { onClick, appUrl, currentUrl } = props; | ||||
|  | ||||
|   return ( | ||||
|     <Card className="min-w-xs sm:min-w-sm"> | ||||
|       <CardHeader> | ||||
|         <CardTitle className="text-3xl">Incorrect Domain</CardTitle> | ||||
|         <CardDescription> | ||||
|           This instance is configured to be accessed from <code>{appUrl}</code>, | ||||
|           but <code>{currentUrl}</code> is being used. Authentication will most | ||||
|           likely fail if you proceed. | ||||
|         </CardDescription> | ||||
|       </CardHeader> | ||||
|       <CardFooter className="flex flex-col items-stretch"> | ||||
|         <Button onClick={onClick} variant="warning"> | ||||
|           Continue | ||||
|         </Button> | ||||
|       </CardFooter> | ||||
|     </Card> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,9 +1,13 @@ | ||||
| import { useAppContext } from "@/context/app-context"; | ||||
| import { LanguageSelector } from "../language/language"; | ||||
| import { Outlet } from "react-router"; | ||||
| import { useState } from "react"; | ||||
| import { DomainWarning } from "../domain-warning/domain-warning"; | ||||
|  | ||||
| export const Layout = () => { | ||||
|   const { backgroundImage } = useAppContext(); | ||||
|   const { backgroundImage, appUrl } = useAppContext(); | ||||
|   const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(false); | ||||
|   const currentUrl = window.location.origin; | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
| @@ -15,7 +19,15 @@ export const Layout = () => { | ||||
|       }} | ||||
|     > | ||||
|       <LanguageSelector /> | ||||
|       {appUrl !== currentUrl && !ignoreDomainWarning ? ( | ||||
|         <DomainWarning | ||||
|           onClick={() => setIgnoreDomainWarning(true)} | ||||
|           appUrl={appUrl} | ||||
|           currentUrl={currentUrl} | ||||
|         /> | ||||
|       ) : ( | ||||
|         <Outlet /> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -22,7 +22,7 @@ const buttonVariants = cva( | ||||
|           "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", | ||||
|         link: "text-primary underline-offset-4 hover:underline", | ||||
|         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: { | ||||
|         default: "h-9 px-4 py-2 has-[>svg]:px-3", | ||||
|   | ||||
| @@ -20,8 +20,7 @@ | ||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", | ||||
|     "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?", | ||||
|     "continueTitle": "Continue", | ||||
|     "continueSubtitle": "Click the button to continue to your app.", | ||||
|     "continueRedirectManually": "Redirect me manually", | ||||
|     "logoutFailTitle": "Failed to log out", | ||||
|     "logoutFailSubtitle": "Please try again", | ||||
|     "logoutSuccessTitle": "Logged out", | ||||
| @@ -45,7 +44,7 @@ | ||||
|     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||
|     "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?", | ||||
|     "untrustedRedirectSubtitle": "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?", | ||||
|     "cancelTitle": "Cancel", | ||||
|     "forgotPasswordTitle": "Forgot your password?", | ||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||
|   | ||||
| @@ -20,8 +20,7 @@ | ||||
|     "continueInvalidRedirectSubtitle": "The redirect URL is invalid", | ||||
|     "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?", | ||||
|     "continueTitle": "Continue", | ||||
|     "continueSubtitle": "Click the button to continue to your app.", | ||||
|     "continueRedirectManually": "Redirect me manually", | ||||
|     "logoutFailTitle": "Failed to log out", | ||||
|     "logoutFailSubtitle": "Please try again", | ||||
|     "logoutSuccessTitle": "Logged out", | ||||
| @@ -45,7 +44,7 @@ | ||||
|     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||
|     "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?", | ||||
|     "untrustedRedirectSubtitle": "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?", | ||||
|     "cancelTitle": "Cancel", | ||||
|     "forgotPasswordTitle": "Forgot your password?", | ||||
|     "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.", | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import { isValidUrl } from "@/lib/utils"; | ||||
| import { Trans, useTranslation } from "react-i18next"; | ||||
| import { Navigate, useLocation, useNavigate } from "react-router"; | ||||
| import DOMPurify from "dompurify"; | ||||
| import { useState } from "react"; | ||||
| import { useEffect, useState } from "react"; | ||||
|  | ||||
| export const ContinuePage = () => { | ||||
|   const { isLoggedIn } = useUserContext(); | ||||
| @@ -21,9 +21,10 @@ export const ContinuePage = () => { | ||||
|     return <Navigate to="/login" />; | ||||
|   } | ||||
|  | ||||
|   const { domain, disableContinue } = useAppContext(); | ||||
|   const { rootDomain } = useAppContext(); | ||||
|   const { search } = useLocation(); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [showRedirectButton, setShowRedirectButton] = useState(false); | ||||
|  | ||||
|   const searchParams = new URLSearchParams(search); | ||||
|   const redirectURI = searchParams.get("redirect_uri"); | ||||
| @@ -36,21 +37,20 @@ export const ContinuePage = () => { | ||||
|     return <Navigate to="/logout" />; | ||||
|   } | ||||
|  | ||||
|   const handleRedirect = () => { | ||||
|     setLoading(true); | ||||
|     window.location.href = DOMPurify.sanitize(redirectURI); | ||||
|   } | ||||
|  | ||||
|   if (disableContinue) { | ||||
|     handleRedirect(); | ||||
|   } | ||||
|  | ||||
|   const { t } = useTranslation(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const url = new URL(redirectURI); | ||||
|   const handleRedirect = () => { | ||||
|     setLoading(true); | ||||
|     window.location.href = DOMPurify.sanitize(redirectURI); | ||||
|   }; | ||||
|  | ||||
|   if (!(url.hostname == domain) && !url.hostname.endsWith(`.${domain}`)) { | ||||
|   const redirectURLObj = new URL(redirectURI); | ||||
|  | ||||
|   if ( | ||||
|     !(redirectURLObj.hostname == rootDomain) && | ||||
|     !redirectURLObj.hostname.endsWith(`.${rootDomain}`) | ||||
|   ) { | ||||
|     return ( | ||||
|       <Card className="min-w-xs sm:min-w-sm"> | ||||
|         <CardHeader> | ||||
| @@ -64,7 +64,7 @@ export const ContinuePage = () => { | ||||
|               components={{ | ||||
|                 code: <code />, | ||||
|               }} | ||||
|               values={{ domain }} | ||||
|               values={{ rootDomain }} | ||||
|             /> | ||||
|           </CardDescription> | ||||
|         </CardHeader> | ||||
| @@ -76,7 +76,11 @@ export const ContinuePage = () => { | ||||
|           > | ||||
|             {t("continueTitle")} | ||||
|           </Button> | ||||
|           <Button onClick={() => navigate("/logout")} variant="outline" disabled={loading}> | ||||
|           <Button | ||||
|             onClick={() => navigate("/logout")} | ||||
|             variant="outline" | ||||
|             disabled={loading} | ||||
|           > | ||||
|             {t("cancelTitle")} | ||||
|           </Button> | ||||
|         </CardFooter> | ||||
| @@ -84,7 +88,10 @@ export const ContinuePage = () => { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   if (url.protocol === "http:" && window.location.protocol === "https:") { | ||||
|   if ( | ||||
|     redirectURLObj.protocol === "http:" && | ||||
|     window.location.protocol === "https:" | ||||
|   ) { | ||||
|     return ( | ||||
|       <Card className="min-w-xs sm:min-w-sm"> | ||||
|         <CardHeader> | ||||
| @@ -102,14 +109,14 @@ export const ContinuePage = () => { | ||||
|           </CardDescription> | ||||
|         </CardHeader> | ||||
|         <CardFooter className="flex flex-col items-stretch gap-2"> | ||||
|           <Button | ||||
|             onClick={handleRedirect} | ||||
|             loading={loading} | ||||
|             variant="warning" | ||||
|           > | ||||
|           <Button onClick={handleRedirect} loading={loading} variant="warning"> | ||||
|             {t("continueTitle")} | ||||
|           </Button> | ||||
|           <Button onClick={() => navigate("/logout")} variant="outline" disabled={loading}> | ||||
|           <Button | ||||
|             onClick={() => navigate("/logout")} | ||||
|             variant="outline" | ||||
|             disabled={loading} | ||||
|           > | ||||
|             {t("cancelTitle")} | ||||
|           </Button> | ||||
|         </CardFooter> | ||||
| @@ -117,20 +124,31 @@ export const ContinuePage = () => { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   useEffect(() => { | ||||
|     setTimeout(() => { | ||||
|       handleRedirect(); | ||||
|     }, 100); | ||||
|     setTimeout(() => { | ||||
|       setLoading(false); | ||||
|       setShowRedirectButton(true); | ||||
|     }, 1000); | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <Card className="min-w-xs sm:min-w-sm"> | ||||
|       <CardHeader> | ||||
|         <CardTitle className="text-3xl">{t("continueTitle")}</CardTitle> | ||||
|         <CardDescription>{t("continueSubtitle")}</CardDescription> | ||||
|         <CardTitle className="text-3xl"> | ||||
|           {t("continueRedirectingTitle")} | ||||
|         </CardTitle> | ||||
|         <CardDescription>{t("continueRedirectingSubtitle")}</CardDescription> | ||||
|       </CardHeader> | ||||
|       {showRedirectButton && ( | ||||
|         <CardFooter className="flex flex-col items-stretch"> | ||||
|         <Button | ||||
|           onClick={handleRedirect} | ||||
|           loading={loading} | ||||
|         > | ||||
|           {t("continueTitle")} | ||||
|           <Button onClick={handleRedirect}> | ||||
|             {t("continueRedirectManually")} | ||||
|           </Button> | ||||
|         </CardFooter> | ||||
|       )} | ||||
|     </Card> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -2,10 +2,10 @@ import { z } from "zod"; | ||||
|  | ||||
| export const appContextSchema = z.object({ | ||||
|   configuredProviders: z.array(z.string()), | ||||
|   disableContinue: z.boolean(), | ||||
|   title: z.string(), | ||||
|   genericName: z.string(), | ||||
|   domain: z.string(), | ||||
|   appUrl: z.string(), | ||||
|   rootDomain: z.string(), | ||||
|   forgotPasswordMessage: z.string(), | ||||
|   oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]), | ||||
|   backgroundImage: z.string(), | ||||
|   | ||||
| @@ -177,10 +177,10 @@ func (app *BootstrapApp) Setup() error { | ||||
| 	// Create controllers | ||||
| 	contextController := controller.NewContextController(controller.ContextControllerConfig{ | ||||
| 		ConfiguredProviders:   configuredProviders, | ||||
| 		DisableContinue:       app.Config.DisableContinue, | ||||
| 		Title:                 app.Config.Title, | ||||
| 		GenericName:           app.Config.GenericName, | ||||
| 		Domain:                domain, | ||||
| 		AppURL:                app.Config.AppURL, | ||||
| 		RootDomain:            domain, | ||||
| 		ForgotPasswordMessage: app.Config.ForgotPasswordMessage, | ||||
| 		BackgroundImage:       app.Config.BackgroundImage, | ||||
| 		OAuthAutoRedirect:     app.Config.OAuthAutoRedirect, | ||||
|   | ||||
| @@ -36,7 +36,6 @@ type Config struct { | ||||
| 	GenericUserURL          string `mapstructure:"generic-user-url"` | ||||
| 	GenericName             string `mapstructure:"generic-name"` | ||||
| 	GenericSkipSSL          bool   `mapstructure:"generic-skip-ssl"` | ||||
| 	DisableContinue         bool   `mapstructure:"disable-continue"` | ||||
| 	OAuthWhitelist          string `mapstructure:"oauth-whitelist"` | ||||
| 	OAuthAutoRedirect       string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` | ||||
| 	SessionExpiry           int    `mapstructure:"session-expiry"` | ||||
|   | ||||
| @@ -15,7 +15,7 @@ type UserContextResponse struct { | ||||
| 	Name        string `json:"name"` | ||||
| 	Email       string `json:"email"` | ||||
| 	Provider    string `json:"provider"` | ||||
| 	Oauth       bool   `json:"oauth"` | ||||
| 	OAuth       bool   `json:"oauth"` | ||||
| 	TotpPending bool   `json:"totpPending"` | ||||
| } | ||||
|  | ||||
| @@ -23,10 +23,10 @@ type AppContextResponse struct { | ||||
| 	Status                int      `json:"status"` | ||||
| 	Message               string   `json:"message"` | ||||
| 	ConfiguredProviders   []string `json:"configuredProviders"` | ||||
| 	DisableContinue       bool     `json:"disableContinue"` | ||||
| 	Title                 string   `json:"title"` | ||||
| 	GenericName           string   `json:"genericName"` | ||||
| 	Domain                string   `json:"domain"` | ||||
| 	AppURL                string   `json:"appUrl"` | ||||
| 	RootDomain            string   `json:"rootDomain"` | ||||
| 	ForgotPasswordMessage string   `json:"forgotPasswordMessage"` | ||||
| 	BackgroundImage       string   `json:"backgroundImage"` | ||||
| 	OAuthAutoRedirect     string   `json:"oauthAutoRedirect"` | ||||
| @@ -37,7 +37,8 @@ type ContextControllerConfig struct { | ||||
| 	DisableContinue       bool | ||||
| 	Title                 string | ||||
| 	GenericName           string | ||||
| 	Domain                string | ||||
| 	AppURL                string | ||||
| 	RootDomain            string | ||||
| 	ForgotPasswordMessage string | ||||
| 	BackgroundImage       string | ||||
| 	OAuthAutoRedirect     string | ||||
| @@ -72,7 +73,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) { | ||||
| 		Name:        context.Name, | ||||
| 		Email:       context.Email, | ||||
| 		Provider:    context.Provider, | ||||
| 		Oauth:       context.OAuth, | ||||
| 		OAuth:       context.OAuth, | ||||
| 		TotpPending: context.TotpPending, | ||||
| 	} | ||||
|  | ||||
| @@ -93,10 +94,10 @@ func (controller *ContextController) appContextHandler(c *gin.Context) { | ||||
| 		Status:                200, | ||||
| 		Message:               "Success", | ||||
| 		ConfiguredProviders:   controller.Config.ConfiguredProviders, | ||||
| 		DisableContinue:       controller.Config.DisableContinue, | ||||
| 		Title:                 controller.Config.Title, | ||||
| 		GenericName:           controller.Config.GenericName, | ||||
| 		Domain:                controller.Config.Domain, | ||||
| 		AppURL:                controller.Config.AppURL, | ||||
| 		RootDomain:            controller.Config.RootDomain, | ||||
| 		ForgotPasswordMessage: controller.Config.ForgotPasswordMessage, | ||||
| 		BackgroundImage:       controller.Config.BackgroundImage, | ||||
| 		OAuthAutoRedirect:     controller.Config.OAuthAutoRedirect, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Stavros
					Stavros