Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/components/RequirePermissions.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
108 changes: 108 additions & 0 deletions src/pages/auth/ForgotPasswordPage.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 (
<div className={`flex items-center justify-center min-h-screen p-4 ${gradientClass}`}>
<div className="flex w-full max-w-xl bg-white rounded-2xl shadow-2xl overflow-hidden">
<div className="w-full p-6 md:p-8">
<div className="flex items-center justify-between mb-6">
{logoUrl ? (
<img
loading="lazy"
src={logoUrl}
alt="Brand Logo"
className={`h-10 rounded-lg border ${borderClass}`}
/>
) : (
<h2 className="text-xl font-bold">{brandName}</h2>
)}
<button type="button" onClick={() => navigate("/login")} className={`text-sm ${textClass}`}>
{t("ForgotPasswordPage.backToLogin", { defaultValue: "Back to Sign In" })}
</button>
</div>

<h1 className="text-2xl md:text-3xl font-bold text-gray-800">
{t("ForgotPasswordPage.title", { defaultValue: "Forgot your password?" })}
</h1>
<p className="mt-2 text-sm text-gray-600">
{t("ForgotPasswordPage.subtitle", { defaultValue: "Enter your email to receive a reset link." })}
</p>

{error && <InlineError message={error} />}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace detected at end of line. Remove the extra space after the closing JSX tag.

Suggested change
{error && <InlineError message={error} />}
{error && <InlineError message={error} />}

Copilot uses AI. Check for mistakes.

{sent ? (
<div className="mt-6 rounded-lg border border-green-300 bg-green-50 p-4 text-green-800 text-sm">
{t("ForgotPasswordPage.sent", {
defaultValue: "If the email exists, we’ve sent a reset link. Please check your inbox."
})}
</div>
) : (
<form className="space-y-6 mt-4" onSubmit={handleSubmit}>
<InputField
label={t("form.emailLabel")}
type="email"
placeholder={t("form.emailPlaceholder")}
color={borderClass}
value={email}
onChange={setEmail}
/>
<button
type="submit"
disabled={pending || !email}
className={`relative flex w-full items-center justify-center gap-2 py-3 rounded-lg font-medium transition-colors ${
pending ? "opacity-60 cursor-not-allowed" : ""
} ${bgClass} text-white`}
>
{pending && (
<svg className="h-4 w-4 animate-spin stroke-current" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" strokeWidth="4" />
<path className="opacity-75" d="M4 12a8 8 0 018-8" strokeWidth="4" strokeLinecap="round" />
</svg>
)}
{t("ForgotPasswordPage.sendLink", { defaultValue: "Send Reset Link" })}
</button>
</form>
)}
</div>
</div>
</div>
);
};
144 changes: 144 additions & 0 deletions src/pages/auth/ResetPasswordPage.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 (
<div className={`flex items-center justify-center min-h-screen p-4 ${gradientClass}`}>
<div className="flex w-full max-w-xl bg-white rounded-2xl shadow-2xl overflow-hidden">
<div className="w-full p-6 md:p-8">
<div className="flex items-center justify-between mb-6">
{logoUrl ? (
<img
loading="lazy"
src={logoUrl}
alt="Brand Logo"
className={`h-10 rounded-lg border ${borderClass}`}
/>
) : (
<h2 className="text-xl font-bold">{brandName}</h2>
)}
<button type="button" onClick={() => navigate("/login")} className={`text-sm ${textClass}`}>
{t("ResetPasswordPage.backToLogin", { defaultValue: "Back to Sign In" })}
</button>
</div>

<h1 className="text-2xl md:text-3xl font-bold text-gray-800">
{t("ResetPasswordPage.title", { defaultValue: "Reset your password" })}
</h1>
<p className="mt-2 text-sm text-gray-600">
{t("ResetPasswordPage.subtitle", { defaultValue: "Choose a new password to access your account." })}
</p>

{error && <InlineError message={error} />}

{!token && (
<div className="mt-6 rounded-lg border border-red-300 bg-red-50 p-4 text-red-800 text-sm">
{t("ResetPasswordPage.invalidLink", { defaultValue: "Invalid reset link." })}
</div>
)}

<form className="space-y-6 mt-4" onSubmit={handleSubmit}>
<InputField
label={t("form.passwordLabel")}
type="password"
placeholder={t("form.passwordPlaceholder")}
color={borderClass}
value={newPassword}
onChange={setNewPassword}
/>
<InputField
label={t("ResetPasswordPage.confirmLabel", { defaultValue: "Confirm Password" })}
type="password"
placeholder={t("ResetPasswordPage.confirmPlaceholder", { defaultValue: "Re-enter your password" })}
color={borderClass}
value={confirmPassword}
onChange={setConfirmPassword}
/>

<button
type="submit"
disabled={pending || !valid}
className={`relative flex w-full items-center justify-center gap-2 py-3 rounded-lg font-medium transition-colors ${
pending ? "opacity-60 cursor-not-allowed" : ""
} ${bgClass} text-white`}
>
{pending && (
<svg className="h-4 w-4 animate-spin stroke-current" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" strokeWidth="4" />
<path className="opacity-75" d="M4 12a8 8 0 018-8" strokeWidth="4" strokeLinecap="round" />
</svg>
)}
{t("ResetPasswordPage.submit", { defaultValue: "Reset Password" })}
</button>
</form>
</div>
</div>
</div>
);
};
7 changes: 3 additions & 4 deletions src/pages/auth/SignInPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon at end of import statement. Add semicolon for consistency with other imports in the file.

