From ed9fa887b305281764593aa0fc5c0369a260961b Mon Sep 17 00:00:00 2001 From: David Katrinka Date: Tue, 25 Nov 2025 20:56:55 +0100 Subject: [PATCH] registration and confirm email --- src/api/authApi.ts | 32 ++++-- src/components/Header/Header.tsx | 2 +- src/components/shared/types/AuthTypes.ts | 11 +++ src/context/AuthContext.tsx | 33 +++---- src/hooks/useActivateAccount.ts | 19 ++++ src/hooks/useCurrentUser.ts | 3 +- src/hooks/useLogin.ts | 27 ++++-- src/hooks/useRegistration.ts | 25 +++++ .../ActivateAccountPage.tsx | 26 +++++ src/pages/LoginPage/LoginPage.tsx | 2 +- .../RegistrationPage/RegistrationPage.tsx | 97 +++++++++++++++++++ src/router/router.tsx | 10 ++ 12 files changed, 249 insertions(+), 38 deletions(-) create mode 100644 src/hooks/useActivateAccount.ts create mode 100644 src/hooks/useRegistration.ts create mode 100644 src/pages/ActivateAccountPage/ActivateAccountPage.tsx create mode 100644 src/pages/RegistrationPage/RegistrationPage.tsx diff --git a/src/api/authApi.ts b/src/api/authApi.ts index 1f24552..7073c89 100644 --- a/src/api/authApi.ts +++ b/src/api/authApi.ts @@ -1,14 +1,14 @@ -import type { - LoginPayload, - LoginResponse, - MeResponse, - User, +import { + type RegistrationResponse, + type LoginPayload, + type LoginResponse, + type MeResponse, + type RegistrationPayload, + type User, } from "../components/shared/types/AuthTypes"; import axiosInstance from "./axiosInstance"; -export const loginRequest = async ( - data: LoginPayload -): Promise => { +export const loginRequest = async (data: LoginPayload) => { const res = await axiosInstance.post("/api/auth/login", data); return res.data; }; @@ -17,3 +17,19 @@ export const fetchMe = async (): Promise => { const res = await axiosInstance.get("/api/auth/me"); return res.data.user; }; + +export const registrationRequest = async (data: RegistrationPayload) => { + const res = await axiosInstance.post( + "/api/auth/register", + data + ); + return res.data; +}; + +export const activateAccount = async (token: string) => { + const res = await axiosInstance.post<{ message: string }>( + "/api/auth/activate-account", + { token } + ); + return res.data; +}; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index ea23ba5..b1fe9a6 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -14,7 +14,7 @@ import { useAuth } from "../../context/AuthContext"; import { useNavigate } from "react-router-dom"; function Header() { - const { user, logout } = useAuth(); + const { user } = useAuth(); const navigate = useNavigate(); const [anchorElNav, setAnchorElNav] = React.useState( diff --git a/src/components/shared/types/AuthTypes.ts b/src/components/shared/types/AuthTypes.ts index 39fb308..29841cb 100644 --- a/src/components/shared/types/AuthTypes.ts +++ b/src/components/shared/types/AuthTypes.ts @@ -22,3 +22,14 @@ export interface LoginPayload { export interface MeResponse { user: User; } + +export interface RegistrationPayload { + username: string; + email: string; + password: string; + password_confirmation: string; +} + +export interface RegistrationResponse { + message: string; +} diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 79a330b..7ca3984 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -2,12 +2,17 @@ import { useQueryClient } from "@tanstack/react-query"; import { toast } from "react-toastify"; import { useCurrentUser } from "../hooks/useCurrentUser"; import { useLogin } from "../hooks/useLogin"; -import type { LoginPayload, User } from "../components/shared/types/AuthTypes"; -import { createContext, useContext, type ReactNode } from "react"; +import type { + LoginPayload, + LoginResponse, + User, +} from "../components/shared/types/AuthTypes"; +import { createContext, useContext } from "react"; +import type { OnlyChildrenProps } from "../components/shared/types/OnlyChildrenProps"; interface AuthContextType { user: User | null; - login: (data: LoginPayload) => Promise; + login: (data: LoginPayload) => Promise; logout: () => void; isLoading: boolean; } @@ -20,24 +25,11 @@ export const useAuth = () => { return context; }; -export const AuthProvider = ({ children }: { children: ReactNode }) => { +export const AuthProvider = ({ children }: OnlyChildrenProps) => { const queryClient = useQueryClient(); const { data: user, isLoading } = useCurrentUser(); const loginMutation = useLogin(); - const login = async (payload: LoginPayload) => { - await loginMutation.mutateAsync(payload, { - onSuccess: (data) => { - localStorage.setItem("access_token", data.access_token); - queryClient.setQueryData(["me"], data.user); - toast.success(`Welcome ${data.user.username}`); - }, - onError: (err: any) => { - toast.error(err.response?.data?.message || "Login failed"); - }, - }); - }; - const logout = () => { localStorage.removeItem("access_token"); queryClient.removeQueries({ queryKey: ["me"] }); @@ -47,7 +39,12 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { return ( {children} diff --git a/src/hooks/useActivateAccount.ts b/src/hooks/useActivateAccount.ts new file mode 100644 index 0000000..b27af3d --- /dev/null +++ b/src/hooks/useActivateAccount.ts @@ -0,0 +1,19 @@ +import { useMutation } from "@tanstack/react-query"; +import { activateAccount } from "../api/authApi"; +import { toast } from "react-toastify"; +import { useNavigate } from "react-router-dom"; + +export const useActivateAccount = () => { + const navigate = useNavigate(); + return useMutation({ + mutationFn: (token: string) => activateAccount(token), + onSuccess: (data) => { + toast.success(data.message); + navigate("/login"); + }, + onError: (err: any) => { + toast.error(err.response?.data?.message || "Activation failed"); + }, + }); +}; + diff --git a/src/hooks/useCurrentUser.ts b/src/hooks/useCurrentUser.ts index 42c056b..d1ad99f 100644 --- a/src/hooks/useCurrentUser.ts +++ b/src/hooks/useCurrentUser.ts @@ -1,9 +1,8 @@ import { useQuery } from "@tanstack/react-query"; -import type { User } from "../components/shared/types/AuthTypes"; import { fetchMe } from "../api/authApi"; export const useCurrentUser = () => - useQuery({ + useQuery({ queryKey: ["me"], queryFn: fetchMe, enabled: !!localStorage.getItem("access_token"), diff --git a/src/hooks/useLogin.ts b/src/hooks/useLogin.ts index ddb965f..95fbc4d 100644 --- a/src/hooks/useLogin.ts +++ b/src/hooks/useLogin.ts @@ -1,11 +1,22 @@ -import { useMutation } from "@tanstack/react-query"; -import type { - LoginPayload, - LoginResponse, -} from "../components/shared/types/AuthTypes"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { loginRequest } from "../api/authApi"; +import { toast } from "react-toastify"; +import type { LoginPayload } from "../components/shared/types/AuthTypes"; -export const useLogin = () => - useMutation({ - mutationFn: loginRequest, +export const useLogin = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (payload: LoginPayload) => loginRequest(payload), + + onSuccess: (data) => { + localStorage.setItem("access_token", data.access_token); + queryClient.setQueryData(["me"], data.user); + toast.success(`Welcome ${data.user.username}`); + }, + + onError: (err: any) => { + toast.error(err.response?.data?.message || "Login failed"); + }, }); +}; diff --git a/src/hooks/useRegistration.ts b/src/hooks/useRegistration.ts new file mode 100644 index 0000000..a699ac5 --- /dev/null +++ b/src/hooks/useRegistration.ts @@ -0,0 +1,25 @@ +import { useMutation } from "@tanstack/react-query"; +import { registrationRequest } from "../api/authApi"; +import type { RegistrationPayload } from "../components/shared/types/AuthTypes"; +import { toast } from "react-toastify"; +import { useNavigate } from "react-router-dom"; + +export const useRegistration = () => { + const navigate = useNavigate(); + return useMutation({ + mutationFn: ({ + username, + email, + password, + password_confirmation, + }: RegistrationPayload) => + registrationRequest({ username, email, password, password_confirmation }), + onSuccess: (data) => { + toast.success(data.message); + navigate("/login"); + }, + onError: (error: any) => { + toast.error(error.response?.data?.message || "Registration failed"); + }, + }); +}; diff --git a/src/pages/ActivateAccountPage/ActivateAccountPage.tsx b/src/pages/ActivateAccountPage/ActivateAccountPage.tsx new file mode 100644 index 0000000..9a44614 --- /dev/null +++ b/src/pages/ActivateAccountPage/ActivateAccountPage.tsx @@ -0,0 +1,26 @@ +import { useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { useActivateAccount } from "../../hooks/useActivateAccount"; +import { CircularProgress, Box, Typography } from "@mui/material"; + +const ActivateAccountPage = () => { + const { token } = useParams<{ token: string }>(); + const activateMutation = useActivateAccount(); + + useEffect(() => { + if (!token) return; + activateMutation.mutate(token); + }, []); + + return ( + + {activateMutation.isPending ? ( + + ) : ( + Activating your account... + )} + + ); +}; + +export default ActivateAccountPage; diff --git a/src/pages/LoginPage/LoginPage.tsx b/src/pages/LoginPage/LoginPage.tsx index 7656dce..ae3a873 100644 --- a/src/pages/LoginPage/LoginPage.tsx +++ b/src/pages/LoginPage/LoginPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useAuth } from "../../context/AuthContext"; import Container from "../../components/shared/Container"; -import { Box, Button, Paper, TextField, Typography } from "@mui/material"; +import { Box, Button, TextField, Typography } from "@mui/material"; import { useNavigate } from "react-router-dom"; const LoginPage = () => { diff --git a/src/pages/RegistrationPage/RegistrationPage.tsx b/src/pages/RegistrationPage/RegistrationPage.tsx new file mode 100644 index 0000000..0cea742 --- /dev/null +++ b/src/pages/RegistrationPage/RegistrationPage.tsx @@ -0,0 +1,97 @@ +import { useEffect, useState } from "react"; +import { useAuth } from "../../context/AuthContext"; +import { useNavigate } from "react-router-dom"; +import Container from "../../components/shared/Container"; +import { Box, Button, TextField, Typography } from "@mui/material"; +import { useRegistration } from "../../hooks/useRegistration"; +import { toast } from "react-toastify"; + +const RegistrationPage = () => { + const navigate = useNavigate(); + const { user } = useAuth(); + + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + const registerMutation = useRegistration(); + + useEffect(() => { + if (user) navigate("/"); + }, [user, navigate]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== confirmPassword) { + toast.error("Passwords do not match"); + return; + } + + registerMutation.mutate({ + username, + email, + password, + password_confirmation: confirmPassword, + }); + }; + + return ( + + + + Register + +
+ setUsername(e.target.value)} + fullWidth + margin="normal" + required + /> + setEmail(e.target.value)} + fullWidth + margin="normal" + required + /> + setPassword(e.target.value)} + fullWidth + margin="normal" + required + /> + setConfirmPassword(e.target.value)} + fullWidth + margin="normal" + required + /> + + +
+
+ ); +}; + +export default RegistrationPage; diff --git a/src/router/router.tsx b/src/router/router.tsx index ff5b336..e40c3bb 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -4,6 +4,8 @@ import IndexPage from "../pages/IndexPage/IndexPage"; import LoginPage from "../pages/LoginPage/LoginPage"; import ProfilePage from "../pages/ProfilePage/Profilepage"; import { ProtectedRoute } from "../components/ProtectedRoute/ProtectedRoute"; +import RegistrationPage from "../pages/RegistrationPage/RegistrationPage"; +import ActivateAccountPage from "../pages/ActivateAccountPage/ActivateAccountPage"; const router = createBrowserRouter([ { @@ -26,6 +28,14 @@ const router = createBrowserRouter([ ), }, + { + path: "/register", + element: , + }, + { + path: "/auth/email-activation/:token", + element: , + }, ], }, ]);