mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-12-12 03:06:38 +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