117 lines
4.2 KiB
JavaScript
117 lines
4.2 KiB
JavaScript
import { Hono } from "hono";
|
|
import { serve } from '@hono/node-server';
|
|
import { serveStatic } from '@hono/node-server/serve-static';
|
|
import { cors } from 'hono/cors';
|
|
import { rpcApp } from "./routes/rpc.js";
|
|
import { caldavApp } from "./routes/caldav.js";
|
|
import { clientEntry } from "./routes/client-entry.js";
|
|
const app = new Hono();
|
|
// CORS Configuration
|
|
const isDev = process.env.NODE_ENV === 'development';
|
|
const domain = process.env.DOMAIN || 'localhost:5173';
|
|
// Build allowed origins list
|
|
const allowedOrigins = [
|
|
`https://${domain}`,
|
|
isDev ? `http://${domain}` : null,
|
|
isDev ? 'http://localhost:5173' : null,
|
|
isDev ? 'http://localhost:3000' : null,
|
|
].filter((origin) => origin !== null);
|
|
app.use('*', cors({
|
|
origin: (origin) => {
|
|
// Allow requests with no origin (e.g., mobile apps, curl, Postman)
|
|
if (!origin)
|
|
return null;
|
|
// Check if origin is in whitelist
|
|
if (allowedOrigins.includes(origin)) {
|
|
return origin;
|
|
}
|
|
// Reject all other origins
|
|
return null;
|
|
},
|
|
credentials: true, // Enable cookies for authentication
|
|
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
allowHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
|
|
exposeHeaders: ['Set-Cookie'],
|
|
maxAge: 86400, // Cache preflight requests for 24 hours
|
|
}));
|
|
// Content-Security-Policy and other security headers
|
|
app.use("*", async (c, next) => {
|
|
const isDev = process.env.NODE_ENV === 'development';
|
|
const directives = [
|
|
"default-src 'self'",
|
|
`script-src 'self'${isDev ? " 'unsafe-inline'" : ''}`,
|
|
"style-src 'self' 'unsafe-inline'",
|
|
"img-src 'self' data: https:",
|
|
"font-src 'self' data:",
|
|
"connect-src 'self'",
|
|
"frame-ancestors 'none'",
|
|
"base-uri 'self'",
|
|
"form-action 'self'",
|
|
];
|
|
const csp = directives.join('; ');
|
|
c.header('Content-Security-Policy', csp);
|
|
c.header('X-Content-Type-Options', 'nosniff');
|
|
c.header('X-Frame-Options', 'DENY');
|
|
c.header('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
await next();
|
|
});
|
|
// Health check endpoint
|
|
app.get("/health", (c) => {
|
|
return c.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
});
|
|
// Legal config endpoint (temporary fix for RPC issue)
|
|
app.get("/api/legal-config", async (c) => {
|
|
try {
|
|
const { getLegalConfig } = await import("./lib/legal-config.js");
|
|
const config = getLegalConfig();
|
|
return c.json(config);
|
|
}
|
|
catch (error) {
|
|
console.error("Legal config error:", error);
|
|
return c.json({ error: "Failed to load legal config" }, 500);
|
|
}
|
|
});
|
|
// Security.txt endpoint (RFC 9116)
|
|
app.get("/.well-known/security.txt", (c) => {
|
|
const securityContact = process.env.SECURITY_CONTACT || "security@example.com";
|
|
const securityText = `Contact: ${securityContact}
|
|
Expires: 2025-12-31T23:59:59.000Z
|
|
Preferred-Languages: de, en
|
|
Canonical: https://${process.env.DOMAIN || 'localhost:5173'}/.well-known/security.txt
|
|
|
|
# Security Policy
|
|
# Please report security vulnerabilities responsibly by contacting us via email.
|
|
# We will respond to security reports within 48 hours.
|
|
#
|
|
# Scope: This security policy applies to the Stargirlnails booking system.
|
|
#
|
|
# Rewards: We appreciate security researchers who help us improve our security.
|
|
# While we don't have a formal bug bounty program, we may offer recognition
|
|
# for significant security improvements.
|
|
`;
|
|
return c.text(securityText, 200, {
|
|
"Content-Type": "text/plain; charset=utf-8",
|
|
"Cache-Control": "public, max-age=86400", // Cache for 24 hours
|
|
});
|
|
});
|
|
// Serve static files (only in production)
|
|
if (process.env.NODE_ENV === 'production') {
|
|
app.use('/static/*', serveStatic({ root: './dist' }));
|
|
app.use('/assets/*', serveStatic({ root: './dist' }));
|
|
}
|
|
app.use('/favicon.png', serveStatic({ path: './public/favicon.png' }));
|
|
app.route("/rpc", rpcApp);
|
|
app.route("/caldav", caldavApp);
|
|
app.get("/*", clientEntry);
|
|
// Start server
|
|
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
|
const host = process.env.HOST || "0.0.0.0";
|
|
console.log(`🚀 Server starting on ${host}:${port}`);
|
|
// Start the server
|
|
serve({
|
|
fetch: app.fetch,
|
|
port,
|
|
hostname: host,
|
|
});
|
|
export default app;
|