registration and confirm email

This commit is contained in:
David Katrinka 2025-11-25 20:56:55 +01:00
parent b427c148af
commit ed9fa887b3
12 changed files with 249 additions and 38 deletions

View File

@ -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<LoginResponse> => {
export const loginRequest = async (data: LoginPayload) => {
const res = await axiosInstance.post<LoginResponse>("/api/auth/login", data);
return res.data;
};
@ -17,3 +17,19 @@ export const fetchMe = async (): Promise<User> => {
const res = await axiosInstance.get<MeResponse>("/api/auth/me");
return res.data.user;
};
export const registrationRequest = async (data: RegistrationPayload) => {
const res = await axiosInstance.post<RegistrationResponse>(
"/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;
};

View File

@ -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<null | HTMLElement>(

View File

@ -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;
}

View File

@ -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<void>;
login: (data: LoginPayload) => Promise<LoginResponse>;
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 (
<AuthContext.Provider
value={{ user: user ?? null, login, logout, isLoading }}
value={{
user: user ?? null,
login: loginMutation.mutateAsync,
logout,
isLoading,
}}
>
{children}
</AuthContext.Provider>

View File

@ -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");
},
});
};

View File

@ -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<User>({
useQuery({
queryKey: ["me"],
queryFn: fetchMe,
enabled: !!localStorage.getItem("access_token"),

View File

@ -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<LoginResponse, any, LoginPayload>({
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");
},
});
};

View File

@ -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");
},
});
};

View File

@ -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 (
<Box sx={{ padding: 4, textAlign: "center" }}>
{activateMutation.isPending ? (
<CircularProgress />
) : (
<Typography variant="h5">Activating your account...</Typography>
)}
</Box>
);
};
export default ActivateAccountPage;

View File

@ -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 = () => {

View File

@ -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 (
<Container>
<Box sx={{ padding: 4, maxWidth: 360, margin: "auto" }}>
<Typography variant="h4" mb={3} textAlign="center">
Register
</Typography>
<form onSubmit={handleSubmit}>
<TextField
label="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
fullWidth
margin="normal"
required
/>
<TextField
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
fullWidth
margin="normal"
required
/>
<TextField
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
fullWidth
margin="normal"
required
/>
<TextField
label="Confirm Password"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
fullWidth
margin="normal"
required
/>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
sx={{ mt: 2 }}
disabled={registerMutation.isPending}
>
{registerMutation.isPending ? "Registering..." : "Register"}
</Button>
</form>
</Box>
</Container>
);
};
export default RegistrationPage;

View File

@ -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([
</ProtectedRoute>
),
},
{
path: "/register",
element: <RegistrationPage />,
},
{
path: "/auth/email-activation/:token",
element: <ActivateAccountPage />,
},
],
},
]);