diff --git a/.env.example b/.env.example index ef7f73f..e131e6b 100644 --- a/.env.example +++ b/.env.example @@ -26,4 +26,5 @@ SESSION_EXPIRY=7200 LOGIN_TIMEOUT=300 LOGIN_MAX_RETRIES=5 LOG_LEVEL=0 -APP_TITLE=Tinyauth SSO \ No newline at end of file +APP_TITLE=Tinyauth SSO +FORGOT_PASSWORD_MESSAGE=Some message about resetting the password \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index ae3e935..fe10166 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -84,12 +84,13 @@ var rootCmd = &cobra.Command{ // Create handlers config handlersConfig := types.HandlersConfig{ - AppURL: config.AppURL, - DisableContinue: config.DisableContinue, - Title: config.Title, - GenericName: config.GenericName, - CookieSecure: config.CookieSecure, - Domain: domain, + AppURL: config.AppURL, + DisableContinue: config.DisableContinue, + Title: config.Title, + GenericName: config.GenericName, + CookieSecure: config.CookieSecure, + Domain: domain, + ForgotPasswordMessage: config.FogotPasswordMessage, } // 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("log-level", 1, "Log level.") 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 viper.BindEnv("port", "PORT") @@ -227,6 +229,7 @@ func init() { viper.BindEnv("app-title", "APP_TITLE") viper.BindEnv("login-timeout", "LOGIN_TIMEOUT") viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES") + viper.BindEnv("forgot-password-message", "FORGOT_PASSWORD_MESSAGE") // Bind flags to viper viper.BindPFlags(rootCmd.Flags()) diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 1dd4596..f5d3b69 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/package.json b/frontend/package.json index 460ee42..8d15784 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.4.1", + "react-markdown": "^10.1.0", "react-router": "^7.1.3", "zod": "^3.24.1" }, diff --git a/frontend/src/components/auth/login-forn.tsx b/frontend/src/components/auth/login-forn.tsx index 0f0616d..33c4c2a 100644 --- a/frontend/src/components/auth/login-forn.tsx +++ b/frontend/src/components/auth/login-forn.tsx @@ -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 { LoginFormValues, loginSchema } from "../../schemas/login-schema"; import { useTranslation } from "react-i18next"; @@ -26,16 +26,25 @@ export const LoginForm = (props: LoginFormProps) => { + + + {t("loginPassword")} + + + window.location.replace("/forgot-password")} pt={2} fw={500} fz="xs"> + {t('forgotPasswordTitle')} + + {{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index b31e2ee..12135fe 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -45,5 +45,6 @@ "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index a5ae8a3..c5a39d6 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -18,6 +18,7 @@ import { InternalServerError } from "./pages/internal-server-error.tsx"; import { TotpPage } from "./pages/totp-page.tsx"; import { AppContextProvider } from "./context/app-context.tsx"; import "./lib/i18n/i18n.ts"; +import { ForgotPasswordPage } from "./pages/forgot-password-page.tsx"; const queryClient = new QueryClient(); @@ -37,6 +38,7 @@ createRoot(document.getElementById("root")!).render( } /> } /> } /> + } /> } /> diff --git a/frontend/src/pages/forgot-password-page.tsx b/frontend/src/pages/forgot-password-page.tsx new file mode 100644 index 0000000..8e06cf2 --- /dev/null +++ b/frontend/src/pages/forgot-password-page.tsx @@ -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 ( + + + + {t("forgotPasswordTitle")} + + + + {forgotPasswordMessage} + + + + + ); +}; diff --git a/frontend/src/schemas/app-context-schema.ts b/frontend/src/schemas/app-context-schema.ts index 3738c3f..5a2b519 100644 --- a/frontend/src/schemas/app-context-schema.ts +++ b/frontend/src/schemas/app-context-schema.ts @@ -6,6 +6,7 @@ export const appContextSchema = z.object({ title: z.string(), genericName: z.string(), domain: z.string(), + forgotPasswordMessage: z.string(), }); export type AppContextSchemaType = z.infer; diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 9e48764..6e80851 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -440,13 +440,14 @@ func (h *Handlers) AppHandler(c *gin.Context) { // Create app context struct appContext := types.AppContext{ - Status: 200, - Message: "OK", - ConfiguredProviders: configuredProviders, - DisableContinue: h.Config.DisableContinue, - Title: h.Config.Title, - GenericName: h.Config.GenericName, - Domain: h.Config.Domain, + Status: 200, + Message: "OK", + ConfiguredProviders: configuredProviders, + DisableContinue: h.Config.DisableContinue, + Title: h.Config.Title, + GenericName: h.Config.GenericName, + Domain: h.Config.Domain, + ForgotPasswordMessage: h.Config.ForgotPasswordMessage, } // Return app context diff --git a/internal/types/api.go b/internal/types/api.go index e9307a6..144bb56 100644 --- a/internal/types/api.go +++ b/internal/types/api.go @@ -40,13 +40,14 @@ type UserContextResponse struct { // App Context is the response for the app context endpoint type AppContext 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"` + 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"` + ForgotPasswordMessage string `json:"forgotPasswordMessage"` } // Totp request is the request for the totp endpoint diff --git a/internal/types/config.go b/internal/types/config.go index 6092a48..88e9169 100644 --- a/internal/types/config.go +++ b/internal/types/config.go @@ -32,16 +32,18 @@ type Config struct { EnvFile string `mapstructure:"env-file"` LoginTimeout int `mapstructure:"login-timeout"` LoginMaxRetries int `mapstructure:"login-max-retries"` + FogotPasswordMessage string `mapstructure:"forgot-password-message" validate:"required"` } // Server configuration type HandlersConfig struct { - AppURL string - Domain string - CookieSecure bool - DisableContinue bool - GenericName string - Title string + AppURL string + Domain string + CookieSecure bool + DisableContinue bool + GenericName string + Title string + ForgotPasswordMessage string } // OAuthConfig is the configuration for the providers