Skip to content

Commit

Permalink
refactor: authentication service and UI improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
atrincas committed Jan 7, 2025
1 parent 6bc2e95 commit 666a133
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 56 deletions.
1 change: 1 addition & 0 deletions client/src/components/button/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const THEME = {
transparent: 'text-grey-0 bg-transparent',
outline: 'text-grey-0 bg-transparent border border-grey-0',
green: 'text-grey-0 bg-green-0 border border-green-0 hover:bg-green-0/90 active:bg-green-0/75',
'green-alt':
'text-grey-0 bg-transparent border border-green-0 hover:bg-grey-0/5 active:bg-grey-0/10',
Expand Down
1 change: 1 addition & 0 deletions client/src/components/button/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LinkProps } from 'next/link';
export interface AnchorButtonProps {
theme?:
| 'transparent'
| 'outline'
| 'green'
| 'green-alt'
| 'blue'
Expand Down
85 changes: 85 additions & 0 deletions client/src/containers/auth/change-password/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { FC, useCallback } from 'react';

import { Form as FormRFF, Field as FieldRFF } from 'react-final-form';

import { useRouter } from 'next/navigation';

import { FORM_ERROR } from 'final-form';

import { AuthWrapper } from 'containers/wrapper/component';

import LinkButton, { Button } from 'components/button/component';
import Input from 'components/forms/input/component';

import authenticationService from 'services/authentication';

const ChangePassword: FC<{ token: string | undefined }> = ({ token }) => {
const router = useRouter();

const handleFormSubmit = useCallback(
async ({
password,
passwordConfirmation,
}: {
password: string;
passwordConfirmation: string;
}) => {
try {
const res = await authenticationService.changePassword(
token,
password,
passwordConfirmation
);

if (res?.status === 200) {
router.push('/auth/signin');
} else {
throw new Error('Failed to change password');
}
} catch (err) {
return {
[FORM_ERROR]: err instanceof Error ? err.message : 'An unexpected error occurred',
};
}
},
[router, token]
);

return (
<AuthWrapper>
<FormRFF
onSubmit={handleFormSubmit}
initialValues={{ password: '', passwordConfirmation: '' }}
>
{({ submitError, handleSubmit }) => (
<form className="space-y-5" onSubmit={handleSubmit} autoComplete="off">
<h2 className="text-3xl text-center font-normal">Change password</h2>
<div>
<label>New password</label>
<FieldRFF name="password" type="password">
{({ input }) => <Input {...input} />}
</FieldRFF>
</div>
<div>
<label>Confirm password</label>
<FieldRFF name="passwordConfirmation" type="password">
{({ input }) => <Input {...input} />}
</FieldRFF>
</div>
{submitError && <div className="text-red-0">{submitError}</div>}
<div className="flex justify-between gap-3">
<LinkButton theme="outline" className="flex-1" href="/auth/signin">
Cancel
</LinkButton>
<Button theme="green" size="base" className="flex-1" type="submit">
<span>Reset password</span>
</Button>
</div>
</form>
)}
</FormRFF>
</AuthWrapper>
);
};

export default ChangePassword;
8 changes: 8 additions & 0 deletions client/src/containers/wrapper/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ export interface WrapperProps {
children: ReactNode;
}

export const AuthWrapper = ({ children }: WrapperProps) => {
return (
<div className="justify-center items-center flex grow">
<div className="bg-white px-40 py-20">{children}</div>
</div>
);
};

const Wrapper = ({ children }: WrapperProps) => {
return (
<div
Expand Down
9 changes: 2 additions & 7 deletions client/src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { NextAuthOptions } from 'next-auth';
import { JWT } from 'next-auth/jwt';
import Credentials from 'next-auth/providers/credentials';

import AUTHENTICATION from 'services/authentication';
import authenticationService from 'services/authentication';

const MAX_AGE = 2 * 60 * 60; // 2 hours

Expand Down Expand Up @@ -40,12 +40,7 @@ export const authOptions: NextAuthOptions = {
const { email, password } = credentials;

try {
const { data } = await AUTHENTICATION.request({
url: '/sign_in',
method: 'POST',
data: { email, password },
headers: { 'Content-Type': 'application/json' },
});
const { data } = await authenticationService.signIn(email, password);

return data;
} catch (err) {
Expand Down
11 changes: 11 additions & 0 deletions client/src/pages/auth/change-password/[token]/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useParams } from 'next/navigation';

import ChangePassword from 'containers/auth/change-password';

const ChangePasswordPage = () => {
const params = useParams<{ token: string } | null>();

return <ChangePassword token={params?.token} />;
};

export default ChangePasswordPage;
79 changes: 79 additions & 0 deletions client/src/pages/auth/forgot-password.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useCallback } from 'react';

import { Form as FormRFF, Field as FieldRFF, useFormState } from 'react-final-form';

import { FORM_ERROR } from 'final-form';

import { AuthWrapper } from 'containers/wrapper/component';

import Button from 'components/button';
import LinkButton from 'components/button/component';
import { Input } from 'components/forms';

import authenticationService from 'services/authentication';

const SuccessMessage = () => {
const formState = useFormState();
const emailValue = formState.values?.email;

return (
<div className="flex flex-col items-center gap-5">
<h2 className="text-3xl font-normal">Check your inbox</h2>
<p className="text-center">
We&apos;ve sent a link to {emailValue}. Please check your email to reset your password. If
you don&apos;t see it, check your spam or junk folder.
</p>
<LinkButton theme="green" size="base" href="/auth/signin">
Ok
</LinkButton>
</div>
);
};

const ForgotPasswordPage = () => {
const handleFormSubmit = useCallback(async ({ email }: { email: string }) => {
try {
await authenticationService.forgotPassword(email);
} catch (err) {
return {
[FORM_ERROR]: err instanceof Error ? err.message : 'An unexpected error occurred',
};
}
}, []);

return (
<AuthWrapper>
<FormRFF onSubmit={handleFormSubmit} initialValues={{ email: '' }}>
{({ submitError, handleSubmit, submitSucceeded }) => (
<>
{submitSucceeded ? (
<SuccessMessage />
) : (
<form className="space-y-5 mb-10" onSubmit={handleSubmit} autoComplete="off">
<h2 className="text-3xl text-center font-normal">Forgot password?</h2>
<p>Enter your email and we&apos;ll send you a link back to your account.</p>
<div>
<label>Email</label>
<FieldRFF name="email" type="email">
{({ input }) => <Input {...input} />}
</FieldRFF>
</div>
{submitError && <div className="text-red-0">{submitError}</div>}
<div className="flex justify-between gap-3">
<LinkButton theme="outline" className="flex-1" href="/auth/signin">
Cancel
</LinkButton>
<Button theme="green" size="base" className="flex-1" type="submit">
Send link
</Button>
</div>
</form>
)}
</>
)}
</FormRFF>
</AuthWrapper>
);
};

export default ForgotPasswordPage;
85 changes: 41 additions & 44 deletions client/src/pages/auth/signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { FORM_ERROR } from 'final-form';
import { getServerSession } from 'next-auth/next';
import { signIn } from 'next-auth/react';

import { AuthWrapper } from 'containers/wrapper/component';

import Button from 'components/button';
import { Input } from 'components/forms/input/component';
import { authOptions } from 'pages/api/auth/[...nextauth]';
Expand Down Expand Up @@ -40,52 +42,47 @@ const SignInPage: React.FC = () => {
);

return (
<div className="justify-center items-center flex grow">
<div className="bg-white px-40 py-20">
<FormRFF
onSubmit={handleFormSubmit}
initialValues={{ email: '[email protected]', password: 'NP!-.9Q*csfnuJXDDchj' }}
<AuthWrapper>
<FormRFF onSubmit={handleFormSubmit} initialValues={{ email: '', password: '' }}>
{({ submitError, handleSubmit }) => (
<form className="space-y-5 mb-10" onSubmit={handleSubmit} autoComplete="off">
<h2 className="text-3xl text-center font-normal">Log in</h2>
<div>
<label>Username</label>
<FieldRFF name="email" type="email">
{({ input }) => <Input {...input} />}
</FieldRFF>
</div>
<div>
<label>Password</label>
<FieldRFF name="password" type="password">
{({ input }) => <Input {...input} />}
</FieldRFF>
<Link href="/auth/forgot-password" className="text-sm text-green-0 hover:underline">
Forgot password?
</Link>
</div>
{submitError && <div className="text-red-0">{submitError}</div>}
<div className="flex justify-center">
<Button theme="green" size="base" type="submit">
Connect
</Button>
</div>
</form>
)}
</FormRFF>
<div className="flex gap-4">
<p>Don’t have an account, but you are a FORA member?</p>
<Link
href="https://forainitiative.org/contact/"
target="_blank"
rel="noopener noreferrer"
className="text-base text-green-0 hover:underline"
>
{({ submitError, handleSubmit }) => (
<form className="space-y-5 mb-10" onSubmit={handleSubmit} autoComplete="off">
<h2 className="text-3xl text-center font-normal">Log in</h2>
<div>
<label>Username</label>
<FieldRFF name="email" type="email">
{({ input }) => <Input {...input} />}
</FieldRFF>
</div>
<div>
<label>Password</label>
<FieldRFF name="password" type="password">
{({ input }) => <Input {...input} />}
</FieldRFF>
<Link href="/auth/forgot-password" className="text-sm text-green-0 hover:underline">
Forgot password?
</Link>
</div>
{submitError && <div className="text-red-0">{submitError}</div>}
<div className="flex justify-center">
<Button theme="green" size="base" type="submit">
Connect
</Button>
</div>
</form>
)}
</FormRFF>
<div className="flex gap-4">
<p>Don’t have an account, but you are a FORA member?</p>
<Link
href="https://forainitiative.org/contact/"
target="_blank"
rel="noopener noreferrer"
className="text-base text-green-0 hover:underline"
>
Contact us
</Link>
</div>
Contact us
</Link>
</div>
</div>
</AuthWrapper>
);
};

Expand Down
11 changes: 11 additions & 0 deletions client/src/pages/auth/signup/[token]/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useParams } from 'next/navigation';

import ChangePassword from 'containers/auth/change-password';

const SignupPage = () => {
const params = useParams<{ token: string } | null>();

return <ChangePassword token={params?.token} />;
};

export default SignupPage;
Loading

0 comments on commit 666a133

Please sign in to comment.