backend.in
Version:
A npm module that could help node.js servers in one line of code
703 lines (702 loc) • 26.3 kB
JavaScript
// src/index.ts
import express from "express";
import mongoose, { Schema } from "mongoose";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import session from "express-session";
import multer from "multer";
import path from "path";
import helmet from "helmet";
import cors from "cors";
import morgan from "morgan";
import rateLimit from "express-rate-limit";
import nodemailer from "nodemailer";
import { createClient } from "redis";
import { v4 as uuidv4 } from "uuid";
import axios from "axios";
var ResetTokenSchema = new Schema({
userId: { type: Schema.Types.ObjectId, required: true, index: true },
token: { type: String, required: true, index: true },
expiresAt: { type: Date, required: true },
used: { type: Boolean, default: false }
});
var ApiLogSchema = new Schema({
projectId: { type: String, required: true, index: true },
path: { type: String, required: true },
method: { type: String, required: true },
statusCode: { type: Number, required: true },
responseTime: { type: Number, required: true },
userId: { type: Schema.Types.ObjectId, index: true },
ipAddress: { type: String },
userAgent: { type: String },
timestamp: { type: Date, default: Date.now, index: true }
});
var ProjectUsageSchema = new Schema({
projectId: { type: String, required: true, unique: true },
requests: { type: Number, default: 0 },
lastReset: { type: Date, default: Date.now },
users: { type: Number, default: 0 },
storage: { type: Number, default: 0 }
// in bytes
});
var ResetToken = mongoose.models.ResetToken || mongoose.model("ResetToken", ResetTokenSchema);
var ApiLog = mongoose.models.ApiLog || mongoose.model("ApiLog", ApiLogSchema);
var ProjectUsage = mongoose.models.ProjectUsage || mongoose.model("ProjectUsage", ProjectUsageSchema);
var DB = null;
var appInstance = null;
var redisClient = null;
var projectConfig = null;
function defaultRoutes() {
return {
signup: "/auth/signup",
login: "/auth/login",
logout: "/auth/logout",
me: "/auth/me",
refresh: "/auth/refresh",
update: "/auth/update",
changePassword: "/auth/change-password",
forgotPassword: "/auth/forgot-password",
resetPassword: "/auth/reset-password",
verifyEmail: "/auth/verify-email"
};
}
function signJwt(payload, secret, options) {
return options ? jwt.sign(payload, secret, options) : jwt.sign(payload, secret);
}
function verifyJwt(token, secret) {
return jwt.verify(token, secret);
}
async function validateProject(apiKey, secret, projectId, validationService) {
if (!validationService) {
const tierConfigs = {
starter: {
id: projectId,
name: "Starter Project",
tier: "starter",
maxRoutes: 3,
maxLibraries: 0,
maxUsers: 100,
maxRequestsPerMonth: 1e4,
features: {
database: false,
advancedAuth: false,
storage: false,
email: false,
redis: false,
rateLimiting: true,
realtime: false,
analytics: false
}
},
basic: {
id: projectId,
name: "Basic Project",
tier: "basic",
maxRoutes: 10,
maxLibraries: 3,
maxUsers: 1e3,
maxRequestsPerMonth: 5e4,
features: {
database: true,
advancedAuth: false,
storage: true,
email: false,
redis: false,
rateLimiting: true,
realtime: false,
analytics: true
}
},
premium: {
id: projectId,
name: "Premium Project",
tier: "premium",
maxRoutes: 50,
maxLibraries: 10,
maxUsers: 1e4,
maxRequestsPerMonth: 25e4,
features: {
database: true,
advancedAuth: true,
storage: true,
email: true,
redis: true,
rateLimiting: true,
realtime: true,
analytics: true
}
},
advanced: {
id: projectId,
name: "Advanced Project",
tier: "advanced",
maxRoutes: Infinity,
maxLibraries: Infinity,
maxUsers: Infinity,
maxRequestsPerMonth: Infinity,
features: {
database: true,
advancedAuth: true,
storage: true,
email: true,
redis: true,
rateLimiting: true,
realtime: true,
analytics: true
}
}
};
return tierConfigs[apiKey] || tierConfigs.starter;
}
try {
const response = await axios.post(
`${validationService.url}${validationService.validateEndpoint}`,
{ projectId },
{
headers: {
"X-API-Key": apiKey,
"X-API-Secret": secret
}
}
);
return response.data;
} catch (error) {
console.error("Project validation failed:", error);
throw new Error("Invalid project credentials or configuration");
}
}
async function reportUsage(projectId, metrics, validationService) {
if (!validationService) return;
try {
await axios.post(
`${validationService.url}${validationService.reportUsageEndpoint}`,
{ projectId, ...metrics },
{ timeout: 5e3 }
);
} catch (error) {
console.error("Failed to report usage metrics:", error);
}
}
async function startServer(options) {
const {
apiKey,
secret,
port,
projectId,
tier,
usage,
validationService,
libraries = [],
routes = [],
credentials,
mongoUri,
baseRoute = "/api",
auth,
session: sessionConfig,
storage,
enableHelmet = true,
enableCors = true,
enableLogging = true,
rateLimit: rateLimitConfig,
email,
redis: redisConfig,
validators = {},
webhooks,
analytics = { enabled: true, ignorePaths: [] }
} = options;
if (!apiKey || !secret || !port || !projectId) {
throw new Error("API key, secret, port and projectId are required");
}
const validKey = credentials?.apiKey || process.env.MY_API_KEY;
const validSecret = credentials?.secret || process.env.MY_API_SECRET;
if (validKey && validSecret) {
if (apiKey !== validKey || secret !== validSecret) {
throw new Error("Invalid starting credentials");
}
}
try {
projectConfig = await validateProject(apiKey, secret, projectId, validationService);
console.log(`\u2705 Project validated: ${projectConfig.name} (${projectConfig.tier} tier)`);
} catch (error) {
throw new Error(`Project validation failed: ${error.message}`);
}
if (routes.length > projectConfig.maxRoutes) {
throw new Error(`Tier ${projectConfig.tier} allows maximum ${projectConfig.maxRoutes} routes (${routes.length} provided)`);
}
if (libraries.length > projectConfig.maxLibraries) {
throw new Error(`Tier ${projectConfig.tier} allows maximum ${projectConfig.maxLibraries} libraries (${libraries.length} provided)`);
}
const app = express();
appInstance = app;
app.use((req, res, next) => {
req.project = projectConfig;
next();
});
if (enableHelmet) app.use(helmet());
if (enableCors) app.use(typeof enableCors === "boolean" ? cors() : cors(enableCors));
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: false, limit: "10mb" }));
if (enableLogging) {
app.use(morgan("dev", {
skip: (req) => (analytics.ignorePaths || []).includes(req.path)
}));
}
if (analytics.enabled) {
app.use(async (req, res, next) => {
const start = Date.now();
const ignorePaths = analytics.ignorePaths || [];
res.on("finish", async () => {
if (!ignorePaths.includes(req.path)) {
try {
const log = new ApiLog({
projectId,
path: req.path,
method: req.method,
statusCode: res.statusCode,
responseTime: Date.now() - start,
userId: req.user?._id,
ipAddress: req.ip,
userAgent: req.get("User-Agent")
});
await log.save();
await ProjectUsage.updateOne(
{ projectId },
{ $inc: { requests: 1 } },
{ upsert: true }
);
if (validationService) {
await reportUsage(projectId, { requests: 1 }, validationService);
}
} catch (error) {
console.error("Failed to log analytics:", error);
}
}
});
next();
});
}
if (projectConfig.features.rateLimiting && rateLimitConfig !== false) {
const baseConfig = {
windowMs: rateLimitConfig?.windowMs ?? 15 * 60 * 1e3,
max: rateLimitConfig?.max ?? 200,
standardHeaders: true,
legacyHeaders: false,
message: rateLimitConfig?.message ?? "Too many requests, please try again later."
};
if (rateLimitConfig?.tierOverrides?.[projectConfig.tier]) {
baseConfig.max = rateLimitConfig.tierOverrides[projectConfig.tier].max;
}
const rl = rateLimit(baseConfig);
app.use(rl);
}
if (projectConfig.features.database) {
const mongoConnectionUri = mongoUri || process.env.MONGO_URI;
if (mongoConnectionUri) {
DB = mongoose;
try {
await mongoose.connect(mongoConnectionUri);
console.log("\u2705 MongoDB connected");
} catch (err) {
console.error("\u274C MongoDB connection failed:", err.message || err);
throw err;
}
} else {
console.warn("\u26A0\uFE0F MongoDB URI not provided, skipping database connection");
}
}
if (projectConfig.features.redis && redisConfig?.enabled) {
try {
redisClient = createClient({
url: redisConfig.url || process.env.REDIS_URL
});
redisClient.on("error", (err) => console.log("Redis Client Error", err));
await redisClient.connect();
console.log("\u2705 Redis connected");
} catch (err) {
console.error("\u274C Redis connection failed:", err.message || err);
}
}
if (sessionConfig?.enabled) {
let sessionStore;
if (sessionConfig.redis && redisClient) {
const redisStoreModule = await import("connect-redis");
const RedisStore = redisStoreModule.default || redisStoreModule;
sessionStore = new RedisStore({ client: redisClient });
}
app.use(session(sessionConfig.options || {
secret,
resave: false,
saveUninitialized: false,
store: sessionStore
}));
console.log("\u2705 Session enabled");
}
let upload = null;
if (projectConfig.features.storage && storage?.enabled) {
const cfg = storage.config || {};
const uploadDir = path.resolve(cfg.uploadDir || "uploads");
const multerStorage = cfg.multerStorage || multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadDir),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `${uuidv4()}${ext}`);
}
});
upload = multer({
storage: multerStorage,
limits: {
fileSize: cfg.maxFileSize || 5 * 1024 * 1024
// 5MB default
},
fileFilter: (req, file, cb) => {
if (cfg.allowedMimeTypes && !cfg.allowedMimeTypes.includes(file.mimetype)) {
return cb(new Error("File type not allowed"));
}
cb(null, true);
}
});
app.use(cfg.staticRoute || "/uploads", express.static(uploadDir));
console.log("\u2705 Storage enabled at", cfg.staticRoute || "/uploads");
}
let transporter = null;
if (projectConfig.features.email && email) {
transporter = nodemailer.createTransport(email.transporterOptions);
const emailConfig = email;
if (!emailConfig.sendVerificationEmail) {
emailConfig.sendVerificationEmail = async (to, token) => {
const url = `${baseRoute}/auth/verify-email?token=${encodeURIComponent(token)}`;
await transporter.sendMail({
from: emailConfig.from,
to,
subject: "Verify your email",
text: `Verify here: ${url}`,
html: `<p>Verify here: <a href="${url}">${url}</a></p>`
});
};
}
if (!emailConfig.sendResetEmail) {
emailConfig.sendResetEmail = async (to, token) => {
const url = `${baseRoute}/auth/reset-password?token=${encodeURIComponent(token)}`;
await transporter.sendMail({
from: emailConfig.from,
to,
subject: "Reset your password",
text: `Reset here: ${url}`,
html: `<p>Reset here: <a href="${url}">${url}</a></p>`
});
};
}
console.log("\u2705 Email configured");
}
if (libraries.length > 0) {
libraries.forEach((lib) => app.use(lib));
console.log(`\u2705 ${libraries.length} custom libraries applied`);
}
if (projectConfig.features.database && auth?.enabled) {
if (!auth.model) throw new Error("When auth.enabled=true you must pass auth.model (Mongoose Model<T>)");
if (!auth.jwt) throw new Error("When auth.enabled=true you must pass auth.jwt config (JWT Config)");
const userModel = auth.model;
const jwtConf = auth.jwt;
const routesPaths = { ...defaultRoutes(), ...auth.routes || {} };
const generateAccessToken = (payload) => signJwt(payload, jwtConf.accessTokenSecret, { expiresIn: jwtConf.accessTokenExpiresIn || "1h" });
const generateRefreshToken = (payload) => {
if (!jwtConf.refreshTokenSecret) throw new Error("refreshTokenSecret required for refresh tokens");
return signJwt(payload, jwtConf.refreshTokenSecret, { expiresIn: jwtConf.refreshTokenExpiresIn || "7d" });
};
const requireAuth = (roles) => {
return async (req, res, next) => {
try {
const header = req.headers.authorization;
if (!header || !header.startsWith("Bearer ")) return res.status(401).json({ message: "No token" });
const token = header.split(" ")[1];
const decoded = verifyJwt(token, jwtConf.accessTokenSecret);
req.user = decoded;
if (roles && roles.length > 0) {
const roleField = auth.roleField || "role";
const user = await userModel.findById(decoded.id).lean();
if (!user) return res.status(403).json({ message: "Forbidden" });
const userObj = user;
if (!roles.includes(userObj[roleField])) return res.status(403).json({ message: "Forbidden" });
}
next();
} catch (err) {
return res.status(401).json({ message: "Invalid token" });
}
};
};
const router = express.Router();
if (projectConfig.features.advancedAuth && auth.advanced) {
router.post(routesPaths.signup, async (req, res) => {
try {
const userCount = await userModel.countDocuments();
if (projectConfig?.maxUsers && userCount >= projectConfig.maxUsers) {
return res.status(429).json({ message: "User limit reached" });
}
const v = validators.signup?.(req.body);
if (v && !v.ok) return res.status(400).json({ message: v.message });
const { email: email2, password, ...rest } = req.body;
if (!email2 || !password) return res.status(400).json({ message: "Email and password required" });
const exists = await userModel.findOne({ email: email2 });
if (exists) return res.status(409).json({ message: "Email already exists" });
const hashed = await bcrypt.hash(password, 12);
const doc = new userModel({ email: email2, password: hashed, ...rest });
await doc.save();
await ProjectUsage.updateOne(
{ projectId },
{ $inc: { users: 1 } },
{ upsert: true }
);
if (webhooks?.onUserSignup) {
try {
await webhooks.onUserSignup(doc.toObject());
} catch (error) {
console.error("Webhook error:", error);
}
}
if (options.auth?.requireEmailVerification && email2) {
const token = signJwt({ id: doc._id }, jwtConf.accessTokenSecret, { expiresIn: "1d" });
if (email2 && options.email) await options.email.sendVerificationEmail(email2, token);
return res.status(201).json({ message: "User created. Verify your email." });
}
return res.status(201).json({ message: "User created", user: { id: doc._id, email: email2 } });
} catch (err) {
if (webhooks?.onError) {
try {
await webhooks.onError(err, "signup");
} catch (error) {
console.error("Error webhook failed:", error);
}
}
return res.status(500).json({ message: err.message || "Server error" });
}
});
}
router.post(routesPaths.login, async (req, res) => {
try {
const v = validators.login?.(req.body);
if (v && !v.ok) return res.status(400).json({ message: v.message });
const { email: email2, password } = req.body;
if (!email2 || !password) return res.status(400).json({ message: "Email and password required" });
const user = await userModel.findOne({ email: email2 });
if (!user) return res.status(401).json({ message: "Invalid credentials" });
const userObj = user;
const match = await bcrypt.compare(password, userObj.password);
if (!match) return res.status(401).json({ message: "Invalid credentials" });
const payload = { id: userObj._id, email: email2 };
const accessToken = generateAccessToken(payload);
const result = { accessToken };
if (jwtConf.refreshTokenSecret) {
const refreshToken = generateRefreshToken(payload);
result.refreshToken = refreshToken;
if (redisClient) {
await redisClient.set(`refresh:${userObj._id}`, refreshToken, {
EX: 7 * 24 * 60 * 60
// 7 days
});
}
}
if (webhooks?.onUserLogin) {
try {
await webhooks.onUserLogin(user.toObject());
} catch (error) {
console.error("Webhook error:", error);
}
}
return res.json(result);
} catch (err) {
if (webhooks?.onError) {
try {
await webhooks.onError(err, "login");
} catch (error) {
console.error("Error webhook failed:", error);
}
}
return res.status(500).json({ message: err.message || "Server error" });
}
});
if (jwtConf.refreshTokenSecret) {
router.post(routesPaths.refresh, async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) return res.status(400).json({ message: "Missing refresh token" });
const decoded = verifyJwt(refreshToken, jwtConf.refreshTokenSecret);
if (redisClient) {
const storedToken = await redisClient.get(`refresh:${decoded.id}`);
if (storedToken !== refreshToken) {
return res.status(401).json({ message: "Invalid refresh token" });
}
}
const payload = { id: decoded.id, email: decoded.email };
const accessToken = generateAccessToken(payload);
return res.json({ accessToken });
} catch (err) {
return res.status(401).json({ message: "Invalid refresh token" });
}
});
}
router.get(routesPaths.me, requireAuth(), async (req, res) => {
const authPayload = req.user;
const user = await userModel.findById(authPayload.id).select("-password");
if (!user) return res.status(404).json({ message: "User not found" });
return res.json(user);
});
router.put(routesPaths.update, requireAuth(), async (req, res) => {
const authPayload = req.user;
const updates = req.body;
delete updates.password;
const updated = await userModel.findByIdAndUpdate(authPayload.id, updates, { new: true }).select("-password");
return res.json(updated);
});
router.put(routesPaths.changePassword, requireAuth(), async (req, res) => {
try {
const authPayload = req.user;
const { oldPassword, newPassword } = req.body;
if (!oldPassword || !newPassword) return res.status(400).json({ message: "Missing fields" });
const user = await userModel.findById(authPayload.id);
if (!user) return res.status(404).json({ message: "User not found" });
const userObj = user;
const match = await bcrypt.compare(oldPassword, userObj.password);
if (!match) return res.status(401).json({ message: "Old password incorrect" });
userObj.password = await bcrypt.hash(newPassword, 12);
await userObj.save();
return res.json({ message: "Password changed" });
} catch (err) {
return res.status(500).json({ message: err.message || "Server error" });
}
});
if (projectConfig.features.advancedAuth) {
router.post(routesPaths.forgotPassword, async (req, res) => {
try {
const { email: reqEmail } = req.body;
if (!reqEmail) return res.status(400).json({ message: "Email required" });
const user = await userModel.findOne({ email: reqEmail });
if (!user) return res.status(404).json({ message: "User not found" });
const token = jwt.sign({ id: user._id }, jwtConf.accessTokenSecret, { expiresIn: "15m" });
const expiresAt = new Date(Date.now() + 15 * 60 * 1e3);
const rt = await ResetToken.create({ userId: user._id, token, expiresAt });
if (email && email.sendResetEmail) {
await email.sendResetEmail(reqEmail, token);
return res.json({ message: "Reset email sent" });
} else {
return res.json({ message: "Reset token generated (dev)", token });
}
} catch (err) {
return res.status(500).json({ message: err.message || "Server error" });
}
});
router.post(routesPaths.resetPassword, async (req, res) => {
try {
const { token, newPassword } = req.body;
const v = validators.resetPassword?.(req.body);
if (v && !v.ok) return res.status(400).json({ message: v.message });
if (!token || !newPassword) return res.status(400).json({ message: "Missing fields" });
const rt = await ResetToken.findOne({ token, used: false });
if (!rt) return res.status(400).json({ message: "Invalid or used token" });
if (rt.expiresAt < /* @__PURE__ */ new Date()) return res.status(400).json({ message: "Token expired" });
try {
verifyJwt(token, jwtConf.accessTokenSecret);
} catch (err) {
return res.status(400).json({ message: "Invalid token payload" });
}
const user = await userModel.findById(rt.userId);
if (!user) return res.status(404).json({ message: "User not found" });
const userObj = user;
userObj.password = await bcrypt.hash(newPassword, 12);
await userObj.save();
rt.used = true;
await rt.save();
return res.json({ message: "Password reset successful" });
} catch (err) {
return res.status(500).json({ message: err.message || "Server error" });
}
});
router.get(routesPaths.verifyEmail, async (req, res) => {
try {
const token = req.query.token;
if (!token) return res.status(400).send("Missing token");
const decoded = verifyJwt(token, jwtConf.accessTokenSecret);
const user = await userModel.findById(decoded.id);
if (!user) return res.status(404).send("User not found");
const userObj = user;
if (userObj.emailVerified !== void 0) {
userObj.emailVerified = true;
await userObj.save();
}
return res.send("Email verified");
} catch (err) {
return res.status(400).send("Invalid or expired token");
}
});
}
app.use(baseRoute, router);
console.log(`\u{1F510} Auth mounted at ${baseRoute}`);
}
if (routes && routes.length) {
for (const r of routes) {
const handlerArray = Array.isArray(r.handlers) ? r.handlers : [r.handlers];
const p = r.path.startsWith("/") ? r.path : `/${r.path}`;
app[r.method.toLowerCase()](baseRoute + p, ...handlerArray);
}
console.log(`\u2699\uFE0F ${routes.length} custom routes mounted under ${baseRoute}`);
}
app.get(`${baseRoute}/health`, (req, res) => {
res.json({
status: "OK",
project: projectConfig?.name,
tier: projectConfig?.tier,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
});
});
app.get(`${baseRoute}/usage`, async (req, res) => {
try {
const usageStats = await ProjectUsage.findOne({ projectId });
res.json(usageStats || { requests: 0, users: 0, storage: 0 });
} catch (error) {
res.status(500).json({ message: "Failed to fetch usage stats" });
}
});
app.use((err, req, res, next) => {
console.error("Unhandled error:", err);
if (webhooks?.onError) {
webhooks.onError(err, "global").catch((e) => console.error("Error webhook failed:", e));
}
res.status(err.status || 500).json({ message: err.message || "Internal server error" });
});
const server = app.listen(port, () => {
console.log(`\u{1F680} Server running at http://localhost:${port}${baseRoute}`);
console.log(`\u{1F4CA} Tier: ${projectConfig?.tier}`);
console.log(`\u{1F4C8} Limits: ${projectConfig?.maxRoutes} routes, ${projectConfig?.maxUsers} users`);
});
const shutdown = async () => {
console.log("Shutting down...");
server.close();
if (DB) await mongoose.disconnect();
if (redisClient) await redisClient.quit();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
DB = mongoose;
return app;
}
async function stopServer() {
try {
if (DB) {
await mongoose.disconnect();
DB = null;
}
if (redisClient) {
await redisClient.quit();
redisClient = null;
}
} catch (err) {
}
}
export {
DB,
appInstance,
projectConfig,
redisClient,
startServer,
stopServer
};