@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.
Files changed (84) hide show
  1. package/README.md +30 -0
  2. package/index.js +327 -0
  3. package/package.json +29 -0
  4. package/templates/mern/backend/.env.example +11 -0
  5. package/templates/mern/backend/Dockerfile +13 -0
  6. package/templates/mern/backend/package.json +23 -0
  7. package/templates/mern/backend/server.js +25 -0
  8. package/templates/mern/backend/src/app.js +48 -0
  9. package/templates/mern/backend/src/config/db.js +17 -0
  10. package/templates/mern/backend/src/config/env.js +38 -0
  11. package/templates/mern/backend/src/middleware/authMiddleware.js +30 -0
  12. package/templates/mern/backend/src/middleware/errorMiddleware.js +17 -0
  13. package/templates/mern/backend/src/middleware/notFound.middleware.js +9 -0
  14. package/templates/mern/backend/src/modules/auth/auth.controller.js +45 -0
  15. package/templates/mern/backend/src/modules/auth/auth.model.js +20 -0
  16. package/templates/mern/backend/src/modules/auth/auth.routes.js +55 -0
  17. package/templates/mern/backend/src/modules/auth/auth.service.js +185 -0
  18. package/templates/mern/backend/src/modules/auth/dto/login.dto.js +12 -0
  19. package/templates/mern/backend/src/modules/auth/dto/register.dto.js +14 -0
  20. package/templates/mern/backend/src/modules/email/dto/sendEmail.dto.js +16 -0
  21. package/templates/mern/backend/src/modules/email/email.controller.js +33 -0
  22. package/templates/mern/backend/src/modules/email/email.routes.js +13 -0
  23. package/templates/mern/backend/src/modules/email/email.service.js +88 -0
  24. package/templates/mern/backend/src/modules/email/templates/resetPassword.html +47 -0
  25. package/templates/mern/backend/src/modules/email/templates/verifyEmail.html +45 -0
  26. package/templates/mern/backend/src/modules/email/templates/welcome.html +47 -0
  27. package/templates/mern/backend/src/utils/apiResponse.util.js +9 -0
  28. package/templates/mern/backend/src/utils/asyncHandler.util.js +6 -0
  29. package/templates/mern/backend/src/utils/generateOTP.util.js +10 -0
  30. package/templates/mern/backend/src/utils/generateResetToken.util.js +8 -0
  31. package/templates/mern/backend/src/utils/generateToken.util.js +17 -0
  32. package/templates/mern/backend/src/utils/hashPassword.util.js +11 -0
  33. package/templates/mern/backend/src/utils/validateDto.util.js +18 -0
  34. package/templates/mern/frontend/.env.example +1 -0
  35. package/templates/mern/frontend/Dockerfile +13 -0
  36. package/templates/mern/frontend/index.html +13 -0
  37. package/templates/mern/frontend/package.json +23 -0
  38. package/templates/mern/frontend/src/App.jsx +102 -0
  39. package/templates/mern/frontend/src/main.jsx +14 -0
  40. package/templates/mern/frontend/src/modules/auth/components/ProtectedRoute.jsx +10 -0
  41. package/templates/mern/frontend/src/modules/auth/index.js +6 -0
  42. package/templates/mern/frontend/src/modules/auth/pages/ForgotPasswordPage.jsx +64 -0
  43. package/templates/mern/frontend/src/modules/auth/pages/LoginPage.jsx +82 -0
  44. package/templates/mern/frontend/src/modules/auth/pages/RegisterPage.jsx +81 -0
  45. package/templates/mern/frontend/src/modules/auth/pages/ResetPasswordPage.jsx +78 -0
  46. package/templates/mern/frontend/src/modules/auth/pages/VerifyEmailPage.jsx +69 -0
  47. package/templates/mern/frontend/src/modules/auth/services/auth.service.js +34 -0
  48. package/templates/mern/frontend/src/modules/auth/store/authStore.js +37 -0
  49. package/templates/mern/frontend/src/modules/dashboard/index.js +2 -0
  50. package/templates/mern/frontend/src/modules/dashboard/pages/DashboardPage.jsx +41 -0
  51. package/templates/mern/frontend/src/shared/components/Button.jsx +31 -0
  52. package/templates/mern/frontend/src/shared/components/Input.jsx +23 -0
  53. package/templates/mern/frontend/src/shared/components/Toast.jsx +52 -0
  54. package/templates/mern/frontend/src/shared/services/api.js +20 -0
  55. package/templates/mern/frontend/src/shared/utils/formatError.util.js +8 -0
  56. package/templates/mern/frontend/src/shared/utils/storage.util.js +25 -0
  57. package/templates/mern/frontend/vite.config.js +13 -0
  58. package/templates/mern/frontend-next/.env.example +1 -0
  59. package/templates/mern/frontend-next/app/forgot-password/page.js +8 -0
  60. package/templates/mern/frontend-next/app/layout.js +15 -0
  61. package/templates/mern/frontend-next/app/login/page.js +8 -0
  62. package/templates/mern/frontend-next/app/page.js +22 -0
  63. package/templates/mern/frontend-next/app/register/page.js +8 -0
  64. package/templates/mern/frontend-next/app/reset-password/page.js +8 -0
  65. package/templates/mern/frontend-next/app/verify-email/page.js +8 -0
  66. package/templates/mern/frontend-next/jsconfig.json +6 -0
  67. package/templates/mern/frontend-next/next.config.mjs +7 -0
  68. package/templates/mern/frontend-next/package.json +18 -0
  69. package/templates/mern/frontend-next/src/modules/auth/components/ProtectedRoute.jsx +19 -0
  70. package/templates/mern/frontend-next/src/modules/auth/pages/ForgotPasswordPage.jsx +66 -0
  71. package/templates/mern/frontend-next/src/modules/auth/pages/LoginPage.jsx +88 -0
  72. package/templates/mern/frontend-next/src/modules/auth/pages/RegisterPage.jsx +84 -0
  73. package/templates/mern/frontend-next/src/modules/auth/pages/ResetPasswordPage.jsx +76 -0
  74. package/templates/mern/frontend-next/src/modules/auth/pages/VerifyEmailPage.jsx +71 -0
  75. package/templates/mern/frontend-next/src/modules/auth/services/auth.service.js +29 -0
  76. package/templates/mern/frontend-next/src/modules/auth/store/authStore.js +37 -0
  77. package/templates/mern/frontend-next/src/modules/dashboard/pages/DashboardPage.jsx +46 -0
  78. package/templates/mern/frontend-next/src/shared/components/Button.jsx +31 -0
  79. package/templates/mern/frontend-next/src/shared/components/Input.jsx +24 -0
  80. package/templates/mern/frontend-next/src/shared/components/Toast.jsx +52 -0
  81. package/templates/mern/frontend-next/src/shared/services/api.js +25 -0
  82. package/templates/mern/frontend-next/src/shared/utils/formatError.util.js +8 -0
  83. package/templates/mern/frontend-next/src/shared/utils/storage.util.js +28 -0
  84. package/templates/mern/package.json +6 -0
@@ -0,0 +1,13 @@
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json package-lock.json* ./
6
+ RUN npm install
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 5173
11
+
12
+ CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
13
+
@@ -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
+
@@ -0,0 +1,2 @@
1
+ export * from "./pages/DashboardPage.jsx";
2
+