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
2 changes: 2 additions & 0 deletions config/env/dev.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ SQL_PORT=5432

LOGIN_REDIRECT_URL=
CORS_ALLOWED_ORIGINS=http://localhost:3000
# Domain used by Djoser for activation and password reset email links (should be the frontend URL)
FRONTEND_DOMAIN=localhost:3000
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
PINECONE_API_KEY=
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export const AUTH_ENDPOINTS = {
USER_ME: `${API_BASE}/auth/users/me/`,
RESET_PASSWORD: `${API_BASE}/auth/users/reset_password/`,
RESET_PASSWORD_CONFIRM: `${API_BASE}/auth/users/reset_password_confirm/`,
USERS_CREATE: `${API_BASE}/auth/users/`,
USERS_ACTIVATION: `${API_BASE}/auth/users/activation/`,
USERS_RESEND_ACTIVATION: `${API_BASE}/auth/users/resend_activation/`,
} as const;

/**
Expand Down
76 changes: 76 additions & 0 deletions frontend/src/pages/Activate/Activate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useEffect, useState } from "react";
import { useParams, Link } from "react-router-dom";
import { useDispatch } from "react-redux";
import { verify, AppDispatch } from "../../services/actions/auth";
import Layout from "../Layout/Layout";
import Spinner from "../../components/LoadingSpinner/LoadingSpinner";

const Activate = () => {
const { uid, token } = useParams<{ uid: string; token: string }>();
const dispatch = useDispatch<AppDispatch>();
const [status, setStatus] = useState<"loading" | "success" | "error">("loading");

useEffect(() => {
if (!uid || !token) {
setStatus("error");
return;
}

(async () => {
try {
await dispatch(verify(uid, token));
setStatus("success");
} catch {
setStatus("error");
}
})();
}, [dispatch, uid, token]);

if (status === "loading") {
return (
<Layout>
<Spinner />
</Layout>
);
}

if (status === "error") {
return (
<Layout>
<section className="mx-auto mt-24 w-[20rem] md:mt-48 md:w-[32rem] text-center">
<div className="mb-4 rounded-md bg-white px-3 pb-12 pt-6 shadow-md ring-1 md:px-12">
<h2 className="blue_gradient mb-4 font-satoshi text-3xl font-bold text-gray-600">
Activation failed
</h2>
<p className="text-gray-600 mb-6">
This activation link is invalid or has already been used. Please register again or request a new activation email.
</p>
<Link to="/register" className="btnBlue w-full text-lg text-center block">
Back to register
</Link>
</div>
</section>
</Layout>
);
}

return (
<Layout>
<section className="mx-auto mt-24 w-[20rem] md:mt-48 md:w-[32rem] text-center">
<div className="mb-4 rounded-md bg-white px-3 pb-12 pt-6 shadow-md ring-1 md:px-12">
<h2 className="blue_gradient mb-4 font-satoshi text-3xl font-bold text-gray-600">
Email verified
</h2>
<p className="text-gray-600 mb-6">
Your account has been activated. You can now log in.
</p>
<Link to="/login" className="btnBlue w-full text-lg text-center block">
Continue to log in
</Link>
</div>
</section>
</Layout>
);
};

export default Activate;
256 changes: 198 additions & 58 deletions frontend/src/pages/Register/RegistrationForm.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,211 @@
import { useFormik } from "formik";
import * as Yup from "yup";
import { Link } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { signup, AppDispatch } from "../../services/actions/auth";
import { RootState } from "../../services/actions/types";
import { useState } from "react";
import axios from "axios";
import { AUTH_ENDPOINTS } from "../../api/endpoints";

const validationSchema = Yup.object({
first_name: Yup.string().required("First name is required"),
last_name: Yup.string().required("Last name is required"),
email: Yup.string().email("Enter a valid email").required("Email is required"),
password: Yup.string()
.min(8, "Password must be at least 8 characters")
.required("Password is required"),
re_password: Yup.string()
.oneOf([Yup.ref("password")], "Passwords must match")
.required("Please confirm your password"),
});

const RegistrationForm = () => {
const dispatch = useDispatch<AppDispatch>();
const signupError = useSelector((state: RootState) => state.auth.error);
const [submitted, setSubmitted] = useState(false);
const [submittedEmail, setSubmittedEmail] = useState("");
const [resendStatus, setResendStatus] = useState<"idle" | "sent" | "error">("idle");

const { handleSubmit, handleChange, handleBlur, values, errors, touched, isSubmitting } =
useFormik({
initialValues: {
first_name: "",
last_name: "",
email: "",
password: "",
re_password: "",
},
validationSchema,
onSubmit: async (values, { setSubmitting }) => {
try {
await dispatch(signup(values.first_name, values.last_name, values.email, values.password, values.re_password));
setSubmittedEmail(values.email);
setSubmitted(true);
} catch {
// error is stored in Redux state and displayed via signupError
} finally {
setSubmitting(false);
}
},
});

const handleResend = async () => {
try {
await axios.post(AUTH_ENDPOINTS.USERS_RESEND_ACTIVATION, { email: submittedEmail });
setResendStatus("sent");
} catch {
setResendStatus("error");
}
};

if (submitted) {
return (
<section className="mx-auto mt-24 w-[20rem] md:mt-48 md:w-[32rem]">
<div className="mb-4 rounded-md bg-white px-3 pb-12 pt-6 shadow-md ring-1 md:px-12 text-center">
<h2 className="blue_gradient mb-4 font-satoshi text-3xl font-bold text-gray-600">
Check your email
</h2>
<p className="text-gray-600 mb-6">
We sent an activation link to <strong>{submittedEmail}</strong>. Click the link to activate your account.
</p>
<div className="flex flex-col gap-3">
<Link to="/login" className="btnBlue w-full text-lg text-center">
Go to log in
</Link>
<button onClick={handleResend} className="text-sm text-blue-600 hover:underline" type="button">
{resendStatus === "sent"
? "Email resent!"
: resendStatus === "error"
? "Failed to resend. Try again."
: "Resend email"}
</button>
</div>
</div>
</section>
);
}

const LoginForm = () => {
const { handleSubmit, handleChange, values } = useFormik({
initialValues: {
email: "",
password: "",
},
onSubmit: (values) => {
console.log("values", values);
// make registration post request here.
},
});
return (
<>
<section className="mt-12 mx-auto w-full max-w-xs">
<h2 className="font-satoshi font-bold text-gray-600 text-xl blue_gradient mb-6">
Register
<section className="mx-auto mt-24 w-[20rem] md:mt-48 md:w-[32rem]">
<form
onSubmit={handleSubmit}
className="mb-4 rounded-md bg-white px-3 pb-12 pt-6 shadow-md ring-1 md:px-12"
>
<h2 className="blue_gradient mb-6 font-satoshi text-3xl font-bold text-gray-600 text-center">
Create account
</h2>
<form
onSubmit={handleSubmit}
className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<div className="mb-4">
<label
htmlFor="email"
className="block text-gray-700 text-sm font-bold mb-2">
Email
</label>
<input
id="email"
name="email"
type="email"
onChange={handleChange}
value={values.email}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
/>
</div>
<div className="mb-6">
<label
htmlFor="email"
className="block text-gray-700 text-sm font-bold mb-2">
Password
</label>
<input
id="password"
name="password"
type="password"
onChange={handleChange}
value={values.password}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
/>
</div>

<button className="black_btn ml-auto block" type="submit">
Register
</button>
</form>
</section>
<p>
{signupError && (
<p className="text-red-500 text-sm mb-4">{signupError}</p>
)}

<div className="mb-4">
<label htmlFor="first_name" className="mb-2 block text-lg font-bold text-gray-700">
First name
</label>
<input
id="first_name"
name="first_name"
type="text"
onChange={handleChange}
onBlur={handleBlur}
value={values.first_name}
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-3 leading-tight text-gray-700 shadow focus:outline-none"
/>
{touched.first_name && errors.first_name && (
<p className="text-red-500 text-sm mt-1">{errors.first_name}</p>
)}
</div>

<div className="mb-4">
<label htmlFor="last_name" className="mb-2 block text-lg font-bold text-gray-700">
Last name
</label>
<input
id="last_name"
name="last_name"
type="text"
onChange={handleChange}
onBlur={handleBlur}
value={values.last_name}
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-3 leading-tight text-gray-700 shadow focus:outline-none"
/>
{touched.last_name && errors.last_name && (
<p className="text-red-500 text-sm mt-1">{errors.last_name}</p>
)}
</div>

<div className="mb-4">
<label htmlFor="email" className="mb-2 block text-lg font-bold text-gray-700">
Email
</label>
<input
id="email"
name="email"
type="email"
onChange={handleChange}
onBlur={handleBlur}
value={values.email}
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-3 leading-tight text-gray-700 shadow focus:outline-none"
/>
{touched.email && errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email}</p>
)}
</div>

<div className="mb-4">
<label htmlFor="password" className="mb-2 block text-lg font-bold text-gray-700">
Password
</label>
<input
id="password"
name="password"
type="password"
onChange={handleChange}
onBlur={handleBlur}
value={values.password}
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-3 leading-tight text-gray-700 shadow focus:outline-none"
/>
{touched.password && errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password}</p>
)}
</div>

<div className="mb-6">
<label htmlFor="re_password" className="mb-2 block text-lg font-bold text-gray-700">
Confirm password
</label>
<input
id="re_password"
name="re_password"
type="password"
onChange={handleChange}
onBlur={handleBlur}
value={values.re_password}
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-3 leading-tight text-gray-700 shadow focus:outline-none"
/>
{touched.re_password && errors.re_password && (
<p className="text-red-500 text-sm mt-1">{errors.re_password}</p>
)}
</div>

<button
className="btnBlue w-full text-lg"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? "Creating account..." : "Create account"}
</button>
</form>
<p className="text-center">
Already have an account?{" "}
<Link to="/login" className="font-bold hover:text-blue-600">
{" "}
Login here.
Log in
</Link>
</p>
</>
</section>
);
};

export default LoginForm;
export default RegistrationForm;
5 changes: 5 additions & 0 deletions frontend/src/routes/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import RulesManager from "../pages/RulesManager/RulesManager.tsx";
import ManageMeds from "../pages/ManageMeds/ManageMeds.tsx";
import ProtectedRoute from "../components/ProtectedRoute/ProtectedRoute.tsx";
import AdminRoute from "../components/ProtectedRoute/AdminRoute.tsx";
import Activate from "../pages/Activate/Activate.tsx";

const routes = [
{
Expand Down Expand Up @@ -49,6 +50,10 @@ const routes = [
path: "register",
element: <RegistrationForm />,
},
{
path: "activate/:uid/:token",
element: <Activate />,
},
{
path: "login",
element: <LoginForm />,
Expand Down
Loading
Loading