@bhudevswayam/blueprint-engine 1.0.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/index.js +4 -0
- package/package.json +21 -0
- package/readme.md +117 -0
- package/src/config/db.js +13 -0
- package/src/controllers/authController.js +98 -0
- package/src/middleware/auth.js +67 -0
- package/src/middleware/errorHandler.js +10 -0
- package/src/middleware/tenant.js +11 -0
- package/src/models/Tenant.js +9 -0
- package/src/models/User.js +20 -0
- package/src/routes/auth.js +17 -0
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhudevswayam/blueprint-engine",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Domain-Driven Architectural Store for Node.js and MongoDB.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": ["architecture", "mongodb", "express", "auth", "blueprint"],
|
|
10
|
+
"author": "Swayam Pandya",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"bcryptjs": "^3.0.3",
|
|
14
|
+
"express-async-handler": "^1.2.0",
|
|
15
|
+
"jsonwebtoken": "^9.0.3"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"express": "^5.2.1",
|
|
19
|
+
"mongoose": "^9.2.1"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# blueprint-engine 🏗️
|
|
2
|
+
|
|
3
|
+
**blueprint-engine** is a high-level architectural framework for Node.js and MongoDB. It is designed to eliminate the "90% boilerplate tax" by providing pre-configured, domain-specific backend blueprints.
|
|
4
|
+
|
|
5
|
+
Instead of writing repetitive logic for authentication, multi-tenancy, or industry-specific workflows, you simply inject your Mongoose models into a blueprint and mount the resulting engine.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🌟 The Vision
|
|
10
|
+
This library acts as a **Domain-Driven Architectural Store**. It allows developers to deploy complex backend structures using standardized "blueprints."
|
|
11
|
+
- **v1.0.x**: Multi-tenant Authentication & Authorization (Current).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 🚀 Features (Auth Blueprint)
|
|
16
|
+
- **Zero-Boilerplate**: Fully automated `/login` and `/register` routes.
|
|
17
|
+
- **Security-as-a-Code**: Built-in Bcrypt hashing and JWT session management.
|
|
18
|
+
- **Dynamic Multi-Tenancy**: Automated `tenantId` generation and logical data isolation.
|
|
19
|
+
- **Inversion of Control**: Pass your own Mongoose models; the engine handles the persistence logic.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install blueprint-engine
|
|
27
|
+
```
|
|
28
|
+
Note: Ensure you have express and mongoose installed in your host project.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 🛠️ Quick Start (Auth Blueprint)
|
|
33
|
+
## 1. Define your Models
|
|
34
|
+
Your models can contain any custom fields, but the engine requires email, password, and tenantId to function.
|
|
35
|
+
|
|
36
|
+
### User Model
|
|
37
|
+
|
|
38
|
+
``` javascript
|
|
39
|
+
// models/User.js
|
|
40
|
+
const mongoose = require("mongoose");
|
|
41
|
+
|
|
42
|
+
const UserSchema = new mongoose.Schema({
|
|
43
|
+
name: { type: String, required: true },
|
|
44
|
+
email: { type: String, unique: true, required: true },
|
|
45
|
+
password: { type: String, required: true },
|
|
46
|
+
role: { type: String, enum: ["superadmin", "business", "user"], default: "user" },
|
|
47
|
+
phoneNo: { type: Number },
|
|
48
|
+
addressLine1: { type: String },
|
|
49
|
+
addressLine2: { type: String },
|
|
50
|
+
city: { type: String },
|
|
51
|
+
state: { type: String },
|
|
52
|
+
zipCode: { type: Number },
|
|
53
|
+
tenantId: { type: String, required: true },
|
|
54
|
+
|
|
55
|
+
// Optional: quick access to bookings (can also just query Booking collection)
|
|
56
|
+
bookings: [{ type: mongoose.Schema.Types.ObjectId, ref: "Booking" }]
|
|
57
|
+
}, { timestamps: true });
|
|
58
|
+
|
|
59
|
+
module.exports = mongoose.model("User", UserSchema);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Tenant Model
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
// models/User.js
|
|
66
|
+
const mongoose = require('mongoose');
|
|
67
|
+
|
|
68
|
+
const TenantSchema = new mongoose.Schema({
|
|
69
|
+
name: { type: String, required: true },
|
|
70
|
+
tenantId: { type: String },
|
|
71
|
+
meta: { type: Object, default: {} }
|
|
72
|
+
}, { timestamps: true });
|
|
73
|
+
|
|
74
|
+
module.exports = mongoose.model('Tenant', TenantSchema);
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 2. Mount the Engine
|
|
79
|
+
Inject your models and your JWT secret into the swayamauth blueprint.
|
|
80
|
+
|
|
81
|
+
``` JavaScript
|
|
82
|
+
const express = require('express');
|
|
83
|
+
const mongoose = require('mongoose');
|
|
84
|
+
const { swayamauth } = require('blueprint-engine');
|
|
85
|
+
|
|
86
|
+
const User = require('./models/User');
|
|
87
|
+
const Tenant = require('./models/Tenant');
|
|
88
|
+
|
|
89
|
+
const app = express();
|
|
90
|
+
app.use(express.json());
|
|
91
|
+
|
|
92
|
+
// Database Connection
|
|
93
|
+
mongoose.connect('mongodb://localhost:27017/your_db');
|
|
94
|
+
|
|
95
|
+
// Mount the Blueprint - One line handles all Auth logic
|
|
96
|
+
app.use('/api/auth', swayamauth(User, Tenant, 'YOUR_JWT_SECRET_KEY'));
|
|
97
|
+
|
|
98
|
+
app.listen(4000, () => console.log('Engine running on port 4000'));
|
|
99
|
+
```
|
|
100
|
+
## 📡 API Reference
|
|
101
|
+
### POST /register
|
|
102
|
+
Creates a new Tenant and a User account. Automatically hashes the password and generates a unique tenantId.
|
|
103
|
+
```json
|
|
104
|
+
Body: { "name": "Name",
|
|
105
|
+
"email": "admin@test.com",
|
|
106
|
+
"password": "password123",
|
|
107
|
+
...customFields
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### POST /login
|
|
112
|
+
Authenticates the user and returns a signed JWT.
|
|
113
|
+
```json
|
|
114
|
+
Body: { "email": "admin@test.com",
|
|
115
|
+
"password": "password123"
|
|
116
|
+
}
|
|
117
|
+
```
|
package/src/config/db.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const connectDB = async (mongoUri) => {
|
|
4
|
+
try {
|
|
5
|
+
await mongoose.connect(mongoUri);
|
|
6
|
+
console.log('MongoDB connected');
|
|
7
|
+
} catch (err) {
|
|
8
|
+
console.error(err);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
module.exports = connectDB;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const asyncHandler = require('express-async-handler');
|
|
2
|
+
const bcrypt = require('bcryptjs');
|
|
3
|
+
const jwt = require('jsonwebtoken');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* createHandlers Factory
|
|
7
|
+
* @param {Object} User - The Mongoose User Model passed from the app
|
|
8
|
+
* @param {Object} Tenant - The Mongoose Tenant Model passed from the app
|
|
9
|
+
* @param {String} jwtSecret - The secret key for signing tokens
|
|
10
|
+
*/
|
|
11
|
+
const createHandlers = (User, Tenant, jwtSecret) => {
|
|
12
|
+
|
|
13
|
+
// Internal helper for token generation
|
|
14
|
+
const generateToken = (user) => {
|
|
15
|
+
return jwt.sign(
|
|
16
|
+
{ id: user._id, role: user.role },
|
|
17
|
+
jwtSecret,
|
|
18
|
+
{ expiresIn: '7d' }
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const register = asyncHandler(async (req, res) => {
|
|
23
|
+
// We capture specific fields and use ...extra for any custom fields the user added
|
|
24
|
+
const { name, email, password, role, ...extra } = req.body;
|
|
25
|
+
|
|
26
|
+
if (!name || !email || !password) {
|
|
27
|
+
res.status(400);
|
|
28
|
+
throw new Error('Missing required fields: name, email, and password are required.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const exists = await User.findOne({ email });
|
|
32
|
+
if (exists) {
|
|
33
|
+
res.status(400);
|
|
34
|
+
throw new Error('User already exists for this email');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Automatic Tenant Creation
|
|
38
|
+
const tenantId = `${name.toLowerCase().replace(/\s+/g, "_")}_${Date.now()}`;
|
|
39
|
+
await Tenant.create({ name, tenantId });
|
|
40
|
+
|
|
41
|
+
// Internal Security Handling (Bcrypt bypass)
|
|
42
|
+
const salt = await bcrypt.genSalt(10);
|
|
43
|
+
const hashed = await bcrypt.hash(password, salt);
|
|
44
|
+
|
|
45
|
+
// Create User with dynamic fields
|
|
46
|
+
const user = await User.create({
|
|
47
|
+
name,
|
|
48
|
+
email,
|
|
49
|
+
password: hashed,
|
|
50
|
+
role: role || 'user',
|
|
51
|
+
tenantId,
|
|
52
|
+
...extra // This allows the user to pass 'city', 'phoneNo', etc.
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
res.status(201).json({
|
|
56
|
+
id: user._id,
|
|
57
|
+
name: user.name,
|
|
58
|
+
email: user.email,
|
|
59
|
+
role: user.role,
|
|
60
|
+
tenantId: user.tenantId,
|
|
61
|
+
token: generateToken(user)
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const login = asyncHandler(async (req, res) => {
|
|
66
|
+
const { email, password } = req.body;
|
|
67
|
+
|
|
68
|
+
if (!email || !password) {
|
|
69
|
+
res.status(400);
|
|
70
|
+
throw new Error('Missing fields');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const user = await User.findOne({ email });
|
|
74
|
+
if (!user) {
|
|
75
|
+
res.status(401);
|
|
76
|
+
throw new Error('Invalid credentials');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const match = await bcrypt.compare(password, user.password);
|
|
80
|
+
if (!match) {
|
|
81
|
+
res.status(401);
|
|
82
|
+
throw new Error('Invalid credentials');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
res.json({
|
|
86
|
+
id: user._id,
|
|
87
|
+
name: user.name,
|
|
88
|
+
email: user.email,
|
|
89
|
+
role: user.role,
|
|
90
|
+
tenantId: user.tenantId,
|
|
91
|
+
token: generateToken(user)
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return { register, login };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
module.exports = { createHandlers };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
const asyncHandler = require('express-async-handler');
|
|
3
|
+
const User = require('../models/User');
|
|
4
|
+
const Business = require('../models/Business');
|
|
5
|
+
|
|
6
|
+
// Protect routes
|
|
7
|
+
const protect = asyncHandler(async (req, res, next) => {
|
|
8
|
+
const auth = req.headers.authorization;
|
|
9
|
+
console.log(auth);
|
|
10
|
+
|
|
11
|
+
if (!auth || !auth.startsWith('Bearer ')) {
|
|
12
|
+
res.status(401);
|
|
13
|
+
throw new Error('Not authorized, token missing');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const token = auth.split(' ')[1];
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
20
|
+
|
|
21
|
+
console.log(decoded);
|
|
22
|
+
// Try finding in User
|
|
23
|
+
let user = await User.findById(decoded.id);
|
|
24
|
+
|
|
25
|
+
// If not found, try Business
|
|
26
|
+
if (!user) {
|
|
27
|
+
user = await Business.findById(decoded.id);
|
|
28
|
+
}
|
|
29
|
+
console.log(user);
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if (!user) {
|
|
34
|
+
res.status(401);
|
|
35
|
+
throw new Error('Not authorized, user is not found');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Allow normal users if tenantId matches OR if superadmin
|
|
39
|
+
if (req.tenantId && user.tenantId !== req.tenantId && user.role !== 'superadmin') {
|
|
40
|
+
res.status(403);
|
|
41
|
+
throw new Error('Tenant mismatch');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
req.user = user; // attach user to request
|
|
45
|
+
next();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.log(err);
|
|
48
|
+
|
|
49
|
+
res.status(401);
|
|
50
|
+
throw new Error('Not authorized, token is this the end invalid');
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Role-based authorization middleware
|
|
55
|
+
const authorize = (...roles) => (req, res, next) => {
|
|
56
|
+
if (!req.user) {
|
|
57
|
+
return res.status(401).json({ message: 'Not authenticated' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!roles.includes(req.user.role)) {
|
|
61
|
+
return res.status(403).json({ message: 'Forbidden - insufficient role' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
next();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
module.exports = { protect, authorize };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const errorHandler = (err, req, res, next) => {
|
|
2
|
+
console.error(err);
|
|
3
|
+
const statusCode = res.statusCode && res.statusCode !== 200 ? res.statusCode : 500;
|
|
4
|
+
res.status(statusCode).json({
|
|
5
|
+
message: err.message || 'Internal Server Error',
|
|
6
|
+
stack: process.env.NODE_ENV === 'production' ? undefined : err.stack
|
|
7
|
+
});
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
module.exports = errorHandler;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// middleware/tenant.js
|
|
2
|
+
const setTenant = (req, res, next) => {
|
|
3
|
+
const tenantId = req.header('x-tenant-id');
|
|
4
|
+
if (!tenantId) {
|
|
5
|
+
return res.status(400).json({ message: 'Missing x-tenant-id header' });
|
|
6
|
+
}
|
|
7
|
+
req.tenantId = tenantId;
|
|
8
|
+
next();
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
module.exports = setTenant;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const TenantSchema = new mongoose.Schema({
|
|
4
|
+
name: { type: String, required: true },
|
|
5
|
+
tenantId: { type: String },
|
|
6
|
+
meta: { type: Object, default: {} }
|
|
7
|
+
}, { timestamps: true });
|
|
8
|
+
|
|
9
|
+
module.exports = mongoose.model('Tenant', TenantSchema);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const mongoose = require("mongoose");
|
|
2
|
+
|
|
3
|
+
const UserSchema = new mongoose.Schema({
|
|
4
|
+
name: { type: String, required: true },
|
|
5
|
+
email: { type: String, unique: true, required: true },
|
|
6
|
+
password: { type: String, required: true },
|
|
7
|
+
role: { type: String, enum: ["superadmin", "business", "user"], default: "user" },
|
|
8
|
+
phoneNo: { type: Number },
|
|
9
|
+
addressLine1: { type: String },
|
|
10
|
+
addressLine2: { type: String },
|
|
11
|
+
city: { type: String },
|
|
12
|
+
state: { type: String },
|
|
13
|
+
zipCode: { type: Number },
|
|
14
|
+
tenantId: { type: String, required: true },
|
|
15
|
+
|
|
16
|
+
// Optional: quick access to bookings (can also just query Booking collection)
|
|
17
|
+
bookings: [{ type: mongoose.Schema.Types.ObjectId, ref: "Booking" }]
|
|
18
|
+
}, { timestamps: true });
|
|
19
|
+
|
|
20
|
+
module.exports = mongoose.model("User", UserSchema);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Inside your library: src/routes/auth.js
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const { createHandlers } = require('../controllers/authController.js');
|
|
4
|
+
|
|
5
|
+
const swayamauth = (User, Tenant, jwtSecret) => {
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
|
|
8
|
+
// We pass the models into the controller factory
|
|
9
|
+
const { register, login } = createHandlers(User, Tenant, jwtSecret);
|
|
10
|
+
|
|
11
|
+
router.post('/register', register);
|
|
12
|
+
router.post('/login', login);
|
|
13
|
+
|
|
14
|
+
return router;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
module.exports = swayamauth;
|