- Pending workspace invites with accept/decline + bell notifications; invite permission extendable via workspace.invite group right - Notifications API (list/unread, mark read, read-all) + realtime push to per-user socket rooms; chat messages notify other members - Workspace team chat API (list/post/delete) + chat:join socket rooms - Image uploads (multer, 5MB image-only) served from /uploads - Teams (managers/members), team-owned workspaces (TEAM badge), team task boards (team_id) and task assignees (workspace members only) - Permission groups with teams.create/edit/invite + workspace.invite, default Team Creators seeding, hasPerm/canManageTeam guards - Public registration disabled after install; admins create users (pending invites auto-link); users gain bg_glass setting - Crash fix: express-async-errors + 400 on malformed IDs instead of unhandled rejections killing the server
91 lines
3.1 KiB
JavaScript
91 lines
3.1 KiB
JavaScript
import 'dotenv/config';
|
|
import 'express-async-errors';
|
|
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 inviteRoutes from './routes/invites.js';
|
|
import notificationRoutes from './routes/notifications.js';
|
|
import chatRoutes from './routes/chat.js';
|
|
import teamRoutes from './routes/teams.js';
|
|
import groupRoutes from './routes/groups.js';
|
|
import uploadRoutes from './routes/upload.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.use('/api/invites', inviteRoutes);
|
|
app.use('/api/notifications', notificationRoutes);
|
|
app.use('/api/chat', chatRoutes);
|
|
app.use('/api/teams', teamRoutes);
|
|
app.use('/api/groups', groupRoutes);
|
|
app.use('/api/upload', uploadRoutes);
|
|
app.use('/uploads', express.static('uploads'));
|
|
|
|
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?.message || err);
|
|
if (err?.code === '22P02') return res.status(400).json({ error: 'Invalid ID format' });
|
|
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})`);
|
|
});
|