@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
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,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,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
|
+
|