Files
timetracker/backend/src/index.ts
simon.franken 64211e6a49 feat: add MCP endpoint and API key management
- Add ApiKey Prisma model (SHA-256 hash, prefix, lastUsedAt) with migration
- Implement ApiKeyService (create, list, delete, verify)
- Extend requireAuth middleware to accept sk_-prefixed API keys alongside JWTs
- Add GET/POST /api-keys routes for creating and revoking keys
- Add stateless Streamable HTTP MCP server at POST/GET /mcp exposing all 20
  time-tracking tools (clients, projects, time entries, timer, statistics,
  client targets and corrections)
- Frontend: ApiKey types, apiKeys API module, useApiKeys hook
- Frontend: ApiKeysPage with key table, one-time raw-key reveal modal, and
  inline revoke confirmation
- Wire /api-keys route and add API Keys link to Management dropdown in Navbar
2026-03-16 15:26:09 +01:00

93 lines
2.4 KiB
TypeScript

import express from "express";
import cors from "cors";
import session from "express-session";
import { PrismaSessionStore } from "@quixo3/prisma-session-store";
import { config, validateConfig } from "./config";
import { connectDatabase, prisma } from "./prisma/client";
import { errorHandler, notFoundHandler } from "./middleware/errorHandler";
// Import routes
import authRoutes from "./routes/auth.routes";
import clientRoutes from "./routes/client.routes";
import projectRoutes from "./routes/project.routes";
import timeEntryRoutes from "./routes/timeEntry.routes";
import timerRoutes from "./routes/timer.routes";
import clientTargetRoutes from "./routes/clientTarget.routes";
import apiKeyRoutes from "./routes/apiKey.routes";
import mcpRoutes from "./routes/mcp.routes";
async function main() {
// Validate configuration
validateConfig();
// Connect to database
await connectDatabase();
const app = express();
app.set("trust proxy", 1);
// CORS
app.use(
cors({
origin: config.cors.origin,
credentials: true,
}),
);
// Body parsing
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Session
app.use(
session({
secret: config.session.secret,
resave: false,
saveUninitialized: false,
name: "sessionId",
store: new PrismaSessionStore(prisma, {
checkPeriod: 2 * 60 * 1000, // ms
dbRecordIdIsSessionId: true,
dbRecordIdFunction: undefined,
}),
cookie: {
secure: config.nodeEnv === "production",
httpOnly: true,
maxAge: config.session.maxAge,
sameSite: "lax",
},
}),
);
// Health check
app.get("/health", (_req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() });
});
// Routes
app.use("/auth", authRoutes);
app.use("/clients", clientRoutes);
app.use("/projects", projectRoutes);
app.use("/time-entries", timeEntryRoutes);
app.use("/timer", timerRoutes);
app.use("/client-targets", clientTargetRoutes);
app.use("/api-keys", apiKeyRoutes);
app.use("/mcp", mcpRoutes);
// Error handling
app.use(notFoundHandler);
app.use(errorHandler);
// Start server
app.listen(config.port, () => {
console.log(`Server running on port ${config.port}`);
console.log(`Environment: ${config.nodeEnv}`);
});
}
main().catch((error) => {
console.error("Failed to start server:", error);
process.exit(1);
});