Skip to content

Latest commit

 

History

History
287 lines (237 loc) · 14.8 KB

README.md

File metadata and controls

287 lines (237 loc) · 14.8 KB

General — Four-Points

What affects the project as a whole: what it is, who uses it, how it is built and how information flows. Each functional module has its own folder in docs/.

What problem it solves

A hotel's internal operations —what the commercial PMS does not manage— are usually split between spreadsheets, paper and loose messages. Four-Points brings them together in a single web application: logbook, parking, maintenance, groups, cashier, blacklist, staff scheduling, shift checklists, F&B revenue and invoice backoffice. It complements the hotel's PMS (Opera, from which it imports PDF reports), it does not replace it. Each person has their own user and permissions according to their role.

Who uses it

Hotel staff, each with a role:

  • admin: everything. They are the only one who creates users, manages departments, sees reports and writes in backoffice.
  • recepcionista: operational modules (logbook, parking, cashier, blacklist, conciliation, checklist, F&B, maintenance). They only query groups.
  • group-admin: the same as reception and, additionally, manages groups and receives their notices.
  • mantenimiento: only the maintenance module, plus the common functions (profile, messages, notifications, search).
  • Public demo account (users.is_demo = 1, only where DEMO_MODE=true): an admin entered with the demo button, without a password. It can do everything the daily reset undoes, but not manage users, departments or the scheduling configuration, upload files, or reset the demo. Each blocked attempt is recorded. Details in ../demo/README.md.

The side menu only hides backoffice and scheduling links from non-admins. The backend is what actually decides: each route checks the role and returns 403 if it does not match.

What it can do

Functional modules — each documented in its folder: logbook, parking, maintenance, groups, scheduling, checklist, cashier, fnb, backoffice, blacklist, conciliation.

Functions common to the whole application (documented here):

  • Access: login with username and password. There is no public registration: users are created by an admin.
  • Profile (/dashboard/profile): own data, avatar, username and password change (both ask for the current password). In Settings, administrators manage users, departments and reports.
  • Internal messaging: direct conversations and groups between users, with urgent messages that arrive as a notification. It lives inside the profile. Detail in messages/.
  • Notifications: bell in the header, which refreshes every minute. They are generated by the system (group notices: payments, rooming list, arrivals), an admin manually or a new message.
  • Global search and recent activity on the home panel.
  • Language Spanish or English and theme light or dark.

What data it handles

  • MySQL 8 on Aiven, a single database (hotel_db) with test data, used by both the deployment and development. Summary in database/; schema, scripts and migration policy in backend/db-mysql/.
  • Users and roles: tables users and roles. Passwords are stored with bcrypt. Deleting a user does not remove them: they are deactivated and renamed.
  • Files (avatars, maintenance and blacklist photos, invoice PDF): in Cloudinary, not on the server.
  • Time: everything that depends on the day is calculated in Europe/Madrid.

What rules it follows

Session

  • On login the backend issues two JWTs in HttpOnly cookies: access_token (15 min) and refresh_token (7 days). In production the cookies are valid for all of .four-points.stackbp.es.
  • When a request receives 401, the client asks for a new token and repeats it. Each renewal also issues a new refresh_token, so the session is extended as long as it is used.
  • Sessions are not stored in the database: there is no way to close them from the server before they expire. Changing the password deletes the cookies from the browser where it is done.

Protection

  • Request limits per IP: 300 every 15 min across the whole API; 5 login attempts every 15 min per IP and user; 3 password changes per hour.
  • Security headers with Helmet on the backend and on Vercel. CORS accepts own domains, Vercel previews and requests without the Origin header (tools, server to server).
  • In production, unhandled errors return a generic message.

Log

  • Every 403 is logged (access_denied: user, role, method and path, without the query string), and so is every time an admin reads, lists or deletes conversations or messages they are not in (admin_override).
  • Passwords and tokens are hidden at the first three levels of each entry, and so is the SQL of a failed query (err.sql), which carries its values.
  • Failed logins keep the username that was typed (decided on 2026-10-12, left as is): it is what reveals a brute-force attack, although someone may type their password in that field.

