@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
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @surajprasad/create-starterkit
2
+
3
+ A production-ready MERN (MongoDB, Express, React, Node) Starterkit generator. Features include optional JWT Auth, Email service (Nodemailer), and Docker support.
4
+
5
+ ## Usage
6
+
7
+ To create a new project:
8
+
9
+ ```bash
10
+ npm create @surajprasad/starterkit@latest my-app
11
+ ```
12
+
13
+ Or using `npx`:
14
+
15
+ ```bash
16
+ npx @surajprasad/create-starterkit@latest my-app
17
+ ```
18
+
19
+ ## Features
20
+
21
+ - **MERN Stack**: Modern backend with Express/MongoDB, and frontend with Vite or Next.js.
22
+ - **JWT Authentication**: Built-in authentication module with sign-in, sign-up, and forgot-password flows (optional).
23
+ - **Email Service**: Integration with Nodemailer for sending verification and transactional emails (optional).
24
+ - **Docker Ready**: One-command setup with `docker-compose` (optional).
25
+ - **Interactive CLI**: Choose only the features you need.
26
+ - **Fast Development**: Pre-configured environment variables and folder hierarchy.
27
+
28
+ ## License
29
+
30
+ MIT
package/index.js ADDED
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import chalk from "chalk";
7
+ import fs from "fs-extra";
8
+ import prompts from "prompts";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ const TEMPLATE_ROOT = path.join(__dirname, "templates");
14
+
15
+ const usage = () => {
16
+ const cmd = chalk.cyan("npm create @surajprasad/starterkit@latest my-app-name");
17
+ return `Usage: ${cmd}`;
18
+ };
19
+
20
+ const parseArgs = (argv) => {
21
+ const flags = new Set(argv.slice(3));
22
+ return {
23
+ yes: flags.has("--yes") || flags.has("--defaults") || flags.has("-y"),
24
+ frontend: flags.has("--frontend=next")
25
+ ? "next"
26
+ : flags.has("--frontend=vite")
27
+ ? "vite"
28
+ : undefined,
29
+ auth: flags.has("--no-auth") ? false : flags.has("--auth") ? true : undefined,
30
+ email: flags.has("--no-email") ? false : flags.has("--email") ? true : undefined,
31
+ docker: flags.has("--docker") ? true : flags.has("--no-docker") ? false : undefined
32
+ };
33
+ };
34
+
35
+ const isValidPackageName = (name) => {
36
+ // npm package name-ish; allow scoped? not needed here
37
+ return /^[a-z0-9]([a-z0-9-_]*[a-z0-9])?$/.test(name);
38
+ };
39
+
40
+ const readJson = async (filePath) => fs.readJson(filePath);
41
+ const writeJson = async (filePath, data) => fs.writeJson(filePath, data, { spaces: 2 });
42
+
43
+ const safeRemove = async (targetPath) => {
44
+ try {
45
+ await fs.remove(targetPath);
46
+ } catch {
47
+ // ignore
48
+ }
49
+ };
50
+
51
+ const replaceInFile = async (filePath, replacements) => {
52
+ const exists = await fs.pathExists(filePath);
53
+ if (!exists) return;
54
+ let content = await fs.readFile(filePath, "utf8");
55
+ for (const { from, to } of replacements) {
56
+ content = content.replace(from, to);
57
+ }
58
+ await fs.writeFile(filePath, content, "utf8");
59
+ };
60
+
61
+ const setPackageName = async (packageJsonPath, newName) => {
62
+ const pkg = await readJson(packageJsonPath);
63
+ pkg.name = newName;
64
+ await writeJson(packageJsonPath, pkg);
65
+ };
66
+
67
+ const setRootPackageNameIfPresent = async (appDir, appName) => {
68
+ const rootPkgPath = path.join(appDir, "package.json");
69
+ if (!(await fs.pathExists(rootPkgPath))) return;
70
+ await setPackageName(rootPkgPath, appName);
71
+ };
72
+
73
+ const ensureEmptyDirDoesNotExist = async (targetDir) => {
74
+ if (await fs.pathExists(targetDir)) {
75
+ console.error(chalk.red(`Error: folder already exists: ${targetDir}`));
76
+ process.exit(1);
77
+ }
78
+ };
79
+
80
+ const renderNextSteps = ({ appName, appDir, includeDocker }) => {
81
+ const cdCmd = chalk.cyan(`cd ${appName}`);
82
+ const backendInstall = chalk.cyan("cd backend && npm install");
83
+ const frontendInstall = chalk.cyan("cd frontend && npm install");
84
+ const backendRun = chalk.cyan("npm run dev");
85
+ const frontendRun = chalk.cyan("npm run dev");
86
+
87
+ console.log("");
88
+ console.log(chalk.green("Success!"), `Created ${chalk.bold(appName)} at ${appDir}`);
89
+ console.log("");
90
+ console.log(chalk.bold("Next steps:"));
91
+ console.log(`- ${cdCmd}`);
92
+ if (includeDocker) {
93
+ console.log(`- ${chalk.cyan("cp backend/.env.example backend/.env")}`);
94
+ console.log(`- ${chalk.cyan("docker compose up --build")}`);
95
+ } else {
96
+ console.log(`- ${backendInstall}`);
97
+ console.log(`- ${chalk.cyan("cp backend/.env.example backend/.env")}`);
98
+ console.log(`- ${chalk.gray("(in one terminal)")} ${backendRun}`);
99
+ console.log(`- ${frontendInstall}`);
100
+ console.log(`- ${chalk.cyan("cp frontend/.env.example frontend/.env")}`);
101
+ console.log(`- ${chalk.gray("(in another terminal)")} ${frontendRun}`);
102
+ }
103
+ console.log("");
104
+ };
105
+
106
+ const generateDockerCompose = async ({ appDir }) => {
107
+ const compose = `version: '3.8'
108
+ services:
109
+ backend:
110
+ build: ./backend
111
+ ports:
112
+ - 5000:5000
113
+ env_file:
114
+ - ./backend/.env
115
+ depends_on:
116
+ - mongodb
117
+ frontend:
118
+ build: ./frontend
119
+ ports:
120
+ - 5173:5173
121
+ mongodb:
122
+ image: mongo:6
123
+ ports:
124
+ - 27017:27017
125
+ volumes:
126
+ - mongo_data:/data/db
127
+ volumes:
128
+ mongo_data:
129
+ `;
130
+ await fs.writeFile(path.join(appDir, "docker-compose.yml"), compose, "utf8");
131
+ };
132
+
133
+ const main = async () => {
134
+ const appName = process.argv[2];
135
+ const flags = parseArgs(process.argv);
136
+
137
+ if (!appName) {
138
+ console.error(chalk.red("Error: missing app name."));
139
+ console.log(usage());
140
+ process.exit(1);
141
+ }
142
+
143
+ if (!isValidPackageName(appName)) {
144
+ console.error(chalk.red(`Error: invalid app name "${appName}".`));
145
+ console.error(chalk.gray("Use lowercase letters, numbers, dashes, underscores."));
146
+ process.exit(1);
147
+ }
148
+
149
+ const appDir = path.resolve(process.cwd(), appName);
150
+ await ensureEmptyDirDoesNotExist(appDir);
151
+
152
+ const promptQuestions = [
153
+ {
154
+ type: "select",
155
+ name: "stack",
156
+ message: "Select a stack",
157
+ choices: [{ title: "MERN (MongoDB, Express, React, Node)", value: "mern" }],
158
+ initial: 0
159
+ },
160
+ {
161
+ type: "select",
162
+ name: "frontend",
163
+ message: "Select a frontend",
164
+ choices: [
165
+ { title: "Vite + React", value: "vite" },
166
+ { title: "Next.js", value: "next" }
167
+ ],
168
+ initial: 0
169
+ },
170
+ {
171
+ type: "toggle",
172
+ name: "auth",
173
+ message: "Include JWT Auth?",
174
+ initial: true,
175
+ active: "yes",
176
+ inactive: "no"
177
+ },
178
+ {
179
+ type: "toggle",
180
+ name: "email",
181
+ message: "Include Email Service (Nodemailer)?",
182
+ initial: true,
183
+ active: "yes",
184
+ inactive: "no"
185
+ },
186
+ {
187
+ type: "toggle",
188
+ name: "docker",
189
+ message: "Include Docker?",
190
+ initial: false,
191
+ active: "yes",
192
+ inactive: "no"
193
+ }
194
+ ];
195
+
196
+ const answers = flags.yes
197
+ ? {
198
+ stack: "mern",
199
+ frontend: flags.frontend ?? "vite",
200
+ auth: flags.auth ?? true,
201
+ email: flags.email ?? true,
202
+ docker: flags.docker ?? false
203
+ }
204
+ : await prompts(promptQuestions, {
205
+ onCancel: () => {
206
+ console.log("");
207
+ console.log(chalk.yellow("Cancelled."));
208
+ process.exit(1);
209
+ }
210
+ });
211
+
212
+ let includeAuth = Boolean(answers.auth);
213
+ const includeEmail = Boolean(answers.email);
214
+ const includeDocker = Boolean(answers.docker);
215
+
216
+ if (!includeAuth && includeEmail) {
217
+ includeAuth = true;
218
+ console.log(
219
+ chalk.yellow(
220
+ "Note: Email service requires Auth (admin-only send). Enabling Auth automatically."
221
+ )
222
+ );
223
+ }
224
+
225
+ const templateDir = path.join(TEMPLATE_ROOT, answers.stack || "mern");
226
+ if (!(await fs.pathExists(templateDir))) {
227
+ console.error(chalk.red(`Error: template not found for stack "${answers.stack}".`));
228
+ process.exit(1);
229
+ }
230
+
231
+ await fs.copy(templateDir, appDir, {
232
+ filter: (src) => {
233
+ const base = path.basename(src);
234
+ if (base === "node_modules" || base === "dist") return false;
235
+ return true;
236
+ }
237
+ });
238
+
239
+ // Rename package.json names
240
+ await setRootPackageNameIfPresent(appDir, appName);
241
+ await setPackageName(path.join(appDir, "backend", "package.json"), `${appName}-backend`);
242
+
243
+ // Choose frontend template (vite by default)
244
+ if (answers.frontend === "next") {
245
+ await safeRemove(path.join(appDir, "frontend"));
246
+ await fs.copy(path.join(templateDir, "frontend-next"), path.join(appDir, "frontend"));
247
+ }
248
+
249
+ await setPackageName(path.join(appDir, "frontend", "package.json"), `${appName}-frontend`);
250
+
251
+ // Remove modules based on answers
252
+ if (!includeAuth) {
253
+ await safeRemove(path.join(appDir, "backend", "src", "modules", "auth"));
254
+ await safeRemove(path.join(appDir, "frontend", "src", "modules", "auth"));
255
+
256
+ // Patch backend app.js to remove auth routes import/mount
257
+ await replaceInFile(path.join(appDir, "backend", "src", "app.js"), [
258
+ { from: /^\s*import authRoutes.*\n/gm, to: "" },
259
+ { from: /^\s*app\.use\(["']\/api\/auth["'],\s*authRoutes\);\s*\n/gm, to: "" }
260
+ ]);
261
+
262
+ // Patch frontend App.jsx to remove auth-protected routing and auth imports
263
+ await replaceInFile(path.join(appDir, "frontend", "src", "App.jsx"), [
264
+ { from: /^\s*import\s+\{\s*ProtectedRoute\s*\}.*\n/gm, to: "" },
265
+ { from: /^\s*import\s+\{\s*LoginPage\s*\}.*\n/gm, to: "" },
266
+ { from: /^\s*import\s+\{\s*RegisterPage\s*\}.*\n/gm, to: "" },
267
+ { from: /^\s*import\s+\{\s*VerifyEmailPage\s*\}.*\n/gm, to: "" },
268
+ { from: /^\s*import\s+\{\s*ForgotPasswordPage\s*\}.*\n/gm, to: "" },
269
+ { from: /^\s*import\s+\{\s*ResetPasswordPage\s*\}.*\n/gm, to: "" },
270
+ { from: /^\s*import\s+\{\s*useAuthStore\s*\}.*\n/gm, to: "" },
271
+ {
272
+ from: /<Route\s+path="\/"\s+element=\{\s*<ProtectedRoute>[\s\S]*?<\/ProtectedRoute>\s*\}\s*\/>\s*/m,
273
+ to: `<Route path="/" element={<DashboardPage />} />\n`
274
+ },
275
+ { from: /^\s*<Route path="\/login"[\s\S]*?\n/gm, to: "" },
276
+ { from: /^\s*<Route path="\/register"[\s\S]*?\n/gm, to: "" },
277
+ { from: /^\s*<Route path="\/verify-email"[\s\S]*?\n/gm, to: "" },
278
+ { from: /^\s*<Route path="\/forgot-password"[\s\S]*?\n/gm, to: "" },
279
+ { from: /^\s*<Route path="\/reset-password"[\s\S]*?\n/gm, to: "" },
280
+ { from: /const refreshMe[\s\S]*?;\n\n/m, to: "" },
281
+ { from: /const token[\s\S]*?;\n/m, to: "" },
282
+ { from: /const user[\s\S]*?;\n/m, to: "" },
283
+ { from: /useEffect\(\(\)\s*=>\s*\{\s*refreshMe\(\)[\s\S]*?\}\s*,\s*\[refreshMe\]\s*\);\s*\n/m, to: "" },
284
+ { from: /useEffect\(\(\)\s*=>\s*\{\s*if\s*\(token[\s\S]*?\}\s*,\s*\[token,\s*user\]\s*\);\s*\n/m, to: "" },
285
+ {
286
+ from: /{!token\s*\?\s*\([\s\S]*?\)\s*:\s*\([\s\S]*?\)\s*}/m,
287
+ to: `<span style={{ color: "#6B7280", fontWeight: 700, fontSize: 13 }}>Welcome</span>`
288
+ }
289
+ ]);
290
+ }
291
+ if (!includeEmail) {
292
+ await safeRemove(path.join(appDir, "backend", "src", "modules", "email"));
293
+
294
+ // Patch backend app.js to remove email routes import/mount
295
+ await replaceInFile(path.join(appDir, "backend", "src", "app.js"), [
296
+ { from: /^\s*import emailRoutes.*\n/gm, to: "" },
297
+ { from: /^\s*app\.use\(["']\/api\/email["'],\s*emailRoutes\);\s*\n/gm, to: "" }
298
+ ]);
299
+
300
+ // Patch auth.service.js to remove email-service dependency
301
+ await replaceInFile(
302
+ path.join(appDir, "backend", "src", "modules", "auth", "auth.service.js"),
303
+ [
304
+ {
305
+ from: /^\s*import\s+\{\s*[\s\S]*?\s*\}\s*from\s*"\.\.\/email\/email\.service\.js";\s*\n/gm,
306
+ to: ""
307
+ },
308
+ { from: /^\s*await\s+sendWelcomeEmail\([\s\S]*?\);\s*\n/gm, to: "" },
309
+ { from: /^\s*await\s+sendVerifyEmail\([\s\S]*?\);\s*\n/gm, to: "" },
310
+ { from: /^\s*await\s+sendPasswordResetEmail\([\s\S]*?\);\s*\n/gm, to: "" }
311
+ ]
312
+ );
313
+ }
314
+
315
+ // If docker enabled, generate docker-compose.yml
316
+ if (includeDocker) {
317
+ await generateDockerCompose({ appDir });
318
+ }
319
+
320
+ renderNextSteps({ appName, appDir, includeDocker });
321
+ };
322
+
323
+ main().catch((err) => {
324
+ console.error(chalk.red("Unexpected error:"), err?.message || err);
325
+ process.exit(1);
326
+ });
327
+
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@surajprasad/create-starterkit",
3
+ "version": "0.1.0",
4
+ "description": "Create a production-ready Starterkit (MERN) with optional auth, email, and Docker.",
5
+ "type": "module",
6
+ "bin": {
7
+ "starterkit": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "templates"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18.18.0"
15
+ },
16
+ "keywords": [
17
+ "create",
18
+ "starterkit",
19
+ "cli",
20
+ "mern",
21
+ "scaffold"
22
+ ],
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "chalk": "^5.4.1",
26
+ "fs-extra": "^11.3.0",
27
+ "prompts": "^2.4.2"
28
+ }
29
+ }
@@ -0,0 +1,11 @@
1
+ PORT=5000
2
+ NODE_ENV=development
3
+ MONGO_URI=mongodb://localhost:27017/myapp
4
+ JWT_SECRET=your_jwt_secret_here
5
+ JWT_EXPIRES_IN=7d
6
+ FRONTEND_URL=http://localhost:5173
7
+ SMTP_HOST=smtp.gmail.com
8
+ SMTP_PORT=587
9
+ SMTP_USER=your_email@gmail.com
10
+ SMTP_PASS=your_app_password
11
+ EMAIL_FROM=Your App <your_email@gmail.com>
@@ -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 --omit=dev
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 5000
11
+
12
+ CMD ["node", "server.js"]
13
+
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "starterkit-backend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "server.js",
7
+ "scripts": {
8
+ "dev": "node --watch server.js",
9
+ "start": "node server.js"
10
+ },
11
+ "dependencies": {
12
+ "bcryptjs": "^3.0.2",
13
+ "cors": "^2.8.5",
14
+ "dotenv": "^16.4.7",
15
+ "express": "^4.21.2",
16
+ "helmet": "^7.1.0",
17
+ "joi": "^17.13.3",
18
+ "jsonwebtoken": "^9.0.2",
19
+ "mongoose": "^8.12.2",
20
+ "morgan": "^1.10.0",
21
+ "nodemailer": "^6.10.0"
22
+ }
23
+ }
@@ -0,0 +1,25 @@
1
+ import { createServer } from "node:http";
2
+
3
+ import app from "./src/app.js";
4
+ import { connectDb } from "./src/config/db.js";
5
+ import { loadEnv } from "./src/config/env.js";
6
+
7
+ const start = async () => {
8
+ loadEnv();
9
+ await connectDb();
10
+
11
+ const port = Number(process.env.PORT || 5000);
12
+ const server = createServer(app);
13
+
14
+ server.listen(port, () => {
15
+ // eslint-disable-next-line no-console
16
+ console.log(`Backend listening on port ${port}`);
17
+ });
18
+ };
19
+
20
+ start().catch((err) => {
21
+ // eslint-disable-next-line no-console
22
+ console.error("Failed to start server:", err);
23
+ process.exit(1);
24
+ });
25
+
@@ -0,0 +1,48 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import helmet from "helmet";
4
+ import morgan from "morgan";
5
+
6
+ import authRoutes from "./modules/auth/auth.routes.js";
7
+ import emailRoutes from "./modules/email/email.routes.js";
8
+
9
+ import { notFound } from "./middleware/notFound.middleware.js";
10
+ import { errorMiddleware } from "./middleware/errorMiddleware.js";
11
+
12
+ const app = express();
13
+
14
+ app.use(helmet());
15
+ const allowedOrigins = [
16
+ process.env.FRONTEND_URL,
17
+ "http://localhost:5173",
18
+ "http://127.0.0.1:5173"
19
+ ].filter(Boolean);
20
+
21
+ const corsOptions = {
22
+ origin(origin, cb) {
23
+ if (!origin) return cb(null, true);
24
+ if (allowedOrigins.includes(origin)) return cb(null, true);
25
+ return cb(null, false);
26
+ },
27
+ credentials: true,
28
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
29
+ allowedHeaders: ["Content-Type", "Authorization"]
30
+ };
31
+
32
+ app.use(cors(corsOptions));
33
+ app.options("*", cors(corsOptions));
34
+ app.use(express.json({ limit: "1mb" }));
35
+ app.use(morgan("dev"));
36
+
37
+ app.get("/health", (req, res) => {
38
+ res.json({ ok: true, timestamp: new Date().toISOString() });
39
+ });
40
+
41
+ app.use("/api/auth", authRoutes);
42
+ app.use("/api/email", emailRoutes);
43
+
44
+ app.use(notFound);
45
+ app.use(errorMiddleware);
46
+
47
+ export default app;
48
+
@@ -0,0 +1,17 @@
1
+ import mongoose from "mongoose";
2
+
3
+ export const connectDb = async () => {
4
+ try {
5
+ const uri = process.env.MONGO_URI;
6
+ if (!uri) throw new Error("MONGO_URI is not set");
7
+
8
+ mongoose.set("strictQuery", true);
9
+ await mongoose.connect(uri);
10
+
11
+ // eslint-disable-next-line no-console
12
+ console.log("Connected to MongoDB");
13
+ } catch (err) {
14
+ throw err;
15
+ }
16
+ };
17
+
@@ -0,0 +1,38 @@
1
+ import dotenv from "dotenv";
2
+ import Joi from "joi";
3
+
4
+ const envSchema = Joi.object({
5
+ PORT: Joi.number().port().default(5000),
6
+ NODE_ENV: Joi.string().valid("development", "production", "test").default("development"),
7
+ MONGO_URI: Joi.string().uri().required(),
8
+ JWT_SECRET: Joi.string().min(10).required(),
9
+ JWT_EXPIRES_IN: Joi.string().default("7d"),
10
+ FRONTEND_URL: Joi.string().uri().required(),
11
+ SMTP_HOST: Joi.string().required(),
12
+ SMTP_PORT: Joi.number().port().required(),
13
+ SMTP_USER: Joi.string().allow("").optional(),
14
+ SMTP_PASS: Joi.string().allow("").optional(),
15
+ EMAIL_FROM: Joi.string().required()
16
+ }).unknown(true);
17
+
18
+ export const loadEnv = () => {
19
+ dotenv.config();
20
+
21
+ const { error, value } = envSchema.validate(process.env, { abortEarly: true });
22
+ if (error) {
23
+ throw new Error(`Invalid environment variables: ${error.message}`);
24
+ }
25
+
26
+ process.env.PORT = String(value.PORT);
27
+ process.env.NODE_ENV = value.NODE_ENV;
28
+ process.env.MONGO_URI = value.MONGO_URI;
29
+ process.env.JWT_SECRET = value.JWT_SECRET;
30
+ process.env.JWT_EXPIRES_IN = value.JWT_EXPIRES_IN;
31
+ process.env.FRONTEND_URL = value.FRONTEND_URL;
32
+ process.env.SMTP_HOST = value.SMTP_HOST;
33
+ process.env.SMTP_PORT = String(value.SMTP_PORT);
34
+ process.env.SMTP_USER = value.SMTP_USER;
35
+ process.env.SMTP_PASS = value.SMTP_PASS;
36
+ process.env.EMAIL_FROM = value.EMAIL_FROM;
37
+ };
38
+
@@ -0,0 +1,30 @@
1
+ import { verifyToken } from "../utils/generateToken.util.js";
2
+ import User from "../modules/auth/auth.model.js";
3
+
4
+ export const authMiddleware = async (req, res, next) => {
5
+ try {
6
+ const header = req.headers.authorization || "";
7
+ const token = header.startsWith("Bearer ") ? header.slice(7) : null;
8
+
9
+ if (!token) {
10
+ const err = new Error("Unauthorized");
11
+ err.statusCode = 401;
12
+ throw err;
13
+ }
14
+
15
+ const decoded = verifyToken(token);
16
+ const user = await User.findById(decoded.userId).select("-password");
17
+ if (!user) {
18
+ const err = new Error("Unauthorized");
19
+ err.statusCode = 401;
20
+ throw err;
21
+ }
22
+
23
+ req.user = user;
24
+ next();
25
+ } catch (err) {
26
+ err.statusCode = err.statusCode || 401;
27
+ next(err);
28
+ }
29
+ };
30
+
@@ -0,0 +1,17 @@
1
+ export const errorMiddleware = (err, req, res, next) => {
2
+ const status = Number(err?.statusCode || err?.status || 500);
3
+ const message = err?.message || "Internal Server Error";
4
+
5
+ if (process.env.NODE_ENV !== "test") {
6
+ // eslint-disable-next-line no-console
7
+ console.error(err);
8
+ }
9
+
10
+ res.status(status).json({
11
+ success: false,
12
+ message,
13
+ data: null,
14
+ timestamp: new Date().toISOString()
15
+ });
16
+ };
17
+
@@ -0,0 +1,9 @@
1
+ export const notFound = (req, res) => {
2
+ res.status(404).json({
3
+ success: false,
4
+ message: `Route not found: ${req.method} ${req.originalUrl}`,
5
+ data: null,
6
+ timestamp: new Date().toISOString()
7
+ });
8
+ };
9
+
@@ -0,0 +1,45 @@
1
+ import { asyncHandler } from "../../utils/asyncHandler.util.js";
2
+ import { apiResponse } from "../../utils/apiResponse.util.js";
3
+ import {
4
+ forgotPasswordService,
5
+ getMeService,
6
+ loginUser,
7
+ registerUser,
8
+ resetPasswordService,
9
+ verifyEmailService
10
+ } from "./auth.service.js";
11
+
12
+ export const register = asyncHandler(async (req, res) => {
13
+ const data = await registerUser(req.body);
14
+ res.status(201).json(apiResponse(true, "Registered successfully", data));
15
+ });
16
+
17
+ export const login = asyncHandler(async (req, res) => {
18
+ const data = await loginUser(req.body);
19
+ res.status(200).json(apiResponse(true, "Logged in successfully", data));
20
+ });
21
+
22
+ export const getMe = asyncHandler(async (req, res) => {
23
+ const data = await getMeService({ userId: req.user._id.toString() });
24
+ res.status(200).json(apiResponse(true, "Fetched profile", data));
25
+ });
26
+
27
+ export const verifyEmail = asyncHandler(async (req, res) => {
28
+ const { otp } = req.body;
29
+ const data = await verifyEmailService({ userId: req.user._id.toString(), otp });
30
+ res.status(200).json(apiResponse(true, "Email verified", data));
31
+ });
32
+
33
+ export const forgotPassword = asyncHandler(async (req, res) => {
34
+ const { email } = req.body;
35
+ const data = await forgotPasswordService({ email });
36
+ res.status(200).json(apiResponse(true, "If the email exists, a reset link was sent", data));
37
+ });
38
+
39
+ export const resetPassword = asyncHandler(async (req, res) => {
40
+ const { token } = req.query;
41
+ const { password } = req.body;
42
+ const data = await resetPasswordService({ rawToken: token, newPassword: password });
43
+ res.status(200).json(apiResponse(true, "Password reset successfully", data));
44
+ });
45
+