Suggested change
import { Link } from "react-router-dom"
import { Link } from "react-router-dom";

Copilot uses AI. Check for mistakes.

export const SignInPage: React.FC<AuthConfigProps> = () => {
const t = useT("authLib");
Expand Down Expand Up @@ -232,9 +233,7 @@ export const SignInPage: React.FC<AuthConfigProps> = () => {
onChange={setPassword}
/>
<div className="ltr:text-right rtl:text-left">
<button className={`text-sm ${textClass}`}>
{t("SignInPage.forgotPassword")}
</button>
<Link to="/forgot-password" className={textClass}>{t("SignInPage.forgotPassword")}</Link>
</div>
<button
type="submit"
Expand Down
2 changes: 1 addition & 1 deletion src/pages/auth/SignUpPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useAuthConfig } from "../../context/AuthConfigContext";
import { useAuthState } from "../../context/AuthStateContext";
import { InlineError } from "../../components/InlineError";
import { useT } from "@ciscode/ui-translate-core";
import { useNavigate, useLocation } from "react-router";
import { useNavigate, useLocation } from "react-router-dom";

export const SignUpPage: React.FC = () => {
const t = useT("authLib");
Expand Down
8 changes: 7 additions & 1 deletion src/providers/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import { attachAuthInterceptor, resetSessionFlag } from '../utils/attachAuthInte
import { SessionExpiredModal } from '../components/SessionExpiredModal';
import { SignInPage } from '../pages/auth/SignInPage';
import { SignUpPage } from '../pages/auth/SignUpPage';
import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router";
import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom";
import { GoogleCallbackPage } from "../pages/auth/GoogleCallbackPage";
import { ForgotPasswordPage } from "../pages/auth/ForgotPasswordPage";
import { ResetPasswordPage } from "../pages/auth/ResetPasswordPage";

interface Props {
config: AuthConfigProps;
Expand Down Expand Up @@ -191,6 +193,10 @@ export const AuthProvider: React.FC<Props> = ({ config, children }) => {
}
/>

{/* public forgot/reset password routes */}
<Route path="forgot-password" element={<ForgotPasswordPage />} />
<Route path="reset-password" element={<ResetPasswordPage />} />

{/* Google OAuth callback route */}
<Route
path="oauth/google/callback"
Expand Down
17 changes: 16 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default defineConfig({
entry: resolve(__dirname, "src/index.ts"),
name: "ciscode-model",
fileName: "index",
formats: ["es", "umd"],
},
rollupOptions: {
external: [
Expand All @@ -17,8 +18,22 @@ export default defineConfig({
"react-cookie",
"axios",
"jwt-decode",
"@ciscode/ui-translate-core"
"@ciscode/ui-translate-core",
"lucide-react",
],
output: {
globals: {
react: "React",
"react-dom": "ReactDOM",
"react-router": "ReactRouter",
"react-router-dom": "ReactRouterDOM",
"react-cookie": "ReactCookie",
axios: "axios",
"jwt-decode": "jwt_decode",
"@ciscode/ui-translate-core": "CISCODETranslateCore",
"lucide-react": "LucideReact",
},
},
},
},
});