docs: RentCharge 007.2026 money privacy + profile status

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Roger 2026-09-19 18:19:56 +00:00
parent 3211342ac1
commit 06acaff8a8
11 changed files with 396 additions and 3 deletions

View file

@ -0,0 +1,95 @@
# SPEC — Users, Access Matrix, registration, sessions and security
**Date:** 2026-09-19
**Status:** hardened Users + Access Matrix; **SMTP configurado e validado (2026-09-19)**.
**Module:** `backend/app/modules/users` · Admin UI `/users` · `/users/access`
## Design references (not a framework swap)
Patterns adapted from production FastAPI auth libraries (kept in-house RBAC matrix):
- [fastapi-fullauth](https://github.com/mdfarhankc/fastapi-fullauth) — lockout, hashed reset tokens, email verification/reset, RBAC permissions, anti-enumeration
- [AuthWarden](https://github.com/timihack/authwarden) — single-use reset tokens, OTP attempt limits, brute-force lockout
- OWASP Authentication / Session / Password-reset cheat sheets
RentCharge keeps its own SQLite models, cookie sessions and **permissions-first Access Matrix** (`module.view.action`).
## Functional scope (both sides)
### A — Self-service (`/login`, `/signup`, `/reset-password`, `/account`)
1. Public registration with strong password + email OTP verification.
2. Forgot-password → single-use email token (SHA-256 stored, 1 hour).
3. Account profile, email verification, password change with strength meter.
4. Forced password change when `must_change_password` is set.
### B — Admin Users + Access Matrix (`/users`, `/users/access`)
1. Create staff logins with **generated strong passwords** (not shared starter defaults).
2. Edit login (username, nickname, display name, recovery email).
3. Set password (modal + strength meter) **or** email a reset token.
4. Require password change on first login / after admin reset.
5. Role assignment + full Role × Permission matrix with dirty-state and default-drift highlight.
6. Sessions revoke + security activity audit.
## Security controls
- **Password rules are chosen by Admin** under `/users/password-policy` (not hard-coded).
Presets: PIN 6+ digits · alphanumeric · strong · complex · custom toggles
(uppercase / lowercase / digit / special / min length / block nick-email / block common).
- Passwords: bcrypt cost 12; validation always follows the saved admin policy.
- Account lockout after 5 consecutive failed logins (15 minutes) + IP/identity rate limits.
- Reset tokens: 256-bit random, stored as SHA-256 only, single-use, invalidate previous tokens.
- Registration OTP: HMAC-SHA256, 10 min, max 5 attempts.
- Sessions: JWT + server-side session id; HttpOnly Secure SameSite=Strict cookie; idle + absolute timeouts.
- Logout / password change / reset / disable revoke sessions.
- Generic public responses to reduce account enumeration.
- Passwords and raw tokens never written to `auth_events`.
## New / updated API
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/auth/password-policy` | Policy hint for UIs |
| POST | `/api/auth/password-strength` | Strength score helper |
| POST | `/api/users/{id}/send-password-reset` | Admin emails recovery token |
| GET | `/api/users/generate-password` | Cryptographically strong starter |
| PUT | `/api/users/{id}` | Now accepts `username` (edit login) |
| POST | `/api/users` | `must_change_password`, `send_invite_email` |
| POST | `/api/users/{id}/set-password` | `must_change_password` flag |
## Database (additive)
`users` columns:
- `must_change_password`
- `failed_login_count`
- `locked_until`
## Mail transport (VM500)
**Validated 2026-09-19** — OTP verification and password-reset emails delivered.
| Setting | Value |
|---------|--------|
| Host | `mail.myvexx.com` |
| Port | `465` (SMTP SSL) |
| User | `admin@myvexx.com` |
| From | `Rent & Charge <admin@myvexx.com>` |
| Config | `/home/mini/rentcharge/config/rentcharge.env` (symlink `/etc/rentcharge.env`) |
| Also | Postfix on VM500 relays `[mail.myvexx.com]:465` with SASL |
App flags: `SMTP_SSL=true`, `SMTP_STARTTLS=false`. Credentials stay only in env (never in Git).
E2E checks: `POST /api/auth/forgot-password` → HTTP 200; inbox received **Rent & Charge password reset** and **Your Rent & Charge verification code**.
## Verification checklist
- [ ] Weak passwords rejected on register / change / admin set
- [ ] Generate strong password fills create form
- [ ] Edit login updates username + email
- [x] Email reset from Users works when SMTP configured
- [ ] Forced change redirects to `/account`
- [ ] Access matrix dirty badge + amber default-drift cells
- [ ] Backend tests: `python -m unittest backend.tests.test_auth_users`

View file

@ -0,0 +1,42 @@
# SPEC — Money privacy (eye button)
**Produto:** Rently / RentCharge
**Data:** 2026-09-19
**Status:** implemented (prod + test)
**Versão:** 007.2026
**Operador:** Roger
**Relacionados:** `SPEC-SYSTEM-DASHBOARD.md` · `SPEC-SESSION-2026-09-19.md`
---
## Purpose
Operators often work in open offices. Euro amounts start **hidden** (`€ ••••`). One control (header + page toolbars) reveals or hides **all** sensitive amounts together. After **30 seconds** idle, amounts hide again.
## UI
| Control | Location |
|---------|----------|
| Show € / Hide € | AppShell header (all authenticated pages) |
| Same button | Page toolbars (Dashboard, Residents, Mapa, Receivables, …) |
## Technical contract
1. Module flag: `frontend/src/lib/moneyFormat.ts` — `euro()`, `privacyMoneyLabel()`, `setMoneyHiddenFlag()`.
2. Flag changes **notify** subscribers (`subscribeMoneyHidden`).
3. React: `useMoneyPrivacyRender()` / `useMoneyPrivacy()` use `useSyncExternalStore` so components re-render when the eye toggles.
4. **Why not only AppShell?** Next.js App Router does not re-render the page slot when a Client layout updates state. Every page that displays euros must subscribe (or use a child that does).
## Rules
- Print / statement handouts use `euroVisible()` (never masked).
- Counts and percentages are not masked.
- Default on load: **hidden**.
## Acceptance
1. Dashboard: eye toggles amounts.
2. `/residents`, `/mapa`, `/receivables`, resident detail: same toggle updates amounts without navigation.
3. Idle ~30s → amounts hide again.
4. Prod (`:3000`) and test (`:3010`) behave the same.

View file

@ -0,0 +1,58 @@
# SPEC — Resident profile status flags + audit
**Produto:** Rently / RentCharge
**Data:** 2026-09-19
**Status:** implemented (prod + test)
**Versão:** 007.2026
**Operador:** Roger
**UI:** `/residents/{id}` Setup · Settings → Edit resident
---
## Flags (boolean on `residents`)
| UI label | Column |
|----------|--------|
| Temporary | `is_temporary` |
| Deceased | `is_deceased` |
| In custody | `is_in_custody` |
| In treatment | `is_in_treatment` |
(`is_active` remains a separate system flag; not part of this audit log.)
## Audit log
Table `resident_status_events` (append-only):
| Column | Meaning |
|--------|---------|
| `resident_id` | FK |
| `flag` | e.g. `is_temporary` |
| `value` | `true` = checked / On · `false` = unchecked / Off |
| `changed_at` | UTC store |
| `changed_by_user_id` / `changed_by_username` | actor |
**History is never deleted** when a box is unchecked — a new Off event is appended.
## Display
Beside each of the four checkboxes:
- Latest event: `On|Off dd/mm/yyyy HH:mm · username` (formatted **Europe/Dublin**)
- Full recent history in the **tooltip** (`title`)
Helper: `frontend/src/lib/statusFlags.ts`.
## API
- `ResidentOut.status_flags`: map flag → `{ last, history }` on get/create/update when `include_status_flags=True`.
- `GET /api/residents/{id}/status-history` — same map.
- Create/update log diffs for the four flags with the authenticated user.
## Acceptance
1. Check Temporary → stamp + user appear beside the box after save.
2. Uncheck → Off event logged; history still available on hover.
3. Settings edit shows the same stamps (loads full resident via `getResident`).
4. Prod + test.

View file

@ -0,0 +1,57 @@
# SPEC — Residents hub (`/people`)
**Produto:** Rently / RentCharge
**Data:** 2026-09-13
**Status:** implementado
**Operador:** Roger
**Relacionados:** `SPEC-SYSTEM-DASHBOARD.md` · Statement service · `SPEC-STATEMENT-PUBLIC-URL.md`
---
## Problema
A pesquisa do header enviava para o **Dashboard**, que não é uma vista de residente. Existiam listas em Control of Rent / Arrears / Maps, mas **não** um hub de consulta/seleção/edição rápida.
## Solução
| Item | Valor |
|------|--------|
| Rota | `/people` |
| Menu | **Residents** (antes de Control of Rent Payments) |
| Header search | → `/people?q=…` |
| Layout | Header estilo ROI Finance; lista à esquerda; painel à direita |
## Painel do residente seleccionado
- Rent arrears / debt **YTD** (mesmas regras do Statement)
- Semanas pagas / overdue no ano (semanas de calendário, incl. sem linha SAGE)
- Credit Book (débito/crédito)
- Acordos abertos
- Último pagamento
- Quick edit: room, notes, active
- Links: full record `/residents/{id}`, Statement, Add new resident
## Regra de cálculo (obrigatória)
**Não** somar só semanas com ficheiro SAGE. Usar `build_statement` / `fetchStatementJson` YTD (`01-01` → hoje), igual ao Statement impresso.
Exemplo validado: **John J** D59 SAGE 411 → **£513** (não £62).
Backend: `enrich_resident` sem `date_from/to` também usa Statement YTD para `total_debt` / `year_debit`.
## Ficheiros
- `frontend/src/app/people/page.tsx`
- `frontend/src/components/AppShell.tsx`, `frontend/src/lib/access.ts`, `frontend/src/lib/api.ts` (`fetchStatementJson`)
- `backend/app/main.py` (`enrich_resident`)
## Critério de aceite
1. Pesquisa header → Residents hub.
2. John J e amostra de 3 residentes: debt = Statement YTD.
3. Prod + test (mesmo frontend).
## Profile status (007.2026)
Flags Temporary / Deceased / In custody / In treatment on the full resident record (`/residents/{id}`) and Settings edit — with audit stamps. See `SPEC-RESIDENT-PROFILE-STATUS.md`.

View file

@ -2,7 +2,7 @@
**Status:** implemented · **Operador:** Roger
**Ambientes:** prod + test (VM500)
**Versão:** **006.2026**
**Versão:** **006.2026** → noite **007.2026**
---
@ -65,3 +65,57 @@
- `npm run build` prod + test (`NEXT_PUBLIC_APP_ENV=test` no test)
- `systemctl restart` `rentcharge-{backend,frontend}` + `rentcharge-test-*`
- Specs → Forgejo `myvexx/rently` + Obsidian CT130 via `scripts/sync_obsidian.py`
---
## Continuação (noite 2026-09-19) — versão **007.2026**
### 7. Money privacy (olho €) — app-wide
**Problema:** o botão Show € / Hide € só actualizava o Dashboard. Nas outras páginas os valores ficavam congelados (Next.js App Router: o layout/AppShell re-renderiza, mas o slot da página não).
**Solução:**
- `moneyFormat.ts` — pub/sub (`subscribeMoneyHidden` / `setMoneyHiddenFlag` notifica listeners)
- `MoneyPrivacy.tsx` — `useMoneyPrivacyRender()` via `useSyncExternalStore`; `useMoneyPrivacy()` também subscreve
- Todas as páginas/componentes que mostram euros chamam o hook (Residents, Mapa, Receivables, ROI, Reports, ficha residente, KPIs, QuickPost, …)
- Idle 30s continua a esconder montantes
- Deploy: **prod + test** (VM500)
### 8. Profile status — audit log (4 flags)
Checkboxes na ficha do residente + Settings edit:
| Flag | Campo |
|------|--------|
| Temporary | `is_temporary` |
| Deceased | `is_deceased` |
| In custody | `is_in_custody` |
| In treatment | `is_in_treatment` |
- Tabela append-only `resident_status_events` (flag, value on/off, `changed_at`, `changed_by_user_id`, `changed_by_username`)
- Ao lado de cada checkbox: último evento `On|Off dd/mm/yyyy HH:mm · user` (Europe/Dublin)
- Histórico completo no tooltip; mantém eventos ao desmarcar
- API: `status_flags` em `ResidentOut` (get/update/create); `GET /api/residents/{id}/status-history`
- UI: `/residents/[id]` + `/settings` edit modal
### 9. SMTP / auth mail (já em AUTH-USERS-SPEC)
- `mail.myvexx.com:465` SSL · `admin@myvexx.com` · E2E forgot-password + OTP OK
- Credenciais só em `rentcharge.env` (não no Git)
### 10. Cybersec hardening (F2–F6 + F1 scrub docs)
- TLS verify no envio de email; cookie-only JWT web; path reset `/reset-password/{token}`; tips anti-phishing nos templates; docs sem passwords em claro
## Ficheiros (007)
- `frontend/src/lib/moneyFormat.ts`, `components/MoneyPrivacy.tsx`, páginas com euros
- `frontend/src/lib/statusFlags.ts`, `app/residents/[id]/page.tsx`, `app/settings/page.tsx`
- `backend/app/database.py` (`ResidentStatusEvent`), `schemas.py`, `main.py`
- `docs/SPEC-MONEY-PRIVACY.md`, `docs/SPEC-RESIDENT-PROFILE-STATUS.md`, `docs/releases/007.2026.md`
## Deploy 007
- `npm run build` + restart `rentcharge-*` e `rentcharge-test-*`
- Specs → Forgejo `myvexx/rently` + Obsidian CT130

View file

@ -0,0 +1,51 @@
# SPEC — System Dashboard
**Date:** 2026-09-09
**Status:** implemented on VM500 (`/`)
**Language:** English UI
## Purpose
The Dashboard is the system's presentation and navigation page. It summarizes the complete resident group and does not duplicate the Residents data grid.
## Route separation
- `/` — consolidated system Dashboard. Do not put the Cash (SA) grid here.
- `/residents` — **Rent Payments Control**: complete resident register and Cash (SA) account grid.
- `/residents/{id}` — individual resident account dashboard.
- The sidebar item **Rent Payments Control** links to `/residents`; **Dashboard** remains `/`.
## Branding
- The Salvation Army shield (`/salvation-army-shield.svg`) replaces the red `SA` badge in the **sidebar** (white circular background). It is not shown next to the Rent Payments Control page title.
## Dashboard content
- Current statement-week hero: collection rate, cash, due, paid, part-paid and unpaid resident counts.
- Residents: all records, currently housed, former/no-room records, occupied rooms and complete weekly due.
- Collections and arrears: expected, cash collected, payment count, collection gap and outstanding balance.
- Agreements and balances: active agreements, agreement balance, overdue installments and new rent arrears.
- Data quality: missing SAGE IDs, room conflicts, duplicate names and missing charge bands.
- System navigation cards: Residents, Account Receivables, Payments MAP WEEK, ROI Finance, Arrears and Reports.
## Data contract
- Totals use the complete API result and are not affected by UI pagination or search filters.
- Current-week collection figures use the same Payment Map report service as `/mapa`.
- Financial values are displayed in full EUR format; counts are displayed as complete integers.
- Every metric card links to its relevant operational page or report.
- The Dashboard contains no Cash (SA) transaction table.
## Visual language
- Hero band: donut gauge for the cash collection rate (green track `#dcefe5`, arc `#0f6b4f`, end dot), a vertical rule, then icon-chip statistics for total collected and total due.
- Hero right block: 2x2 tile grid separated by hairlines, each tile with an icon chip — Residents (blue), Paid (green filled check), Part paid (amber clock), Unpaid (red alert).
- Metric cards: horizontal layout with a rounded `9px` tinted icon chip on the left and label, value and note stacked on the right. No corner arrow.
- Section headers: bold 13px title on the left, muted 10px context note right-aligned.
- Colour is semantic: green for healthy/collected, amber for review, red for arrears and exceptions, blue for neutral volume.
## Money privacy (007.2026)
- Header and Dashboard toolbars include **Show € / Hide €**.
- Amounts use `privacyMoneyLabel` / `euro()` with app-wide subscription — see `SPEC-MONEY-PRIVACY.md`.
- Default: amounts hidden until the operator reveals them.

View file

@ -223,6 +223,14 @@ Templates HTML em `backend/app/templates/` (`letter_debt.html`, `letter_agreemen
Porta 22 Tailscale do big = `SSH-2.0-Tailscale`, não OpenSSH. `ssh.exe` timeout no KEX; paramiko funciona. WAN 51.171.219.235:22/80/443 fechados neste contexto. Acesso público do **app** é pelo túnel Cloudflare, não por abrir WAN.
## Atualização 2026-09-19 noite (007.2026)
- Money privacy (olho €) app-wide — `SPEC-MONEY-PRIVACY.md`
- Profile status audit (Temporary / Deceased / Custody / Treatment) — `SPEC-RESIDENT-PROFILE-STATUS.md`
- SMTP auth mail validado — `AUTH-USERS-SPEC.md`
- Prod + test em VM500 alinhados para estas features
## Checklist agente
1. Hop só no **big**, nunca big1 para esta VM.

View file

@ -1,6 +1,9 @@
# RentCharge version
**Current: 006.2026** — 2026-09-19
**Current: 007.2026** — 2026-09-19
## 007.2026
Money privacy eye works app-wide (pub/sub + `useMoneyPrivacyRender` on every euro page — Next.js layout slot fix); resident Profile status audit log (4 flags, Dublin stamp + user beside checkbox, full history kept); prod+test deploy. Specs: `SPEC-MONEY-PRIVACY.md`, `SPEC-RESIDENT-PROFILE-STATUS.md`, session 2026-09-19 night section.
## 006.2026
Public Txn short form for ancillary cash (`000084-ME`, `000010-EL`, … — 9 chars `NNNNNN-AA`); rent `txn_hash` at post; Week Payments Maps — All cash payments (Hash/Ticket/Txn/Rent·Other·Total) + Residents in period (weekly due / cash done / 6 prior weeks); Invoice/Ticket **Required** flag on all +Add payment modals (+ edit); People search pulse on All Residents; table titles Payment history / Remittance by account; legacy CS-/long ME- rewrite + EOD refresh (prod + test). Specs: `SPEC-TRANSACTION-IDS.md`, `SPEC-SESSION-2026-09-18.md`, `SPEC-SESSION-2026-09-19.md`.

View file

@ -0,0 +1,14 @@
# Release 007.2026 — 2026-09-19
## Summary
- Money privacy eye works on **all** pages (not only Dashboard).
- Resident profile status flags keep a full On/Off history with user + Dublin datetime beside each checkbox.
- Deployed to **production and test** on VM500.
## Specs
- `SPEC-MONEY-PRIVACY.md`
- `SPEC-RESIDENT-PROFILE-STATUS.md`
- `SPEC-SESSION-2026-09-19.md` (night section)
- `VERSION.md` → Current 007.2026

View file

@ -1,6 +1,9 @@
# RentCharge version
**Current: 006.2026** — 2026-09-19
**Current: 007.2026** — 2026-09-19
## 007.2026
Money privacy eye works app-wide (pub/sub + `useMoneyPrivacyRender` on every euro page — Next.js layout slot fix); resident Profile status audit log (4 flags, Dublin stamp + user beside checkbox, full history kept); prod+test deploy. Specs: `SPEC-MONEY-PRIVACY.md`, `SPEC-RESIDENT-PROFILE-STATUS.md`, session 2026-09-19 night section.
## 006.2026
Public Txn short form for ancillary cash (`000084-ME`, `000010-EL`, … — 9 chars `NNNNNN-AA`); rent `txn_hash` at post; Week Payments Maps — All cash payments (Hash/Ticket/Txn/Rent·Other·Total) + Residents in period (weekly due / cash done / 6 prior weeks); Invoice/Ticket **Required** flag on all +Add payment modals (+ edit); People search pulse on All Residents; table titles Payment history / Remittance by account; legacy CS-/long ME- rewrite + EOD refresh (prod + test). Specs: `SPEC-TRANSACTION-IDS.md`, `SPEC-SESSION-2026-09-18.md`, `SPEC-SESSION-2026-09-19.md`.

View file

@ -223,6 +223,14 @@ Templates HTML em `backend/app/templates/` (`letter_debt.html`, `letter_agreemen
Porta 22 Tailscale do big = `SSH-2.0-Tailscale`, não OpenSSH. `ssh.exe` timeout no KEX; paramiko funciona. WAN 51.171.219.235:22/80/443 fechados neste contexto. Acesso público do **app** é pelo túnel Cloudflare, não por abrir WAN.
## Atualização 2026-09-19 noite (007.2026)
- Money privacy (olho €) app-wide — `SPEC-MONEY-PRIVACY.md`
- Profile status audit (Temporary / Deceased / Custody / Treatment) — `SPEC-RESIDENT-PROFILE-STATUS.md`
- SMTP auth mail validado — `AUTH-USERS-SPEC.md`
- Prod + test em VM500 alinhados para estas features
## Checklist agente
1. Hop só no **big**, nunca big1 para esta VM.