formies/server.js
Mohamad.Elsena 2927013a6d Update environment configuration, add API documentation, and implement user authentication system
- Updated `.env` and added `.env.test` for environment variables.
- Introduced API documentation in `API_DOCUMENTATION.md`.
- Added authentication setup guide in `AUTHENTICATION_SETUP.md`.
- Implemented user authentication with JWT and email verification.
- Created new routes for user management and form submissions.
- Added middleware for API key authentication and error handling.
- Set up Redis for rate limiting and notifications.
- Removed obsolete files and configurations related to the previous Rust implementation.
2025-05-28 11:18:35 +02:00

196 lines
5.0 KiB
JavaScript

require("dotenv").config();
const express = require("express");
const path = require("path");
const fs = require("fs"); // Added for fs operations
const db = require("./src/config/database"); // SQLite db instance
const helmet = require("helmet");
const session = require("express-session");
const passport = require("./src/config/passport");
const logger = require("./config/logger");
const errorHandler = require("./middleware/errorHandler");
const { connectRedis, closeRedis } = require("./src/config/redis");
// Import routes
const publicRoutes = require("./src/routes/public");
const authRoutes = require("./src/routes/auth");
const dashboardRoutes = require("./src/routes/dashboard");
const apiV1Routes = require("./src/routes/api_v1");
const app = express();
const PORT = process.env.PORT || 3000;
// Function to initialize the database
async function initializeDatabase() {
const dbPath = path.resolve(__dirname, "formies.sqlite");
const dbExists = fs.existsSync(dbPath);
if (!dbExists) {
logger.info("Database file not found, creating and initializing...");
try {
// The 'db' instance from './src/config/database' should already create the file.
// Now, run the init.sql script.
const initSql = fs.readFileSync(
path.resolve(__dirname, "init.sql"),
"utf8"
);
// SQLite driver's `exec` method can run multiple statements
await new Promise((resolve, reject) => {
db.exec(initSql, (err) => {
if (err) {
logger.error("Failed to initialize database:", err);
return reject(err);
}
logger.info("Database initialized successfully.");
resolve();
});
});
} catch (error) {
logger.error("Error during database initialization:", error);
process.exit(1); // Exit if DB initialization fails
}
} else {
logger.info("Database file found.");
}
}
// Initialize Redis connection and Database
async function initializeApp() {
// Initialize Redis first, but don't block on failure
connectRedis().catch(() => {
logger.warn(
"Redis connection failed, continuing with in-memory rate limiting"
);
});
try {
await initializeDatabase(); // Initialize SQLite database
} catch (error) {
logger.error("Failed to initialize database:", error);
process.exit(1); // Exit if DB initialization fails
}
// Middleware
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
})
);
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// Session configuration (for development only, use Redis in production)
app.use(
session({
secret:
process.env.SESSION_SECRET || "fallback-secret-change-in-production",
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
})
);
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
// Set view engine
app.set("view engine", "ejs");
// API Routes
app.use("/api/auth", authRoutes);
// API V1 Routes
app.use("/api/v1", apiV1Routes);
// User Dashboard Routes
app.use("/dashboard", dashboardRoutes);
// Existing routes (maintaining backward compatibility)
app.use("/", publicRoutes);
// Health check endpoint
app.get("/health", (req, res) => {
res.json({
status: "healthy",
timestamp: new Date().toISOString(),
version: "1.0.0",
});
});
// Global error handler - should be the last middleware
app.use(errorHandler);
// 404 handler
app.use((req, res) => {
logger.warn(
`404 - Endpoint not found: ${req.originalUrl} - Method: ${req.method} - IP: ${req.ip}`
);
res.status(404).json({
error: {
message: "Endpoint not found",
code: "NOT_FOUND",
},
});
});
// Start server
app.listen(PORT, () => {
logger.info(`Server running on http://localhost:${PORT}`);
// Environment checks
if (!process.env.JWT_SECRET) {
logger.warn(
"WARNING: JWT_SECRET not set. Authentication will not work properly."
);
}
if (process.env.NTFY_ENABLED === "true" && process.env.NTFY_TOPIC_URL) {
logger.info(
`Ntfy notifications enabled for topic: ${process.env.NTFY_TOPIC_URL}`
);
} else {
logger.info("Ntfy notifications disabled or topic not configured.");
}
// Start cleanup of expired sessions every hour
setInterval(
() => {
const jwtService = require("./src/services/jwtService");
jwtService.cleanupExpiredSessions();
},
60 * 60 * 1000
);
});
// Graceful shutdown
process.on("SIGINT", async () => {
logger.info("Received SIGINT, shutting down gracefully...");
await closeRedis();
process.exit(0);
});
process.on("SIGTERM", async () => {
logger.info("Received SIGTERM, shutting down gracefully...");
await closeRedis();
process.exit(0);
});
}
// Initialize the application
initializeApp().catch((error) => {
logger.error("Failed to initialize application:", error);
process.exit(1);
});