@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,20 @@
1
+ import mongoose from "mongoose";
2
+
3
+ const userSchema = new mongoose.Schema(
4
+ {
5
+ name: { type: String, trim: true, required: true },
6
+ email: { type: String, trim: true, lowercase: true, unique: true, required: true, index: true },
7
+ password: { type: String, required: true, select: false },
8
+ role: { type: String, enum: ["user", "admin"], default: "user" },
9
+ isEmailVerified: { type: Boolean, default: false },
10
+ otp: { type: String, default: null },
11
+ otpExpiry: { type: Date, default: null },
12
+ resetPasswordToken: { type: String, default: null },
13
+ resetPasswordExpiry: { type: Date, default: null }
14
+ },
15
+ { timestamps: true }
16
+ );
17
+
18
+ const User = mongoose.model("User", userSchema);
19
+ export default User;
20
+
@@ -0,0 +1,55 @@
1
+ import express from "express";
2
+ import Joi from "joi";
3
+
4
+ import { validateDto } from "../../utils/validateDto.util.js";
5
+ import { authMiddleware } from "../../middleware/authMiddleware.js";
6
+ import { loginSchema } from "./dto/login.dto.js";
7
+ import { registerSchema } from "./dto/register.dto.js";
8
+ import {
9
+ forgotPassword,
10
+ getMe,
11
+ login,
12
+ register,
13
+ resetPassword,
14
+ verifyEmail
15
+ } from "./auth.controller.js";
16
+
17
+ const router = express.Router();
18
+
19
+ router.post("/register", validateDto(registerSchema), register);
20
+ router.post("/login", validateDto(loginSchema), login);
21
+
22
+ router.get("/me", authMiddleware, getMe);
23
+ router.post(
24
+ "/verify-email",
25
+ authMiddleware,
26
+ validateDto(
27
+ Joi.object({
28
+ otp: Joi.string().length(6).required()
29
+ })
30
+ ),
31
+ verifyEmail
32
+ );
33
+
34
+ router.post(
35
+ "/forgot-password",
36
+ validateDto(
37
+ Joi.object({
38
+ email: Joi.string().email().required()
39
+ })
40
+ ),
41
+ forgotPassword
42
+ );
43
+
44
+ router.post(
45
+ "/reset-password",
46
+ validateDto(
47
+ Joi.object({
48
+ password: Joi.string().min(6).required()
49
+ })
50
+ ),
51
+ resetPassword
52
+ );
53
+
54
+ export default router;
55
+
@@ -0,0 +1,185 @@
1
+ import crypto from "node:crypto";
2
+
3
+ import User from "./auth.model.js";
4
+ import { hashPassword, comparePassword } from "../../utils/hashPassword.util.js";
5
+ import { signToken } from "../../utils/generateToken.util.js";
6
+ import { generateOTP, getOTPExpiry } from "../../utils/generateOTP.util.js";
7
+ import { generateResetToken } from "../../utils/generateResetToken.util.js";
8
+ import {
9
+ sendPasswordResetEmail,
10
+ sendVerifyEmail,
11
+ sendWelcomeEmail
12
+ } from "../email/email.service.js";
13
+
14
+ const sanitizeUser = (userDoc) => {
15
+ if (!userDoc) return null;
16
+ const user = userDoc.toObject ? userDoc.toObject() : userDoc;
17
+ delete user.password;
18
+ delete user.otp;
19
+ delete user.otpExpiry;
20
+ delete user.resetPasswordToken;
21
+ delete user.resetPasswordExpiry;
22
+ return user;
23
+ };
24
+
25
+ export const registerUser = async ({ name, email, password }) => {
26
+ try {
27
+ const existing = await User.findOne({ email }).select("_id");
28
+ if (existing) {
29
+ const err = new Error("Email already in use");
30
+ err.statusCode = 409;
31
+ throw err;
32
+ }
33
+
34
+ const hashed = await hashPassword(password);
35
+ const otp = generateOTP();
36
+ const otpExpiry = getOTPExpiry();
37
+
38
+ const user = await User.create({
39
+ name,
40
+ email,
41
+ password: hashed,
42
+ isEmailVerified: false,
43
+ otp,
44
+ otpExpiry
45
+ });
46
+
47
+ const token = signToken(user._id.toString());
48
+
49
+ await sendWelcomeEmail(user.email, user.name);
50
+ await sendVerifyEmail(user.email, user.name, otp);
51
+
52
+ const safeUser = await User.findById(user._id).select("-password");
53
+ return { user: sanitizeUser(safeUser), token };
54
+ } catch (err) {
55
+ throw err;
56
+ }
57
+ };
58
+
59
+ export const loginUser = async ({ email, password }) => {
60
+ try {
61
+ const user = await User.findOne({ email }).select("+password");
62
+ if (!user) {
63
+ const err = new Error("Invalid credentials");
64
+ err.statusCode = 401;
65
+ throw err;
66
+ }
67
+
68
+ const ok = await comparePassword(password, user.password);
69
+ if (!ok) {
70
+ const err = new Error("Invalid credentials");
71
+ err.statusCode = 401;
72
+ throw err;
73
+ }
74
+
75
+ const token = signToken(user._id.toString());
76
+ const safeUser = await User.findById(user._id).select("-password");
77
+ return { user: sanitizeUser(safeUser), token };
78
+ } catch (err) {
79
+ throw err;
80
+ }
81
+ };
82
+
83
+ export const getMeService = async ({ userId }) => {
84
+ try {
85
+ const user = await User.findById(userId).select("-password");
86
+ if (!user) {
87
+ const err = new Error("User not found");
88
+ err.statusCode = 404;
89
+ throw err;
90
+ }
91
+ return { user: sanitizeUser(user) };
92
+ } catch (err) {
93
+ throw err;
94
+ }
95
+ };
96
+
97
+ export const verifyEmailService = async ({ userId, otp }) => {
98
+ try {
99
+ const user = await User.findById(userId).select("-password");
100
+ if (!user) {
101
+ const err = new Error("User not found");
102
+ err.statusCode = 404;
103
+ throw err;
104
+ }
105
+
106
+ if (user.isEmailVerified) {
107
+ return { user: sanitizeUser(user) };
108
+ }
109
+
110
+ const now = new Date();
111
+ if (!user.otp || !user.otpExpiry) {
112
+ const err = new Error("No OTP found");
113
+ err.statusCode = 400;
114
+ throw err;
115
+ }
116
+
117
+ if (user.otp !== otp) {
118
+ const err = new Error("Invalid OTP");
119
+ err.statusCode = 400;
120
+ throw err;
121
+ }
122
+
123
+ if (user.otpExpiry < now) {
124
+ const err = new Error("OTP expired");
125
+ err.statusCode = 400;
126
+ throw err;
127
+ }
128
+
129
+ user.isEmailVerified = true;
130
+ user.otp = null;
131
+ user.otpExpiry = null;
132
+ await user.save();
133
+
134
+ return { user: sanitizeUser(user) };
135
+ } catch (err) {
136
+ throw err;
137
+ }
138
+ };
139
+
140
+ export const forgotPasswordService = async ({ email }) => {
141
+ try {
142
+ const user = await User.findOne({ email }).select("-password");
143
+ if (!user) {
144
+ return { ok: true };
145
+ }
146
+
147
+ const { rawToken, hashedToken } = generateResetToken();
148
+ user.resetPasswordToken = hashedToken;
149
+ user.resetPasswordExpiry = new Date(Date.now() + 60 * 60 * 1000);
150
+ await user.save();
151
+
152
+ const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${rawToken}`;
153
+ await sendPasswordResetEmail(user.email, user.name, resetLink);
154
+
155
+ return { ok: true };
156
+ } catch (err) {
157
+ throw err;
158
+ }
159
+ };
160
+
161
+ export const resetPasswordService = async ({ rawToken, newPassword }) => {
162
+ try {
163
+ const hashed = crypto.createHash("sha256").update(rawToken).digest("hex");
164
+ const user = await User.findOne({
165
+ resetPasswordToken: hashed,
166
+ resetPasswordExpiry: { $gt: new Date() }
167
+ }).select("+password");
168
+
169
+ if (!user) {
170
+ const err = new Error("Invalid or expired reset token");
171
+ err.statusCode = 400;
172
+ throw err;
173
+ }
174
+
175
+ user.password = await hashPassword(newPassword);
176
+ user.resetPasswordToken = null;
177
+ user.resetPasswordExpiry = null;
178
+ await user.save();
179
+
180
+ return { ok: true };
181
+ } catch (err) {
182
+ throw err;
183
+ }
184
+ };
185
+
@@ -0,0 +1,12 @@
1
+ import Joi from "joi";
2
+
3
+ export const loginShape = {
4
+ email: "string",
5
+ password: "string"
6
+ };
7
+
8
+ export const loginSchema = Joi.object({
9
+ email: Joi.string().email().required(),
10
+ password: Joi.string().min(6).required()
11
+ });
12
+
@@ -0,0 +1,14 @@
1
+ import Joi from "joi";
2
+
3
+ export const registerShape = {
4
+ name: "string",
5
+ email: "string",
6
+ password: "string"
7
+ };
8
+
9
+ export const registerSchema = Joi.object({
10
+ name: Joi.string().min(2).max(50).required(),
11
+ email: Joi.string().email().required(),
12
+ password: Joi.string().min(6).required()
13
+ });
14
+
@@ -0,0 +1,16 @@
1
+ import Joi from "joi";
2
+
3
+ export const sendEmailShape = {
4
+ to: "string",
5
+ subject: "string",
6
+ template: "string",
7
+ data: "object"
8
+ };
9
+
10
+ export const sendEmailSchema = Joi.object({
11
+ to: Joi.string().email().required(),
12
+ subject: Joi.string().min(2).max(150).required(),
13
+ template: Joi.string().valid("welcome", "verifyEmail", "resetPassword").required(),
14
+ data: Joi.object().default({})
15
+ });
16
+
@@ -0,0 +1,33 @@
1
+ import { asyncHandler } from "../../utils/asyncHandler.util.js";
2
+ import { apiResponse } from "../../utils/apiResponse.util.js";
3
+ import {
4
+ sendPasswordResetEmail,
5
+ sendVerifyEmail,
6
+ sendWelcomeEmail
7
+ } from "./email.service.js";
8
+
9
+ const requireAdmin = (req) => {
10
+ return req.user && req.user.role === "admin";
11
+ };
12
+
13
+ export const sendEmail = asyncHandler(async (req, res) => {
14
+ if (!requireAdmin(req)) {
15
+ const err = new Error("Forbidden");
16
+ err.statusCode = 403;
17
+ throw err;
18
+ }
19
+
20
+ const { to, subject, template, data } = req.body;
21
+ const name = data?.name || "there";
22
+
23
+ if (template === "welcome") {
24
+ await sendWelcomeEmail(to, name);
25
+ } else if (template === "verifyEmail") {
26
+ await sendVerifyEmail(to, name, data?.otp || "000000");
27
+ } else if (template === "resetPassword") {
28
+ await sendPasswordResetEmail(to, name, data?.resetLink || process.env.FRONTEND_URL);
29
+ }
30
+
31
+ res.status(200).json(apiResponse(true, subject, { to, template }));
32
+ });
33
+
@@ -0,0 +1,13 @@
1
+ import express from "express";
2
+
3
+ import { authMiddleware } from "../../middleware/authMiddleware.js";
4
+ import { validateDto } from "../../utils/validateDto.util.js";
5
+ import { sendEmailSchema } from "./dto/sendEmail.dto.js";
6
+ import { sendEmail } from "./email.controller.js";
7
+
8
+ const router = express.Router();
9
+
10
+ router.post("/send", authMiddleware, validateDto(sendEmailSchema), sendEmail);
11
+
12
+ export default router;
13
+
@@ -0,0 +1,88 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import fs from "fs-extra";
5
+ import nodemailer from "nodemailer";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ const getTransporter = () => {
11
+ const host = process.env.SMTP_HOST;
12
+ const port = Number(process.env.SMTP_PORT);
13
+ const user = process.env.SMTP_USER;
14
+ const pass = process.env.SMTP_PASS;
15
+
16
+ const auth = user && pass ? { user, pass } : undefined;
17
+ return nodemailer.createTransport({ host, port, secure: port === 465, auth });
18
+ };
19
+
20
+ const readTemplate = (templateFile) => {
21
+ const full = path.join(__dirname, "templates", templateFile);
22
+ return fs.readFileSync(full, "utf8");
23
+ };
24
+
25
+ const replacePlaceholders = (html, data) => {
26
+ return Object.entries(data).reduce((acc, [key, value]) => {
27
+ const pattern = new RegExp(`{{\\s*${key}\\s*}}`, "g");
28
+ return acc.replace(pattern, String(value));
29
+ }, html);
30
+ };
31
+
32
+ const sendHtmlEmail = async ({ to, subject, html }) => {
33
+ try {
34
+ const transporter = getTransporter();
35
+ await transporter.sendMail({
36
+ from: process.env.EMAIL_FROM,
37
+ to,
38
+ subject,
39
+ html
40
+ });
41
+ } catch (err) {
42
+ // eslint-disable-next-line no-console
43
+ console.error("Email send failed:", err?.message || err);
44
+ }
45
+ };
46
+
47
+ export const sendWelcomeEmail = async (to, name) => {
48
+ try {
49
+ const html = replacePlaceholders(readTemplate("welcome.html"), {
50
+ name,
51
+ appName: "StarterKit",
52
+ frontendUrl: process.env.FRONTEND_URL
53
+ });
54
+ await sendHtmlEmail({ to, subject: "Welcome to StarterKit", html });
55
+ } catch (err) {
56
+ // eslint-disable-next-line no-console
57
+ console.error("sendWelcomeEmail failed:", err?.message || err);
58
+ }
59
+ };
60
+
61
+ export const sendVerifyEmail = async (to, name, otp) => {
62
+ try {
63
+ const html = replacePlaceholders(readTemplate("verifyEmail.html"), {
64
+ name,
65
+ otp,
66
+ appName: "StarterKit"
67
+ });
68
+ await sendHtmlEmail({ to, subject: "Verify your email", html });
69
+ } catch (err) {
70
+ // eslint-disable-next-line no-console
71
+ console.error("sendVerifyEmail failed:", err?.message || err);
72
+ }
73
+ };
74
+
75
+ export const sendPasswordResetEmail = async (to, name, resetLink) => {
76
+ try {
77
+ const html = replacePlaceholders(readTemplate("resetPassword.html"), {
78
+ name,
79
+ resetLink,
80
+ appName: "StarterKit"
81
+ });
82
+ await sendHtmlEmail({ to, subject: "Reset your password", html });
83
+ } catch (err) {
84
+ // eslint-disable-next-line no-console
85
+ console.error("sendPasswordResetEmail failed:", err?.message || err);
86
+ }
87
+ };
88
+
@@ -0,0 +1,47 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Reset Password</title>
7
+ </head>
8
+ <body style="margin:0;padding:0;background:#F9FAFB;font-family:Arial,Helvetica,sans-serif;color:#111827;">
9
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#F9FAFB;padding:24px 0;">
10
+ <tr>
11
+ <td align="center">
12
+ <table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #E5E7EB;">
13
+ <tr>
14
+ <td style="background:#4F46E5;padding:20px 24px;">
15
+ <div style="font-size:18px;font-weight:700;color:#ffffff;">{{appName}}</div>
16
+ </td>
17
+ </tr>
18
+ <tr>
19
+ <td style="padding:24px;">
20
+ <h1 style="margin:0 0 12px 0;font-size:22px;line-height:1.3;">Reset your password</h1>
21
+ <p style="margin:0 0 16px 0;font-size:14px;line-height:1.6;color:#374151;">
22
+ Hi {{name}}, click the button below to reset your password.
23
+ </p>
24
+ <div style="margin:24px 0;">
25
+ <a href="{{resetLink}}" style="display:inline-block;background:#4F46E5;color:#ffffff;text-decoration:none;padding:12px 16px;border-radius:10px;font-weight:700;font-size:14px;">
26
+ Reset password
27
+ </a>
28
+ </div>
29
+ <p style="margin:0;font-size:12px;line-height:1.6;color:#6B7280;">
30
+ This link expires in 1 hour. If you didn’t request a password reset, you can ignore this email.
31
+ </p>
32
+ </td>
33
+ </tr>
34
+ <tr>
35
+ <td style="padding:16px 24px;background:#F9FAFB;border-top:1px solid #E5E7EB;">
36
+ <p style="margin:0;font-size:12px;line-height:1.6;color:#6B7280;">
37
+ {{appName}} — This is an automated email, please do not reply.
38
+ </p>
39
+ </td>
40
+ </tr>
41
+ </table>
42
+ </td>
43
+ </tr>
44
+ </table>
45
+ </body>
46
+ </html>
47
+
@@ -0,0 +1,45 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Verify Email</title>
7
+ </head>
8
+ <body style="margin:0;padding:0;background:#F9FAFB;font-family:Arial,Helvetica,sans-serif;color:#111827;">
9
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#F9FAFB;padding:24px 0;">
10
+ <tr>
11
+ <td align="center">
12
+ <table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #E5E7EB;">
13
+ <tr>
14
+ <td style="background:#4F46E5;padding:20px 24px;">
15
+ <div style="font-size:18px;font-weight:700;color:#ffffff;">{{appName}}</div>
16
+ </td>
17
+ </tr>
18
+ <tr>
19
+ <td style="padding:24px;">
20
+ <h1 style="margin:0 0 12px 0;font-size:22px;line-height:1.3;">Verify your email</h1>
21
+ <p style="margin:0 0 16px 0;font-size:14px;line-height:1.6;color:#374151;">
22
+ Hi {{name}}, use the OTP below to verify your email address.
23
+ </p>
24
+ <div style="margin:18px 0;padding:16px;border:1px dashed #D1D5DB;border-radius:12px;background:#F9FAFB;text-align:center;">
25
+ <div style="font-size:28px;letter-spacing:6px;font-weight:800;color:#111827;">{{otp}}</div>
26
+ </div>
27
+ <p style="margin:0;font-size:12px;line-height:1.6;color:#6B7280;">
28
+ This OTP expires in 10 minutes.
29
+ </p>
30
+ </td>
31
+ </tr>
32
+ <tr>
33
+ <td style="padding:16px 24px;background:#F9FAFB;border-top:1px solid #E5E7EB;">
34
+ <p style="margin:0;font-size:12px;line-height:1.6;color:#6B7280;">
35
+ {{appName}} — This is an automated email, please do not reply.
36
+ </p>
37
+ </td>
38
+ </tr>
39
+ </table>
40
+ </td>
41
+ </tr>
42
+ </table>
43
+ </body>
44
+ </html>
45
+
@@ -0,0 +1,47 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Welcome</title>
7
+ </head>
8
+ <body style="margin:0;padding:0;background:#F9FAFB;font-family:Arial,Helvetica,sans-serif;color:#111827;">
9
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#F9FAFB;padding:24px 0;">
10
+ <tr>
11
+ <td align="center">
12
+ <table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #E5E7EB;">
13
+ <tr>
14
+ <td style="background:#4F46E5;padding:20px 24px;">
15
+ <div style="font-size:18px;font-weight:700;color:#ffffff;">{{appName}}</div>
16
+ </td>
17
+ </tr>
18
+ <tr>
19
+ <td style="padding:24px;">
20
+ <h1 style="margin:0 0 12px 0;font-size:22px;line-height:1.3;">Welcome, {{name}}!</h1>
21
+ <p style="margin:0 0 16px 0;font-size:14px;line-height:1.6;color:#374151;">
22
+ Your account is ready. You can now sign in and start using the app.
23
+ </p>
24
+ <div style="margin:24px 0;">
25
+ <a href="{{frontendUrl}}" style="display:inline-block;background:#4F46E5;color:#ffffff;text-decoration:none;padding:12px 16px;border-radius:10px;font-weight:700;font-size:14px;">
26
+ Get started
27
+ </a>
28
+ </div>
29
+ <p style="margin:0;font-size:12px;line-height:1.6;color:#6B7280;">
30
+ If you didn’t create this account, you can ignore this email.
31
+ </p>
32
+ </td>
33
+ </tr>
34
+ <tr>
35
+ <td style="padding:16px 24px;background:#F9FAFB;border-top:1px solid #E5E7EB;">
36
+ <p style="margin:0;font-size:12px;line-height:1.6;color:#6B7280;">
37
+ {{appName}} — This is an automated email, please do not reply.
38
+ </p>
39
+ </td>
40
+ </tr>
41
+ </table>
42
+ </td>
43
+ </tr>
44
+ </table>
45
+ </body>
46
+ </html>
47
+
@@ -0,0 +1,9 @@
1
+ export const apiResponse = (success, message, data) => {
2
+ return {
3
+ success: Boolean(success),
4
+ message,
5
+ data: data ?? null,
6
+ timestamp: new Date().toISOString()
7
+ };
8
+ };
9
+
@@ -0,0 +1,6 @@
1
+ export const asyncHandler = (fn) => {
2
+ return (req, res, next) => {
3
+ Promise.resolve(fn(req, res, next)).catch(next);
4
+ };
5
+ };
6
+
@@ -0,0 +1,10 @@
1
+ export const generateOTP = () => {
2
+ const otp = Math.floor(100000 + Math.random() * 900000);
3
+ return String(otp);
4
+ };
5
+
6
+ export const getOTPExpiry = () => {
7
+ const now = Date.now();
8
+ return new Date(now + 10 * 60 * 1000);
9
+ };
10
+
@@ -0,0 +1,8 @@
1
+ import crypto from "node:crypto";
2
+
3
+ export const generateResetToken = () => {
4
+ const rawToken = crypto.randomBytes(32).toString("hex");
5
+ const hashedToken = crypto.createHash("sha256").update(rawToken).digest("hex");
6
+ return { rawToken, hashedToken };
7
+ };
8
+
@@ -0,0 +1,17 @@
1
+ import jwt from "jsonwebtoken";
2
+
3
+ export const signToken = (userId) => {
4
+ const secret = process.env.JWT_SECRET;
5
+ const expiresIn = process.env.JWT_EXPIRES_IN || "7d";
6
+ if (!secret) throw new Error("JWT_SECRET is not set");
7
+
8
+ return jwt.sign({ userId }, secret, { expiresIn });
9
+ };
10
+
11
+ export const verifyToken = (token) => {
12
+ const secret = process.env.JWT_SECRET;
13
+ if (!secret) throw new Error("JWT_SECRET is not set");
14
+
15
+ return jwt.verify(token, secret);
16
+ };
17
+
@@ -0,0 +1,11 @@
1
+ import bcrypt from "bcryptjs";
2
+
3
+ export const hashPassword = async (plain) => {
4
+ const saltRounds = 10;
5
+ return bcrypt.hash(plain, saltRounds);
6
+ };
7
+
8
+ export const comparePassword = async (plain, hashed) => {
9
+ return bcrypt.compare(plain, hashed);
10
+ };
11
+
@@ -0,0 +1,18 @@
1
+ import { apiResponse } from "./apiResponse.util.js";
2
+
3
+ export const validateDto = (schema) => {
4
+ return (req, res, next) => {
5
+ const { error, value } = schema.validate(req.body, {
6
+ abortEarly: true,
7
+ stripUnknown: true
8
+ });
9
+
10
+ if (error) {
11
+ return res.status(400).json(apiResponse(false, error.message, null));
12
+ }
13
+
14
+ req.body = value;
15
+ next();
16
+ };
17
+ };
18
+
@@ -0,0 +1 @@
1
+ VITE_API_URL=http://localhost:5000