diff --git a/package-lock.json b/package-lock.json index 1b4b4a2..33db94c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ciscode/ui-authentication-kit", - "version": "1.0.5", + "version": "1.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ciscode/ui-authentication-kit", - "version": "1.0.5", + "version": "1.0.6", "license": "ISC", "devDependencies": { "@types/node": "^22.13.1", diff --git a/package.json b/package.json index 50a4c6b..17611c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ciscode/ui-authentication-kit", - "version": "1.0.5", + "version": "1.0.6", "description": "", "main": "dist/index.umd.js", "module": "dist/index.mjs", diff --git a/src/components/RequirePermissions.tsx b/src/components/RequirePermissions.tsx index 2bb7c43..7673b68 100644 --- a/src/components/RequirePermissions.tsx +++ b/src/components/RequirePermissions.tsx @@ -1,6 +1,6 @@ // src/components/auth/RequirePermissions.tsx import React from 'react'; -import { Navigate } from 'react-router'; // or useNavigate() +import { Navigate } from 'react-router-dom'; // or useNavigate() import { useCan, useHasRole } from '../hooks/useAbility'; // your hooks interface Props { diff --git a/src/pages/auth/ForgotPasswordPage.tsx b/src/pages/auth/ForgotPasswordPage.tsx new file mode 100644 index 0000000..32537c8 --- /dev/null +++ b/src/pages/auth/ForgotPasswordPage.tsx @@ -0,0 +1,108 @@ +import React, { useState } from "react"; +import { useT } from "@ciscode/ui-translate-core"; +import { useNavigate } from "react-router-dom"; +import { InputField } from "../../components/actions/InputField"; +import { InlineError } from "../../components/InlineError"; +import { useAuthConfig } from "../../context/AuthConfigContext"; +import { useAuthState } from "../../context/AuthStateContext"; +import { toTailwindColorClasses } from "../../utils/colorHelpers"; + +export const ForgotPasswordPage: React.FC = () => { + const t = useT("authLib"); + const navigate = useNavigate(); + const { colors, brandName = t("brandName", { defaultValue: "MyBrand" }), logoUrl } = useAuthConfig(); + const { api } = useAuthState(); + + const { bgClass, textClass, borderClass } = toTailwindColorClasses(colors); + const gradientClass = `${bgClass} bg-gradient-to-r from-white/10 via-white/0 to-white/0`; + + const [email, setEmail] = useState(""); + const [pending, setPending] = useState(false); + const [error, setError] = useState(null); + const [sent, setSent] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (pending) return; + setError(null); + setPending(true); + try { + await api.post("/api/auth/forgot-password", { email }); + // Always show generic success regardless of user existence + setSent(true); + } catch (err) { + // Do not enumerate users; still show success message + setSent(true); + console.error("Forgot password request failed", err); + } finally { + setPending(false); + } + } + + return ( +
+
+
+
+ {logoUrl ? ( + Brand Logo + ) : ( +

{brandName}

+ )} + +
+ +

+ {t("ForgotPasswordPage.title", { defaultValue: "Forgot your password?" })} +

+

+ {t("ForgotPasswordPage.subtitle", { defaultValue: "Enter your email to receive a reset link." })} +

+ + {error && } + + {sent ? ( +
+ {t("ForgotPasswordPage.sent", { + defaultValue: "If the email exists, we’ve sent a reset link. Please check your inbox." + })} +
+ ) : ( +
+ + + + )} +
+
+
+ ); +}; diff --git a/src/pages/auth/ResetPasswordPage.tsx b/src/pages/auth/ResetPasswordPage.tsx new file mode 100644 index 0000000..cf0df5e --- /dev/null +++ b/src/pages/auth/ResetPasswordPage.tsx @@ -0,0 +1,144 @@ +import React, { useMemo, useState } from "react"; +import { useT } from "@ciscode/ui-translate-core"; +import { useLocation, useNavigate } from "react-router-dom"; +import { InputField } from "../../components/actions/InputField"; +import { InlineError } from "../../components/InlineError"; +import { useAuthConfig } from "../../context/AuthConfigContext"; +import { useAuthState } from "../../context/AuthStateContext"; +import { toTailwindColorClasses } from "../../utils/colorHelpers"; + +export const ResetPasswordPage: React.FC = () => { + const t = useT("authLib"); + const navigate = useNavigate(); + const location = useLocation(); + + const { colors, brandName = t("brandName", { defaultValue: "MyBrand" }), logoUrl } = useAuthConfig(); + const { api } = useAuthState(); + + const { bgClass, textClass, borderClass } = toTailwindColorClasses(colors); + const gradientClass = `${bgClass} bg-gradient-to-r from-white/10 via-white/0 to-white/0`; + + const token = useMemo(() => new URLSearchParams(location.search).get("token"), [location.search]); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [pending, setPending] = useState(false); + const [error, setError] = useState(null); + + const minLength = 6; + const valid = token && newPassword.length >= minLength && newPassword === confirmPassword; + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (pending) return; + setError(null); + + if (!token) { + setError(t("ResetPasswordPage.invalidLink", { defaultValue: "Invalid reset link." })); + return; + } + if (newPassword.length < minLength) { + setError( + t("ResetPasswordPage.tooShort", { defaultValue: `Password must be at least ${minLength} characters.` }) + ); + return; + } + if (newPassword !== confirmPassword) { + setError(t("ResetPasswordPage.mismatch", { defaultValue: "Passwords do not match." })); + return; + } + + setPending(true); + try { + await api.post("/api/auth/reset-password", { token, newPassword }); + // On success, show brief confirmation then navigate to login + navigate("/login", { replace: true }); + } catch (err: any) { + const status = err?.response?.status; + if (status === 400 || status === 401 || status === 410) { + setError( + t("ResetPasswordPage.invalidOrExpired", { + defaultValue: "Reset link is invalid or has expired. Request a new one.", + }) + ); + } else { + setError(t("errors.generic", { defaultValue: "Something went wrong. Please try again." })); + } + } finally { + setPending(false); + } + } + + return ( +
+
+
+
+ {logoUrl ? ( + Brand Logo + ) : ( +

{brandName}

+ )} + +
+ +

+ {t("ResetPasswordPage.title", { defaultValue: "Reset your password" })} +

+

+ {t("ResetPasswordPage.subtitle", { defaultValue: "Choose a new password to access your account." })} +

+ + {error && } + + {!token && ( +
+ {t("ResetPasswordPage.invalidLink", { defaultValue: "Invalid reset link." })} +
+ )} + +
+ + + + + +
+
+
+ ); +}; diff --git a/src/pages/auth/SignInPage.tsx b/src/pages/auth/SignInPage.tsx index 460953a..1e0a03b 100644 --- a/src/pages/auth/SignInPage.tsx +++ b/src/pages/auth/SignInPage.tsx @@ -9,7 +9,8 @@ import { useAuthState } from "../../context/AuthStateContext"; import { InlineError } from "../../components/InlineError"; import { AuthConfigProps } from "../../models/AuthConfig"; import { useT } from "@ciscode/ui-translate-core"; -import { useNavigate, useLocation } from "react-router"; +import { useNavigate, useLocation } from "react-router-dom"; +import { Link } from "react-router-dom" export const SignInPage: React.FC = () => { const t = useT("authLib"); @@ -232,9 +233,7 @@ export const SignInPage: React.FC = () => { onChange={setPassword} />
- + {t("SignInPage.forgotPassword")}