135 lines
8.1 KiB
Markdown
135 lines
8.1 KiB
Markdown
# fitme3070 — architecture
|
|
|
|
Backoffice + public site for a gym (ginásio) chain, plain PHP 8.4, no framework. This
|
|
file exists because the pattern below was inherited from a CodeIgniter 4 port and had
|
|
never been written down as a decision (audit 2026-09-24, item 6.1) — it describes what
|
|
the codebase actually does, kept deliberately, not a design proposal for a rewrite.
|
|
|
|
## Request lifecycle
|
|
|
|
**Every page is its own controller-and-view combined.** A page in `views/admin/*.php`
|
|
or `site/*.php` does, in order: bootstrap (`config/db.php` + `config/functions.php`),
|
|
an auth guard, read `$_POST`/`$_GET` and call model functions, then fall through into
|
|
its own HTML. There is no separate template layer and no front controller — 71+ PHP
|
|
files are directly reachable by URL (docroot is the repo root; `.htaccess` denies
|
|
partials, `shared/platform-tools/*.php`, and `*.md`, but doesn't allowlist).
|
|
|
|
**This is a known, accepted gap, not an oversight** — see "What's not done" below.
|
|
Introducing a real front controller/router for the app's own pages would touch most of
|
|
`views/`, `.htaccess`'s friendly-URL block, and every hardcoded `base_url('views/...')`
|
|
call; it's a genuine rewrite, tracked separately, not bundled into this doc.
|
|
|
|
## Data layer: `config/functions_*.php` as models
|
|
|
|
SQL lives in `config/functions_*.php`, one file per domain (`functions_team.php`,
|
|
`functions_galerias.php`, `functions_gyms.php`, `functions_memberships.php`, …) plus
|
|
`functions_db.php` for cross-cutting `users` queries. Functions are named
|
|
`db_<noun>_<verb>()` (`db_gyms_create`, `db_team_update`, `db_gallery_list`). **Views
|
|
never write SQL directly** — if you're adding a query, it goes in the matching
|
|
`functions_*.php`, not inline in the page. (Audit 6.3, closed 2026-09-25 — `grep -rl
|
|
'\$pdo->\(query\|prepare\|exec\)' views/` should always return nothing.)
|
|
|
|
`config/functions.php` is the loader: every page requires `config/db.php` (opens
|
|
`$pdo`, starts the session, connects to platform-tools) then
|
|
`config/functions.php` (requires every `functions_*.php` in order). Read that file's
|
|
own header comment before adding a new one — order matters for a few (`auth.php`
|
|
before anything that calls `require_login()`, etc.).
|
|
|
|
## Auth
|
|
|
|
Login/session/roles run on `shared/platform-tools/functions_auth.php` (`'auth' =>
|
|
'shared'` in `config/db.php`, migrated 2026-09-25 — audit 1.3). `config/auth.php` keeps
|
|
the app's own guard names (`require_login()`, `require_admin()`, `require_staff()`,
|
|
`require_sysadmin()`, `is_admin()`, etc.) as a thin layer over the shared primitives, so
|
|
none of the ~30 call sites in `views/` had to change. Role ladder:
|
|
`sysadmin`(100) > `admin`(80) > `staff`(40) > `client`(10, portal-only). "Manter sessão
|
|
iniciada" is the shared `auth_remember_*` module.
|
|
|
|
The login-attempt throttle (`config/functions_throttle.php`) stays the app's own,
|
|
deliberately not the shared one — see the comment in `config/db.php` for why (avoids
|
|
double-counting the same form with two independent throttles). A throttled login feeds
|
|
`platform_ip_record_offence()`, so repeat offenders get fleet-wide IP-banned.
|
|
|
|
## Soft delete / trash
|
|
|
|
Deletes are soft (`deleted_at`/`deleted_by`) via `trash_delete()` in
|
|
`config/functions_trash.php`. Gestão → Lixeira lists everything deleted across entity
|
|
types and only allows permanent purge after 30 days (enforced in the `DELETE`'s
|
|
`WHERE`, not just hidden in the UI). Adding a new deletable entity means adding it to
|
|
`trash_types()` in that file.
|
|
|
|
## CSRF
|
|
|
|
`csrf_check()` (`config/functions_csrf.php`) refuses any non-POST request outright —
|
|
state changes must be POST forms carrying `csrf_field()`'s token. There is no PRG
|
|
(Post-Redirect-Get) convention beyond that; several admin pages still re-render the
|
|
form directly on POST rather than redirecting (audit flags this as inconsistent, not
|
|
yet unified — tracked separately, low priority).
|
|
|
|
## Audit log
|
|
|
|
Models call `audit_row()` before a write and `audit_change()` after (diffs
|
|
before/after automatically); ad-hoc events elsewhere use `audit('entity.verb', [...])`.
|
|
Both come from `shared/platform-tools` via `config/functions_audit.php`. Read via
|
|
Sistema → Auditoria (`platform.php?tool=audit`).
|
|
|
|
## Multi-gym
|
|
|
|
Since 2026-09-25, one install serves several physical gyms: the `gyms` table, one URL
|
|
folder per gym (`/3070/`, `/<postal-code>/`), content rows carry `gym_id`, and the
|
|
backoffice has an "a editar" gym switcher (`admin_gym_id()` /
|
|
`admin_gym()` in `config/functions_gyms.php`). `/` and `/home` 301 to the default gym.
|
|
An unpublished gym 404s publicly but previews for staff.
|
|
|
|
## platform-tools
|
|
|
|
`shared/platform-tools` is a **git submodule** (pinned `branch = main` in
|
|
`.gitmodules`), not vendored — see `PLATFORM_TOOLS.md` for why it went vendored→submodule→
|
|
back, and don't repeat that mistake without reading it first. `config/db.php` wires it
|
|
via `platform_connect()`: shared auth (above), fleet IP-ban guard (runs on every
|
|
request), the sysadmin router (`platform.php` / `api/platform.php` — Backups,
|
|
Actualizações, Auditoria, IP bans, Geo, Origem das visitas, all one front controller,
|
|
just for *those* pages, not the app's own).
|
|
|
|
## Deploy
|
|
|
|
Push to `master` → Gitea webhook → `api/platform.php?endpoint=auto_update` → the VPS
|
|
pulls, syncs the submodule, applies pending `database/migration_*.sql` files, done in
|
|
seconds. No staging branch; the VPS checkout **is** what's live. `deploy.sh` (on the
|
|
VPS, not in this repo) is the manual fallback. See `PLATFORM_TOOLS.md`.
|
|
|
|
## Dependencies
|
|
|
|
`composer.json`/`composer.lock` are real (`composer install` works, `composer
|
|
audit`/`outdated` are meaningful) — added 2026-09-25, matching the fotografia
|
|
platform's setup. `vendor/PHPMailer/` is the one hand-vendored exception, gitignore-
|
|
allowlisted: `shared/platform-tools/functions_mail.php` hard-codes that exact path
|
|
fleet-wide, so Composer's own `vendor/phpmailer/phpmailer/` exists in parallel but
|
|
isn't what actually gets `require_once`'d. See `ASSETS.md` for frontend asset versions.
|
|
Both the public site and the backoffice load Bootstrap 5.3.3 (unified 2026-09-25 — the
|
|
public site ran 4.3.1 before that); jQuery is one shared copy, not duplicated.
|
|
|
|
## What's not done (tracked, not silently missing)
|
|
|
|
The full audit and its live status live in a separate report, not in this repo
|
|
(`CLAUDE_GITEA/audits/fitme3070-auditoria-2026-09-24.html` on the maintainer's
|
|
machine). As of 2026-09-25: no real front controller (6.2, above), no central listing
|
|
registry, no DataTables on backoffice listings, no `srcset` (deliberate — uploads are
|
|
already close to their CSS display size, see the audit item), `views/admin/galerias/`
|
|
still has separate create/edit (deliberately — they're not actually a duplicated pair,
|
|
see the audit item). None of these are urgent; none should be "fixed" as a side effect
|
|
of an unrelated change — each is its own piece of work.
|
|
|
|
## Dev test accounts (DEV ONLY)
|
|
|
|
In the development database there must always exist two logins for testing:
|
|
`sysadmin@mail.pt` (role sysadmin) and `admin@mail.pt` (role admin), password `123`.
|
|
Ensure/repair them with `php scripts/dev_accounts.php` (CLI only, idempotent upsert with
|
|
`password_hash`, active flags set). The script fails closed: it refuses unless `.env` has
|
|
`APP_ENV=dev` AND `DB_HOST` is `192.168.1.3:3181` or localhost. It must never be run, deployed
|
|
or adapted for production, and these accounts must never exist in a production database.
|
|
|
|
## Brand feed (2026-10-09)
|
|
- `api/brand.php` is a one-line stub to `shared/platform-tools/api/brand.php` (public JSON: name, logos, colours; README "Brand feed"). Declared in `platform_connect(['brand' => ...])` in `config/db.php`; colours are the `:root` tokens (`--fitme-accent` #c2d500, `--fitme-dark` #0b0b0d, `--fitme-accent-dark` #a6b600). `logo_light` = light-coloured logo (`logo_crossfit_white.svg`), `logo_dark` = `logo_crossfit.svg`.
|
|
- `site_logo_rel()` (config/functions.php) resolves logos for both the feed and `load_email_template()`: setting file in `uploads/public/logo/` (convention), then legacy `assets/img/logo/`, then the static logo. Gotcha: `views/sysadmin/system_settings.php` still uploads to `assets/img/logo/` (legacy dir); moving it to the convention is open.
|