Add forgot password

pull/3813/head
Kamran Ahmed 2 years ago
parent 82334bbcae
commit 8b285cc600
  1. 29
      src/components/AuthenticationFlow/EmailLoginForm.tsx
  2. 104
      src/components/Profile/ForgotPasswordForm.tsx
  3. 1
      src/hooks/use-auth.ts
  4. 124
      src/lib/http.ts
  5. 13
      src/pages/forgot-password.astro
  6. 9
      src/pages/login.astro

@ -16,7 +16,7 @@ const EmailLoginForm: FunctionComponent<{}> = () => {
setIsLoading(true);
setError('');
const res = await fetch(`${import.meta.env.PUBLIC_API_URL}/v1-login`, {
fetch(`${import.meta.env.PUBLIC_API_URL}/v1-login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -25,27 +25,32 @@ const EmailLoginForm: FunctionComponent<{}> = () => {
email,
password,
}),
});
const json = await res.json();
if (json.type === 'user_not_verified') {
})
.then((res) => res.json())
.then((data) => {
if (data.type === 'user_not_verified') {
window.location.href = `/verification-pending?email=${encodeURIComponent(
email
)}`;
return;
}
if (!json.token) {
if (!data.token) {
setIsLoading(false);
setError(json.message || 'Something went wrong. Please try again later.');
setError(
data.message || 'Something went wrong. Please try again later.'
);
return;
}
// If the response is ok, we'll set the token in a cookie
Cookies.set(TOKEN_COOKIE_NAME, json.token);
Cookies.set(TOKEN_COOKIE_NAME, data.token);
window.location.href = '/';
})
.catch((err) => {
setIsLoading(false);
setError('Something went wrong. Please try again later.');
});
};
return (
@ -86,7 +91,9 @@ const EmailLoginForm: FunctionComponent<{}> = () => {
</a>
</p>
{error && <p className="bg-red-100 text-red-800">{error}</p>}
{error && (
<p className="mb-2 rounded-md bg-red-100 p-2 text-red-800">{error}</p>
)}
<button
type="submit"

@ -1,112 +1,64 @@
import { useState } from 'preact/hooks';
import Spinner from '../Spinner';
import { callPostApi } from '../../lib/http';
export default function ForgotPasswordForm() {
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState<{
type: 'error' | 'success' | 'info';
message: string;
}>();
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const handleSubmit = async (e: Event) => {
e.preventDefault();
setIsLoading(true);
setError('');
const previousLocalStorageValue = localStorage.getItem(
'forgot-password-send-at'
);
if (previousLocalStorageValue) {
if (new Date().getTime() < Number(previousLocalStorageValue)) {
setIsLoading(false);
return setMessage({
type: 'error',
message: 'Please wait a few seconds before trying again.',
});
}
}
const res = await fetch('http://localhost:8080/v1-forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
const data = await res.json();
if (!res.ok) {
setIsLoading(false);
return setMessage({
type: 'error',
message: data.message,
});
const { response, error } = await callPostApi(
`${import.meta.env.PUBLIC_API_URL}/v1-forgot-password`,
{
email,
}
);
setIsLoading(false);
if (error) {
setError(error.message);
} else {
setEmail('');
setMessage({
type: 'success',
message: 'Check your email for a link to reset your password.',
});
// TODO: Set a time in localStorage to prevent spamming the API (now + 5s)
const now = new Date();
const time = now.getTime();
const expireTime = time + 5 * 1000;
now.setTime(expireTime);
localStorage.setItem('forgot-password-send-at', now.getTime().toString());
setSuccess('Check your email for a link to reset your password.');
}
};
return (
<form className="mx-auto max-w-md" onSubmit={handleSubmit}>
<h2 className="text-3xl font-bold sm:text-center sm:text-4xl">
Forgot password?
</h2>
<p className="mt-2 mb-6 sm:text-center">
Enter the email address associated with your account, and we'll email
you a link to reset your password.
</p>
<label
for="email"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
>
Email
</label>
<form onSubmit={handleSubmit} class="w-full">
<input
type="email"
name="email"
id="email"
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required
placeholder="john@example.com"
placeholder="Email Address"
value={email}
onInput={(e) => setEmail((e.target as HTMLInputElement).value)}
/>
{message && (
<div
className={`mt-2 rounded-lg p-2 ${
message.type === 'error'
? 'bg-red-100 text-red-700'
: message.type === 'success'
? 'bg-green-100 text-green-700'
: 'bg-blue-100 text-blue-700'
}`}
>
{message.message}
</div>
{error && (
<p className="mt-2 rounded-lg bg-red-100 p-2 text-sm text-red-700">
{error}
</p>
)}
{success && (
<p className="mt-2 rounded-lg bg-green-100 p-2 text-sm text-green-700">
{success}
</p>
)}
<button
type="submit"
disabled={isLoading}
className="mt-2 inline-flex h-10 w-full items-center justify-center rounded-lg border border-slate-300 bg-black p-2 px-4 text-sm font-medium text-white outline-none transition duration-150 ease-in-out focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
className="mt-3 inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
>
{isLoading ? <Spinner className="text-white" /> : 'Continue'}
{isLoading ? 'Please wait...' : 'Continue'}
</button>
</form>
);

@ -10,6 +10,7 @@ export const useAuth = () => {
useEffect(() => {
const token = Cookies.get(TOKEN_COOKIE_NAME);
const payload = token ? decodeToken(token) : null;
setUser(payload);
setIsLoading(false);
}, []);

@ -0,0 +1,124 @@
import Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME } from './constants';
type AppResponse = Record<string, any>;
type ApiReturn<ResponseType> = {
response?: ResponseType;
error?: {
status: number;
message: string;
errors?: { message: string; location: string }[];
};
};
/**
* Wrapper around fetch to make it easy to handle errors
*
* @param url
* @param options
*/
export async function callApi<ResponseType = AppResponse>(
url: string,
options?: RequestInit
): Promise<ApiReturn<ResponseType>> {
try {
const response = await fetch(url, {
credentials: 'include',
...options,
headers: new Headers({
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${Cookies.get(TOKEN_COOKIE_NAME)}`,
...(options?.headers ?? {}),
}),
});
const data = await response.json();
if (response.ok) {
return {
response: data as ResponseType,
error: undefined,
};
}
return {
response: undefined,
error: {
status: response.status,
message: data.message || 'Something went wrong. Please try again later.',
errors: data.errors,
},
};
} catch (error: any) {
return {
response: undefined,
error: {
status: 0,
message: error.message,
errors: [],
},
};
}
}
export async function callPostApi<ResponseType = AppResponse>(
url: string,
body: Record<string, any>,
options?: RequestInit
): Promise<ApiReturn<ResponseType>> {
return callApi<ResponseType>(url, {
...options,
method: 'POST',
body: JSON.stringify(body),
});
}
export async function callGetApi<ResponseType = AppResponse>(
url: string,
queryParams?: Record<string, any>,
options?: RequestInit
): Promise<ApiReturn<ResponseType>> {
const searchParams = new URLSearchParams(queryParams).toString();
const queryUrl = searchParams ? `${url}?${searchParams}` : url;
return callApi<ResponseType>(queryUrl, {
credentials: 'include',
method: 'GET',
...options,
});
}
export async function callPatchApi<ResponseType = AppResponse>(
url: string,
body: Record<string, any>,
options?: RequestInit
): Promise<ApiReturn<ResponseType>> {
return callApi<ResponseType>(url, {
...options,
method: 'PATCH',
body: JSON.stringify(body),
});
}
export async function callPutApi<ResponseType = AppResponse>(
url: string,
body: Record<string, any>,
options?: RequestInit
): Promise<ApiReturn<ResponseType>> {
return callApi<ResponseType>(url, {
...options,
method: 'PUT',
body: JSON.stringify(body),
});
}
export async function callDeleteApi<ResponseType = AppResponse>(
url: string,
options?: RequestInit
): Promise<ApiReturn<ResponseType>> {
return callApi<ResponseType>(url, {
...options,
method: 'DELETE',
});
}