Database

  • Every schema change is a new, idempotent script in backend/db-mysql/scripts/ and recorded in its INDEX.md. The full installation is never run against a database with data.

Scheduled tasks (Madrid time, inside the backend process)

When What it does
Every day, 06:30 Resets the day's checklists
Every day, 07:00 Sends scheduled notifications and generates group notices
Monday, 04:00 Deletes checklist event logs older than 7 days
10th, 23:59 Marks validated invoices from the previous month as paid

How information flows

Browser ──────────────► Frontend  (Next.js · Vercel)
    │                      │  server components: serverFetch
    │  fetch + cookies     ▼
    └──────────────────► Backend   (Express · Render)
                            │  routes → controllers → services → repositories
                            ├──► MySQL        (Aiven)
                            ├──► Cloudinary   (files)
                            └──► Python solver (scheduling, child process)
  1. The browser calls the backend directly (api.four-points.stackbp.es) with credentials: 'include', through app/lib/apiClient.ts, which also renews the token. There is no intermediate proxy: the frontend has no app/api routes (the unused ones were removed on 2026-10-02).
  2. Server components use app/lib/serverFetch.ts, which forwards the access cookie to the backend.
  3. proxy.ts (the Next.js 16 middleware) redirects to login if there are no session cookies. It only checks whether they exist and whether they have expired: the signature is verified by the backend.
  4. On the backend, every request goes through authenticateToken → role check → controller → validation with Zod → service → repository → MySQL.
  5. Server state on the client is managed by React Query; Zustand stores local state for some modules (cashier, groups, maintenance, notifications).

Stack

  • Frontend: Next.js 16 (App Router, Turbopack), React 19, Tailwind CSS 3, NextUI, React Query 5, Zustand 5, next-intl 4 and Zod 3.
  • Backend: Express 5 and TypeScript run with tsx (no compilation), mysql2, Zod 4, pino, node-cron and multer.
  • Scheduling: Python 3.11 and OR-Tools (CP-SAT), as a persistent process launched by the backend.
  • Tests: Vitest on the backend (auth, checklist, F&B, scheduling) and pytest on the solver. The frontend has no tests.

frontend/ and backend/ are two independent pnpm projects, each with its own lockfile. Node ≥ 22.16.

Deployment

Piece Where Detail
Frontend Vercel four-points.stackbp.es, region cdg1 (Paris)
Backend Render api.four-points.stackbp.es. The build creates the solver's Python env
DB Aiven MySQL with TLS; CA certificate in backend/config/certs/
Files Cloudinary
  • DNS: four-points.stackbp.es is a CNAME to Vercel and api.four-points.stackbp.es a CNAME to four-points.onrender.com.
  • NODE_ENV=production is required on the backend. Only then are the session cookies Secure and scoped to .four-points.stackbp.es, so the frontend and the API share them. Without it, login succeeds but every following request answers 401.
  • Closing every session at once: both tokens are signed with SECRET_JWT_KEY and the server stores no sessions, so changing that variable on Render and redeploying logs everyone out.
  • The build and start commands are set in the Render and Vercel dashboards; there is no render.yaml in the repository.

Preview environment (branch claude/compassionate-planck-gh6aof)

A second, complete stack to test a branch in production conditions before it is merged into main. Created on 2026-10-04 for phase 1c (ADR-027); why it looks like this is in ADR-030.

Piece Where Detail
Frontend Vercel preview.four-points.stackbp.es, a project domain bound to the branch
Backend Render service four-points-api-preview (free), api-preview.four-points.stackbp.es
DB Aiven the production database — every write from preview is real
  • Render: same root directory, build and start commands as four-points-api, deploys the branch on every commit. Environment variables are a copy of production's plus FRONTEND_URL=https://preview.four-points.stackbp.es, the extra CORS origin read by backend/index.ts.
  • Vercel: NEXT_PUBLIC_API_URL=https://api-preview.four-points.stackbp.es, scoped to Preview and to this branch only. The production value (All Environments) is untouched.
  • DNS (Hostinger, nameservers dns-parking.com; there is no Cloudflare account): preview.four-points CNAME to the same Vercel target as production, api-preview.four-points CNAME to four-points-api-preview.onrender.com.
  • Why not the *.vercel.app URL: the API is on another site, so the SameSite=Lax cookies are not sent and the CORS list rejects it. Under four-points.stackbp.es both work without touching code.

