@surajprasad/create-starterkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/index.js +327 -0
- package/package.json +29 -0
- package/templates/mern/backend/.env.example +11 -0
- package/templates/mern/backend/Dockerfile +13 -0
- package/templates/mern/backend/package.json +23 -0
- package/templates/mern/backend/server.js +25 -0
- package/templates/mern/backend/src/app.js +48 -0
- package/templates/mern/backend/src/config/db.js +17 -0
- package/templates/mern/backend/src/config/env.js +38 -0
- package/templates/mern/backend/src/middleware/authMiddleware.js +30 -0
- package/templates/mern/backend/src/middleware/errorMiddleware.js +17 -0
- package/templates/mern/backend/src/middleware/notFound.middleware.js +9 -0
- package/templates/mern/backend/src/modules/auth/auth.controller.js +45 -0
- package/templates/mern/backend/src/modules/auth/auth.model.js +20 -0
- package/templates/mern/backend/src/modules/auth/auth.routes.js +55 -0
- package/templates/mern/backend/src/modules/auth/auth.service.js +185 -0
- package/templates/mern/backend/src/modules/auth/dto/login.dto.js +12 -0
- package/templates/mern/backend/src/modules/auth/dto/register.dto.js +14 -0
- package/templates/mern/backend/src/modules/email/dto/sendEmail.dto.js +16 -0
- package/templates/mern/backend/src/modules/email/email.controller.js +33 -0
- package/templates/mern/backend/src/modules/email/email.routes.js +13 -0
- package/templates/mern/backend/src/modules/email/email.service.js +88 -0
- package/templates/mern/backend/src/modules/email/templates/resetPassword.html +47 -0
- package/templates/mern/backend/src/modules/email/templates/verifyEmail.html +45 -0
- package/templates/mern/backend/src/modules/email/templates/welcome.html +47 -0
- package/templates/mern/backend/src/utils/apiResponse.util.js +9 -0
- package/templates/mern/backend/src/utils/asyncHandler.util.js +6 -0
- package/templates/mern/backend/src/utils/generateOTP.util.js +10 -0
- package/templates/mern/backend/src/utils/generateResetToken.util.js +8 -0
- package/templates/mern/backend/src/utils/generateToken.util.js +17 -0
- package/templates/mern/backend/src/utils/hashPassword.util.js +11 -0
- package/templates/mern/backend/src/utils/validateDto.util.js +18 -0
- package/templates/mern/frontend/.env.example +1 -0
- package/templates/mern/frontend/Dockerfile +13 -0
- package/templates/mern/frontend/index.html +13 -0
- package/templates/mern/frontend/package.json +23 -0
- package/templates/mern/frontend/src/App.jsx +102 -0
- package/templates/mern/frontend/src/main.jsx +14 -0
- package/templates/mern/frontend/src/modules/auth/components/ProtectedRoute.jsx +10 -0
- package/templates/mern/frontend/src/modules/auth/index.js +6 -0
- package/templates/mern/frontend/src/modules/auth/pages/ForgotPasswordPage.jsx +64 -0
- package/templates/mern/frontend/src/modules/auth/pages/LoginPage.jsx +82 -0
- package/templates/mern/frontend/src/modules/auth/pages/RegisterPage.jsx +81 -0
- package/templates/mern/frontend/src/modules/auth/pages/ResetPasswordPage.jsx +78 -0
- package/templates/mern/frontend/src/modules/auth/pages/VerifyEmailPage.jsx +69 -0
- package/templates/mern/frontend/src/modules/auth/services/auth.service.js +34 -0
- package/templates/mern/frontend/src/modules/auth/store/authStore.js +37 -0
- package/templates/mern/frontend/src/modules/dashboard/index.js +2 -0
- package/templates/mern/frontend/src/modules/dashboard/pages/DashboardPage.jsx +41 -0
- package/templates/mern/frontend/src/shared/components/Button.jsx +31 -0
- package/templates/mern/frontend/src/shared/components/Input.jsx +23 -0
- package/templates/mern/frontend/src/shared/components/Toast.jsx +52 -0
- package/templates/mern/frontend/src/shared/services/api.js +20 -0
- package/templates/mern/frontend/src/shared/utils/formatError.util.js +8 -0
- package/templates/mern/frontend/src/shared/utils/storage.util.js +25 -0
- package/templates/mern/frontend/vite.config.js +13 -0
- package/templates/mern/frontend-next/.env.example +1 -0
- package/templates/mern/frontend-next/app/forgot-password/page.js +8 -0
- package/templates/mern/frontend-next/app/layout.js +15 -0
- package/templates/mern/frontend-next/app/login/page.js +8 -0
- package/templates/mern/frontend-next/app/page.js +22 -0
- package/templates/mern/frontend-next/app/register/page.js +8 -0
- package/templates/mern/frontend-next/app/reset-password/page.js +8 -0
- package/templates/mern/frontend-next/app/verify-email/page.js +8 -0
- package/templates/mern/frontend-next/jsconfig.json +6 -0
- package/templates/mern/frontend-next/next.config.mjs +7 -0
- package/templates/mern/frontend-next/package.json +18 -0
- package/templates/mern/frontend-next/src/modules/auth/components/ProtectedRoute.jsx +19 -0
- package/templates/mern/frontend-next/src/modules/auth/pages/ForgotPasswordPage.jsx +66 -0
- package/templates/mern/frontend-next/src/modules/auth/pages/LoginPage.jsx +88 -0
- package/templates/mern/frontend-next/src/modules/auth/pages/RegisterPage.jsx +84 -0
- package/templates/mern/frontend-next/src/modules/auth/pages/ResetPasswordPage.jsx +76 -0
- package/templates/mern/frontend-next/src/modules/auth/pages/VerifyEmailPage.jsx +71 -0
- package/templates/mern/frontend-next/src/modules/auth/services/auth.service.js +29 -0
- package/templates/mern/frontend-next/src/modules/auth/store/authStore.js +37 -0
- package/templates/mern/frontend-next/src/modules/dashboard/pages/DashboardPage.jsx +46 -0
- package/templates/mern/frontend-next/src/shared/components/Button.jsx +31 -0
- package/templates/mern/frontend-next/src/shared/components/Input.jsx +24 -0
- package/templates/mern/frontend-next/src/shared/components/Toast.jsx +52 -0
- package/templates/mern/frontend-next/src/shared/services/api.js +25 -0
- package/templates/mern/frontend-next/src/shared/utils/formatError.util.js +8 -0
- package/templates/mern/frontend-next/src/shared/utils/storage.util.js +28 -0
- package/templates/mern/package.json +6 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>StarterKit</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body style="margin:0;background:#F9FAFB;">
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
13
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "starterkit-frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axios": "^1.7.9",
|
|
13
|
+
"react": "^18.3.1",
|
|
14
|
+
"react-dom": "^18.3.1",
|
|
15
|
+
"react-router-dom": "^6.28.2",
|
|
16
|
+
"zustand": "^5.0.3"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
20
|
+
"vite": "^5.4.14"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Link, Route, Routes } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import { Toast } from "./shared/components/Toast.jsx";
|
|
5
|
+
import { DashboardPage } from "./modules/dashboard/pages/DashboardPage.jsx";
|
|
6
|
+
import { ProtectedRoute } from "./modules/auth/components/ProtectedRoute.jsx";
|
|
7
|
+
import { LoginPage } from "./modules/auth/pages/LoginPage.jsx";
|
|
8
|
+
import { RegisterPage } from "./modules/auth/pages/RegisterPage.jsx";
|
|
9
|
+
import { VerifyEmailPage } from "./modules/auth/pages/VerifyEmailPage.jsx";
|
|
10
|
+
import { ForgotPasswordPage } from "./modules/auth/pages/ForgotPasswordPage.jsx";
|
|
11
|
+
import { ResetPasswordPage } from "./modules/auth/pages/ResetPasswordPage.jsx";
|
|
12
|
+
import { useAuthStore } from "./modules/auth/store/authStore.js";
|
|
13
|
+
|
|
14
|
+
export const App = () => {
|
|
15
|
+
const refreshMe = useAuthStore((s) => s.refreshMe);
|
|
16
|
+
const token = useAuthStore((s) => s.token);
|
|
17
|
+
const user = useAuthStore((s) => s.user);
|
|
18
|
+
const [toast, setToast] = useState(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
refreshMe().catch(() => {});
|
|
22
|
+
}, [refreshMe]);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (token && user && user.isEmailVerified === false) {
|
|
26
|
+
setToast({ type: "info", message: "Please verify your email to complete setup." });
|
|
27
|
+
}
|
|
28
|
+
}, [token, user]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div style={{ minHeight: "100vh" }}>
|
|
32
|
+
<header
|
|
33
|
+
style={{
|
|
34
|
+
background: "white",
|
|
35
|
+
borderBottom: "1px solid #E5E7EB",
|
|
36
|
+
padding: "12px 16px"
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<div style={{ maxWidth: 860, margin: "0 auto", display: "flex", gap: 12 }}>
|
|
40
|
+
<Link
|
|
41
|
+
to="/"
|
|
42
|
+
style={{
|
|
43
|
+
fontWeight: 900,
|
|
44
|
+
color: "#111827",
|
|
45
|
+
textDecoration: "none"
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
StarterKit
|
|
49
|
+
</Link>
|
|
50
|
+
<div style={{ marginLeft: "auto", display: "flex", gap: 12 }}>
|
|
51
|
+
{!token ? (
|
|
52
|
+
<>
|
|
53
|
+
<Link to="/login" style={{ color: "#4F46E5", fontWeight: 800 }}>
|
|
54
|
+
Login
|
|
55
|
+
</Link>
|
|
56
|
+
<Link to="/register" style={{ color: "#4F46E5", fontWeight: 800 }}>
|
|
57
|
+
Register
|
|
58
|
+
</Link>
|
|
59
|
+
</>
|
|
60
|
+
) : (
|
|
61
|
+
<span style={{ color: "#6B7280", fontWeight: 700, fontSize: 13 }}>
|
|
62
|
+
{user?.email}
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</header>
|
|
68
|
+
|
|
69
|
+
<Routes>
|
|
70
|
+
<Route
|
|
71
|
+
path="/"
|
|
72
|
+
element={
|
|
73
|
+
<ProtectedRoute>
|
|
74
|
+
<DashboardPage />
|
|
75
|
+
</ProtectedRoute>
|
|
76
|
+
}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<Route path="/login" element={<LoginPage />} />
|
|
80
|
+
<Route path="/register" element={<RegisterPage />} />
|
|
81
|
+
<Route path="/verify-email" element={<VerifyEmailPage />} />
|
|
82
|
+
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
|
|
83
|
+
<Route path="/reset-password" element={<ResetPasswordPage />} />
|
|
84
|
+
|
|
85
|
+
<Route
|
|
86
|
+
path="*"
|
|
87
|
+
element={
|
|
88
|
+
<div style={{ padding: 24 }}>
|
|
89
|
+
<div style={{ maxWidth: 860, margin: "0 auto" }}>
|
|
90
|
+
<h2 style={{ margin: 0 }}>Not Found</h2>
|
|
91
|
+
<p style={{ color: "#6B7280" }}>The page does not exist.</p>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
}
|
|
95
|
+
/>
|
|
96
|
+
</Routes>
|
|
97
|
+
|
|
98
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ReactDOM from "react-dom/client";
|
|
3
|
+
import { BrowserRouter } from "react-router-dom";
|
|
4
|
+
|
|
5
|
+
import { App } from "./App.jsx";
|
|
6
|
+
|
|
7
|
+
ReactDOM.createRoot(document.getElementById("root")).render(
|
|
8
|
+
<React.StrictMode>
|
|
9
|
+
<BrowserRouter>
|
|
10
|
+
<App />
|
|
11
|
+
</BrowserRouter>
|
|
12
|
+
</React.StrictMode>
|
|
13
|
+
);
|
|
14
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Navigate } from "react-router-dom";
|
|
2
|
+
|
|
3
|
+
import { useAuthStore } from "../store/authStore.js";
|
|
4
|
+
|
|
5
|
+
export const ProtectedRoute = ({ children }) => {
|
|
6
|
+
const token = useAuthStore((s) => s.token);
|
|
7
|
+
if (!token) return <Navigate to="/login" replace />;
|
|
8
|
+
return children;
|
|
9
|
+
};
|
|
10
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "./components/ProtectedRoute.jsx";
|
|
2
|
+
export * from "./pages/LoginPage.jsx";
|
|
3
|
+
export * from "./pages/RegisterPage.jsx";
|
|
4
|
+
export * from "./pages/VerifyEmailPage.jsx";
|
|
5
|
+
export * from "./pages/ForgotPasswordPage.jsx";
|
|
6
|
+
export * from "./pages/ResetPasswordPage.jsx";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import { Button } from "../../../shared/components/Button.jsx";
|
|
5
|
+
import { Input } from "../../../shared/components/Input.jsx";
|
|
6
|
+
import { Toast } from "../../../shared/components/Toast.jsx";
|
|
7
|
+
import { formatError } from "../../../shared/utils/formatError.util.js";
|
|
8
|
+
import { forgotPasswordApi } from "../services/auth.service.js";
|
|
9
|
+
|
|
10
|
+
export const ForgotPasswordPage = () => {
|
|
11
|
+
const [email, setEmail] = useState("");
|
|
12
|
+
const [loading, setLoading] = useState(false);
|
|
13
|
+
const [toast, setToast] = useState(null);
|
|
14
|
+
|
|
15
|
+
const onSubmit = async (e) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
setLoading(true);
|
|
18
|
+
try {
|
|
19
|
+
await forgotPasswordApi({ email });
|
|
20
|
+
setToast({ type: "success", message: "If the email exists, a reset link was sent." });
|
|
21
|
+
} catch (err) {
|
|
22
|
+
setToast({ type: "error", message: formatError(err) });
|
|
23
|
+
} finally {
|
|
24
|
+
setLoading(false);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}>
|
|
30
|
+
<form
|
|
31
|
+
onSubmit={onSubmit}
|
|
32
|
+
style={{
|
|
33
|
+
width: "100%",
|
|
34
|
+
maxWidth: 420,
|
|
35
|
+
background: "white",
|
|
36
|
+
border: "1px solid #E5E7EB",
|
|
37
|
+
borderRadius: 16,
|
|
38
|
+
padding: 20
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<h1 style={{ margin: 0, fontSize: 22 }}>Forgot password</h1>
|
|
42
|
+
<p style={{ marginTop: 6, marginBottom: 16, color: "#6B7280", fontSize: 13 }}>
|
|
43
|
+
We will email you a reset link.
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<div style={{ display: "grid", gap: 12 }}>
|
|
47
|
+
<Input label="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
48
|
+
<Button disabled={loading} type="submit">
|
|
49
|
+
{loading ? "Please wait..." : "Send reset link"}
|
|
50
|
+
</Button>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div style={{ marginTop: 14 }}>
|
|
54
|
+
<Link to="/login" style={{ color: "#4F46E5", fontWeight: 700, fontSize: 13 }}>
|
|
55
|
+
Back to login
|
|
56
|
+
</Link>
|
|
57
|
+
</div>
|
|
58
|
+
</form>
|
|
59
|
+
|
|
60
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Link, useNavigate } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import { Button } from "../../../shared/components/Button.jsx";
|
|
5
|
+
import { Input } from "../../../shared/components/Input.jsx";
|
|
6
|
+
import { Toast } from "../../../shared/components/Toast.jsx";
|
|
7
|
+
import { formatError } from "../../../shared/utils/formatError.util.js";
|
|
8
|
+
import { loginApi } from "../services/auth.service.js";
|
|
9
|
+
import { useAuthStore } from "../store/authStore.js";
|
|
10
|
+
|
|
11
|
+
export const LoginPage = () => {
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const setAuth = useAuthStore((s) => s.setAuth);
|
|
14
|
+
|
|
15
|
+
const [email, setEmail] = useState("");
|
|
16
|
+
const [password, setPassword] = useState("");
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
const [toast, setToast] = useState(null);
|
|
19
|
+
|
|
20
|
+
const onSubmit = async (e) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
setLoading(true);
|
|
23
|
+
try {
|
|
24
|
+
const res = await loginApi({ email, password });
|
|
25
|
+
const token = res?.data?.token;
|
|
26
|
+
const user = res?.data?.user;
|
|
27
|
+
setAuth({ token, user });
|
|
28
|
+
setToast({ type: "success", message: "Logged in" });
|
|
29
|
+
navigate("/", { replace: true });
|
|
30
|
+
} catch (err) {
|
|
31
|
+
setToast({ type: "error", message: formatError(err) });
|
|
32
|
+
} finally {
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}>
|
|
39
|
+
<form
|
|
40
|
+
onSubmit={onSubmit}
|
|
41
|
+
style={{
|
|
42
|
+
width: "100%",
|
|
43
|
+
maxWidth: 420,
|
|
44
|
+
background: "white",
|
|
45
|
+
border: "1px solid #E5E7EB",
|
|
46
|
+
borderRadius: 16,
|
|
47
|
+
padding: 20
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<h1 style={{ margin: 0, fontSize: 22 }}>Login</h1>
|
|
51
|
+
<p style={{ marginTop: 6, marginBottom: 16, color: "#6B7280", fontSize: 13 }}>
|
|
52
|
+
Sign in to your account.
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<div style={{ display: "grid", gap: 12 }}>
|
|
56
|
+
<Input label="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
57
|
+
<Input
|
|
58
|
+
label="Password"
|
|
59
|
+
type="password"
|
|
60
|
+
value={password}
|
|
61
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
62
|
+
/>
|
|
63
|
+
<Button disabled={loading} type="submit">
|
|
64
|
+
{loading ? "Please wait..." : "Login"}
|
|
65
|
+
</Button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 14 }}>
|
|
69
|
+
<Link to="/register" style={{ color: "#4F46E5", fontWeight: 700, fontSize: 13 }}>
|
|
70
|
+
Create account
|
|
71
|
+
</Link>
|
|
72
|
+
<Link to="/forgot-password" style={{ color: "#4F46E5", fontWeight: 700, fontSize: 13 }}>
|
|
73
|
+
Forgot password?
|
|
74
|
+
</Link>
|
|
75
|
+
</div>
|
|
76
|
+
</form>
|
|
77
|
+
|
|
78
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Link, useNavigate } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import { Button } from "../../../shared/components/Button.jsx";
|
|
5
|
+
import { Input } from "../../../shared/components/Input.jsx";
|
|
6
|
+
import { Toast } from "../../../shared/components/Toast.jsx";
|
|
7
|
+
import { formatError } from "../../../shared/utils/formatError.util.js";
|
|
8
|
+
import { registerApi } from "../services/auth.service.js";
|
|
9
|
+
import { useAuthStore } from "../store/authStore.js";
|
|
10
|
+
|
|
11
|
+
export const RegisterPage = () => {
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const setAuth = useAuthStore((s) => s.setAuth);
|
|
14
|
+
|
|
15
|
+
const [name, setName] = useState("");
|
|
16
|
+
const [email, setEmail] = useState("");
|
|
17
|
+
const [password, setPassword] = useState("");
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [toast, setToast] = useState(null);
|
|
20
|
+
|
|
21
|
+
const onSubmit = async (e) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setLoading(true);
|
|
24
|
+
try {
|
|
25
|
+
const res = await registerApi({ name, email, password });
|
|
26
|
+
const token = res?.data?.token;
|
|
27
|
+
const user = res?.data?.user;
|
|
28
|
+
setAuth({ token, user });
|
|
29
|
+
setToast({ type: "success", message: "Registered. Verify your email with OTP." });
|
|
30
|
+
navigate("/verify-email", { replace: true });
|
|
31
|
+
} catch (err) {
|
|
32
|
+
setToast({ type: "error", message: formatError(err) });
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}>
|
|
40
|
+
<form
|
|
41
|
+
onSubmit={onSubmit}
|
|
42
|
+
style={{
|
|
43
|
+
width: "100%",
|
|
44
|
+
maxWidth: 420,
|
|
45
|
+
background: "white",
|
|
46
|
+
border: "1px solid #E5E7EB",
|
|
47
|
+
borderRadius: 16,
|
|
48
|
+
padding: 20
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<h1 style={{ margin: 0, fontSize: 22 }}>Create account</h1>
|
|
52
|
+
<p style={{ marginTop: 6, marginBottom: 16, color: "#6B7280", fontSize: 13 }}>
|
|
53
|
+
Get started with StarterKit.
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
<div style={{ display: "grid", gap: 12 }}>
|
|
57
|
+
<Input label="Name" value={name} onChange={(e) => setName(e.target.value)} />
|
|
58
|
+
<Input label="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
59
|
+
<Input
|
|
60
|
+
label="Password"
|
|
61
|
+
type="password"
|
|
62
|
+
value={password}
|
|
63
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
64
|
+
/>
|
|
65
|
+
<Button disabled={loading} type="submit">
|
|
66
|
+
{loading ? "Please wait..." : "Register"}
|
|
67
|
+
</Button>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div style={{ marginTop: 14 }}>
|
|
71
|
+
<Link to="/login" style={{ color: "#4F46E5", fontWeight: 700, fontSize: 13 }}>
|
|
72
|
+
Already have an account? Login
|
|
73
|
+
</Link>
|
|
74
|
+
</div>
|
|
75
|
+
</form>
|
|
76
|
+
|
|
77
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { Link, useSearchParams } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import { Button } from "../../../shared/components/Button.jsx";
|
|
5
|
+
import { Input } from "../../../shared/components/Input.jsx";
|
|
6
|
+
import { Toast } from "../../../shared/components/Toast.jsx";
|
|
7
|
+
import { formatError } from "../../../shared/utils/formatError.util.js";
|
|
8
|
+
import { resetPasswordApi } from "../services/auth.service.js";
|
|
9
|
+
|
|
10
|
+
export const ResetPasswordPage = () => {
|
|
11
|
+
const [params] = useSearchParams();
|
|
12
|
+
const token = useMemo(() => params.get("token") || "", [params]);
|
|
13
|
+
|
|
14
|
+
const [password, setPassword] = useState("");
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
const [toast, setToast] = useState(null);
|
|
17
|
+
|
|
18
|
+
const onSubmit = async (e) => {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
setLoading(true);
|
|
21
|
+
try {
|
|
22
|
+
await resetPasswordApi({ token, password });
|
|
23
|
+
setToast({ type: "success", message: "Password reset successfully. You can login now." });
|
|
24
|
+
} catch (err) {
|
|
25
|
+
setToast({ type: "error", message: formatError(err) });
|
|
26
|
+
} finally {
|
|
27
|
+
setLoading(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}>
|
|
33
|
+
<form
|
|
34
|
+
onSubmit={onSubmit}
|
|
35
|
+
style={{
|
|
36
|
+
width: "100%",
|
|
37
|
+
maxWidth: 420,
|
|
38
|
+
background: "white",
|
|
39
|
+
border: "1px solid #E5E7EB",
|
|
40
|
+
borderRadius: 16,
|
|
41
|
+
padding: 20
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<h1 style={{ margin: 0, fontSize: 22 }}>Reset password</h1>
|
|
45
|
+
<p style={{ marginTop: 6, marginBottom: 16, color: "#6B7280", fontSize: 13 }}>
|
|
46
|
+
Set a new password for your account.
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<div style={{ display: "grid", gap: 12 }}>
|
|
50
|
+
<Input
|
|
51
|
+
label="Reset token"
|
|
52
|
+
value={token}
|
|
53
|
+
readOnly
|
|
54
|
+
style={{ background: "#F9FAFB" }}
|
|
55
|
+
/>
|
|
56
|
+
<Input
|
|
57
|
+
label="New password"
|
|
58
|
+
type="password"
|
|
59
|
+
value={password}
|
|
60
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
61
|
+
/>
|
|
62
|
+
<Button disabled={loading || !token} type="submit">
|
|
63
|
+
{loading ? "Please wait..." : "Reset password"}
|
|
64
|
+
</Button>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div style={{ marginTop: 14 }}>
|
|
68
|
+
<Link to="/login" style={{ color: "#4F46E5", fontWeight: 700, fontSize: 13 }}>
|
|
69
|
+
Back to login
|
|
70
|
+
</Link>
|
|
71
|
+
</div>
|
|
72
|
+
</form>
|
|
73
|
+
|
|
74
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import { Button } from "../../../shared/components/Button.jsx";
|
|
5
|
+
import { Input } from "../../../shared/components/Input.jsx";
|
|
6
|
+
import { Toast } from "../../../shared/components/Toast.jsx";
|
|
7
|
+
import { formatError } from "../../../shared/utils/formatError.util.js";
|
|
8
|
+
import { verifyEmailApi } from "../services/auth.service.js";
|
|
9
|
+
import { useAuthStore } from "../store/authStore.js";
|
|
10
|
+
|
|
11
|
+
export const VerifyEmailPage = () => {
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const refreshMe = useAuthStore((s) => s.refreshMe);
|
|
14
|
+
|
|
15
|
+
const [otp, setOtp] = useState("");
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
const [toast, setToast] = useState(null);
|
|
18
|
+
|
|
19
|
+
const onSubmit = async (e) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
setLoading(true);
|
|
22
|
+
try {
|
|
23
|
+
await verifyEmailApi({ otp });
|
|
24
|
+
await refreshMe();
|
|
25
|
+
setToast({ type: "success", message: "Email verified" });
|
|
26
|
+
navigate("/", { replace: true });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
setToast({ type: "error", message: formatError(err) });
|
|
29
|
+
} finally {
|
|
30
|
+
setLoading(false);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}>
|
|
36
|
+
<form
|
|
37
|
+
onSubmit={onSubmit}
|
|
38
|
+
style={{
|
|
39
|
+
width: "100%",
|
|
40
|
+
maxWidth: 420,
|
|
41
|
+
background: "white",
|
|
42
|
+
border: "1px solid #E5E7EB",
|
|
43
|
+
borderRadius: 16,
|
|
44
|
+
padding: 20
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<h1 style={{ margin: 0, fontSize: 22 }}>Verify Email</h1>
|
|
48
|
+
<p style={{ marginTop: 6, marginBottom: 16, color: "#6B7280", fontSize: 13 }}>
|
|
49
|
+
Enter the 6-digit OTP sent to your email.
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<div style={{ display: "grid", gap: 12 }}>
|
|
53
|
+
<Input
|
|
54
|
+
label="OTP"
|
|
55
|
+
value={otp}
|
|
56
|
+
onChange={(e) => setOtp(e.target.value)}
|
|
57
|
+
placeholder="123456"
|
|
58
|
+
/>
|
|
59
|
+
<Button disabled={loading} type="submit">
|
|
60
|
+
{loading ? "Please wait..." : "Verify"}
|
|
61
|
+
</Button>
|
|
62
|
+
</div>
|
|
63
|
+
</form>
|
|
64
|
+
|
|
65
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { api } from "../../../shared/services/api.js";
|
|
2
|
+
|
|
3
|
+
export const registerApi = async ({ name, email, password }) => {
|
|
4
|
+
const res = await api.post("/api/auth/register", { name, email, password });
|
|
5
|
+
return res.data;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const loginApi = async ({ email, password }) => {
|
|
9
|
+
const res = await api.post("/api/auth/login", { email, password });
|
|
10
|
+
return res.data;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const meApi = async () => {
|
|
14
|
+
const res = await api.get("/api/auth/me");
|
|
15
|
+
return res.data;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const verifyEmailApi = async ({ otp }) => {
|
|
19
|
+
const res = await api.post("/api/auth/verify-email", { otp });
|
|
20
|
+
return res.data;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const forgotPasswordApi = async ({ email }) => {
|
|
24
|
+
const res = await api.post("/api/auth/forgot-password", { email });
|
|
25
|
+
return res.data;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const resetPasswordApi = async ({ token, password }) => {
|
|
29
|
+
const res = await api.post(`/api/auth/reset-password?token=${encodeURIComponent(token)}`, {
|
|
30
|
+
password
|
|
31
|
+
});
|
|
32
|
+
return res.data;
|
|
33
|
+
};
|
|
34
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
import { storage } from "../../../shared/utils/storage.util.js";
|
|
4
|
+
import { meApi } from "../services/auth.service.js";
|
|
5
|
+
|
|
6
|
+
export const useAuthStore = create((set, get) => ({
|
|
7
|
+
user: storage.get("user"),
|
|
8
|
+
token: storage.get("token"),
|
|
9
|
+
loading: false,
|
|
10
|
+
|
|
11
|
+
setAuth({ user, token }) {
|
|
12
|
+
storage.set("user", user);
|
|
13
|
+
storage.set("token", token);
|
|
14
|
+
set({ user, token });
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
clearAuth() {
|
|
18
|
+
storage.remove("user");
|
|
19
|
+
storage.remove("token");
|
|
20
|
+
set({ user: null, token: null });
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async refreshMe() {
|
|
24
|
+
const token = get().token;
|
|
25
|
+
if (!token) return;
|
|
26
|
+
set({ loading: true });
|
|
27
|
+
try {
|
|
28
|
+
const res = await meApi();
|
|
29
|
+
const user = res?.data?.user || null;
|
|
30
|
+
storage.set("user", user);
|
|
31
|
+
set({ user });
|
|
32
|
+
} finally {
|
|
33
|
+
set({ loading: false });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
|