@ -4,7 +4,17 @@ import SettingLayout from '../layouts/SettingLayout.astro';
---
<SettingLayout title='Forgot Password'>
<div class='container py-16'>
<div class='container'>
<div
class='mx-auto flex flex-col items-start justify-start pb-28 pt-10 sm:max-w-[400px] sm:items-center sm:justify-center sm:pt-20'
>
<div class='mb-2 text-left sm:mb-5 sm:text-center'>
<h1 class='mb-2 text-3xl font-semibold sm:mb-5 sm:text-5xl'>Forgot Password?</h1>
<p class='mb-3 text-base leading-6 text-gray-600'>
Enter your email address below and we will send you a link to reset your password.
</p>
</div>
<ForgotPasswordForm client:load />
<div class='mt-6 text-center text-sm'>
@ -15,4 +25,5 @@ import SettingLayout from '../layouts/SettingLayout.astro';
>
</div>
</div>
</div>
</SettingLayout>

@ -13,15 +13,10 @@ import SettingLayout from '../layouts/SettingLayout.astro';
noIndex={true}
>
<div class='container'>
<div
class='mx-auto flex flex-col items-start justify-start pb-28 pt-10 sm:max-w-[400px] sm:items-center sm:justify-center sm:pt-20'
>
<div class='mx-auto flex flex-col items-start justify-start pb-28 pt-10 sm:max-w-[400px] sm:items-center sm:justify-center sm:pt-20'>
<div class='mb-2 text-left sm:mb-5 sm:text-center'>
<h1 class='mb-2 text-3xl font-semibold sm:mb-5 sm:text-5xl'>Login</h1>
<p class='text-base hidden text-gray-600 sm:block leading-6 mb-3'>
Welcome back! Let's take you to your account.
</p>
<p class='block text-sm text-gray-600 mb-3 sm:hidden'>
<p class='text-base text-gray-600 leading-6 mb-3'>
Welcome back! Let's take you to your account.
</p>
</div>

Loading…
Cancel
Save