Files
openStack/backend/src/index.js
T
x3 b852583499 feat(backend): books, tasks, calendar feed, time tracking, profiles, admin API
- Schema: books, tasks (book/page links, status, priority, due_at),
  time_entries (open entry = running timer), user profile columns
  (avatar, locale, theme, bg_image, bg_blur)
- Books CRUD; pages can live in a book (book_id, validated same-workspace)
- Tasks CRUD with status filter and due_at range query for the calendar
- Time API: start (409 if running), stop (computes duration), list with
  open timer, update, delete
- Users: fuzzy discovery for invites, PATCH /me profile/appearance,
  password change; members list now includes avatars
- Admin API (is_admin guard): overview counts, user list, grant/revoke
  admin, delete user (self-demote/delete blocked)
2026-09-22 13:56:25 +02:00

76 lines
2.4 KiB
JavaScript

import 'dotenv/config';
import http from 'node:http';
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import rateLimit from 'express-rate-limit';
import { pool } from './config/db.js';
import authRoutes from './routes/auth.js';
import setupRoutes, { isInstalled } from './routes/setup.js';
import workspaceRoutes from './routes/workspaces.js';
import pageRoutes from './routes/pages.js';
import userRoutes from './routes/users.js';
import bookRoutes from './routes/books.js';
import taskRoutes from './routes/tasks.js';
import timeRoutes from './routes/time.js';
import adminRoutes from './routes/admin.js';
import { initSocket } from './socket.js';
const app = express();
const PORT = Number(process.env.PORT || 3000);
const CORS_ORIGIN = process.env.CORS_ORIGIN || 'http://localhost:5173';
app.use(helmet());
app.use(morgan('dev'));
app.use(express.json({ limit: '2mb' }));
app.use(
cors({
origin: CORS_ORIGIN.split(',').map((s) => s.trim()),
credentials: true,
}),
);
app.use('/api/setup', rateLimit({ windowMs: 15 * 60 * 1000, max: 20 }), setupRoutes);
// First-run guard: until the web installer completes, every API route
// except /api/setup/* and /api/health answers 503 { setupRequired: true }.
app.use('/api', async (req, res, next) => {
if (req.path === '/health' || req.path.startsWith('/setup')) return next();
if (!(await isInstalled())) {
return res.status(503).json({ error: 'Initial setup required', setupRequired: true });
}
return next();
});
app.use('/api/auth', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }), authRoutes);
app.use('/api/workspaces', workspaceRoutes);
app.use('/api/pages', pageRoutes);
app.use('/api/users', userRoutes);
app.use('/api/books', bookRoutes);
app.use('/api/tasks', taskRoutes);
app.use('/api/time', timeRoutes);
app.use('/api/admin', adminRoutes);
app.get('/api/health', async (_req, res) => {
try {
await pool.query('SELECT 1');
res.json({ ok: true, db: 'up' });
} catch {
res.status(503).json({ ok: false, db: 'down' });
}
});
app.use((err, _req, res, _next) => {
console.error('[unhandled]', err);
res.status(500).json({ error: 'Internal server error' });
});
const server = http.createServer(app);
const io = initSocket(server, CORS_ORIGIN);
app.set('io', io);
server.listen(PORT, () => {
console.log(`[api] listening on :${PORT} (CORS: ${CORS_ORIGIN})`);
});