Skip to content

Commit 50fad9f

Browse files
authored
Merge pull request #493 from amahuli03/auth-registration-email-activation
[auth] account registration form and email activation
2 parents 38a437d + 9ed73e6 commit 50fad9f

7 files changed

Lines changed: 349 additions & 120 deletions

File tree

config/env/dev.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ SQL_PORT=5432
3232

3333
LOGIN_REDIRECT_URL=
3434
CORS_ALLOWED_ORIGINS=http://localhost:3000
35+
# Domain used by Djoser for activation and password reset email links (should be the frontend URL)
36+
FRONTEND_DOMAIN=localhost:3000
3537
OPENAI_API_KEY=
3638
ANTHROPIC_API_KEY=
3739
PINECONE_API_KEY=

frontend/src/api/endpoints.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export const AUTH_ENDPOINTS = {
1919
USER_ME: `${API_BASE}/auth/users/me/`,
2020
RESET_PASSWORD: `${API_BASE}/auth/users/reset_password/`,
2121
RESET_PASSWORD_CONFIRM: `${API_BASE}/auth/users/reset_password_confirm/`,
22+
USERS_CREATE: `${API_BASE}/auth/users/`,
23+
USERS_ACTIVATION: `${API_BASE}/auth/users/activation/`,
24+
USERS_RESEND_ACTIVATION: `${API_BASE}/auth/users/resend_activation/`,
2225
} as const;
2326

