create-enterprise-backend 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/bin/cli.js +8 -0
- package/core/install.js +5 -0
- package/core/run.js +95 -0
- package/core/scaffold.js +37 -0
- package/package.json +28 -0
- package/templates/express/mongo/nodemon.json +6 -0
- package/templates/express/mongo/package.json +21 -0
- package/templates/express/mongo/src/app.js +41 -0
- package/templates/express/mongo/src/config/db.js +16 -0
- package/templates/express/mongo/src/config/env.js +9 -0
- package/templates/express/mongo/src/middlewares/db-guard.js +11 -0
- package/templates/express/mongo/src/middlewares/jwt.guard.js +18 -0
- package/templates/express/mongo/src/modules/auth/auth.controller.js +32 -0
- package/templates/express/mongo/src/modules/auth/auth.routes.js +12 -0
- package/templates/express/mongo/src/modules/auth/auth.service.js +68 -0
- package/templates/express/mongo/src/modules/auth/user.model.js +14 -0
- package/templates/express/mongo/src/routes.js +5 -0
- package/templates/express/mongo/src/server.js +11 -0
- package/templates/express/mongo/src/utils/error-handler.js +10 -0
- package/templates/fastify/mongo/nodemon.json +6 -0
- package/templates/fastify/mongo/package.json +23 -0
- package/templates/fastify/mongo/src/app.js +40 -0
- package/templates/fastify/mongo/src/config/db.js +16 -0
- package/templates/fastify/mongo/src/config/env.js +9 -0
- package/templates/fastify/mongo/src/middlewares/README.md +2 -0
- package/templates/fastify/mongo/src/modules/auth/auth.controller.js +32 -0
- package/templates/fastify/mongo/src/modules/auth/auth.routes.js +24 -0
- package/templates/fastify/mongo/src/modules/auth/auth.schema.js +22 -0
- package/templates/fastify/mongo/src/modules/auth/auth.service.js +68 -0
- package/templates/fastify/mongo/src/modules/auth/user.model.js +14 -0
- package/templates/fastify/mongo/src/plugins/jwt.js +8 -0
- package/templates/fastify/mongo/src/routes.js +11 -0
- package/templates/fastify/mongo/src/server.js +15 -0
- package/templates/fastify/mongo/src/services/README.md +2 -0
- package/templates/fastify/mongo/src/utils/db-guard.js +11 -0
- package/templates/fastify/mongo/src/utils/error-handler.js +18 -0
- package/templates/fastify/mongo/src/utils/jwt-guard.js +10 -0
- package/templates/fastify/supabase/package.json +18 -0
- package/templates/fastify/supabase/src/app.js +23 -0
- package/templates/fastify/supabase/src/config/env.js +18 -0
- package/templates/fastify/supabase/src/config/supabase.js +23 -0
- package/templates/fastify/supabase/src/modules/auth/auth.controller.js +7 -0
- package/templates/fastify/supabase/src/modules/auth/auth.routes.js +9 -0
- package/templates/fastify/supabase/src/modules/auth/auth.service.js +7 -0
- package/templates/fastify/supabase/src/plugins/request-content.js +16 -0
- package/templates/fastify/supabase/src/routes.js +5 -0
- package/templates/fastify/supabase/src/server.js +27 -0
- package/templates/fastify/supabase/src/utils/auth-guard.js +27 -0
- package/templates/fastify/supabase/src/utils/error-handler.js +10 -0
package/bin/cli.js
ADDED
package/core/install.js
ADDED
package/core/run.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import prompts from "prompts";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { generateProject } from "./scaffold.js";
|
|
6
|
+
import { installDeps } from "./install.js";
|
|
7
|
+
|
|
8
|
+
export async function run() {
|
|
9
|
+
console.log(chalk.cyan.bold("\nš Create Backend (Enterprise Scaffolder)\n"));
|
|
10
|
+
|
|
11
|
+
const projectNameArg = process.argv[2];
|
|
12
|
+
|
|
13
|
+
const answers = await prompts(
|
|
14
|
+
[
|
|
15
|
+
{
|
|
16
|
+
type: projectNameArg ? null : "text",
|
|
17
|
+
name: "projectName",
|
|
18
|
+
message: "Project name?",
|
|
19
|
+
validate: (v) =>
|
|
20
|
+
v?.trim()?.length ? true : "Project name is required"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: "select",
|
|
24
|
+
name: "framework",
|
|
25
|
+
message: "Select framework",
|
|
26
|
+
choices: [
|
|
27
|
+
{ title: "Express", value: "express" },
|
|
28
|
+
{ title: "Fastify", value: "fastify" }
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: "select",
|
|
33
|
+
name: "database",
|
|
34
|
+
message: "Select database",
|
|
35
|
+
choices: [
|
|
36
|
+
{ title: "MongoDB", value: "mongo" },
|
|
37
|
+
{ title: "Supabase", value: "supabase" },
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "toggle",
|
|
42
|
+
name: "install",
|
|
43
|
+
message: "Install dependencies now?",
|
|
44
|
+
initial: true,
|
|
45
|
+
active: "yes",
|
|
46
|
+
inactive: "no"
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
{
|
|
50
|
+
onCancel: () => {
|
|
51
|
+
console.log(chalk.yellow("\nā Cancelled.\n"));
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const projectName = (projectNameArg || answers.projectName).trim();
|
|
58
|
+
const { framework, database } = answers;
|
|
59
|
+
|
|
60
|
+
const templatePath = path.resolve(
|
|
61
|
+
"templates",
|
|
62
|
+
framework,
|
|
63
|
+
database
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// ā
Validate template exists
|
|
67
|
+
if (!fs.existsSync(templatePath)) {
|
|
68
|
+
console.log(
|
|
69
|
+
chalk.red(
|
|
70
|
+
`\nā Template not found for: ${framework} + ${database}\n`
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const targetDir = path.join(process.cwd(), projectName);
|
|
77
|
+
|
|
78
|
+
console.log(chalk.gray("\nš¦ Generating project..."));
|
|
79
|
+
await generateProject({
|
|
80
|
+
framework,
|
|
81
|
+
database,
|
|
82
|
+
targetDir,
|
|
83
|
+
projectName
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (answers.install) {
|
|
87
|
+
console.log(chalk.gray("\nš„ Installing dependencies..."));
|
|
88
|
+
await installDeps({ targetDir });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(chalk.green.bold("\nā
Done!\n"));
|
|
92
|
+
console.log(
|
|
93
|
+
`Next steps:\n cd ${projectName}\n npm run dev\n`
|
|
94
|
+
);
|
|
95
|
+
}
|
package/core/scaffold.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
function getPackageRoot() {
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
return path.resolve(path.dirname(__filename), "..");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function generateProject({ framework, database, targetDir, projectName }) {
|
|
11
|
+
const root = getPackageRoot();
|
|
12
|
+
|
|
13
|
+
const templateDir = path.join(root, "templates", framework, database);
|
|
14
|
+
|
|
15
|
+
const exists = await fs.pathExists(templateDir);
|
|
16
|
+
if (!exists) {
|
|
17
|
+
throw new Error(`Template not found: ${framework}/${database}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (await fs.pathExists(targetDir)) {
|
|
21
|
+
throw new Error(`Target folder already exists: ${projectName}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await fs.ensureDir(targetDir);
|
|
25
|
+
await fs.copy(templateDir, targetDir, {
|
|
26
|
+
overwrite: false,
|
|
27
|
+
errorOnExist: true
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Rename package name inside generated package.json (if template has one)
|
|
31
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
32
|
+
if (await fs.pathExists(pkgPath)) {
|
|
33
|
+
const pkg = await fs.readJson(pkgPath);
|
|
34
|
+
pkg.name = projectName;
|
|
35
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
36
|
+
}
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-enterprise-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Enterprise backend scaffolder (Express/Fastify + Mongo/Supabase)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-enterprise-backend": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["bin", "core", "templates", "README.md"],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"cli",
|
|
12
|
+
"scaffold",
|
|
13
|
+
"backend",
|
|
14
|
+
"express",
|
|
15
|
+
"fastify",
|
|
16
|
+
"mongodb",
|
|
17
|
+
"supabase"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"chalk": "^5.3.0",
|
|
25
|
+
"fs-extra": "^11.2.0",
|
|
26
|
+
"prompts": "^2.4.2"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backend-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "nodemon src/server.js",
|
|
8
|
+
"start": "node src/server.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"express": "^4.19.2",
|
|
12
|
+
"mongoose": "^8.3.0",
|
|
13
|
+
"dotenv": "^16.4.5",
|
|
14
|
+
"bcryptjs": "^2.4.3",
|
|
15
|
+
"jsonwebtoken": "^9.0.2",
|
|
16
|
+
"cors": "^2.8.5"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"nodemon": "^3.0.3"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import { registerRoutes } from "./routes.js";
|
|
4
|
+
import { globalErrorHandler } from "./utils/error-handler.js";
|
|
5
|
+
|
|
6
|
+
export function buildApp() {
|
|
7
|
+
const app = express();
|
|
8
|
+
|
|
9
|
+
app.use(cors());
|
|
10
|
+
app.use(express.json());
|
|
11
|
+
|
|
12
|
+
// Root
|
|
13
|
+
app.get("/", (req, res) => {
|
|
14
|
+
res.json({
|
|
15
|
+
status: "ok",
|
|
16
|
+
message: "Express API is running"
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Health
|
|
21
|
+
app.get("/health", (req, res) => {
|
|
22
|
+
res.json({ status: "ok" });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Register ALL routes
|
|
26
|
+
registerRoutes(app);
|
|
27
|
+
|
|
28
|
+
// ā 404 Not Found Handler (Express way)
|
|
29
|
+
app.use((req, res, next) => {
|
|
30
|
+
res.status(404).json({
|
|
31
|
+
statusCode: 404,
|
|
32
|
+
message: "Route not found",
|
|
33
|
+
path: req.originalUrl
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ā
Global Error Handler (MUST be last)
|
|
38
|
+
app.use(globalErrorHandler);
|
|
39
|
+
|
|
40
|
+
return app;
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
export async function connectDB(uri) {
|
|
4
|
+
if (!uri) {
|
|
5
|
+
console.warn("ā ļø MongoDB disabled (MONGO_URI not set)");
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
await mongoose.connect(uri);
|
|
11
|
+
console.log("ā
MongoDB connected");
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error("ā MongoDB connection failed:", err.message);
|
|
14
|
+
console.warn("ā ļø App running without database");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
export function requireDB(req, res, next) {
|
|
4
|
+
if (mongoose.connection.readyState !== 1) {
|
|
5
|
+
return res.status(503).json({
|
|
6
|
+
statusCode: 503,
|
|
7
|
+
message: "Database not connected. Please configure MongoDB."
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
next();
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
import { env } from "../config/env.js";
|
|
3
|
+
|
|
4
|
+
export function requireAuth(req, res, next) {
|
|
5
|
+
const authHeader = req.headers.authorization;
|
|
6
|
+
|
|
7
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
8
|
+
return res.status(401).json({ message: "Unauthorized" });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const token = authHeader.split(" ")[1];
|
|
13
|
+
req.user = jwt.verify(token, env.JWT_SECRET);
|
|
14
|
+
next();
|
|
15
|
+
} catch {
|
|
16
|
+
res.status(401).json({ message: "Invalid token" });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { registerUser, loginUser } from "./auth.service.js";
|
|
2
|
+
import { env } from "../../config/env.js";
|
|
3
|
+
import { User } from "./user.model.js";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export async function register(req, reply) {
|
|
7
|
+
try {
|
|
8
|
+
const result = await registerUser(req.body, env.JWT_SECRET);
|
|
9
|
+
reply.code(201).send(result);
|
|
10
|
+
} catch (err) {
|
|
11
|
+
reply.code(400).send({ message: err.message });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function login(req, reply) {
|
|
16
|
+
try {
|
|
17
|
+
const result = await loginUser(req.body, env.JWT_SECRET);
|
|
18
|
+
reply.send(result);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
reply.code(400).send({ message: err.message });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export async function me(req, reply) {
|
|
26
|
+
const userId = req.user.id;
|
|
27
|
+
|
|
28
|
+
const user = await User.findById(userId).select("-password");
|
|
29
|
+
|
|
30
|
+
reply.send(user);
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { register, login, me } from "./auth.controller.js";
|
|
3
|
+
import { requireDB } from "../../middlewares/db-guard.js";
|
|
4
|
+
import { requireAuth } from "../../middlewares/jwt.guard.js";
|
|
5
|
+
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
|
|
8
|
+
router.post("/register", requireDB, register);
|
|
9
|
+
router.post("/login", requireDB, login);
|
|
10
|
+
router.get("/me", requireDB, requireAuth, me);
|
|
11
|
+
|
|
12
|
+
export default router;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import bcrypt from "bcryptjs";
|
|
2
|
+
import jwt from "jsonwebtoken";
|
|
3
|
+
import { User } from "./user.model.js";
|
|
4
|
+
|
|
5
|
+
export async function registerUser({ name, email, password }, jwtSecret) {
|
|
6
|
+
if (!jwtSecret) {
|
|
7
|
+
throw new Error("JWT_SECRET not configured");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const exists = await User.findOne({ email });
|
|
11
|
+
if (exists) {
|
|
12
|
+
throw new Error("Email already exists");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
16
|
+
|
|
17
|
+
const user = await User.create({
|
|
18
|
+
name,
|
|
19
|
+
email,
|
|
20
|
+
password: hashedPassword
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const token = jwt.sign(
|
|
24
|
+
{ id: user._id, email: user.email },
|
|
25
|
+
jwtSecret,
|
|
26
|
+
{ expiresIn: "7d" }
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
token,
|
|
31
|
+
user: {
|
|
32
|
+
id: user._id,
|
|
33
|
+
name: user.name,
|
|
34
|
+
email: user.email
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function loginUser({ email, password }, jwtSecret) {
|
|
40
|
+
if (!jwtSecret) {
|
|
41
|
+
throw new Error("JWT_SECRET not configured");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const user = await User.findOne({ email });
|
|
45
|
+
if (!user) {
|
|
46
|
+
throw new Error("Invalid credentials");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const match = await bcrypt.compare(password, user.password);
|
|
50
|
+
if (!match) {
|
|
51
|
+
throw new Error("Invalid credentials");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const token = jwt.sign(
|
|
55
|
+
{ id: user._id, email: user.email },
|
|
56
|
+
jwtSecret,
|
|
57
|
+
{ expiresIn: "7d" }
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
token,
|
|
62
|
+
user: {
|
|
63
|
+
id: user._id,
|
|
64
|
+
name: user.name,
|
|
65
|
+
email: user.email
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
const userSchema = new mongoose.Schema(
|
|
4
|
+
{
|
|
5
|
+
name: { type: String, required: true },
|
|
6
|
+
email: { type: String, required: true, unique: true, lowercase: true },
|
|
7
|
+
password: { type: String, required: true }
|
|
8
|
+
},
|
|
9
|
+
{ timestamps: true }
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
// ā ļø Prevent model overwrite in dev / watch mode
|
|
13
|
+
export const User =
|
|
14
|
+
mongoose.models.User || mongoose.model("User", userSchema);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { buildApp } from "./app.js";
|
|
2
|
+
import { connectDB } from "./config/db.js";
|
|
3
|
+
import { env } from "./config/env.js";
|
|
4
|
+
|
|
5
|
+
const app = buildApp();
|
|
6
|
+
|
|
7
|
+
await connectDB(env.MONGO_URI);
|
|
8
|
+
|
|
9
|
+
app.listen(env.PORT, () => {
|
|
10
|
+
console.log(`š Express server running on http://localhost:${env.PORT}`);
|
|
11
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function globalErrorHandler(err, req, res, next) {
|
|
2
|
+
console.error(err);
|
|
3
|
+
|
|
4
|
+
res.status(err.statusCode || 500).json({
|
|
5
|
+
statusCode: err.statusCode || 500,
|
|
6
|
+
message: err.message || "Internal Server Error",
|
|
7
|
+
path: req.originalUrl,
|
|
8
|
+
timestamp: new Date().toISOString()
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backend-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "nodemon src/server.js",
|
|
7
|
+
"start": "node src/server.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@fastify/jwt": "^7.0.1",
|
|
11
|
+
"bcryptjs": "^2.4.3",
|
|
12
|
+
"dotenv": "^16.4.5",
|
|
13
|
+
"fastify": "^4.27.0",
|
|
14
|
+
"fastify-plugin": "^5.1.0",
|
|
15
|
+
"jsonwebtoken": "^9.0.2",
|
|
16
|
+
"mongoose": "^8.3.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"nodemon": "^3.0.3"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import { globalErrorHandler } from "./utils/error-handler.js";
|
|
3
|
+
import { registerRoutes } from "./routes.js";
|
|
4
|
+
import jwtPlugin from "./plugins/jwt.js";
|
|
5
|
+
import { env } from "./config/env.js";
|
|
6
|
+
|
|
7
|
+
export function buildApp() {
|
|
8
|
+
const app = Fastify({ logger: true });
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// JWT plugin
|
|
13
|
+
app.register(jwtPlugin, { secret: env.JWT_SECRET });
|
|
14
|
+
|
|
15
|
+
// Root
|
|
16
|
+
app.get("/", async () => ({
|
|
17
|
+
status: "ok",
|
|
18
|
+
message: "Fastify API is running"
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Health
|
|
22
|
+
app.get("/health", async () => ({
|
|
23
|
+
status: "ok"
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// Register ALL routes here
|
|
27
|
+
registerRoutes(app);
|
|
28
|
+
|
|
29
|
+
// Global error handler (must be last)
|
|
30
|
+
app.setNotFoundHandler((request, reply) => {
|
|
31
|
+
reply.code(404).send({
|
|
32
|
+
statusCode: 404,
|
|
33
|
+
message: "Route not found",
|
|
34
|
+
path: request.url
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
return app;
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
export async function connectDB(uri, logger = console) {
|
|
4
|
+
if (!uri) {
|
|
5
|
+
logger.warn("ā ļø MongoDB disabled (MONGO_URI not set)");
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
await mongoose.connect(uri);
|
|
11
|
+
logger.info("ā
MongoDB connected");
|
|
12
|
+
} catch (err) {
|
|
13
|
+
logger.error("ā MongoDB connection failed:", err.message);
|
|
14
|
+
logger.warn("ā ļø Continuing without database");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { registerUser, loginUser } from "./auth.service.js";
|
|
2
|
+
import { env } from "../../config/env.js";
|
|
3
|
+
import { User } from "./user.model.js";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export async function register(req, reply) {
|
|
7
|
+
try {
|
|
8
|
+
const result = await registerUser(req.body, env.JWT_SECRET);
|
|
9
|
+
reply.code(201).send(result);
|
|
10
|
+
} catch (err) {
|
|
11
|
+
reply.code(400).send({ message: err.message });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function login(req, reply) {
|
|
16
|
+
try {
|
|
17
|
+
const result = await loginUser(req.body, env.JWT_SECRET);
|
|
18
|
+
reply.send(result);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
reply.code(400).send({ message: err.message });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export async function me(req, reply) {
|
|
26
|
+
const userId = req.user.id;
|
|
27
|
+
|
|
28
|
+
const user = await User.findById(userId).select("-password");
|
|
29
|
+
|
|
30
|
+
reply.send(user);
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { register, login, me } from "./auth.controller.js";
|
|
2
|
+
import { registerSchema, loginSchema } from "./auth.schema.js";
|
|
3
|
+
import { requireDB } from "../../utils/db-guard.js";
|
|
4
|
+
import { requireAuth } from "../../utils/jwt-guard.js";
|
|
5
|
+
|
|
6
|
+
export default async function authRoutes(app) {
|
|
7
|
+
app.post("/register", {
|
|
8
|
+
preHandler: requireDB,
|
|
9
|
+
schema: registerSchema,
|
|
10
|
+
handler: register
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
app.post("/login", {
|
|
14
|
+
preHandler: requireDB,
|
|
15
|
+
schema: loginSchema,
|
|
16
|
+
handler: login
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// š JWT protected route
|
|
20
|
+
app.get("/me", {
|
|
21
|
+
preHandler: [requireDB, requireAuth],
|
|
22
|
+
handler: me
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const registerSchema = {
|
|
2
|
+
body: {
|
|
3
|
+
type: "object",
|
|
4
|
+
required: ["name", "email", "password"],
|
|
5
|
+
properties: {
|
|
6
|
+
name: { type: "string", minLength: 2 },
|
|
7
|
+
email: { type: "string", format: "email" },
|
|
8
|
+
password: { type: "string", minLength: 6 }
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const loginSchema = {
|
|
14
|
+
body: {
|
|
15
|
+
type: "object",
|
|
16
|
+
required: ["email", "password"],
|
|
17
|
+
properties: {
|
|
18
|
+
email: { type: "string", format: "email" },
|
|
19
|
+
password: { type: "string", minLength: 6 }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import bcrypt from "bcryptjs";
|
|
2
|
+
import jwt from "jsonwebtoken";
|
|
3
|
+
import { User } from "./user.model.js";
|
|
4
|
+
|
|
5
|
+
export async function registerUser({ name, email, password }, jwtSecret) {
|
|
6
|
+
if (!jwtSecret) {
|
|
7
|
+
throw new Error("JWT_SECRET not configured");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const exists = await User.findOne({ email });
|
|
11
|
+
if (exists) {
|
|
12
|
+
throw new Error("Email already exists");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
16
|
+
|
|
17
|
+
const user = await User.create({
|
|
18
|
+
name,
|
|
19
|
+
email,
|
|
20
|
+
password: hashedPassword
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const token = jwt.sign(
|
|
24
|
+
{ id: user._id, email: user.email },
|
|
25
|
+
jwtSecret,
|
|
26
|
+
{ expiresIn: "7d" }
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
token,
|
|
31
|
+
user: {
|
|
32
|
+
id: user._id,
|
|
33
|
+
name: user.name,
|
|
34
|
+
email: user.email
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function loginUser({ email, password }, jwtSecret) {
|
|
40
|
+
if (!jwtSecret) {
|
|
41
|
+
throw new Error("JWT_SECRET not configured");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const user = await User.findOne({ email });
|
|
45
|
+
if (!user) {
|
|
46
|
+
throw new Error("Invalid credentials");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const match = await bcrypt.compare(password, user.password);
|
|
50
|
+
if (!match) {
|
|
51
|
+
throw new Error("Invalid credentials");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const token = jwt.sign(
|
|
55
|
+
{ id: user._id, email: user.email },
|
|
56
|
+
jwtSecret,
|
|
57
|
+
{ expiresIn: "7d" }
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
token,
|
|
62
|
+
user: {
|
|
63
|
+
id: user._id,
|
|
64
|
+
name: user.name,
|
|
65
|
+
email: user.email
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
const userSchema = new mongoose.Schema(
|
|
4
|
+
{
|
|
5
|
+
name: { type: String, required: true },
|
|
6
|
+
email: { type: String, required: true, unique: true, lowercase: true },
|
|
7
|
+
password: { type: String, required: true }
|
|
8
|
+
},
|
|
9
|
+
{ timestamps: true }
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
// ā ļø Prevent model overwrite in dev / watch mode
|
|
13
|
+
export const User =
|
|
14
|
+
mongoose.models.User || mongoose.model("User", userSchema);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import authRoutes from "./modules/auth/auth.routes.js";
|
|
2
|
+
// later you can add more:
|
|
3
|
+
// import userRoutes from "./modules/users/user.routes.js";
|
|
4
|
+
|
|
5
|
+
export async function registerRoutes(app) {
|
|
6
|
+
// Auth routes
|
|
7
|
+
app.register(authRoutes, { prefix: "/api/auth" });
|
|
8
|
+
|
|
9
|
+
// Future modules
|
|
10
|
+
// app.register(userRoutes, { prefix: "/api/users" });
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { buildApp } from "./app.js";
|
|
2
|
+
import { connectDB } from "./config/db.js";
|
|
3
|
+
import { env } from "./config/env.js";
|
|
4
|
+
|
|
5
|
+
const app = buildApp();
|
|
6
|
+
|
|
7
|
+
await connectDB(env.MONGO_URI, app.log);
|
|
8
|
+
|
|
9
|
+
app.listen({ port: env.PORT }, (err, address) => {
|
|
10
|
+
if (err) {
|
|
11
|
+
app.log.error(err);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
app.log.info(`š Server running at ${address}`);
|
|
15
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
export function requireDB(req, reply, done) {
|
|
4
|
+
if (mongoose.connection.readyState !== 1) {
|
|
5
|
+
return reply.code(503).send({
|
|
6
|
+
statusCode: 503,
|
|
7
|
+
message: "Database not connected. Please configure MongoDB."
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
done();
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function globalErrorHandler(error, request, reply) {
|
|
2
|
+
// Log full error (for dev / prod logs)
|
|
3
|
+
request.log.error(error);
|
|
4
|
+
|
|
5
|
+
// Default values
|
|
6
|
+
const statusCode = error.statusCode || 500;
|
|
7
|
+
const message =
|
|
8
|
+
statusCode === 500
|
|
9
|
+
? "Internal Server Error"
|
|
10
|
+
: error.message;
|
|
11
|
+
|
|
12
|
+
reply.code(statusCode).send({
|
|
13
|
+
statusCode,
|
|
14
|
+
message,
|
|
15
|
+
path: request.url,
|
|
16
|
+
timestamp: new Date().toISOString()
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fastify-supabase-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "nodemon src/server.js",
|
|
7
|
+
"start": "node src/server.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@supabase/supabase-js": "^2.45.0",
|
|
11
|
+
"dotenv": "^16.4.5",
|
|
12
|
+
"fastify": "^4.27.0",
|
|
13
|
+
"fastify-plugin": "^5.1.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"nodemon": "^3.0.3"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import requestContext from "./plugins/request-content.js";
|
|
3
|
+
import { registerRoutes } from "./routes.js";
|
|
4
|
+
import { globalErrorHandler } from "./utils/error-handler.js";
|
|
5
|
+
|
|
6
|
+
export function buildApp() {
|
|
7
|
+
const app = Fastify({ logger: true });
|
|
8
|
+
|
|
9
|
+
app.register(requestContext);
|
|
10
|
+
|
|
11
|
+
app.get("/", async () => ({
|
|
12
|
+
status: "ok",
|
|
13
|
+
message: "Fastify Supabase Enterprise API"
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
app.get("/health", async () => ({ status: "ok" }));
|
|
17
|
+
|
|
18
|
+
registerRoutes(app);
|
|
19
|
+
|
|
20
|
+
app.setErrorHandler(globalErrorHandler);
|
|
21
|
+
|
|
22
|
+
return app;
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
|
|
3
|
+
dotenv.config();
|
|
4
|
+
|
|
5
|
+
function required(key) {
|
|
6
|
+
if (!process.env[key]) {
|
|
7
|
+
throw new Error(`Missing required environment variable: ${key}`);
|
|
8
|
+
}
|
|
9
|
+
return process.env[key];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const env = {
|
|
13
|
+
PORT: process.env.PORT || 5000,
|
|
14
|
+
|
|
15
|
+
SUPABASE_URL: process.env.SUPABASE_URL,
|
|
16
|
+
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY,
|
|
17
|
+
SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
18
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
import { env } from "./env.js";
|
|
3
|
+
|
|
4
|
+
function createSupabaseClient(name, url, key) {
|
|
5
|
+
if (!url || !key) {
|
|
6
|
+
console.warn(`ā ļø Supabase ${name} client disabled (missing env vars)`);
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return createClient(url, key);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const supabasePublic = createSupabaseClient(
|
|
14
|
+
"public",
|
|
15
|
+
env.SUPABASE_URL,
|
|
16
|
+
env.SUPABASE_ANON_KEY
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const supabaseAdmin = createSupabaseClient(
|
|
20
|
+
"admin",
|
|
21
|
+
env.SUPABASE_URL,
|
|
22
|
+
env.SUPABASE_SERVICE_ROLE_KEY
|
|
23
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
import { supabasePublic } from "../config/supabase.js";
|
|
3
|
+
|
|
4
|
+
export default fp(async (app) => {
|
|
5
|
+
app.decorateRequest("user", null);
|
|
6
|
+
|
|
7
|
+
app.addHook("preHandler", async (req) => {
|
|
8
|
+
const auth = req.headers.authorization;
|
|
9
|
+
if (!auth?.startsWith("Bearer ")) return;
|
|
10
|
+
|
|
11
|
+
const token = auth.split(" ")[1];
|
|
12
|
+
const { data } = await supabasePublic.auth.getUser(token);
|
|
13
|
+
|
|
14
|
+
req.user = data?.user || null;
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { buildApp } from "./app.js";
|
|
2
|
+
import { env } from "./config/env.js";
|
|
3
|
+
|
|
4
|
+
const app = buildApp();
|
|
5
|
+
|
|
6
|
+
const MAX_PORT_TRIES = 20;
|
|
7
|
+
|
|
8
|
+
async function listenSilently(startPort) {
|
|
9
|
+
let port = startPort;
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < MAX_PORT_TRIES; i++) {
|
|
12
|
+
try {
|
|
13
|
+
await app.listen({ port });
|
|
14
|
+
app.log.info(`š Server running at http://localhost:${port}`);
|
|
15
|
+
return;
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (err.code !== "EADDRINUSE") {
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
port++;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
throw new Error("No available ports found");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
listenSilently(env.PORT);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { supabasePublic } from "../config/supabase.js";
|
|
2
|
+
|
|
3
|
+
export async function requireAuth(request, reply) {
|
|
4
|
+
if (!supabasePublic) {
|
|
5
|
+
return reply.code(503).send({
|
|
6
|
+
message: "Auth service not configured (Supabase disabled)"
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const auth = request.headers.authorization;
|
|
11
|
+
|
|
12
|
+
if (!auth || !auth.startsWith("Bearer ")) {
|
|
13
|
+
return reply.code(401).send({ message: "Unauthorized" });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const token = auth.split(" ")[1];
|
|
17
|
+
|
|
18
|
+
const { data, error } = await supabasePublic.auth.getUser(token);
|
|
19
|
+
|
|
20
|
+
if (error || !data?.user) {
|
|
21
|
+
return reply.code(401).send({
|
|
22
|
+
message: "Invalid or expired token"
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
request.user = data.user;
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function globalErrorHandler(error, request, reply) {
|
|
2
|
+
request.log.error(error);
|
|
3
|
+
|
|
4
|
+
reply.code(error.statusCode || 500).send({
|
|
5
|
+
statusCode: error.statusCode || 500,
|
|
6
|
+
message: error.message || "Internal Server Error",
|
|
7
|
+
path: request.url,
|
|
8
|
+
timestamp: new Date().toISOString()
|
|
9
|
+
});
|
|
10
|
+
}
|