Test users (created on 2026-10-04 to verify the pre-release fixes role by role)

User Role Email
qa_recep recepcionista qa_recep@qa.invalid
qa_gadmin group-admin qa_gadmin@qa.invalid
qa_mant mantenimiento qa_mant@qa.invalid
  • They live in the production database, so production sees them too.
  • Their random passwords, and the admin one used for the tests, are only in docs/_archive/verify/creds.env on the main PC (dz). Git ignores that folder (ADR-009); the helper fp.sh next to it logs in with them.
  • Any data a test creates starts with QA-.

Side effects to keep in mind

  • Shared cookies: both APIs set the same cookie names on .four-points.stackbp.es and sign them with the same SECRET_JWT_KEY. Logging in on preview replaces the production session in that browser, and the session is valid on both.
  • Scheduled tasks run twice while the preview service is awake: both backends start the same node-cron jobs against the same database. Checklist reset, log purge and batch payment are idempotent; the 07:00 notices may be duplicated. Free services sleep after 15 minutes without traffic, so it only happens if preview is awake at that time.
  • Vercel toolbar on preview: Vercel injects its feedback toolbar (feedback.js) and the "Vercel Authentication" protection into preview deployments only. They log OPTIONS 400 and /.well-known/vercel/jwe 503 in the console and some ?_rsc= prefetches answer 503. It is not the app: production shows none of it. The toolbar can be turned off in Vercel → Settings → General.
  • Free limits: the Render workspace has 750 instance hours a month shared by every free service (if they run out, production is suspended too) and 2 custom domains, both now in use.

After a merge into main: keep working on the same branch, so the preview keeps its domain, variable and Render service. A merge commit has the branch as a parent, so it only needs a fast-forward: git switch claude/compassionate-planck-gh6aof && git merge --ff-only origin/main. PR #7 merged the first round on 2026-10-04 (942fdfa).

Removing it when it is no longer needed: delete the service four-points-api-preview on Render, the domain preview.four-points.stackbp.es and the branch-scoped NEXT_PUBLIC_API_URL on Vercel, and both CNAMEs on Hostinger. Delete or deactivate the qa_* users and the QA- test data, and docs/_archive/verify/. Nothing in main depends on it.

Local development

Installing from scratch (MySQL in Docker or your own, pnpm setup:local, the local admin admin / fourpoints-local) is in the root README.md. Day to day:

cd backend && pnpm dev:local    # API on :4000 against the local MySQL (dev:aiven: against Aiven)
cd frontend && pnpm dev         # http://localhost:3000

Backend variables: PORT (4000 locally), NODE_ENV, SECRET_JWT_KEY, SALT_ROUNDS, DB_ENVIRONMENT (local or aiven), LOCAL_DB_* or AIVEN_DB_* + AIVEN_PASSWORD, CLOUDINARY_*, LOG_LEVEL, FRONTEND_URL and DEMO_MODE (true only where the public demo runs). Frontend: NEXT_PUBLIC_API_URL, NEXT_SERVER_API_URL (optional), NEXT_PUBLIC_APP_URL, which the user creation route uses to accept the request origin, and NEXT_PUBLIC_DEMO_MODE, which shows the demo button on the login page.

Tests: pnpm test in backend/ (also test:watch and test:coverage). Some suites (login, checklist, F&B, scheduling requests) use the configured database, which is the shared Aiven one: the F&B suite writes to the year 2099 and deletes it afterwards. The solver's own tests are described in ../scheduling/solver.md.

Languages: Spanish (default) and English with next-intl, one JSON file per module in frontend/messages/es and frontend/messages/en. The language comes from the NEXT_LOCALE cookie, then the visitor's country (Vercel header), then the browser, then Spanish.