2427
/**
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useEffect, useState } from "react";
2+
import { useParams, Link } from "react-router-dom";
3+
import { useDispatch } from "react-redux";
4+
import { verify, AppDispatch } from "../../services/actions/auth";
5+
import Layout from "../Layout/Layout";
6+
import Spinner from "../../components/LoadingSpinner/LoadingSpinner";
7+
8+
const Activate = () => {
9+
const { uid, token } = useParams<{ uid: string; token: string }>();
10+
const dispatch = useDispatch<AppDispatch>();
11+
const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
12+
13+
useEffect(() => {
14+
if (!uid || !token) {
15+
setStatus("error");
16+
return;
17+
}
18+
19+
(async () => {
20+
try {
21+
await dispatch(verify(uid, token));
22+
setStatus("success");
23+
} catch {
24+
setStatus("error");
25+
}
26+
})();
27+
}, [dispatch, uid, token]);
28+
29+
if (status === "loading") {
30+
return (
31+
<Layout>
32+
<Spinner />
33+
</Layout>
34+
);
35+
}
36+
37+
if (status === "error") {
38+
return (
39+
<Layout>
40+
<section className="mx-auto mt-24 w-[20rem] md:mt-48 md:w-[32rem] text-center">
41+
<div className="mb-4 rounded-md bg-white px-3 pb-12 pt-6 shadow-md ring-1 md:px-12">
42+
<h2 className="blue_gradient mb-4 font-satoshi text-3xl font-bold text-gray-600">
43+
Activation failed
44+
</h2>
45+
<p className="text-gray-600 mb-6">
46+
This activation link is invalid or has already been used. Please register again or request a new activation email.
47+
</p>
48+
<Link to="/register" className="btnBlue w-full text-lg text-center block">
49+
Back to register
50+
</Link>
51+
</div>
52+
</section>
53+
</Layout>
54+
);
55+
}
56+
57+
return (
58+
<Layout>
59+
<section className="mx-auto mt-24 w-[20rem] md:mt-48 md:w-[32rem] text-center">
60+
<div className="mb-4 rounded-md bg-white px-3 pb-12 pt-6 shadow-md ring-1 md:px-12">
61+
<h2 className="blue_gradient mb-4 font-satoshi text-3xl font-bold text-gray-600">
62+
Email verified
63+
</h2>
64+
<p className="text-gray-600 mb-6">
65+
Your account has been activated. You can now log in.
66+
</p>
67+
<Link to="/login" className="btnBlue w-full text-lg text-center block">
68+
Continue to log in
69+
</Link>
70+
</div>
71+
</section>
72+
</Layout>
73+
);
74+
};
75+
76+
export default Activate;
Lines changed: 198 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,211 @@
11
import { useFormik } from "formik";
2+
import * as Yup from "yup";
23
import { Link } from "react-router-dom";
4+
import { useDispatch, useSelector } from "react-redux";
5+
import { signup, AppDispatch } from "../../services/actions/auth";
6+
import { RootState } from "../../services/actions/types";
7+
import { useState } from "react";
8+
import axios from "axios";
9+
import { AUTH_ENDPOINTS } from "../../api/endpoints";
10+
11+
const validationSchema = Yup.object({
12+
first_name: Yup.string().required("First name is required"),
13+
last_name: Yup.string().required("Last name is required"),
14+
email: Yup.string().email("Enter a valid email").required("Email is required"),
15+
password: Yup.string()
16+
.min(8, "Password must be at least 8 characters")
17+
.required("Password is required"),
18+
re_password: Yup.string()
19+
.oneOf([Yup.ref("password")], "Passwords must match")
20+
.required("Please confirm your password"),
21+
});
22+
23+
const RegistrationForm = () => {
24+
const dispatch = useDispatch<AppDispatch>();
25+
const signupError = useSelector((state: RootState) => state.auth.error);
26+
const [submitted, setSubmitted] = useState(false);
27+
const [submittedEmail, setSubmittedEmail] = useState("");
28+
const [resendStatus, setResendStatus] = useState<"idle" | "sent" | "error">("idle");
29+
30+
const { handleSubmit, handleChange, handleBlur, values, errors, touched, isSubmitting } =
31+
useFormik({
32+
initialValues: {
33+
first_name: "",
34+
last_name: "",
35+
email: "",
36+
password: "",
37+
re_password: "",
38+
},
39+
validationSchema,
40+
onSubmit: async (values, { setSubmitting }) => {
41+
try {
42+
await dispatch(signup(values.first_name, values.last_name, values.email, values.password, values.re_password));
43+
setSubmittedEmail(values.email);
44+
setSubmitted(true);
45+
} catch {
46+
// error is stored in Redux state and displayed via signupError
47+
} finally {
48+
setSubmitting(false);
49+
}
50+
},
51+
});
52+
53+
const handleResend = async () => {
54+
try {
55+
await axios.post(AUTH_ENDPOINTS.USERS_RESEND_ACTIVATION, { email: submittedEmail });
56+
setResendStatus("sent");
57+
} catch {
58+
setResendStatus("error");
59+
}
60+
};
61+
62+
if (submitted) {
63+
return (
64+
<section className="mx-auto mt-24 w-[20rem] md:mt-48 md:w-[32rem]">
65+
<div className="mb-4 rounded-md bg-white px-3 pb-12 pt-6 shadow-md ring-1 md:px-12 text-center">
66+
<h2 className="blue_gradient mb-4 font-satoshi text-3xl font-bold text-gray-600">
67+
Check your email
68+
</h2>
69+
<p className="text-gray-600 mb-6">
70+
We sent an activation link to <strong>{submittedEmail}</strong>. Click the link to activate your account.
71+
</p>
72+
<div className="flex flex-col gap-3">
73+
<Link to="/login" className="btnBlue w-full text-lg text-center">
74+
Go to log in
75+
</Link>
76+
<button onClick={handleResend} className="text-sm text-blue-600 hover:underline" type="button">
77+
{resendStatus === "sent"
78+
? "Email resent!"
79+
: resendStatus === "error"
80+
? "Failed to resend. Try again."
81+
: "Resend email"}
82+
</button>
83+
</div>
84+
</div>
85+
</section>
86+
);
87+
}
388

4-
const LoginForm = () => {
5-
const { handleSubmit, handleChange, values } = useFormik({
6-
initialValues: {
7-
email: "",
8-
password: "",
9-
},
10-
onSubmit: (values) => {
11-
console.log("values", values);
12-
// make registration post request here.
13-
},
14-
});
1589
return (
16-
<>
17-
<section className="mt-12 mx-auto w-full max-w-xs">
18-
<h2 className="font-satoshi font-bold text-gray-600 text-xl blue_gradient mb-6">
19-
Register
90+
<section className="mx-auto mt-24 w-[20rem] md:mt-48 md:w-[32rem]">
91+
<form
92+
onSubmit={handleSubmit}
93+
className="mb-4 rounded-md bg-white px-3 pb-12 pt-6 shadow-md ring-1 md:px-12"
94+
>
95+
<h2 className="blue_gradient mb-6 font-satoshi text-3xl font-bold text-gray-600 text-center">
96+
Create account
2097
</h2>
21-
<form
22-
onSubmit={handleSubmit}
23-
className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
24-
<div className="mb-4">
25-
<label
26-
htmlFor="email"
27-
className="block text-gray-700 text-sm font-bold mb-2">
28-
Email
29-
</label>
30-
<input
31-
id="email"
32-
name="email"
33-
type="email"
34-
onChange={handleChange}
35-
value={values.email}
36-
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
37-
/>
38-
</div>
39-
<div className="mb-6">
40-
<label
41-
htmlFor="email"
42-
className="block text-gray-700 text-sm font-bold mb-2">
43-
Password
44-
</label>
45-
<input
46-
id="password"
47-
name="password"
48-
type="password"
49-
onChange={handleChange}
50-
value={values.password}
51-
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
52-
/>
53-
</div>
5498

55-
<button className="black_btn ml-auto block" type="submit">
56-
Register
57-
</button>
58-
</form>
59-
</section>
60-
<p>
99+
{signupError && (
100+
<p className="text-red-500 text-sm mb-4">{signupError}</p>
101+
)}
102+
103+
<div className="mb-4">
104+
<label htmlFor="first_name" className="mb-2 block text-lg font-bold text-gray-700">
105+
First name
106+
</label>
107+
<input
108+
id="first_name"
109+
name="first_name"
110+
type="text"
111+
onChange={handleChange}
112+
onBlur={handleBlur}
113+
value={values.first_name}
114+
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-3 leading-tight text-gray-700 shadow focus:outline-none"
115+
/>
116+
{touched.first_name && errors.first_name && (
117+
<p className="text-red-500 text-sm mt-1">{errors.first_name}</p>
118+
)}
119+
</div>
120+
121+
<div className="mb-4">
122+
<label htmlFor="last_name" className="mb-2 block text-lg font-bold text-gray-700">
123+
Last name
124+
</label>
125+
<input
126+
id="last_name"
127+
name="last_name"
128+
type="text"
129+
onChange={handleChange}
130+
onBlur={handleBlur}
131+
value={values.last_name}
132+
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-3 leading-tight text-gray-700 shadow focus:outline-none"
133+
/>
134+
{touched.last_name && errors.last_name && (
135+
<p className="text-red-500 text-sm mt-1">{errors.last_name}</p>
136+
)}
137+
</div>
138+
139+
<div className="mb-4">
140+
<label htmlFor="email" className="mb-2 block text-lg font-bold text-gray-700">
141+
Email
142+
</label>
143+
<input
144+
id="email"
145+
name="email"
146+
type="email"
147+
onChange={handleChange}
148+
onBlur={handleBlur}
149+
value={values.email}
150+
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-3 leading-tight text-gray-700 shadow focus:outline-none"
151+
/>
152+
{touched.email && errors.email && (
153+
<p className="text-red-500 text-sm mt-1">{errors.email}</p>
154+
)}
155+
</div>
156+
157+
<div className="mb-4">
158+
<label htmlFor="password" className="mb-2 block text-lg font-bold text-gray-700">
159+
Password
160+
</label>
161+
<input
162+
id="password"
163+
name="password"
164+
type="password"
165+
onChange={handleChange}
166+
onBlur={handleBlur}
167+
value={values.password}
168+
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-3 leading-tight text-gray-700 shadow focus:outline-none"
169+
/>
170+
{touched.password && errors.password && (
171+
<p className="text-red-500 text-sm mt-1">{errors.password}</p>
172+
)}
173+
</div>
174+
175+
<div className="mb-6">
176+
<label htmlFor="re_password" className="mb-2 block text-lg font-bold text-gray-700">
177+
Confirm password
178+
</label>
179+
<input
180+
id="re_password"
181+
name="re_password"
182+
type="password"
183+
onChange={handleChange}
184+
onBlur={handleBlur}
185+
value={values.re_password}
186+
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-3 leading-tight text-gray-700 shadow focus:outline-none"
187+
/>
188+
{touched.re_password && errors.re_password && (
189+
<p className="text-red-500 text-sm mt-1">{errors.re_password}</p>
190+
)}
191+
</div>
192+
193+
<button
194+
className="btnBlue w-full text-lg"
195+
type="submit"
196+
disabled={isSubmitting}
197+
>
198+
{isSubmitting ? "Creating account..." : "Create account"}
199+
</button>
200+
</form>
201+
<p className="text-center">
61202
Already have an account?{" "}
62203
<Link to="/login" className="font-bold hover:text-blue-600">
63-
{" "}
64-
Login here.
204+
Log in
65205
</Link>
66206
</p>
67-
</>
207+
</section>
68208
);
69209
};
70210

71-
export default LoginForm;
211+
export default RegistrationForm;

frontend/src/routes/routes.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import RulesManager from "../pages/RulesManager/RulesManager.tsx";
2020
import ManageMeds from "../pages/ManageMeds/ManageMeds.tsx";
2121
import ProtectedRoute from "../components/ProtectedRoute/ProtectedRoute.tsx";
2222
import AdminRoute from "../components/ProtectedRoute/AdminRoute.tsx";
23+
import Activate from "../pages/Activate/Activate.tsx";
2324

2425
const routes = [
2526
{
@@ -49,6 +50,10 @@ const routes = [
4950
path: "register",
5051
element: <RegistrationForm />,
5152
},
53+
{
54+
path: "activate/:uid/:token",
55+
element: <Activate />,
56+
},
5257
{
5358
path: "login",
5459
element: <LoginForm />,

0 commit comments

Comments
 (0)