feat(backend): Express + Socket.IO API with PostgreSQL

- Auth: register/login/me with bcrypt (12 rounds) and JWT bearer tokens
- Workspaces: CRUD, role-based members (owner/admin/member/viewer), invite by email
- Pages: nested hierarchy, TipTap JSON+HTML content, archive, search, comments
- Realtime: Socket.IO rooms per page with JWT handshake, live edits, presence, typing
- DB schema with triggers (owner membership, updated_at) and migrate script
- Helmet, CORS allow-list, rate limiting, /api/health with DB check
- Dockerfile (migrate-on-boot) for compose/production
This commit is contained in:
x3
2026-09-22 13:33:22 +02:00
parent 29c35f506d
commit 4ba8f8bd21
14 changed files with 2219 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
import jwt from 'jsonwebtoken';
function getSecret() {
const s = process.env.JWT_SECRET;
if (!s) throw new Error('JWT_SECRET is not set');
return s;
}
export function signToken(user) {
return jwt.sign(
{ sub: user.id, email: user.email },
getSecret(),
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
);
}
export function verifyToken(token) {
return jwt.verify(token, getSecret());
}
/** Express middleware: requires Authorization: Bearer <jwt> */
export function requireAuth(req, res, next) {
const header = req.headers.authorization || '';
const [scheme, token] = header.split(' ');
if (scheme !== 'Bearer' || !token) {
return res.status(401).json({ error: 'Missing bearer token' });
}
try {
const payload = verifyToken(token);
req.user = { id: payload.sub, email: payload.email };
return next();
} catch {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}