starter-structure-cli 0.2.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 +92 -0
- package/bin/starter-structure-cli.js +942 -0
- package/package.json +34 -0
- package/scripts/check-templates.js +66 -0
- package/templates/backend-only/express-mongoose-jwt/.env.example +6 -0
- package/templates/backend-only/express-mongoose-jwt/README.md +43 -0
- package/templates/backend-only/express-mongoose-jwt/config/db.js +19 -0
- package/templates/backend-only/express-mongoose-jwt/controllers/auth/index.js +91 -0
- package/templates/backend-only/express-mongoose-jwt/index.js +44 -0
- package/templates/backend-only/express-mongoose-jwt/middleware/authenticate.js +35 -0
- package/templates/backend-only/express-mongoose-jwt/middleware/error-handler.js +16 -0
- package/templates/backend-only/express-mongoose-jwt/middleware/not-found.js +7 -0
- package/templates/backend-only/express-mongoose-jwt/models/user.js +54 -0
- package/templates/backend-only/express-mongoose-jwt/package.json +30 -0
- package/templates/backend-only/express-mongoose-jwt/routes/auth/index.js +12 -0
- package/templates/backend-only/express-mongoose-jwt/routes/index.js +16 -0
- package/templates/backend-only/express-mongoose-jwt/utils/api-response.js +20 -0
- package/templates/backend-only/express-mongoose-jwt/utils/generate-token.js +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "starter-structure-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Scaffold starter projects from your own stack-based template folders",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin",
|
|
8
|
+
"scripts",
|
|
9
|
+
"templates",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"check:templates": "node ./scripts/check-templates.js",
|
|
14
|
+
"prepack": "node ./scripts/check-templates.js"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"starter-structure-cli": "bin/starter-structure-cli.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"cli",
|
|
21
|
+
"scaffold",
|
|
22
|
+
"starter",
|
|
23
|
+
"template",
|
|
24
|
+
"boilerplate"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@clack/prompts": "^0.11.0",
|
|
31
|
+
"picocolors": "^1.1.1"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT"
|
|
34
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const templatesRoot = path.resolve(__dirname, "..", "templates");
|
|
8
|
+
|
|
9
|
+
function hasAnyFile(dir) {
|
|
10
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
11
|
+
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const entryPath = path.join(dir, entry.name);
|
|
14
|
+
if (entry.isFile()) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (entry.isDirectory() && hasAnyFile(entryPath)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getTemplateDirs(rootDir) {
|
|
27
|
+
if (!fs.existsSync(rootDir)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const templateDirs = [];
|
|
32
|
+
const categories = fs
|
|
33
|
+
.readdirSync(rootDir, { withFileTypes: true })
|
|
34
|
+
.filter((entry) => entry.isDirectory())
|
|
35
|
+
.map((entry) => entry.name);
|
|
36
|
+
|
|
37
|
+
for (const category of categories) {
|
|
38
|
+
const categoryDir = path.join(rootDir, category);
|
|
39
|
+
const starters = fs
|
|
40
|
+
.readdirSync(categoryDir, { withFileTypes: true })
|
|
41
|
+
.filter((entry) => entry.isDirectory())
|
|
42
|
+
.map((entry) => path.join(categoryDir, entry.name));
|
|
43
|
+
|
|
44
|
+
templateDirs.push(...starters);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return templateDirs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const emptyTemplates = getTemplateDirs(templatesRoot)
|
|
51
|
+
.filter((templateDir) => !hasAnyFile(templateDir))
|
|
52
|
+
.map((templateDir) => path.relative(templatesRoot, templateDir));
|
|
53
|
+
|
|
54
|
+
if (emptyTemplates.length > 0) {
|
|
55
|
+
console.error("Template validation failed.");
|
|
56
|
+
console.error("These template directories do not contain any files:");
|
|
57
|
+
for (const templateDir of emptyTemplates) {
|
|
58
|
+
console.error(`- ${templateDir}`);
|
|
59
|
+
}
|
|
60
|
+
console.error("");
|
|
61
|
+
console.error("Add the real starter files before publishing.");
|
|
62
|
+
console.error("If a template intentionally contains only empty folders, add a .gitkeep file.");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(`Template validation passed for ${getTemplateDirs(templatesRoot).length} templates.`);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
Express + Mongoose + JWT starter API.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- User registration and login
|
|
8
|
+
- Password hashing with `bcryptjs`
|
|
9
|
+
- JWT authentication middleware
|
|
10
|
+
- MongoDB connection with Mongoose
|
|
11
|
+
- Structured route/controller/model layout
|
|
12
|
+
- Standard API response helpers
|
|
13
|
+
|
|
14
|
+
## Project structure
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
.
|
|
18
|
+
├── config/
|
|
19
|
+
├── controllers/
|
|
20
|
+
├── middleware/
|
|
21
|
+
├── models/
|
|
22
|
+
├── routes/
|
|
23
|
+
├── utils/
|
|
24
|
+
├── .env.example
|
|
25
|
+
├── .gitignore
|
|
26
|
+
├── index.js
|
|
27
|
+
└── package.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Getting started
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install
|
|
34
|
+
cp .env.example .env
|
|
35
|
+
npm run dev
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## API endpoints
|
|
39
|
+
|
|
40
|
+
- `GET /api/health`
|
|
41
|
+
- `POST /api/auth/register`
|
|
42
|
+
- `POST /api/auth/login`
|
|
43
|
+
- `GET /api/auth/me`
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const mongoose = require("mongoose");
|
|
2
|
+
|
|
3
|
+
async function connectDB() {
|
|
4
|
+
const mongoURI = process.env.MONGODB_URI;
|
|
5
|
+
|
|
6
|
+
if (!mongoURI) {
|
|
7
|
+
throw new Error("MONGODB_URI is not defined in environment variables.");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
await mongoose.connect(mongoURI);
|
|
12
|
+
console.log("MongoDB connected");
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error("MongoDB connection failed:", error.message);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = connectDB;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const User = require("../../models/user");
|
|
2
|
+
const { successResponse, errorResponse } = require("../../utils/api-response");
|
|
3
|
+
const generateToken = require("../../utils/generate-token");
|
|
4
|
+
|
|
5
|
+
async function register(req, res, next) {
|
|
6
|
+
try {
|
|
7
|
+
const { name, email, password } = req.body;
|
|
8
|
+
|
|
9
|
+
if (!name || !email || !password) {
|
|
10
|
+
return errorResponse(res, "Name, email, and password are required.", 400);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const existingUser = await User.findOne({ email: email.toLowerCase() });
|
|
14
|
+
if (existingUser) {
|
|
15
|
+
return errorResponse(res, "User already exists with this email.", 409);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const user = await User.create({
|
|
19
|
+
name,
|
|
20
|
+
email: email.toLowerCase(),
|
|
21
|
+
password
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const token = generateToken(user._id);
|
|
25
|
+
|
|
26
|
+
return successResponse(
|
|
27
|
+
res,
|
|
28
|
+
{
|
|
29
|
+
user,
|
|
30
|
+
token
|
|
31
|
+
},
|
|
32
|
+
"Registration successful.",
|
|
33
|
+
201
|
|
34
|
+
);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
next(error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function login(req, res, next) {
|
|
41
|
+
try {
|
|
42
|
+
const { email, password } = req.body;
|
|
43
|
+
|
|
44
|
+
if (!email || !password) {
|
|
45
|
+
return errorResponse(res, "Email and password are required.", 400);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const user = await User.findOne({ email: email.toLowerCase() });
|
|
49
|
+
if (!user) {
|
|
50
|
+
return errorResponse(res, "Invalid email or password.", 401);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isPasswordValid = await user.comparePassword(password);
|
|
54
|
+
if (!isPasswordValid) {
|
|
55
|
+
return errorResponse(res, "Invalid email or password.", 401);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const token = generateToken(user._id);
|
|
59
|
+
|
|
60
|
+
return successResponse(
|
|
61
|
+
res,
|
|
62
|
+
{
|
|
63
|
+
user,
|
|
64
|
+
token
|
|
65
|
+
},
|
|
66
|
+
"Login successful."
|
|
67
|
+
);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
next(error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function me(req, res, next) {
|
|
74
|
+
try {
|
|
75
|
+
const user = await User.findById(req.user.id);
|
|
76
|
+
|
|
77
|
+
if (!user) {
|
|
78
|
+
return errorResponse(res, "User not found.", 404);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return successResponse(res, { user }, "Authenticated user fetched.");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
next(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
register,
|
|
89
|
+
login,
|
|
90
|
+
me
|
|
91
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const cors = require("cors");
|
|
3
|
+
const dotenv = require("dotenv");
|
|
4
|
+
const helmet = require("helmet");
|
|
5
|
+
const morgan = require("morgan");
|
|
6
|
+
|
|
7
|
+
const connectDB = require("./config/db");
|
|
8
|
+
const routes = require("./routes");
|
|
9
|
+
const notFound = require("./middleware/not-found");
|
|
10
|
+
const errorHandler = require("./middleware/error-handler");
|
|
11
|
+
|
|
12
|
+
dotenv.config();
|
|
13
|
+
|
|
14
|
+
const app = express();
|
|
15
|
+
const PORT = process.env.PORT || 5000;
|
|
16
|
+
|
|
17
|
+
connectDB();
|
|
18
|
+
|
|
19
|
+
app.use(
|
|
20
|
+
cors({
|
|
21
|
+
origin: process.env.CLIENT_URL || "*",
|
|
22
|
+
credentials: true
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
app.use(helmet());
|
|
26
|
+
app.use(morgan("dev"));
|
|
27
|
+
app.use(express.json());
|
|
28
|
+
app.use(express.urlencoded({ extended: true }));
|
|
29
|
+
|
|
30
|
+
app.get("/", (_req, res) => {
|
|
31
|
+
res.json({
|
|
32
|
+
success: true,
|
|
33
|
+
message: "__APP_NAME__ API is running"
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.use("/api", routes);
|
|
38
|
+
|
|
39
|
+
app.use(notFound);
|
|
40
|
+
app.use(errorHandler);
|
|
41
|
+
|
|
42
|
+
app.listen(PORT, () => {
|
|
43
|
+
console.log(`Server running on port ${PORT}`);
|
|
44
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const jwt = require("jsonwebtoken");
|
|
2
|
+
|
|
3
|
+
const User = require("../models/user");
|
|
4
|
+
const { errorResponse } = require("../utils/api-response");
|
|
5
|
+
|
|
6
|
+
async function authenticate(req, res, next) {
|
|
7
|
+
try {
|
|
8
|
+
const authHeader = req.headers.authorization;
|
|
9
|
+
|
|
10
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
11
|
+
return errorResponse(res, "Unauthorized access.", 401);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const token = authHeader.split(" ")[1];
|
|
15
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
16
|
+
|
|
17
|
+
const user = await User.findById(decoded.id).select("_id name email role");
|
|
18
|
+
if (!user) {
|
|
19
|
+
return errorResponse(res, "User not found.", 401);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
req.user = {
|
|
23
|
+
id: user._id.toString(),
|
|
24
|
+
name: user.name,
|
|
25
|
+
email: user.email,
|
|
26
|
+
role: user.role
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
next();
|
|
30
|
+
} catch (_error) {
|
|
31
|
+
return errorResponse(res, "Invalid or expired token.", 401);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = authenticate;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { errorResponse } = require("../utils/api-response");
|
|
2
|
+
|
|
3
|
+
function errorHandler(error, _req, res, _next) {
|
|
4
|
+
const statusCode = error.statusCode || 500;
|
|
5
|
+
const message = error.message || "Internal server error.";
|
|
6
|
+
|
|
7
|
+
if (process.env.NODE_ENV !== "test") {
|
|
8
|
+
console.error(error);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return errorResponse(res, message, statusCode, {
|
|
12
|
+
stack: process.env.NODE_ENV === "development" ? error.stack : undefined
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = errorHandler;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const bcrypt = require("bcryptjs");
|
|
2
|
+
const mongoose = require("mongoose");
|
|
3
|
+
|
|
4
|
+
const userSchema = new mongoose.Schema(
|
|
5
|
+
{
|
|
6
|
+
name: {
|
|
7
|
+
type: String,
|
|
8
|
+
required: true,
|
|
9
|
+
trim: true
|
|
10
|
+
},
|
|
11
|
+
email: {
|
|
12
|
+
type: String,
|
|
13
|
+
required: true,
|
|
14
|
+
unique: true,
|
|
15
|
+
lowercase: true,
|
|
16
|
+
trim: true
|
|
17
|
+
},
|
|
18
|
+
password: {
|
|
19
|
+
type: String,
|
|
20
|
+
required: true,
|
|
21
|
+
minlength: 6
|
|
22
|
+
},
|
|
23
|
+
role: {
|
|
24
|
+
type: String,
|
|
25
|
+
enum: ["admin", "user"],
|
|
26
|
+
default: "user"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
timestamps: true
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
userSchema.pre("save", async function preSave(next) {
|
|
35
|
+
if (!this.isModified("password")) {
|
|
36
|
+
return next();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const salt = await bcrypt.genSalt(10);
|
|
40
|
+
this.password = await bcrypt.hash(this.password, salt);
|
|
41
|
+
next();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
userSchema.methods.comparePassword = function comparePassword(candidatePassword) {
|
|
45
|
+
return bcrypt.compare(candidatePassword, this.password);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
userSchema.methods.toJSON = function toJSON() {
|
|
49
|
+
const userObject = this.toObject();
|
|
50
|
+
delete userObject.password;
|
|
51
|
+
return userObject;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
module.exports = mongoose.model("User", userSchema);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME__",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Express + Mongoose + JWT starter API",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "nodemon index.js",
|
|
8
|
+
"start": "node index.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"express",
|
|
12
|
+
"mongoose",
|
|
13
|
+
"jwt",
|
|
14
|
+
"api"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"bcryptjs": "^2.4.3",
|
|
19
|
+
"cors": "^2.8.5",
|
|
20
|
+
"dotenv": "^16.4.7",
|
|
21
|
+
"express": "^4.21.2",
|
|
22
|
+
"helmet": "^8.0.0",
|
|
23
|
+
"jsonwebtoken": "^9.0.2",
|
|
24
|
+
"mongoose": "^8.9.5",
|
|
25
|
+
"morgan": "^1.10.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"nodemon": "^3.1.9"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
|
|
3
|
+
const { login, me, register } = require("../../controllers/auth");
|
|
4
|
+
const authenticate = require("../../middleware/authenticate");
|
|
5
|
+
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
|
|
8
|
+
router.post("/register", register);
|
|
9
|
+
router.post("/login", login);
|
|
10
|
+
router.get("/me", authenticate, me);
|
|
11
|
+
|
|
12
|
+
module.exports = router;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
|
|
3
|
+
const authRoutes = require("./auth");
|
|
4
|
+
|
|
5
|
+
const router = express.Router();
|
|
6
|
+
|
|
7
|
+
router.get("/health", (_req, res) => {
|
|
8
|
+
res.json({
|
|
9
|
+
success: true,
|
|
10
|
+
message: "Server is healthy"
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
router.use("/auth", authRoutes);
|
|
15
|
+
|
|
16
|
+
module.exports = router;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function successResponse(res, data = null, message = "Success", statusCode = 200) {
|
|
2
|
+
return res.status(statusCode).json({
|
|
3
|
+
success: true,
|
|
4
|
+
message,
|
|
5
|
+
data
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function errorResponse(res, message = "Something went wrong", statusCode = 500, errors = null) {
|
|
10
|
+
return res.status(statusCode).json({
|
|
11
|
+
success: false,
|
|
12
|
+
message,
|
|
13
|
+
errors
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
successResponse,
|
|
19
|
+
errorResponse
|
|
20
|
+
};
|