Skip to content

Configurable registration fields + admin-defined custom fields (#255)#256

Merged
fabiodalez-dev merged 7 commits into
mainfrom
feat/configurable-registration-255
Jul 17, 2026
Merged

Configurable registration fields + admin-defined custom fields (#255)#256
fabiodalez-dev merged 7 commits into
mainfrom
feat/configurable-registration-255

Conversation

@fabiodalez-dev

@fabiodalez-dev fabiodalez-dev commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Closes #255.

What this does

1. Per-field requirement toggles. Surname, phone and address are no longer hardcoded as required at self-registration: each gets a checkbox under Settings → Email → Registrazione utenti. Defaults keep today's all-required behaviour, so existing installs change nothing until the admin opts out. The toggles flow end-to-end: public form (labels + required attributes), registration controller, user profile and the admin create/edit user forms.

2. Optional surname — deliberately stored as '', not NULL. The issue asked for a nullable cognome; I kept the column NOT NULL on purpose: 70+ display paths (loan emails, member lists, exports, plugins) build the full name with CONCAT(nome, ' ', cognome), and a NULL there blanks the whole name in MySQL. An empty string delivers "surname not required" with zero migration risk and zero display sweep. Empty phone/address are stored as NULL (those columns were already nullable).

3. Custom registration fields. The admin defines label / type (text, textarea, email, URL, number, checkbox) / required / active rows; they render on the registration form and in the user profile with server-side type validation and a 1000-char cap, and show read-only on the admin user detail — e.g. a Telegram username for a community that notifies through it. Definitions live in registrazione_campi, per-user values in utenti_campi_valori (PK utente_id+campo_id, FK CASCADE both ways). The account row and the custom values insert atomically at registration. The bundled Mobile API exposes the requirement flags and custom definitions through /api/v1/health, accepts custom values on /auth/register and PATCH /me, and returns them from GET /me; the companion Android app consumes the same contract in its separate repository.

Schema

migrate_0.7.37.sql — two CREATE TABLE IF NOT EXISTS, idempotent; the same tables ship in schema.sql for fresh installs; every reader degrades gracefully during the pre-migration window. version.json → 0.7.37 (this PR assumes the 0.7.36 line from #254 ships first; I'll rename the migration if the order changes).

Verification

  • tests/migration-0.7.37.unit.php — the real migration against sandbox-renamed tables: both tables + PK/FKs, idempotent second run, both FK cascades, the '' surname contract, and the full validation matrix (14 checks).
  • tests/issue-255-registration.spec.js — real-browser E2E wired into ci-e2e as a required step: defaults are required → admin unticks the toggles and defines a Telegram field → a registration without surname/phone/address succeeds and stores the custom value (cognome='', telefono/indirizzo NULL) → restoring the toggles makes the fields required again.
  • PHPStan L5 = 0 · 38/38 standalone unit files · full-test.spec.js 136 passed · i18n key parity across it/en/de/fr.

Summary by CodeRabbit

  • Nuove funzionalità
    • Impostazioni admin per requisiti condizionali dei campi di registrazione e creazione campi personalizzati (tipo, etichetta, attivo/obbligatorio).
    • Campi personalizzati supportati su registrazione e profilo; integrazione Mobile API con configurazione e custom_fields.
    • Changelog aggiornato alla v0.7.37.
  • Correzioni
    • Validazioni e salvataggi più robusti con controlli condizionali e gestione corretta di valori vuoti/NULL; messaggi di errore dedicati.
  • Test
    • Estesi i test E2E e aggiunti test unitari per validazione e migrazione, inclusa verifica dell’integrità dello schema.

…ields

Small communities can't force members to disclose personal data, so the
registration form's hardcoded requirements become admin choices:

- Per-field toggles (Settings → Email → Registrazione utenti) decide whether
  surname, phone and address are required at self-registration. Defaults keep
  the historical all-required behaviour; the toggles flow through ConfigStore
  (registration.require_cognome/telefono/indirizzo) into the public form
  (labels + required attributes), the registration controller, the profile
  and the admin user forms. An optional surname is stored as an empty string
  on purpose — the column stays NOT NULL because 70+ display paths CONCAT
  nome+cognome and a NULL there would blank the whole name; empty phone and
  address are stored as NULL (the columns were already nullable).
- Custom fields: the admin defines label/type(text, textarea, email, url,
  number, checkbox)/required/active rows that render on the registration form
  and in the user profile, with server-side type validation and a 1000-char
  cap. Values are shown read-only on the admin user detail. Definitions live
  in registrazione_campi, per-user values in utenti_campi_valori
  (PK utente_id+campo_id, FK CASCADE both ways); account row and custom
  values insert atomically at registration.
- Migration migrate_0.7.37.sql (CREATE TABLE IF NOT EXISTS only, idempotent)
  + the same tables in schema.sql for fresh installs; every reader degrades
  gracefully while the tables are not migrated yet. version.json → 0.7.37,
  What's New section added.
- i18n: nine new strings in all four locales (key parity verified).
- Tests: migration-0.7.37.unit.php (real migration against sandbox tables,
  idempotency, FK cascades, validation contract — 14 checks) and
  issue-255-registration.spec.js (real-browser flow: defaults required →
  admin relaxes + defines a Telegram field → registration without
  surname/phone/address stores the custom value → restore), wired into
  ci-e2e as a required step.

Closes #255 (custom-field API exposure for notification integrations lands
with the notifications work).
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

La versione 0.7.37 aggiunge requisiti configurabili, campi personalizzati con validazione e persistenza, visualizzazione nei profili, supporto Mobile API e verifiche automatizzate.

Changes

Campi di registrazione configurabili

Layer / File(s) Summary
Contratti, configurazione e persistenza
app/Support/*, installer/database/*
Definisce toggle, schema, validazione e persistenza dei campi personalizzati.
Configurazione amministrativa
app/Controllers/SettingsController.php, app/Views/settings/index.php, app/Routes/web.php
Gestisce requisiti built-in e ciclo di vita dei campi custom.
Registrazione pubblica e salvataggio atomico
app/Controllers/RegistrationController.php, app/Views/auth/register.php
Applica i requisiti, renderizza i campi dinamici e salva i dati in transazione.
Profilo e gestione utenti
app/Controllers/ProfileController.php, app/Controllers/UsersController.php, app/Views/profile/*, app/Views/utenti/*
Applica i requisiti e mostra i valori custom nei profili e nelle viste admin.
Mobile API, test e rilascio
storage/plugins/mobile-api/*, tests/*, .github/workflows/*, locale/*, README.md, version.json
Estende gli endpoint e gli schemi API, aggiunge test, CI, traduzioni, changelog e aggiorna la versione.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant RegistrationController
  participant RegistrationFields
  participant Database
  Admin->>RegistrationController: configura requisiti e campi
  RegistrationController->>RegistrationFields: valida i dati inviati
  RegistrationController->>Database: inserisce l’utente
  RegistrationController->>RegistrationFields: salva i valori custom
  RegistrationFields->>Database: persiste i valori associati
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Il titolo riassume bene l’aggiunta dei campi di registrazione configurabili e dei campi custom.
Linked Issues check ✅ Passed Le modifiche coprono toggle configurabili, campi custom, migrazione, UI e Mobile API richiesti da #255.
Out of Scope Changes check ✅ Passed Non emergono modifiche chiaramente estranee: i file toccati supportano direttamente il nuovo flusso di registrazione.
Docstring Coverage ✅ Passed Docstring coverage is 67.65% which is sufficient. The required threshold is 60.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/configurable-registration-255

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci-e2e.yml:
- Around line 254-259: Make the migration database checks mandatory: in
.github/workflows/ci-e2e.yml lines 254-259, add php
tests/migration-0.7.37.unit.php as a required workflow step; in
tests/migration-0.7.37.unit.php lines 87-90, update the database connection
failure path to return a non-zero error status instead of success.

In `@app/Controllers/ProfileController.php`:
- Around line 165-176: Propaga i tre requisiti configurabili in tutti i flussi:
in app/Controllers/ProfileController.php:165-176 valida anche telefono e
indirizzo prima dell'UPDATE; in app/Views/profile/index.php:406-408 applica
indicatore, required e aria-required a entrambi; in
app/Controllers/UsersController.php:91-127 normalizza l'indirizzo prima della
creazione e applica require_indirizzo; in
app/Controllers/UsersController.php:332-339 applica require_indirizzo durante
l'aggiornamento, riutilizzando RegistrationFields.

In `@app/Controllers/SettingsController.php`:
- Around line 253-305: Wrap the custom-field persistence flow in
SettingsController’s update/delete/insert logic
(app/Controllers/SettingsController.php:253-305) in a database transaction,
check every prepare, execute, and bind operation, roll back and propagate the
failure to the caller instead of silently returning or logging success; commit
only after all operations succeed. Apply the same transaction boundary to the
profile and custom-value saves in ProfileController
(app/Controllers/ProfileController.php:201-223), rolling back both when either
persistence path fails.

In `@app/Support/RegistrationFields.php`:
- Around line 93-100: Nel flusso che costruisce $values a partire da $submitted,
valida ogni valore prima di convertirlo con trim o cast a stringa: accetta
soltanto valori scalari o null e tratta array/oggetti come valori assenti o non
validi. Aggiorna il ciclo sui $definitions mantenendo invariato il comportamento
dei valori scalari e impedendo che input non scalari venga persistito o
considerato selezionato.

In `@app/Views/profile/index.php`:
- Around line 465-469: Sostituisci le chiamate a App\Support\HtmlHelper::e() nei
valori di textarea e input della view con htmlspecialchars(..., ENT_QUOTES,
'UTF-8'), mantenendo invariati gli attributi e il comportamento di escaping
esistente.

In `@app/Views/utenti/modifica_utente.php`:
- Around line 110-123: Replace both HtmlHelper::e() calls in the
customFieldValues rendering with htmlspecialchars(..., ENT_QUOTES, 'UTF-8'),
preserving the existing escaped output for cfv['etichetta'] and cfv['valore']
while leaving the checkbox label handling unchanged.

In `@tests/issue-255-registration.spec.js`:
- Around line 82-83: Update both form-save flows in the registration test to
wait for the SweetAlert confirmation button `.swal2-confirm` after submitting,
click it, and only then continue or wait for page loading so settings
persistence is confirmed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fa0d31f5-5cd1-4595-ac8f-d7e2e591da4f

📥 Commits

Reviewing files that changed from the base of the PR and between 63f5c04 and fac4df5.

📒 Files selected for processing (22)
  • .github/workflows/ci-e2e.yml
  • README.md
  • app/Controllers/ProfileController.php
  • app/Controllers/RegistrationController.php
  • app/Controllers/SettingsController.php
  • app/Controllers/UsersController.php
  • app/Routes/web.php
  • app/Support/ConfigStore.php
  • app/Support/RegistrationFields.php
  • app/Views/auth/register.php
  • app/Views/profile/index.php
  • app/Views/settings/index.php
  • app/Views/utenti/modifica_utente.php
  • installer/database/migrations/migrate_0.7.37.sql
  • installer/database/schema.sql
  • locale/de_DE.json
  • locale/en_US.json
  • locale/fr_FR.json
  • locale/it_IT.json
  • tests/issue-255-registration.spec.js
  • tests/migration-0.7.37.unit.php
  • version.json

Comment thread .github/workflows/ci-e2e.yml
Comment thread app/Controllers/ProfileController.php Outdated
Comment thread app/Controllers/SettingsController.php Outdated
Comment thread app/Support/RegistrationFields.php Outdated
Comment thread app/Views/profile/index.php Outdated
Comment thread app/Views/utenti/modifica_utente.php
Comment thread tests/issue-255-registration.spec.js Outdated
…+ 25-check E2E

Address the review round and move the registration settings where they
belong:

- New Settings → Registrazione tab with its own form and POST endpoint
  (/admin/settings/registration): admin-approval flag, the three built-in
  requirement toggles and the custom-field editor move out of the email
  form. Email settings save is registration-free again.
- Validation hardening: RegistrationFields::validate() rejects non-scalar
  payloads (custom_field[id][]=x would cast to "Array", pass required checks
  and tick checkboxes); the requirement toggles now also govern phone and
  address in the profile (view + server) and address in the admin
  create/edit forms, so a member can't blank a field the library requires.
- Atomic persistence: the custom-field definitions save runs in a
  transaction with per-statement checks and a visible error flash on
  rollback; the profile UPDATE and the user's custom values commit or roll
  back together.
- View escaping: htmlspecialchars(..., ENT_QUOTES, 'UTF-8') replaces
  HtmlHelper::e() in the new view code (project rule).
- migration-0.7.37.unit.php now FAILS when no database is reachable — a
  silently-skipped migration test is a false green in CI (both workflows
  provision MySQL). The redundant extra ci-e2e step was skipped on purpose:
  ci-quality already runs every tests/*.unit.php against MySQL.
- issue-255-registration.spec.js grows from 4 to 25 real-browser checks:
  server-side enforcement of each built-in requirement via novalidate
  submits, all six custom types created and rendered with the right
  controls, the full 2^3 toggle matrix, a registration storing every type,
  invalid values (bad email, javascript: URL, non-numeric number, missing
  required, >1000 chars) rejected by the server with crafted POSTs, label
  tags neutralised on save, script-carrying values rendered escaped on the
  admin detail (never executed), and inactive fields disappearing from the
  form.

Verified: PHPStan L5 = 0, unit 38/38, issue-255 E2E 25/25,
full-test.spec.js 136 passed (one external scraping-provider 504 flake
reran green).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
app/Views/settings/index.php (1)

402-402: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correggi la nota sul salvataggio.

Questo form invia solo le impostazioni di registrazione, non quelle email.

Correzione proposta
-<?= __("Le modifiche ai campi vengono salvate insieme alle impostazioni email.") ?>
+<?= __("Le modifiche ai campi vengono salvate insieme alle impostazioni di registrazione.") ?>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/settings/index.php` at line 402, Correggi il testo della nota nella
vista delle impostazioni affinché indichi che le modifiche ai campi vengono
salvate insieme alle impostazioni di registrazione, non a quelle email.
app/Controllers/ProfileController.php (1)

174-183: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Aggiungi required_fields alla mappa degli errori del profilo.

Entrambi i redirect producono questa chiave, ma app/Views/profile/index.php non la gestisce e mostra soltanto l’errore generico.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/ProfileController.php` around lines 174 - 183, Aggiorna la
mappa degli errori in app/Views/profile/index.php per gestire la chiave
required_fields prodotta dai redirect di ProfileController. Collega
required_fields al messaggio dedicato ai campi obbligatori, mantenendo invariati
gli altri errori e il fallback generico.
app/Controllers/UsersController.php (1)

335-365: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Allinea il form admin ai requisiti configurabili.

app/Views/utenti/modifica_utente.php mantiene il cognome sempre required e il JavaScript rende il telefono obbligatorio per ogni non-admin, anche quando questi toggle sono disattivati. L’indirizzo, invece, non riceve required quando il server lo pretende. Il browser blocca quindi modifiche valide o rinvia l’errore al server.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/UsersController.php` around lines 335 - 365, Allinea i
requisiti client-side del form admin con i controlli server basati su
RegistrationFields::isRequired(). Aggiorna il campo cognome per usare required
solo quando configurato, limita l’obbligatorietà del telefono ai non-admin
quando il relativo toggle è attivo e aggiungi required all’indirizzo quando
richiesto dal server; correggi anche la validazione JavaScript per mantenere lo
stesso comportamento.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Controllers/SettingsController.php`:
- Around line 285-306: Nel ciclo di aggiornamento dei campi personalizzati in
SettingsController, valida che ogni valore annidato usato da delete, etichetta,
tipo, obbligatorio e attivo sia scalare prima di applicare empty(), cast o
sanitizzazione; se un valore non è scalare, interrompi l’elaborazione e annulla
la transazione invece di ignorarlo o interpretarlo. Mantieni invariati i
controlli sui valori validi e il flusso di bind_param per i dati accettati.
- Around line 225-233: Convert the toggle values to actual booleans before
calling ConfigStore::set in the requireApproval assignment and the
BUILTIN_TOGGLES loop. Preserve the repository’s existing '1'/'0' string values,
but pass false when a setting is unchecked and true when checked.

In `@app/Views/settings/index.php`:
- Line 328: Sostituisci HtmlHelper::e() nel campo hidden csrf_token della view
con htmlspecialchars(Csrf::ensureToken(), ENT_QUOTES, 'UTF-8'), mantenendo
invariato il valore del token e la struttura dell’input.

In `@locale/it_IT.json`:
- Line 6536: Update the Italian translation value for “Le modifiche ai campi
vengono salvate insieme alle impostazioni email.” to reference registration
settings instead of email settings, matching the section where the field changes
are configured.

In `@tests/issue-255-registration.spec.js`:
- Around line 111-115: Update the test.afterAll cleanup to delete matching rows
from utenti_campi_valori before deleting registrazione_campi and utenti;
preserve the existing TOKEN-based filters and close the admin context afterward.
- Line 108: Remove the setToggles(true, true, true) initialization before tests
1–4 so they exercise the application’s actual default toggle values. Keep
explicit toggle setup only where later tests require non-default state.

---

Outside diff comments:
In `@app/Controllers/ProfileController.php`:
- Around line 174-183: Aggiorna la mappa degli errori in
app/Views/profile/index.php per gestire la chiave required_fields prodotta dai
redirect di ProfileController. Collega required_fields al messaggio dedicato ai
campi obbligatori, mantenendo invariati gli altri errori e il fallback generico.

In `@app/Controllers/UsersController.php`:
- Around line 335-365: Allinea i requisiti client-side del form admin con i
controlli server basati su RegistrationFields::isRequired(). Aggiorna il campo
cognome per usare required solo quando configurato, limita l’obbligatorietà del
telefono ai non-admin quando il relativo toggle è attivo e aggiungi required
all’indirizzo quando richiesto dal server; correggi anche la validazione
JavaScript per mantenere lo stesso comportamento.

In `@app/Views/settings/index.php`:
- Line 402: Correggi il testo della nota nella vista delle impostazioni affinché
indichi che le modifiche ai campi vengono salvate insieme alle impostazioni di
registrazione, non a quelle email.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b1d21cd0-7485-4051-89e1-7ea29b315e24

📥 Commits

Reviewing files that changed from the base of the PR and between fac4df5 and 15aecd0.

📒 Files selected for processing (14)
  • app/Controllers/ProfileController.php
  • app/Controllers/SettingsController.php
  • app/Controllers/UsersController.php
  • app/Routes/web.php
  • app/Support/RegistrationFields.php
  • app/Views/profile/index.php
  • app/Views/settings/index.php
  • app/Views/utenti/modifica_utente.php
  • locale/de_DE.json
  • locale/en_US.json
  • locale/fr_FR.json
  • locale/it_IT.json
  • tests/issue-255-registration.spec.js
  • tests/migration-0.7.37.unit.php

Comment thread app/Controllers/SettingsController.php
Comment thread app/Controllers/SettingsController.php
Comment thread app/Views/settings/index.php Outdated
Comment thread locale/it_IT.json Outdated
Comment thread tests/issue-255-registration.spec.js Outdated
Comment thread tests/issue-255-registration.spec.js
- schema-integrity.spec.js: register the two new tables in the hardcoded
  CORE_TABLES list (57 → 59) so the CI post-install verification passes.
  Note: PR #254 replaces this list with a schema.sql-derived one; on merge
  the dynamic version supersedes this entry.
- SettingsController: a crafted nested payload
  (custom_fields[5][etichetta][]=x) aborts the batch — an array is truthy
  for empty() (phantom delete) and casts to "Array" (garbage label).
- Registration-tab CSRF hidden input uses htmlspecialchars (view rule);
  the custom-fields note now says the changes save with the REGISTRATION
  settings (stale "email" wording swapped in all four locales).
- Profile error map handles the required_fields code the controller
  actually emits (previously fell through to the generic message).
- Admin create/edit user forms: the surname required attribute follows
  the require_cognome toggle like the server does.
- issue-255-registration.spec.js: tests 1-4 now exercise the CODE default
  (toggle rows deleted, absent row = required) instead of a value the
  suite planted; teardown restores the pristine state.

Skipped from the review with reasons: ConfigStore::set already receives
booleans ((bool)'0' === false); the explicit utenti_campi_valori cleanup
is unnecessary — both FKs are ON DELETE CASCADE and the migration test
asserts exactly that.

Verified: PHPStan L5 = 0, unit 38/38, issue-255 E2E 25/25,
schema-integrity 18/18.
…leftovers

- crea/modifica utente: the JS that forced the phone required for every
  non-admin now honours require_telefono, and the address gains the same
  toggle-driven required the server enforces; the surname asterisk follows
  its toggle like the input's required attribute already did.
- SettingsController: the non-scalar payload guard also covers the
  new_custom_field_* inputs (an array would cast to "Array" and create a
  garbage definition).
- E2E teardown deletes utenti_campi_valori rows before their parents:
  FK-safe order per project test rules — teardown must not depend on the
  cascades the suite itself verifies.

Already addressed in the previous commit (review overlap): registration-tab
note wording, required_fields in the profile error map, CSRF hidden escaping,
stale it_IT string, default-purity of tests 1-4, hardcoded CORE_TABLES.
Skipped with evidence: the ConfigStore boolean finding — (bool)'0' is FALSE
in PHP ('0' is falsy), so unchecked toggles already reach ConfigStore as
false.

Verified: PHPStan L5 = 0, issue-255 E2E 25/25.
@fabiodalez-dev

Copy link
Copy Markdown
Owner Author

Review — PR #256 (configurable registration fields, #255)

Reviewed 532c4bc1 (24 files, +1439/−56) with 6 lenses (diff-local, structural, CLAUDE.md, comments, UX, security). CLAUDE.md compliance and security lenses returned clean — no injection, escaping, CSRF, or access-control gaps. 18 raw candidates → 12 deduped → 4 over the scoring gate → validated below.

Confirmed (worth addressing)

1. nome . ' ' . cognome renders a trailing space for single-name members · ux · 6 sites
This PR is the first to allow cognome = '' system-wide, but 6 unchanged display sites still concatenate without trim(), so a surname-less member shows as "Mario ":

  • app/Views/utenti/index.php:83, app/Views/admin/stats.php:251, app/Views/settings/messages-tab.php:55
  • app/Controllers/PrestitiController.php:92, app/Controllers/UsersController.php:599 & :630

(utenti/index.php:351 already does it right in JS: cognome ? ${nome} ${cognome} : nome — that's the pattern the PHP sites should follow.) Cosmetic, but it's the direct blast radius of the new optional-surname path. Cleanest fix: a small full_name($u) helper, or wrap each in trim().

2. A newly-required custom field retroactively blocks existing users' profile saves · ux · ProfileController.php:178
Profile save validates against the active custom definitions. The moment an admin marks a custom field obbligatorio, every pre-existing user (and every admin-created user) who has no stored value is refused on any profile edit — e.g. changing their phone — until they fill the new field. Defensible as "required means required", but it bounces unrelated edits, and combined with #3 the user can't tell why.

3. Format-invalid custom values collapse into the generic "fill required fields" banner · ux · RegistrationController.php:88, ProfileController.php:178
A bad email/URL/number in a custom field redirects to the same ?error=missing_fields / ?error=required_fields message used for empty required fields — it never names the field or says "wrong format". A user who did fill the field is told to fill it in. Worth a distinct error code + message.

Reviewed and cleared

F001 — admin create/edit user doesn't run custom-field validation/save. Not a defect: crea_utente.php / modifica_utente.php deliberately don't render editable custom fields (they're shown read-only with "L'utente li gestisce dal proprio profilo"). Calling validate() there would break admin user creation whenever a required custom field exists. The only real residual is finding #2 above. No change recommended.

Below the gate (noted, not blocking)

  • JS addressField.required is nested inside if (phoneField) (crea_utente.php:193, modifica_utente.php:202) — defensive-only; #telefono always exists in both forms, so it works today. Tidier to set each field independently.
  • updateRegistrationSettings isn't atomic across toggle-writes vs. the custom-field transaction — toggles commit immediately, then the field batch runs in its own transaction; a field-batch failure leaves toggles applied under a generic "save failed" flash. Low impact (re-saving reconciles).
  • Stale comment in resolveEmailSettings() (SettingsController.php:430) still says the approval flag "is edited from this (email) form" — this PR moved it to the Registration tab.
  • Delete-a-custom-field is a plain checkbox with no confirmation despite cascading to every user's stored value (settings/index.php:364).

Security, migration versioning, escaping, and locale parity all check out. None of the confirmed findings is a merge blocker; #1 and #3 are the two I'd fix before release.

…s, custom-field UX)

Self-review of this branch surfaced three confirmed issues plus a few
lower-severity ones; all fixed here.

Confirmed:
- Optional surname left a trailing space. cognome='' (the new optional path)
  made 6 unchanged display sites render "Mario " via naive
  nome.' '.cognome. Added a reusable full_name() helper that collapses the
  gap and applied it at all six (utenti/index, admin/stats,
  settings/messages-tab, PrestitiController, two UsersController audit fields).
- A custom field made required AFTER signup walled existing users out of
  unrelated profile edits. RegistrationFields::validate() gained an
  $enforceRequired flag; the profile passes false so required is enforced at
  registration (the field's contract) but a blank pre-existing value no longer
  blocks a profile save. Format checks still apply to anything typed.
- Format-invalid custom values collapsed into the generic "fill required
  fields" banner. validate() now returns error_reason (missing|format) and both
  registration and profile map a format failure to a distinct
  custom_field_invalid code + message, so a user who DID fill the field isn't
  told to fill it.

Lower-severity:
- Un-nested the admin-form JS so addressField.required no longer depends on the
  phone field's DOM presence.
- updateRegistrationSettings now persists the custom-field batch first and bails
  before writing the toggles on failure, so a field-batch error never leaves
  toggles applied under a "save failed" flash. saveRegistrationCustomFields
  returns bool instead of signalling through the session.
- Deleting a custom field (cascades to every user's stored value) now asks for
  confirmation at submit.
- Fixed a stale comment in resolveEmailSettings that still pointed at the email
  form.

New i18n strings in all four locales (parity verified). Tests: migration unit
grows to 21 checks (error_reason, enforceRequired=false, full_name);
issue-255 E2E grows to 27 (distinct error code, no-trailing-space). PHPStan
L5 = 0, unit 38/38, full-test.spec.js 136 passed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Views/admin/stats.php`:
- Line 251: Sostituisci l’uso di HtmlHelper::e() con htmlspecialchars(...,
ENT_QUOTES, 'UTF-8') mantenendo invariato il valore già passato: aggiorna
app/Views/admin/stats.php:251, app/Views/settings/messages-tab.php:55 e
app/Views/utenti/index.php:83, inclusi i riferimenti qualificati con namespace.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 096e72bc-a9c8-42e2-aee0-27ba5023f09e

📥 Commits

Reviewing files that changed from the base of the PR and between 15aecd0 and 4b66056.

📒 Files selected for processing (22)
  • app/Controllers/PrestitiController.php
  • app/Controllers/ProfileController.php
  • app/Controllers/RegistrationController.php
  • app/Controllers/SettingsController.php
  • app/Controllers/UsersController.php
  • app/Support/RegistrationFields.php
  • app/Views/admin/stats.php
  • app/Views/auth/register.php
  • app/Views/profile/index.php
  • app/Views/settings/index.php
  • app/Views/settings/messages-tab.php
  • app/Views/utenti/crea_utente.php
  • app/Views/utenti/index.php
  • app/Views/utenti/modifica_utente.php
  • app/helpers.php
  • locale/de_DE.json
  • locale/en_US.json
  • locale/fr_FR.json
  • locale/it_IT.json
  • tests/issue-255-registration.spec.js
  • tests/migration-0.7.37.unit.php
  • tests/schema-integrity.spec.js

Comment thread app/Views/admin/stats.php Outdated
…ouched views

- issue-255-registration.spec.js dbQuery now prefers TCP host/port (CI's MySQL
  listens on 127.0.0.1, not the default socket) and falls back to the socket
  for local dev — mirrors schema-integrity.spec.js. The required CI step was
  red with "Can't connect through socket /var/run/mysqld/mysqld.sock".
- The three display sites I touched for the full_name() fix now escape with
  htmlspecialchars(..., ENT_QUOTES, 'UTF-8') instead of HtmlHelper::e(), per
  the view-escaping rule.
…e API + hardening

Extends the feature to the companion-app surface and hardens the admin editor.

Mobile API (mobile-api 1.2.2 → 1.3.0, requires_app 0.7.37):
- GET /api/v1/health advertises the built-in requirements (require_cognome/
  telefono/indirizzo) and the active custom-field definitions, so the app can
  render the right registration form.
- POST /api/v1/auth/register reaches web parity: built-in requirements follow
  the toggles, telefono/indirizzo are omitted (NULL) when optional and blank,
  custom fields are validated and saved in the same transaction as the account
  (1062 → email_exists), notifications run AFTER commit so a mail failure can't
  500 a created account, and requires_approval reflects the real setting.
- GET/PATCH /api/v1/me expose and update custom_fields with PATCH semantics
  (omitted ids preserved, an explicit empty value clears one field); also
  surface email_verificata + stato. OpenAPI schemas updated (incl. the
  tipo_utente enum fix: standard/premium/staff/admin).

Web hardening:
- Changing a custom field's TYPE is refused when it already has stored values
  (RegistrationFields::hasStoredValues + a DomainException whose message
  reaches the admin), preventing silent data corruption (e.g. text→number).
- Profile custom-field validation runs only when the section was actually
  submitted (custom_fields_present marker), so a partial profile POST never
  wipes or spuriously requires custom values.
- User detail page shows the stored custom values read-only.

New RegistrationFields helpers reused across both surfaces: apiDefinitions(),
editableFieldsForUser(), hasStoredValues().

Tests: new tests/registration-fields-255.unit.php (26 DB-backed checks over the
API helpers + the validate() enforceRequired matrix + save/read round-trip);
issue-255 E2E grows to 29 (no-block existing user, destructive type-change
guard); mobile-api E2E adds register-with-custom-fields + PATCH /me cases.
i18n parity across all four locales. PHPStan L5 = 0, unit 39/39, issue-255
29/29, mobile-api 76/76 + idempotency 41/41.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@storage/plugins/mobile-api/src/Controllers/AuthController.php`:
- Around line 299-304: Allinea lo stato creato dell’account al valore di
$requiresApproval: recupera questa impostazione prima dell’INSERT e assegna
stato='attivo' quando l’approvazione non è richiesta, mantenendo stato='sospeso'
quando è richiesta. Aggiorna il flusso di registrazione in AuthController senza
modificare il valore restituito di requires_approval.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9844c00c-4130-426d-b4bc-0c66591b87a4

📥 Commits

Reviewing files that changed from the base of the PR and between 4b66056 and 916a11c.

📒 Files selected for processing (24)
  • README.md
  • app/Controllers/ProfileController.php
  • app/Controllers/SettingsController.php
  • app/Controllers/UsersController.php
  • app/Support/RegistrationFields.php
  • app/Views/admin/stats.php
  • app/Views/profile/index.php
  • app/Views/settings/messages-tab.php
  • app/Views/utenti/dettagli_utente.php
  • app/Views/utenti/index.php
  • locale/de_DE.json
  • locale/en_US.json
  • locale/fr_FR.json
  • locale/it_IT.json
  • storage/plugins/mobile-api/MobileApiPlugin.php
  • storage/plugins/mobile-api/plugin.json
  • storage/plugins/mobile-api/src/Controllers/ActionsController.php
  • storage/plugins/mobile-api/src/Controllers/AuthController.php
  • storage/plugins/mobile-api/src/Controllers/HealthController.php
  • storage/plugins/mobile-api/src/Controllers/OpenApiController.php
  • tests/issue-255-registration.spec.js
  • tests/mobile-api-idempotency.spec.js
  • tests/mobile-api.spec.js
  • tests/registration-fields-255.unit.php

Comment thread storage/plugins/mobile-api/src/Controllers/AuthController.php
fabiodalez-dev added a commit that referenced this pull request Jul 17, 2026
…l-plugins packaging

- Settings: the #256 merge kept the email-test IIFE but dropped its HTML (the
  "Invia email di prova" input + button), leaving the script targeting elements
  that no longer existed. Restored the block in the Email panel; the
  /admin/settings/email/test route + sendTestEmail() were already present.
- FrontendController: add social_bluesky to the sanitized Organization sameAs
  (the earlier fix covered only four of the five socials).
- .rsync-filter: bundle every source plugin (`storage/plugins/*/**`) minus the
  premium scraping-pro, instead of a stale explicit list of five — this now
  matches BundledPlugins::LIST exactly (19), so the build verifier passes as the
  plugin set grows. Public uploads ship only the cover placeholder + directory
  sentinels, never installation data.
- build-release.sh: the package verifier rejects more dev artifacts
  (.code-review-graph, .playwright-*, phpstan.neon(+baseline), .coderabbit.yaml,
  .semgrepignore, graphify-out, test-upgrade) and asserts the uploads contents.
- Tests: new release-0.7.37-integration.spec.js guards the merged settings
  contract (email-test controls live only in the Email tab; the delete-confirm
  belongs to Registration; CSRF on the AJAX), wired into ci-e2e as required.
  social-links and issue-255 teardowns now snapshot+restore the exact pre-suite
  state (row absence included) instead of blanket-deleting.
@fabiodalez-dev
fabiodalez-dev merged commit 8fb4a9b into main Jul 17, 2026
10 checks passed
Himura2la pushed a commit to hackerembassy/Pinakes that referenced this pull request Jul 17, 2026
…-rc.1; rename fabiodalez-dev#255 migration test to -rc.1

Bundling fabiodalez-dev#253+fabiodalez-dev#254+fabiodalez-dev#256 into 0.7.37-rc.1 moves version.json past fabiodalez-dev#237's
migration, so issue-237-regression's statement-count + enum-drift checks (which
are specific to the contributor migration) now pin to migrate_0.7.36-rc.1.sql
instead of "the release migration". migration-0.7.37.unit.php is renamed to
migration-0.7.37-rc.1.unit.php (matching the RC-named migration) and its
internal file reference updated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configurable registration fields

1 participant