AgendaPro v1.0 - multi-tenant SaaS, mobile calendar, public booking

Multi-tenant scheduling SaaS (AgendaPro-equivalent):
- Backend: Node + Express + node:sqlite, multi-tenant (admin/owner/employee),
  versioned migrations, 4 templates (estetica-spa, barberia, clinica, blank).
- Frontend: React 18 + TS + Vite + Tailwind, FullCalendar drag-and-drop,
  Recharts dashboard, mobile-first (day/list views on mobile).
- Features: calendar+services+employees+clients+tickets, dashboard with
  best employee/service, top tickets, top/frequent clients, commissions,
  cancellation policy + no-show tracking, public online booking (/b/:slug),
  cash register (cierre de caja), reminders/notifications center.
- Verification: 65/65 e2e API, 58/58 visual (0 overflow, 0 console errors),
  typecheck clean, vite build OK.
This commit is contained in:
AgendaPro Dev
2026-07-25 13:45:53 -06:00
commit e8d2435cc2
63 changed files with 16539 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
import type { Request, Response, NextFunction } from "express";
import { db } from "../db.ts";
import type { User } from "../../shared/types.ts";
export interface AuthedRequest extends Request {
user?: User;
}
export function authRequired(req: AuthedRequest, res: Response, next: NextFunction) {
const header = req.header("authorization") || "";
const token = header.startsWith("Bearer ") ? header.slice(7) : req.header("x-user-id");
if (!token) {
res.status(401).json({ error: "No autorizado" });
return;
}
const userId = Number(token);
if (!Number.isFinite(userId)) {
res.status(401).json({ error: "Token inválido" });
return;
}
const user = db
.prepare(`SELECT id, business_id, email, name, role, employee_id, avatar_color FROM users WHERE id = ?`)
.get(userId) as User | undefined;
if (!user) {
res.status(401).json({ error: "Usuario no encontrado" });
return;
}
req.user = user;
next();
}
export function ownerOnly(req: AuthedRequest, res: Response, next: NextFunction) {
if (req.user?.role !== "owner") {
res.status(403).json({ error: "Solo el dueño puede realizar esta acción" });
return;
}
next();
}
export function adminOnly(req: AuthedRequest, res: Response, next: NextFunction) {
if (req.user?.role !== "admin") {
res.status(403).json({ error: "Acceso restringido al administrador de la plataforma" });
return;
}
next();
}
export function err(res: Response, status: number, message: string) {
return res.status(status).json({ error: message });
}