diff --git a/.env.example b/.env.example
index ef7f73fd..e131e6b8 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 ae3e9355..fe10166f 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 1dd4596c..f5d3b69d 100755
Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ
diff --git a/frontend/package.json b/frontend/package.json
index 460ee428..8d157841 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 0f0616d0..33c4c2aa 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 b31e2ee5..12135fe3 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 a5ae8a3d..c5a39d67 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 00000000..8e06cf2f
--- /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 3738c3f1..5a2b519c 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 9e487640..6e80851f 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 e9307a60..144bb56a 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 6092a48b..88e9169f 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