mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-28 04:35:40 +00:00 
			
		
		
		
	feat: add custom forgot password message
This commit is contained in:
		| @@ -26,4 +26,5 @@ SESSION_EXPIRY=7200 | |||||||
| LOGIN_TIMEOUT=300 | LOGIN_TIMEOUT=300 | ||||||
| LOGIN_MAX_RETRIES=5 | LOGIN_MAX_RETRIES=5 | ||||||
| LOG_LEVEL=0 | LOG_LEVEL=0 | ||||||
| APP_TITLE=Tinyauth SSO | APP_TITLE=Tinyauth SSO | ||||||
|  | FORGOT_PASSWORD_MESSAGE=Some message about resetting the password | ||||||
							
								
								
									
										15
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								cmd/root.go
									
									
									
									
									
								
							| @@ -84,12 +84,13 @@ var rootCmd = &cobra.Command{ | |||||||
|  |  | ||||||
| 		// Create handlers config | 		// Create handlers config | ||||||
| 		handlersConfig := types.HandlersConfig{ | 		handlersConfig := types.HandlersConfig{ | ||||||
| 			AppURL:          config.AppURL, | 			AppURL:                config.AppURL, | ||||||
| 			DisableContinue: config.DisableContinue, | 			DisableContinue:       config.DisableContinue, | ||||||
| 			Title:           config.Title, | 			Title:                 config.Title, | ||||||
| 			GenericName:     config.GenericName, | 			GenericName:           config.GenericName, | ||||||
| 			CookieSecure:    config.CookieSecure, | 			CookieSecure:          config.CookieSecure, | ||||||
| 			Domain:          domain, | 			Domain:                domain, | ||||||
|  | 			ForgotPasswordMessage: config.FogotPasswordMessage, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Create api config | 		// Create api config | ||||||
| @@ -196,6 +197,7 @@ func init() { | |||||||
| 	rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).") | 	rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).") | ||||||
| 	rootCmd.Flags().Int("log-level", 1, "Log level.") | 	rootCmd.Flags().Int("log-level", 1, "Log level.") | ||||||
| 	rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.") | 	rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.") | ||||||
|  | 	rootCmd.Flags().String("forgot-password-message", "You can reset your password by changing the `USERS` environment variable.", "Message to show on the forgot password page.") | ||||||
|  |  | ||||||
| 	// Bind flags to environment | 	// Bind flags to environment | ||||||
| 	viper.BindEnv("port", "PORT") | 	viper.BindEnv("port", "PORT") | ||||||
| @@ -227,6 +229,7 @@ func init() { | |||||||
| 	viper.BindEnv("app-title", "APP_TITLE") | 	viper.BindEnv("app-title", "APP_TITLE") | ||||||
| 	viper.BindEnv("login-timeout", "LOGIN_TIMEOUT") | 	viper.BindEnv("login-timeout", "LOGIN_TIMEOUT") | ||||||
| 	viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES") | 	viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES") | ||||||
|  | 	viper.BindEnv("forgot-password-message", "FORGOT_PASSWORD_MESSAGE") | ||||||
|  |  | ||||||
| 	// Bind flags to viper | 	// Bind flags to viper | ||||||
| 	viper.BindPFlags(rootCmd.Flags()) | 	viper.BindPFlags(rootCmd.Flags()) | ||||||
|   | |||||||
										
											Binary file not shown.
										
									
								
							| @@ -24,6 +24,7 @@ | |||||||
|     "react": "^19.1.0", |     "react": "^19.1.0", | ||||||
|     "react-dom": "^19.1.0", |     "react-dom": "^19.1.0", | ||||||
|     "react-i18next": "^15.4.1", |     "react-i18next": "^15.4.1", | ||||||
|  |     "react-markdown": "^10.1.0", | ||||||
|     "react-router": "^7.1.3", |     "react-router": "^7.1.3", | ||||||
|     "zod": "^3.24.1" |     "zod": "^3.24.1" | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { TextInput, PasswordInput, Button } from "@mantine/core"; | import { TextInput, PasswordInput, Button, Anchor, Group, Text } from "@mantine/core"; | ||||||
| import { useForm, zodResolver } from "@mantine/form"; | import { useForm, zodResolver } from "@mantine/form"; | ||||||
| import { LoginFormValues, loginSchema } from "../../schemas/login-schema"; | import { LoginFormValues, loginSchema } from "../../schemas/login-schema"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| @@ -26,16 +26,25 @@ export const LoginForm = (props: LoginFormProps) => { | |||||||
|       <TextInput |       <TextInput | ||||||
|         label={t("loginUsername")} |         label={t("loginUsername")} | ||||||
|         placeholder="username" |         placeholder="username" | ||||||
|         required |  | ||||||
|         disabled={isPending} |         disabled={isPending} | ||||||
|  |         required | ||||||
|  |         withAsterisk={false} | ||||||
|         key={form.key("username")} |         key={form.key("username")} | ||||||
|         {...form.getInputProps("username")} |         {...form.getInputProps("username")} | ||||||
|       /> |       /> | ||||||
|  |       <Group justify="space-between" mb={5} mt="md"> | ||||||
|  |         <Text component="label" htmlFor=".password-input" size="sm" fw={500}> | ||||||
|  |         {t("loginPassword")} | ||||||
|  |         </Text> | ||||||
|  |  | ||||||
|  |         <Anchor href="#" onClick={() => window.location.replace("/forgot-password")} pt={2} fw={500} fz="xs"> | ||||||
|  |           {t('forgotPasswordTitle')} | ||||||
|  |         </Anchor> | ||||||
|  |       </Group> | ||||||
|       <PasswordInput |       <PasswordInput | ||||||
|         label={t("loginPassword")} |         className="password-input" | ||||||
|         placeholder="password" |         placeholder="password" | ||||||
|         required |         required | ||||||
|         mt="md" |  | ||||||
|         disabled={isPending} |         disabled={isPending} | ||||||
|         key={form.key("password")} |         key={form.key("password")} | ||||||
|         {...form.getInputProps("password")} |         {...form.getInputProps("password")} | ||||||
|   | |||||||
| @@ -45,5 +45,6 @@ | |||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "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>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -45,5 +45,6 @@ | |||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "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>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -18,6 +18,7 @@ import { InternalServerError } from "./pages/internal-server-error.tsx"; | |||||||
| import { TotpPage } from "./pages/totp-page.tsx"; | import { TotpPage } from "./pages/totp-page.tsx"; | ||||||
| import { AppContextProvider } from "./context/app-context.tsx"; | import { AppContextProvider } from "./context/app-context.tsx"; | ||||||
| import "./lib/i18n/i18n.ts"; | import "./lib/i18n/i18n.ts"; | ||||||
|  | import { ForgotPasswordPage } from "./pages/forgot-password-page.tsx"; | ||||||
|  |  | ||||||
| const queryClient = new QueryClient(); | const queryClient = new QueryClient(); | ||||||
|  |  | ||||||
| @@ -37,6 +38,7 @@ createRoot(document.getElementById("root")!).render( | |||||||
|                 <Route path="/continue" element={<ContinuePage />} /> |                 <Route path="/continue" element={<ContinuePage />} /> | ||||||
|                 <Route path="/unauthorized" element={<UnauthorizedPage />} /> |                 <Route path="/unauthorized" element={<UnauthorizedPage />} /> | ||||||
|                 <Route path="/error" element={<InternalServerError />} /> |                 <Route path="/error" element={<InternalServerError />} /> | ||||||
|  |                 <Route path="/forgot-password" element={<ForgotPasswordPage />} /> | ||||||
|                 <Route path="*" element={<NotFoundPage />} /> |                 <Route path="*" element={<NotFoundPage />} /> | ||||||
|               </Routes> |               </Routes> | ||||||
|             </BrowserRouter> |             </BrowserRouter> | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								frontend/src/pages/forgot-password-page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/src/pages/forgot-password-page.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | import { Paper, Text, TypographyStylesProvider } from "@mantine/core"; | ||||||
|  | import { Layout } from "../components/layouts/layout"; | ||||||
|  | import { useTranslation } from "react-i18next"; | ||||||
|  | import { useAppContext } from "../context/app-context"; | ||||||
|  | import Markdown from 'react-markdown' | ||||||
|  |  | ||||||
|  | export const ForgotPasswordPage = () => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const { forgotPasswordMessage } = useAppContext(); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Layout> | ||||||
|  |       <Paper shadow="md" p={30} mt={30} radius="md" withBorder> | ||||||
|  |         <Text size="xl" fw={700}> | ||||||
|  |           {t("forgotPasswordTitle")} | ||||||
|  |         </Text> | ||||||
|  |         <TypographyStylesProvider> | ||||||
|  |             <Markdown> | ||||||
|  |                 {forgotPasswordMessage} | ||||||
|  |             </Markdown> | ||||||
|  |         </TypographyStylesProvider> | ||||||
|  |       </Paper> | ||||||
|  |     </Layout> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -6,6 +6,7 @@ export const appContextSchema = z.object({ | |||||||
|   title: z.string(), |   title: z.string(), | ||||||
|   genericName: z.string(), |   genericName: z.string(), | ||||||
|   domain: z.string(), |   domain: z.string(), | ||||||
|  |   forgotPasswordMessage: z.string(), | ||||||
| }); | }); | ||||||
|  |  | ||||||
| export type AppContextSchemaType = z.infer<typeof appContextSchema>; | export type AppContextSchemaType = z.infer<typeof appContextSchema>; | ||||||
|   | |||||||
| @@ -440,13 +440,14 @@ func (h *Handlers) AppHandler(c *gin.Context) { | |||||||
|  |  | ||||||
| 	// Create app context struct | 	// Create app context struct | ||||||
| 	appContext := types.AppContext{ | 	appContext := types.AppContext{ | ||||||
| 		Status:              200, | 		Status:                200, | ||||||
| 		Message:             "OK", | 		Message:               "OK", | ||||||
| 		ConfiguredProviders: configuredProviders, | 		ConfiguredProviders:   configuredProviders, | ||||||
| 		DisableContinue:     h.Config.DisableContinue, | 		DisableContinue:       h.Config.DisableContinue, | ||||||
| 		Title:               h.Config.Title, | 		Title:                 h.Config.Title, | ||||||
| 		GenericName:         h.Config.GenericName, | 		GenericName:           h.Config.GenericName, | ||||||
| 		Domain:              h.Config.Domain, | 		Domain:                h.Config.Domain, | ||||||
|  | 		ForgotPasswordMessage: h.Config.ForgotPasswordMessage, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return app context | 	// Return app context | ||||||
|   | |||||||
| @@ -40,13 +40,14 @@ type UserContextResponse struct { | |||||||
|  |  | ||||||
| // App Context is the response for the app context endpoint | // App Context is the response for the app context endpoint | ||||||
| type AppContext struct { | type AppContext 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"` | 	DisableContinue       bool     `json:"disableContinue"` | ||||||
| 	Title               string   `json:"title"` | 	Title                 string   `json:"title"` | ||||||
| 	GenericName         string   `json:"genericName"` | 	GenericName           string   `json:"genericName"` | ||||||
| 	Domain              string   `json:"domain"` | 	Domain                string   `json:"domain"` | ||||||
|  | 	ForgotPasswordMessage string   `json:"forgotPasswordMessage"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Totp request is the request for the totp endpoint | // Totp request is the request for the totp endpoint | ||||||
|   | |||||||
| @@ -32,16 +32,18 @@ type Config struct { | |||||||
| 	EnvFile                 string `mapstructure:"env-file"` | 	EnvFile                 string `mapstructure:"env-file"` | ||||||
| 	LoginTimeout            int    `mapstructure:"login-timeout"` | 	LoginTimeout            int    `mapstructure:"login-timeout"` | ||||||
| 	LoginMaxRetries         int    `mapstructure:"login-max-retries"` | 	LoginMaxRetries         int    `mapstructure:"login-max-retries"` | ||||||
|  | 	FogotPasswordMessage    string `mapstructure:"forgot-password-message" validate:"required"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Server configuration | // Server configuration | ||||||
| type HandlersConfig struct { | type HandlersConfig struct { | ||||||
| 	AppURL          string | 	AppURL                string | ||||||
| 	Domain          string | 	Domain                string | ||||||
| 	CookieSecure    bool | 	CookieSecure          bool | ||||||
| 	DisableContinue bool | 	DisableContinue       bool | ||||||
| 	GenericName     string | 	GenericName           string | ||||||
| 	Title           string | 	Title                 string | ||||||
|  | 	ForgotPasswordMessage string | ||||||
| } | } | ||||||
|  |  | ||||||
| // OAuthConfig is the configuration for the providers | // OAuthConfig is the configuration for the providers | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Stavros
					Stavros