Skip to content

Commit

Permalink
Email sign up flow (#282)
Browse files Browse the repository at this point in the history
* base for email sign up flow

* CRUD and routers for EmailSignUpToken, two part register flow almost done

* EmailSignUp token creation done, refined routes

* fix email-signup-get and improved register UI

* User creation from email sign up

* redirect to login after User account creation

* partial email/pass login logic

* fix edge cases and type issues in API routes

* Log in completed

* Fix subject of registration email

* fix type of /delete route for signup token

* Delete signup token on registration

* Actually add API key to email /login route

* apparently I have to await a result I don't even care about

thanks mypy

* Use Google-style comments

Co-authored-by: Ben Bolte <[email protected]>

* email_router and google style comments

* changed register to be signup

* Fixed styling/linting error

---------

Co-authored-by: Dennis Chen <[email protected]>
Co-authored-by: Dennis Chen <[email protected]>
Co-authored-by: Ben Bolte <[email protected]>
  • Loading branch information
4 people authored Aug 12, 2024
1 parent c39cbfe commit 328105b
Show file tree
Hide file tree
Showing 17 changed files with 710 additions and 32 deletions.
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Login from "pages/Login";
import Logout from "pages/Logout";
import NotFound from "pages/NotFound";
import Profile from "pages/Profile";
import Signup from "pages/Signup";

import Container from "components/Container";
import NotFoundRedirect from "components/NotFoundRedirect";
Expand All @@ -39,6 +40,7 @@ const App = () => {
<Route path="/profile" element={<Profile />} />
<Route path="/login" element={<Login />} />
<Route path="/logout" element={<Logout />} />
<Route path="/signup/:id" element={<Signup />} />
<Route path="/create" element={<Create />} />
<Route path="/browse/:page?" element={<Browse />} />
<Route path="/item/:id" element={<ListingDetails />} />
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/auth/AuthBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Spinner from "components/ui/Spinner";

import AuthProvider from "./AuthProvider";
import LoginForm from "./LoginForm";
import SignupForm from "./SignupForm";
import SignupWithEmail from "./SignupWithEmail";

export const AuthBlockInner = () => {
const auth = useAuthentication();
Expand Down Expand Up @@ -66,7 +66,9 @@ export const AuthBlockInner = () => {

return (
<>
<CardContent>{isSignup ? <SignupForm /> : <LoginForm />}</CardContent>
<CardContent>
{isSignup ? <SignupWithEmail /> : <LoginForm />}
</CardContent>
<CardFooter>
<AuthProvider handleGithubSubmit={handleGithubSubmit} />
</CardFooter>
Expand Down
20 changes: 17 additions & 3 deletions frontend/src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SubmitHandler, useForm } from "react-hook-form";

import { zodResolver } from "@hookform/resolvers/zod";
import { useAlertQueue } from "hooks/useAlertQueue";
import { useAuthentication } from "hooks/useAuth";
import { LoginSchema, LoginType } from "types";

import { Button } from "components/ui/Button/Button";
Expand All @@ -18,11 +19,24 @@ const LoginForm = () => {
resolver: zodResolver(LoginSchema),
});

const { addAlert } = useAlertQueue();
const { addAlert, addErrorAlert } = useAlertQueue();
const auth = useAuthentication();

const onSubmit: SubmitHandler<LoginType> = async (data: LoginType) => {
// TODO: Add an api endpoint to send the credentials details to backend and email verification.
addAlert(`Not yet implemented: ${data.email}`, "success");
try {
const { data: response, error } = await auth.client.POST("/users/login", {
body: data,
});

if (error) {
addErrorAlert(error);
} else {
addAlert(`Logged in! Welcome back!`, "success");
auth.login(response.token);
}
} catch {
addErrorAlert("An unexpected error occurred during login.");
}
};

return (
Expand Down
37 changes: 31 additions & 6 deletions frontend/src/components/auth/SignupForm.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { SubmitHandler, useForm } from "react-hook-form";
import { Link } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";

import { zodResolver } from "@hookform/resolvers/zod";
import { useAlertQueue } from "hooks/useAlertQueue";
import { useAuthentication } from "hooks/useAuth";
import { SignUpSchema, SignupType } from "types";
import zxcvbn from "zxcvbn";

Expand All @@ -11,7 +12,15 @@ import ErrorMessage from "components/ui/ErrorMessage";
import { Input } from "components/ui/Input/Input";
import PasswordInput from "components/ui/Input/PasswordInput";

const SignupForm = () => {
interface SignupFormProps {
signupTokenId: string;
}

const SignupForm: React.FC<SignupFormProps> = ({ signupTokenId }) => {
const auth = useAuthentication();
const { addAlert, addErrorAlert } = useAlertQueue();
const navigate = useNavigate();

const {
register,
handleSubmit,
Expand All @@ -25,8 +34,6 @@ const SignupForm = () => {
const confirmPassword = watch("confirmPassword") || "";
const passwordStrength = password.length > 0 ? zxcvbn(password).score : 0;

const { addAlert, addErrorAlert } = useAlertQueue();

const onSubmit: SubmitHandler<SignupType> = async (data: SignupType) => {
// Exit account creation early if password too weak or not matching
if (passwordStrength < 2) {
Expand All @@ -37,8 +44,26 @@ const SignupForm = () => {
return;
}

// TODO: Add an api endpoint to send the credentials details to backend and email verification.
addAlert(`Not yet implemented: ${data.email}`, "success");
try {
const { error } = await auth.client.POST("/users/signup", {
body: {
signup_token_id: signupTokenId,
email: data.email,
password: data.password,
},
});

if (error) {
addErrorAlert(error);
} else {
addAlert("Registration successful! You can now log in.", "success");
navigate("/login");
// Sign user in automatically?
}
} catch (err) {
addErrorAlert(err);
}
console.log(data);
};

return (
Expand Down
67 changes: 67 additions & 0 deletions frontend/src/components/auth/SignupWithEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useForm } from "react-hook-form";

import { zodResolver } from "@hookform/resolvers/zod";
import { useAlertQueue } from "hooks/useAlertQueue";
import { useAuthentication } from "hooks/useAuth";
import { EmailSignupSchema, EmailSignupType } from "types";

import { Button } from "components/ui/Button/Button";
import ErrorMessage from "components/ui/ErrorMessage";
import { Input } from "components/ui/Input/Input";

interface EmailSignUpResponse {
message: string;
}

const SignupWithEmail = () => {
const auth = useAuthentication();
const { addAlert, addErrorAlert } = useAlertQueue();

const {
register,
handleSubmit,
formState: { errors },
} = useForm<EmailSignupType>({
resolver: zodResolver(EmailSignupSchema),
});

const onSubmit = async ({ email }: EmailSignupType) => {
const { data, error } = await auth.client.POST("/email/signup/create/", {
body: {
email,
},
});

if (error) {
addErrorAlert(error);
} else {
const responseData = data as EmailSignUpResponse;
const successMessage =
responseData?.message ||
"Sign up email sent! Follow the link sent to you to continue registration.";
addAlert(successMessage, "success");
}
};

return (
<form
onSubmit={handleSubmit(onSubmit)}
className="grid grid-cols-1 space-y-6"
>
{/* Email Input */}
<div className="relative">
<Input placeholder="Email" type="text" {...register("email")} />
{errors?.email && <ErrorMessage>{errors?.email?.message}</ErrorMessage>}
</div>
{/* Signup Button */}
<Button
variant="outline"
className="w-full text-white bg-blue-600 hover:bg-opacity-70"
>
Sign up with email
</Button>
</form>
);
};

export default SignupWithEmail;
3 changes: 2 additions & 1 deletion frontend/src/components/footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const Footer = () => {
// - to hide footer on a page add path to this
const showFooter =
pathname?.startsWith("/browse") === false &&
pathname?.startsWith("/login") === false;
pathname?.startsWith("/login") === false &&
pathname?.startsWith("/signup") === false;

if (!showFooter) {
return null;
Expand Down
23 changes: 14 additions & 9 deletions frontend/src/components/ui/Input/PasswordInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,17 @@ const PasswordInput = <T extends FieldValues>({
const getStrengthColor = (score: number) => {
switch (score) {
case 0:
return "bg-red-500";
return "red-500";
case 1:
return "bg-orange-500";
return "orange-500";
case 2:
return "bg-yellow-500";
return "yellow-500";
case 3:
return "bg-blue-500";
return "blue-500";
case 4:
return "bg-green-500";
return "green-500";
default:
return "bg-gray-300";
return "gray-300";
}
};

Expand Down Expand Up @@ -103,12 +103,17 @@ const PasswordInput = <T extends FieldValues>({
<>
<div className="mt-4 h-2 w-full bg-gray-200 rounded">
<div
className={`h-full ${getStrengthColor(passwordStrength)} rounded`}
className={`h-full bg-${getStrengthColor(passwordStrength)} rounded`}
style={{ width: `${(passwordStrength + 1) * 20}%` }}
/>
</div>
<div className="mt-2 text-sm text-gray-600">
Password Strength: {getStrengthLabel(passwordStrength)}
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Password Strength:{" "}
<span
className={`font-semibold text-${getStrengthColor(passwordStrength)}`}
>
{getStrengthLabel(passwordStrength)}
</span>
</div>
{passwordStrength < 2 ? (
<div className="mt-1 text-xs text-red-500">
Expand Down
Loading

0 comments on commit 328105b

Please sign in to comment.