Commit Graph

5 Commits

Author SHA1 Message Date
roof 649378b59b fix: resolve all Cell Identity banner and cert issues
Unit Tests / test (push) Successful in 7m17s
Four bugs fixed:

1. Banner delay (up to 5 s): DraftConfigContext now exposes isDirty as
   reactive useState so App.jsx re-renders immediately when any section
   marks itself dirty, instead of waiting for the next checkPending() poll.

2. Banner re-triggers after Apply (race): For non-'*' container restarts
   (e.g., cell_name → DNS restart) the background thread took ~300 ms to
   clear _pending_restart. A concurrent checkPending() poll could see
   needs_restart=True and overwrite the frontend's optimistic clear.
   Fix: set needs_restart=False and applying=True synchronously before
   spawning the thread.

3. Apply showed banner during applyPending() when hasDirty()==false:
   setApplyStatus('saving') was skipped for the auto-save-then-apply
   path, leaving applyStatus=null while applyPending() ran and the
   banner stayed visible. Always set 'saving' before applyPending().

4. Cert status always 'unknown' in pic_ngo mode: _check_cert_via_ssl
   connected to cell-caddy:443 but sent SNI='cell-caddy'. Caddy finds no
   matching cert and returns nothing. Fix: pass the effective public
   domain (e.g. pic1.pic.ngo) as SNI so Caddy returns the right cert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 04:17:56 -04:00
roof ec8995d41e fix: Cell Identity changes now show Configuration changes pending banner
Unit Tests / test (push) Successful in 7m26s
DraftConfig dirty state (set when any Cell Identity field changes) was
tracked in refs but never checked by the banner, which only looked at
backend pending state. Cell name changes in pic_ngo mode intentionally
block auto-save (to prevent premature DDNS re-registration), so the
backend never marked pending and the banner never appeared.

Fix: show the banner when hasDirty() is true in addition to backend
pending. Add clearAllDirty() to DraftConfigContext so Cancel immediately
clears frontend dirty state without waiting for the next 5-second poll.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:17:51 -04:00
roof 8650704316 feat: add authentication and authorization system
Backend:
- AuthManager (api/auth_manager.py): server-side user store with bcrypt
  password hashing, account lockout after 5 failed attempts (15 min),
  and atomic file writes
- AuthRoutes (api/auth_routes.py): Blueprint at /api/auth/* — login,
  logout, me, change-password, admin reset-password, list-users
- app.py: register auth_bp blueprint; add enforce_auth before_request
  hook (401 for unauthenticated, 403 for wrong role; only active when
  auth store has users so pre-auth tests remain green); instantiate
  AuthManager; update POST /api/peers to require password >= 10 chars
  and auto-provision email + calendar + files + auth accounts with full
  rollback on any failure; extend DELETE /api/peers to tear down all
  four service accounts; add /api/peer/dashboard and /api/peer/services
  peer-scoped routes; fix is_local_request to also trust the last
  X-Forwarded-For entry appended by the reverse proxy (Caddy)
- Role-based access: admin for /api/* (except /api/auth/* which is
  public and /api/peer/* which is peer-only)
- setup_cell.py: generate and print initial admin password, store in
  .admin_initial_password with 0600 permissions; cleaned up on first
  admin login

Frontend:
- AuthContext.jsx: React context with login/logout/me state and Axios
  interceptor for automatic 401 redirect
- PrivateRoute.jsx: route guard component
- Login.jsx: login page with error handling and must-change-password
  redirect
- AccountSettings.jsx: change-password form for any authenticated user
- PeerDashboard.jsx: peer-role landing page (IP, service list)
- MyServices.jsx: peer service links page
- App.jsx, Sidebar.jsx: AuthContext integration, logout button,
  PrivateRoute wrappers, peer-role routing
- Peers.jsx, WireGuard.jsx, api.js: auth-aware API calls

Tests: 100 new auth tests all pass (test_auth_manager, test_auth_routes,
test_route_protection, test_peer_provisioning). Fix pre-existing test
failures: update WireGuard test keys to valid 44-char base64 format
(test_wireguard_manager, test_peer_wg_integration), add password field
and service manager mocks to test_api_endpoints peer tests, add auth
helpers to conftest.py. Full suite: 845 passed, 0 failures.

Fixed: .admin_initial_password security cleanup on bootstrap, username
minimum length (3 chars enforced by USERNAME_RE regex)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:00:06 -04:00
roof 768571f2b7 feat: port conflict validation, autosave on Apply, extended integration tests
Port conflict validation:
- api/port_registry.py: detect_conflicts() checks all service sections for shared port values
- api/app.py: returns HTTP 409 on port conflict after existing range validation
- webui/src/pages/Settings.jsx: JS-side detectPortConflicts() with useMemo shows inline
  conflict errors and blocks Save before the request is made; catch blocks surface server
  error messages (including 409) instead of generic fallbacks

Config autosave on Apply:
- webui/src/contexts/DraftConfigContext.jsx: new context; Settings registers flush callbacks
  per section; App calls flushAll() before applyPending() when any section is dirty
- webui/src/App.jsx: wraps tree with DraftConfigProvider, handleApply shows 'saving' banner
  state and awaits flushAll()
- webui/src/pages/Settings.jsx: registers identity + per-service flushers; propagates dirty
  state into context via setDirty; uses refs to avoid stale closures

Extended integration test coverage (114 new tests):
- tests/integration/test_config_api.py: GET/PUT config, export, import, backup lifecycle
- tests/integration/test_network_services.py: DNS records + DHCP reservations CRUD
- tests/integration/test_containers.py: list, restart, logs, stats; recovery polling
- tests/integration/test_negative_scenarios.py: error-path coverage for all endpoints
- tests/test_port_conflicts.py: 20 unit tests for port_registry.detect_conflicts()

Pre-commit hook updated to skip tests/integration/ (live-stack tests require a running
stack and must be run explicitly via `make test-integration`).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 04:45:47 -04:00
roof 1f3386d43b fix: all service pages use live domain; cell_name/domain propagate to DNS; /api/status reads stored identity
Changes:
- ConfigContext.jsx: React context that loads /api/config once; exposes domain,
  cell_name, refresh() — wraps entire app in App.jsx
- Email/Calendar/Files pages: replace hardcoded 'mail.cell', 'calendar.cell',
  'files.cell', 'webdav.cell' with domain from ConfigContext; hostname updates
  immediately after Settings save (refreshConfig() called on save)
- /api/status: cell_name and domain now read from stored _identity in config_manager,
  not hardcoded 'personal-internet-cell' / 'cell.local'
- network_manager.apply_cell_name(old, new): updates hostname A-record in primary
  zone file and reloads CoreDNS; called from PUT /api/config when cell_name changes
- Old identity captured before save so apply_cell_name gets the correct old value
- Settings EmailForm: smtp/imap ports are read-only with note (docker-compose.yml level)
- Settings FilesForm: port is read-only with note (Caddy proxies on 80 externally)
- Settings CalendarForm: port labeled "Internal port; clients use 80 via Caddy"

Tests added:
- test_apply_cell_name_renames_host_record
- test_apply_cell_name_noop_when_same

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 05:05:51 -04:00