registration and confirm email
This commit is contained in:
parent
b427c148af
commit
ed9fa887b3
@ -1,14 +1,14 @@
|
|||||||
import type {
|
import {
|
||||||
LoginPayload,
|
type RegistrationResponse,
|
||||||
LoginResponse,
|
type LoginPayload,
|
||||||
MeResponse,
|
type LoginResponse,
|
||||||
User,
|
type MeResponse,
|
||||||
|
type RegistrationPayload,
|
||||||
|
type User,
|
||||||
} from "../components/shared/types/AuthTypes";
|
} from "../components/shared/types/AuthTypes";
|
||||||
import axiosInstance from "./axiosInstance";
|
import axiosInstance from "./axiosInstance";
|
||||||
|
|
||||||
export const loginRequest = async (
|
export const loginRequest = async (data: LoginPayload) => {
|
||||||
data: LoginPayload
|
|
||||||
): Promise<LoginResponse> => {
|
|
||||||
const res = await axiosInstance.post<LoginResponse>("/api/auth/login", data);
|
const res = await axiosInstance.post<LoginResponse>("/api/auth/login", data);
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
@ -17,3 +17,19 @@ export const fetchMe = async (): Promise<User> => {
|
|||||||
const res = await axiosInstance.get<MeResponse>("/api/auth/me");
|
const res = await axiosInstance.get<MeResponse>("/api/auth/me");
|
||||||
return res.data.user;
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { useAuth } from "../../context/AuthContext";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
const { user, logout } = useAuth();
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(
|
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(
|
||||||
|
|||||||
@ -22,3 +22,14 @@ export interface LoginPayload {
|
|||||||
export interface MeResponse {
|
export interface MeResponse {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RegistrationPayload {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
password_confirmation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistrationResponse {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|||||||
@ -2,12 +2,17 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { useCurrentUser } from "../hooks/useCurrentUser";
|
import { useCurrentUser } from "../hooks/useCurrentUser";
|
||||||
import { useLogin } from "../hooks/useLogin";
|
import { useLogin } from "../hooks/useLogin";
|
||||||
import type { LoginPayload, User } from "../components/shared/types/AuthTypes";
|
import type {
|
||||||
import { createContext, useContext, type ReactNode } from "react";
|
LoginPayload,
|
||||||
|
LoginResponse,
|
||||||
|
User,
|
||||||
|
} from "../components/shared/types/AuthTypes";
|
||||||
|
import { createContext, useContext } from "react";
|
||||||
|
import type { OnlyChildrenProps } from "../components/shared/types/OnlyChildrenProps";
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
login: (data: LoginPayload) => Promise<void>;
|
login: (data: LoginPayload) => Promise<LoginResponse>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
@ -20,24 +25,11 @@ export const useAuth = () => {
|
|||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
export const AuthProvider = ({ children }: OnlyChildrenProps) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: user, isLoading } = useCurrentUser();
|
const { data: user, isLoading } = useCurrentUser();
|
||||||
const loginMutation = useLogin();
|
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 = () => {
|
const logout = () => {
|
||||||
localStorage.removeItem("access_token");
|
localStorage.removeItem("access_token");
|
||||||
queryClient.removeQueries({ queryKey: ["me"] });
|
queryClient.removeQueries({ queryKey: ["me"] });
|
||||||
@ -47,7 +39,12 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{ user: user ?? null, login, logout, isLoading }}
|
value={{
|
||||||
|
user: user ?? null,
|
||||||
|
login: loginMutation.mutateAsync,
|
||||||
|
logout,
|
||||||
|
isLoading,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
|
|||||||
19
src/hooks/useActivateAccount.ts
Normal file
19
src/hooks/useActivateAccount.ts
Normal 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");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
@ -1,9 +1,8 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import type { User } from "../components/shared/types/AuthTypes";
|
|
||||||
import { fetchMe } from "../api/authApi";
|
import { fetchMe } from "../api/authApi";
|
||||||
|
|
||||||
export const useCurrentUser = () =>
|
export const useCurrentUser = () =>
|
||||||
useQuery<User>({
|
useQuery({
|
||||||
queryKey: ["me"],
|
queryKey: ["me"],
|
||||||
queryFn: fetchMe,
|
queryFn: fetchMe,
|
||||||
enabled: !!localStorage.getItem("access_token"),
|
enabled: !!localStorage.getItem("access_token"),
|
||||||
|
|||||||
@ -1,11 +1,22 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import type {
|
|
||||||
LoginPayload,
|
|
||||||
LoginResponse,
|
|
||||||
} from "../components/shared/types/AuthTypes";
|
|
||||||
import { loginRequest } from "../api/authApi";
|
import { loginRequest } from "../api/authApi";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import type { LoginPayload } from "../components/shared/types/AuthTypes";
|
||||||
|
|
||||||
export const useLogin = () =>
|
export const useLogin = () => {
|
||||||
useMutation<LoginResponse, any, LoginPayload>({
|
const queryClient = useQueryClient();
|
||||||
mutationFn: loginRequest,
|
|
||||||
|
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");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|||||||
25
src/hooks/useRegistration.ts
Normal file
25
src/hooks/useRegistration.ts
Normal 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");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
26
src/pages/ActivateAccountPage/ActivateAccountPage.tsx
Normal file
26
src/pages/ActivateAccountPage/ActivateAccountPage.tsx
Normal 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;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useAuth } from "../../context/AuthContext";
|
import { useAuth } from "../../context/AuthContext";
|
||||||
import Container from "../../components/shared/Container";
|
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";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
|
|||||||
97
src/pages/RegistrationPage/RegistrationPage.tsx
Normal file
97
src/pages/RegistrationPage/RegistrationPage.tsx
Normal 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;
|
||||||
@ -4,6 +4,8 @@ import IndexPage from "../pages/IndexPage/IndexPage";
|
|||||||
import LoginPage from "../pages/LoginPage/LoginPage";
|
import LoginPage from "../pages/LoginPage/LoginPage";
|
||||||
import ProfilePage from "../pages/ProfilePage/Profilepage";
|
import ProfilePage from "../pages/ProfilePage/Profilepage";
|
||||||
import { ProtectedRoute } from "../components/ProtectedRoute/ProtectedRoute";
|
import { ProtectedRoute } from "../components/ProtectedRoute/ProtectedRoute";
|
||||||
|
import RegistrationPage from "../pages/RegistrationPage/RegistrationPage";
|
||||||
|
import ActivateAccountPage from "../pages/ActivateAccountPage/ActivateAccountPage";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -26,6 +28,14 @@ const router = createBrowserRouter([
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/register",
|
||||||
|
element: <RegistrationPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/auth/email-activation/:token",
|
||||||
|
element: <ActivateAccountPage />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user