Compare commits

...

2 Commits

Author SHA1 Message Date
Karan Balani
78c97c670e chore: use v2 2026-01-22 16:50:36 +05:30
Karan Balani
491791f2c4 feat: add frontend for self reset password 2026-01-22 16:50:36 +05:30
10 changed files with 305 additions and 6 deletions

View File

@@ -202,6 +202,10 @@ export const PasswordReset = Loadable(
() => import(/* webpackChunkName: "ResetPassword" */ 'pages/ResetPassword'),
);
export const ForgotPassword = Loadable(
() => import(/* webpackChunkName: "ForgotPassword" */ 'pages/ForgotPassword'),
);
export const SomethingWentWrong = Loadable(
() =>
import(

View File

@@ -17,6 +17,7 @@ import {
DashboardWidget,
EditRulesPage,
ErrorDetails,
ForgotPassword,
Home,
InfrastructureMonitoring,
InstalledIntegrations,
@@ -353,6 +354,13 @@ const routes: AppRoutes[] = [
key: 'PASSWORD_RESET',
isPrivate: false,
},
{
path: ROUTES.FORGOT_PASSWORD,
exact: true,
component: ForgotPassword,
key: 'FORGOT_PASSWORD',
isPrivate: false,
},
{
path: ROUTES.SOMETHING_WENT_WRONG,
exact: true,

View File

@@ -0,0 +1,15 @@
import { ApiV2Instance } from 'api';
interface ForgotPasswordPayload {
orgId: string;
email: string;
frontendBaseURL: string;
}
const forgotPassword = async (
payload: ForgotPasswordPayload,
): Promise<void> => {
await ApiV2Instance.post('/factor_password/forgot', payload);
};
export default forgotPassword;

View File

@@ -50,6 +50,7 @@ const ROUTES = {
LIVE_LOGS: '/logs/logs-explorer/live',
LOGS_PIPELINES: '/logs/pipelines',
PASSWORD_RESET: '/password-reset',
FORGOT_PASSWORD: '/forgot-password',
LIST_LICENSES: '/licenses',
LOGS_INDEX_FIELDS: '/logs-explorer/index-fields',
TRACE_EXPLORER: '/trace-explorer',

View File

@@ -0,0 +1,68 @@
.forgot-password-card {
width: 100%;
max-width: 400px;
}
.forgot-password-header {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin-bottom: 24px;
}
.forgot-password-header-icon {
width: 64px;
height: 64px;
border-radius: 50%;
background: var(--bg-slate-400);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
color: var(--text-vanilla-400);
&--success {
background: var(--bg-forest-400);
color: var(--text-forest-400);
}
}
.forgot-password-header-title {
margin-bottom: 8px !important;
}
.forgot-password-header-subtitle {
color: var(--text-vanilla-400);
margin-bottom: 0 !important;
}
.forgot-password-form-container {
margin-bottom: 16px;
}
.forgot-password-field-container {
margin-bottom: 16px;
}
.forgot-password-form-input {
height: 40px;
}
.forgot-password-warning-callout {
margin-bottom: 16px;
}
.forgot-password-form-actions {
display: flex;
gap: 12px;
justify-content: space-between;
}
.forgot-password-back-button {
flex: 0 0 auto;
}
.forgot-password-submit-button {
flex: 1;
}

View File

@@ -0,0 +1,179 @@
import './ForgotPassword.styles.scss';
import { Button } from '@signozhq/button';
import { Callout } from '@signozhq/callout';
import { Form, Input, Typography } from 'antd';
import forgotPasswordApi from 'api/v2/factor_password/forgotPassword';
import AuthError from 'components/AuthError/AuthError';
import AuthPageContainer from 'components/AuthPageContainer';
import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { ArrowLeft, ArrowRight, CheckCircle, KeyRound } from 'lucide-react';
import { Label } from 'pages/SignUp/styles';
import { useState } from 'react';
import { useLocation } from 'react-use';
import APIError from 'types/api/error';
import { FormContainer } from './styles';
type FormValues = { email: string };
function ForgotPassword(): JSX.Element {
const [loading, setLoading] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [errorMessage, setErrorMessage] = useState<APIError | null>(null);
const { notifications } = useNotifications();
const { search } = useLocation();
const params = new URLSearchParams(search);
const emailFromQuery = params.get('email') || '';
const orgIdFromQuery = params.get('orgId') || '';
const [form] = Form.useForm<FormValues>();
const handleSubmit = async (): Promise<void> => {
try {
setLoading(true);
setErrorMessage(null);
const { email } = form.getFieldsValue();
await forgotPasswordApi({
email: email || emailFromQuery,
orgId: orgIdFromQuery,
frontendBaseURL: window.location.origin,
});
setSubmitted(true);
notifications.success({
message: 'Password reset email sent',
});
} catch (error) {
setErrorMessage(error as APIError);
} finally {
setLoading(false);
}
};
const handleBackToLogin = (): void => {
history.push(ROUTES.LOGIN);
};
// Success state after submission
if (submitted) {
return (
<AuthPageContainer>
<div className="forgot-password-card">
<div className="forgot-password-header">
<div className="forgot-password-header-icon forgot-password-header-icon--success">
<CheckCircle size={32} />
</div>
<Typography.Title level={4} className="forgot-password-header-title">
Check Your Email
</Typography.Title>
<Typography.Paragraph className="forgot-password-header-subtitle">
We have sent a password reset link to your email address. Please check
your inbox and follow the instructions to reset your password.
</Typography.Paragraph>
</div>
<div className="forgot-password-form-actions">
<Button
variant="solid"
color="primary"
onClick={handleBackToLogin}
className="forgot-password-submit-button"
prefixIcon={<ArrowLeft size={16} />}
>
Back to Login
</Button>
</div>
</div>
</AuthPageContainer>
);
}
return (
<AuthPageContainer>
<div className="forgot-password-card">
<div className="forgot-password-header">
<div className="forgot-password-header-icon">
<KeyRound size={32} />
</div>
<Typography.Title level={4} className="forgot-password-header-title">
Forgot Password?
</Typography.Title>
<Typography.Paragraph className="forgot-password-header-subtitle">
Enter your email address and we will send you a link to reset your
password.
</Typography.Paragraph>
</div>
<FormContainer
form={form}
onFinish={handleSubmit}
className="forgot-password-form"
>
<div className="forgot-password-form-container">
<div className="forgot-password-form-fields">
<div className="forgot-password-field-container">
<Label htmlFor="email">Email Address</Label>
<Form.Item
name="email"
initialValue={emailFromQuery}
rules={[
{ required: true, message: 'Please enter your email!' },
{ type: 'email', message: 'Please enter a valid email!' },
]}
>
<Input
type="email"
id="email"
data-testid="email"
placeholder="Enter your email address"
className="forgot-password-form-input"
disabled={!!emailFromQuery}
/>
</Form.Item>
</div>
</div>
</div>
{!orgIdFromQuery && (
<Callout
type="warning"
size="small"
className="forgot-password-warning-callout"
description="Please go back to the login page and enter your email first to reset your password."
/>
)}
{errorMessage && <AuthError error={errorMessage} />}
<div className="forgot-password-form-actions">
<Button
variant="ghost"
onClick={handleBackToLogin}
className="forgot-password-back-button"
prefixIcon={<ArrowLeft size={16} />}
>
Back to Login
</Button>
<Button
variant="solid"
color="primary"
type="submit"
disabled={loading || !orgIdFromQuery}
className="forgot-password-submit-button"
suffixIcon={<ArrowRight size={16} />}
>
{loading ? 'Sending...' : 'Send Reset Link'}
</Button>
</div>
</FormContainer>
</div>
</AuthPageContainer>
);
}
export default ForgotPassword;

View File

@@ -0,0 +1,8 @@
import { Form } from 'antd';
import styled from 'styled-components';
export const FormContainer = styled(Form)`
& .ant-form-item {
margin-bottom: 0px;
}
`;

View File

@@ -1,7 +1,7 @@
import './Login.styles.scss';
import { Button } from '@signozhq/button';
import { Form, Input, Select, Tooltip, Typography } from 'antd';
import { Form, Input, Select, Typography } from 'antd';
import getVersion from 'api/v1/version/get';
import get from 'api/v2/sessions/context/get';
import post from 'api/v2/sessions/email_password/post';
@@ -343,11 +343,19 @@ function Login(): JSX.Element {
<ParentContainer>
<div className="password-label-container">
<Label htmlFor="Password">Password</Label>
<Tooltip title="Ask your admin to reset your password and send you a new invite link">
<Typography.Link className="forgot-password-link">
Forgot password?
</Typography.Link>
</Tooltip>
<Typography.Link
className="forgot-password-link"
onClick={(): void => {
const email = form.getFieldValue('email');
history.push(
`${ROUTES.FORGOT_PASSWORD}?email=${encodeURIComponent(
email,
)}&orgId=${encodeURIComponent(sessionsOrgId)}`,
);
}}
>
Forgot password?
</Typography.Link>
</div>
<FormContainer.Item name="password">
<Input.Password

View File

@@ -0,0 +1,7 @@
import ForgotPasswordContainer from 'container/ForgotPassword';
function ForgotPassword(): JSX.Element {
return <ForgotPasswordContainer />;
}
export default ForgotPassword;

View File

@@ -70,6 +70,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
LOGIN: ['ADMIN', 'EDITOR', 'VIEWER'],
NOT_FOUND: ['ADMIN', 'VIEWER', 'EDITOR'],
PASSWORD_RESET: ['ADMIN', 'EDITOR', 'VIEWER'],
FORGOT_PASSWORD: ['ADMIN', 'EDITOR', 'VIEWER'],
SERVICE_METRICS: ['ADMIN', 'EDITOR', 'VIEWER'],
SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
SIGN_UP: ['ADMIN', 'EDITOR', 'VIEWER'],