- Add inbox router with CRUD endpoints - Add inbox stats endpoint - Add team inbox overview for managers - Support task assignment to team members - Add database migration for inbox_items table
286 lines
10 KiB
TypeScript
286 lines
10 KiB
TypeScript
import { Application } from "@oak/oak";
|
|
import "@std/dotenv/load";
|
|
|
|
// Routes
|
|
import { authRouter } from "./routes/auth.ts";
|
|
import { usersRouter } from "./routes/users.ts";
|
|
import inboxRouter from "./routes/inbox.ts";
|
|
import { contactsRouter } from "./routes/contacts.ts";
|
|
import { companiesRouter } from "./routes/companies.ts";
|
|
import { dealsRouter } from "./routes/deals.ts";
|
|
import { activitiesRouter } from "./routes/activities.ts";
|
|
import { pipelinesRouter } from "./routes/pipelines.ts";
|
|
|
|
// Database
|
|
import { checkHealth as checkDbHealth } from "./db/connection.ts";
|
|
|
|
const app = new Application();
|
|
const PORT = parseInt(Deno.env.get("PORT") || "8000");
|
|
const NODE_ENV = Deno.env.get("NODE_ENV") || "development";
|
|
|
|
// ============================================
|
|
// MIDDLEWARE
|
|
// ============================================
|
|
|
|
// CORS Middleware
|
|
app.use(async (ctx, next) => {
|
|
const allowedOrigins = Deno.env.get("CORS_ORIGINS")?.split(",") || ["*"];
|
|
const origin = ctx.request.headers.get("origin");
|
|
|
|
if (origin && (allowedOrigins.includes("*") || allowedOrigins.includes(origin))) {
|
|
ctx.response.headers.set("Access-Control-Allow-Origin", origin);
|
|
} else if (allowedOrigins.includes("*")) {
|
|
ctx.response.headers.set("Access-Control-Allow-Origin", "*");
|
|
}
|
|
|
|
ctx.response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH");
|
|
ctx.response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
ctx.response.headers.set("Access-Control-Allow-Credentials", "true");
|
|
ctx.response.headers.set("Access-Control-Max-Age", "86400");
|
|
|
|
if (ctx.request.method === "OPTIONS") {
|
|
ctx.response.status = 204;
|
|
return;
|
|
}
|
|
|
|
await next();
|
|
});
|
|
|
|
// Security Headers
|
|
app.use(async (ctx, next) => {
|
|
ctx.response.headers.set("X-Content-Type-Options", "nosniff");
|
|
ctx.response.headers.set("X-Frame-Options", "DENY");
|
|
ctx.response.headers.set("X-XSS-Protection", "1; mode=block");
|
|
ctx.response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
await next();
|
|
});
|
|
|
|
// Request Logger
|
|
app.use(async (ctx, next) => {
|
|
const start = Date.now();
|
|
await next();
|
|
const ms = Date.now() - start;
|
|
const status = ctx.response.status;
|
|
const statusColor = status >= 500 ? "\x1b[31m" : status >= 400 ? "\x1b[33m" : "\x1b[32m";
|
|
console.log(`${statusColor}${status}\x1b[0m ${ctx.request.method} ${ctx.request.url.pathname} - ${ms}ms`);
|
|
});
|
|
|
|
// Error Handler
|
|
app.use(async (ctx, next) => {
|
|
try {
|
|
await next();
|
|
} catch (err) {
|
|
console.error("Error:", err);
|
|
|
|
const status = err.status || 500;
|
|
ctx.response.status = status;
|
|
ctx.response.body = {
|
|
success: false,
|
|
error: {
|
|
code: status === 500 ? "INTERNAL_ERROR" : "ERROR",
|
|
message: NODE_ENV === "development"
|
|
? err.message
|
|
: status === 500
|
|
? "An internal error occurred"
|
|
: err.message,
|
|
},
|
|
};
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// SYSTEM ROUTES
|
|
// ============================================
|
|
|
|
// Health Check (includes DB status)
|
|
app.use(async (ctx, next) => {
|
|
if (ctx.request.url.pathname === "/health") {
|
|
const dbHealthy = await checkDbHealth();
|
|
|
|
ctx.response.status = dbHealthy ? 200 : 503;
|
|
ctx.response.body = {
|
|
status: dbHealthy ? "ok" : "degraded",
|
|
service: "pulse-crm-backend",
|
|
version: "0.1.0",
|
|
timestamp: new Date().toISOString(),
|
|
checks: {
|
|
database: dbHealthy ? "ok" : "error",
|
|
},
|
|
};
|
|
return;
|
|
}
|
|
await next();
|
|
});
|
|
|
|
// Liveness probe (simple check)
|
|
app.use(async (ctx, next) => {
|
|
if (ctx.request.url.pathname === "/live") {
|
|
ctx.response.body = { status: "ok" };
|
|
return;
|
|
}
|
|
await next();
|
|
});
|
|
|
|
// API Info
|
|
app.use(async (ctx, next) => {
|
|
if (ctx.request.url.pathname === "/api" || ctx.request.url.pathname === "/api/v1") {
|
|
ctx.response.body = {
|
|
name: "Pulse CRM API",
|
|
version: "1.0.0",
|
|
description: "Der Herzschlag deines Business",
|
|
documentation: "/api/v1/docs",
|
|
endpoints: {
|
|
auth: {
|
|
"POST /api/v1/auth/register": "Register new user & organization",
|
|
"POST /api/v1/auth/login": "Login",
|
|
"POST /api/v1/auth/refresh": "Refresh access token",
|
|
"POST /api/v1/auth/logout": "Logout (revoke token)",
|
|
"POST /api/v1/auth/logout-all": "Logout from all devices",
|
|
"POST /api/v1/auth/forgot-password": "Request password reset",
|
|
"POST /api/v1/auth/reset-password": "Reset password with token",
|
|
"POST /api/v1/auth/verify-email": "Verify email address",
|
|
"GET /api/v1/auth/me": "Get current user",
|
|
},
|
|
users: {
|
|
"GET /api/v1/users": "List organization users (admin/owner)",
|
|
"GET /api/v1/users/:id": "Get user details",
|
|
"POST /api/v1/users": "Create/invite new user (admin/owner)",
|
|
"PUT /api/v1/users/:id": "Update user (admin/owner)",
|
|
"DELETE /api/v1/users/:id": "Delete user (admin/owner)",
|
|
"POST /api/v1/users/:id/reset-password": "Reset user password (admin/owner)",
|
|
},
|
|
inbox: {
|
|
"GET /api/v1/inbox": "List inbox items (tasks, appointments, emails)",
|
|
"GET /api/v1/inbox/stats": "Inbox statistics",
|
|
"GET /api/v1/inbox/team": "Team inbox overview (manager+)",
|
|
"POST /api/v1/inbox": "Create inbox item / assign task",
|
|
"PUT /api/v1/inbox/:id": "Update inbox item",
|
|
"PUT /api/v1/inbox/:id/status": "Quick status update",
|
|
"DELETE /api/v1/inbox/:id": "Delete inbox item",
|
|
},
|
|
contacts: {
|
|
"GET /api/v1/contacts": "List contacts",
|
|
"GET /api/v1/contacts/stats": "Contact statistics",
|
|
"GET /api/v1/contacts/export": "Export contacts (DSGVO)",
|
|
"GET /api/v1/contacts/:id": "Get contact",
|
|
"POST /api/v1/contacts": "Create contact",
|
|
"POST /api/v1/contacts/import": "Bulk import contacts",
|
|
"PUT /api/v1/contacts/:id": "Update contact",
|
|
"DELETE /api/v1/contacts/:id": "Soft delete contact",
|
|
"DELETE /api/v1/contacts/:id/permanent": "Permanent delete (DSGVO)",
|
|
},
|
|
companies: {
|
|
"GET /api/v1/companies": "List companies",
|
|
"GET /api/v1/companies/industries": "Get industries",
|
|
"GET /api/v1/companies/:id": "Get company",
|
|
"GET /api/v1/companies/:id/contacts": "Get company contacts",
|
|
"POST /api/v1/companies": "Create company",
|
|
"PUT /api/v1/companies/:id": "Update company",
|
|
"DELETE /api/v1/companies/:id": "Delete company",
|
|
},
|
|
deals: {
|
|
"GET /api/v1/deals": "List deals with filters",
|
|
"GET /api/v1/deals/stats": "Deal statistics",
|
|
"GET /api/v1/deals/forecast": "Sales forecast",
|
|
"GET /api/v1/deals/pipeline/:pipelineId": "Kanban board view",
|
|
"GET /api/v1/deals/:id": "Get deal",
|
|
"POST /api/v1/deals": "Create deal",
|
|
"PUT /api/v1/deals/:id": "Update deal",
|
|
"POST /api/v1/deals/:id/move": "Move to stage",
|
|
"POST /api/v1/deals/:id/won": "Mark as won",
|
|
"POST /api/v1/deals/:id/lost": "Mark as lost",
|
|
"POST /api/v1/deals/:id/reopen": "Reopen closed deal",
|
|
"DELETE /api/v1/deals/:id": "Delete deal",
|
|
},
|
|
pipelines: {
|
|
"GET /api/v1/pipelines": "List pipelines",
|
|
"GET /api/v1/pipelines/default": "Get/create default pipeline",
|
|
"GET /api/v1/pipelines/:id": "Get pipeline",
|
|
"POST /api/v1/pipelines": "Create pipeline",
|
|
"PUT /api/v1/pipelines/:id": "Update pipeline",
|
|
"PUT /api/v1/pipelines/:id/stages": "Update stages",
|
|
"DELETE /api/v1/pipelines/:id": "Delete pipeline",
|
|
},
|
|
activities: {
|
|
"GET /api/v1/activities": "List activities",
|
|
"GET /api/v1/activities/stats": "Activity statistics",
|
|
"GET /api/v1/activities/upcoming": "Upcoming tasks",
|
|
"GET /api/v1/activities/overdue": "Overdue tasks",
|
|
"GET /api/v1/activities/timeline/:type/:id": "Entity timeline",
|
|
"GET /api/v1/activities/:id": "Get activity",
|
|
"POST /api/v1/activities": "Create activity",
|
|
"PUT /api/v1/activities/:id": "Update activity",
|
|
"POST /api/v1/activities/:id/complete": "Mark completed",
|
|
"POST /api/v1/activities/:id/reopen": "Reopen activity",
|
|
"DELETE /api/v1/activities/:id": "Delete activity",
|
|
},
|
|
pipelines: {
|
|
"GET /api/v1/pipelines": "List pipelines",
|
|
"POST /api/v1/pipelines": "Create pipeline",
|
|
"PUT /api/v1/pipelines/:id/stages": "Update stages",
|
|
},
|
|
},
|
|
};
|
|
return;
|
|
}
|
|
await next();
|
|
});
|
|
|
|
// ============================================
|
|
// API ROUTES
|
|
// ============================================
|
|
|
|
app.use(authRouter.routes());
|
|
app.use(authRouter.allowedMethods());
|
|
|
|
app.use(usersRouter.routes());
|
|
app.use(usersRouter.allowedMethods());
|
|
|
|
app.use(inboxRouter.routes());
|
|
app.use(inboxRouter.allowedMethods());
|
|
|
|
app.use(contactsRouter.routes());
|
|
app.use(contactsRouter.allowedMethods());
|
|
|
|
app.use(companiesRouter.routes());
|
|
app.use(companiesRouter.allowedMethods());
|
|
|
|
app.use(dealsRouter.routes());
|
|
app.use(dealsRouter.allowedMethods());
|
|
|
|
app.use(activitiesRouter.routes());
|
|
app.use(activitiesRouter.allowedMethods());
|
|
|
|
app.use(pipelinesRouter.routes());
|
|
app.use(pipelinesRouter.allowedMethods());
|
|
|
|
// ============================================
|
|
// 404 HANDLER
|
|
// ============================================
|
|
|
|
app.use((ctx) => {
|
|
ctx.response.status = 404;
|
|
ctx.response.body = {
|
|
success: false,
|
|
error: {
|
|
code: "NOT_FOUND",
|
|
message: `Endpoint ${ctx.request.method} ${ctx.request.url.pathname} not found`,
|
|
},
|
|
};
|
|
});
|
|
|
|
// ============================================
|
|
// START SERVER
|
|
// ============================================
|
|
|
|
console.log("");
|
|
console.log(" 🫀 Pulse CRM Backend");
|
|
console.log(" ====================");
|
|
console.log(` 📡 Server: http://localhost:${PORT}`);
|
|
console.log(` 📚 API: http://localhost:${PORT}/api/v1`);
|
|
console.log(` ❤️ Health: http://localhost:${PORT}/health`);
|
|
console.log(` 🔧 Mode: ${NODE_ENV}`);
|
|
console.log("");
|
|
|
|
await app.listen({ port: PORT });
|