Ayman, home

Case studies / Every role sees and does only its own part

Every role sees and does only its own part.

A complete platform for a learning-project hotel chain: public site with bookings, a client area and a staff ERP with three roles.

role
Class project, rewritten on my own
year
2025
stack
Java 21Spring BootPostgreSQLReactTypeScriptTestcontainersaxe
links
Code ↗Demo ↗
Zellige Hotels home page with the headline and an arched photo of a courtyard
// context

Context.

It started as a class project in my web development diploma. I then rewrote it from scratch on my own to learn what I really wanted: Spring Boot on the backend, React on the frontend, real authentication, database constraints and tests against a real PostgreSQL.

It is the project that taught me most about roles and permissions: which roles exist and what each of them can do.

I later gave it its own design with scroll-linked motion, tidied the frontend and made it accessible. The target is WCAG 2.2 AA and axe checks it in CI.

// problem

The problem.

Admins, managers, receptionists and guests share one platform, but each of them must only see and touch their own part.

// decisions

What I decided, and why.

01

Four profiles, three of them staff

what
Admin, manager and receptionist in the ERP, and guest in their own area. The rules live in the backend; the frontend only shows what each role can use.
why
If the rule only lived in the interface, calling the API directly would bypass it.
02

Rotating refresh tokens

what
The access token lives in memory and the refresh token changes on every use. If one is used twice, the whole token family is revoked.
why
My first version issued a long-lived token and called it done. Reading OWASP properly showed me that rotation with reuse detection was cheap and far safer.
03

Let the database block overlapping bookings

what
The service rejects overlapping bookings and PostgreSQL also has an EXCLUDE USING gist constraint as a safety net.
why
My first two hand-written checks were wrong: one on the checkout day, one on time zones. The third time I let the database do it, and the test is one line.
04

Accessibility checked on every PR

what
A CI job walks 18 screens at 390 and 1440 px with axe and measures horizontal overflow. It fails on serious or critical violations.
why
A manual review at the end gets forgotten. With the check on every PR, a regression shows up the same day.
05

Demo data that does not expire

what
An optional job refreshes the demo bookings, invoices and tasks so the dashboard does not show 0 € or 0 bookings. Demo guests have no account and their bookings send no emails. It is off by default and is switched on with an environment variable.
why
An empty dashboard does not show what the ERP does, and hand-written dates expire.
Admin dashboard with KPIs, monthly revenue and occupancy
Admin dashboard with KPIs, monthly revenue and occupancy
The three hotels as cards that enter on scroll
The three hotels as cards that enter on scroll
// result

Result.

  • 31 backend tests, including Testcontainers tests against PostgreSQL.
  • axe in CI: 18 screens at 390 and 1440 px, with no serious or critical violations.
  • Free deployment with a public demo.
// what i learned

What I learned.

  • Refresh-token rotation is not optional.
  • A database constraint is worth more than three hand-written checks.
  • TanStack Query changed how I think about frontend state.

What I'd do differently

  • I would start with bigger components and split them only when something genuinely repeats.