diff --git a/CHANGELOG.md b/CHANGELOG.md index a8086e28..63a687a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **OIDC refresh-token consumption — silent session renewal + revocation propagation** (opt-in, provider-agnostic): building on the OIDC login above, PoracleWeb can now optionally consume the provider's **refresh token** instead of discarding it, so an SSO session renews silently in the background (no 24-hour hard re-login) and a disable/logout at the provider propagates to PoracleWeb within one short access-token lifetime. It is **off by default** (`OIDC_USE_REFRESH_TOKENS=false`) — existing deployments and providers that don't issue refresh tokens are completely unaffected (the login cleanly falls back to a standard full-lifetime session). The provider refresh token is brokered **entirely server-side**: it's encrypted at rest with DataProtection in a new `oidc_sessions` table (added via EF migration `AddOidcSessions`) and **never sent to the browser**; the browser instead holds an opaque PoracleWeb token in `localStorage` that keys a rotation **family**. A new `POST /api/auth/oidc/refresh` endpoint redeems the stored refresh token against the provider, **re-validates the user live** (existence, `enable_oidc` gate, role access, admin-disable) on every refresh, rotates both tokens, and family-revokes on replay/reuse or when the provider rejects the refresh (revocation propagation); `POST /api/auth/oidc/refresh/revoke` ends a session on logout, and an `OidcSessionCleanupService` reaps expired/stale rows. Refresh-backed OIDC sessions get a short **per-login** JWT (`OIDC_ACCESS_TOKEN_MINUTES`, default 30) while Discord/Telegram/local logins keep the 24-hour JWT — the lifetime override is scoped so non-refresh logins aren't shortened. The implementation is **fully OIDC-provider-agnostic**: `OIDC_OFFLINE_ACCESS_SCOPE` (default `offline_access`) is appended to the authorize request so standards-compliant providers issue a refresh token; `OIDC_TOKEN_AUTH_METHOD` supports both `client_secret_post` and `client_secret_basic`; non-rotating providers (no new refresh token on refresh) are handled by carrying the prior token forward; and nothing relies on discovery/JWKS/`id_token`. The frontend adds a single-flight `TokenStoreService` + an `oidcRefreshInterceptor` (proactive pre-expiry refresh and reactive 401-retry, with a null-refresh-token guard so every non-refresh login keeps the existing "401 → logout" path). Refresh on/off is controlled solely by the `OIDC_USE_REFRESH_TOKENS` env flag — there is intentionally **no** runtime admin toggle, since refresh is coupled to the per-login JWT lifetime (disabling it mid-session would strand already-issued short-lived tokens); its active state is surfaced read-only on `GET /api/auth/providers` (`oidc.refresh`) and `GET /api/settings/oidc-config`. New `OIDC_*` keys documented in `.env.example` with a per-provider config matrix (PogoAlerts, Keycloak, Authentik, Auth0, Google, Azure AD/Entra, Okta), and a full **OIDC Refresh Tokens** documentation page (configuration reference, five Mermaid flow diagrams, the provider matrix, and the security model) added to the docs site. Backend tests cover the session rotation/replay/cap/cleanup mechanics and the provider-agnostic client (auth method, optional/non-rotating refresh tokens); frontend tests cover the token store's single-flight refresh and the interceptor's proactive/reactive/loop-guard behavior. ### Changed +- **Removed stale AutoMapper references from the docs site** ([#241](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/241)): AutoMapper was dropped in v2.6.0 ([#173](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/173)) in favour of manual mapping extensions, but four docs pages still described it as the live mapping layer — `architecture/backend.md` even carried a `.ForAllMembers(opts => opts.Condition(...))` snippet that exists nowhere in the codebase. The mapping sections in `architecture/overview.md` and `architecture/backend.md` now describe `AlarmMappingExtensions` (`To*()` / `ApplyUpdate()`) and `EntityMappingExtensions` (`ToModel()` / `ToEntity()` / `ApplyTo()`), with a real `ApplyUpdate` snippet showing the explicit null-skip guards; the `Core.Mappings/` line in the solution tree, the test-coverage bullet in `development/testing.md`, and a passing mention in `architecture/poracleng-proxy.md` are corrected to match. The last piece of AutoMapper residue outside the docs goes with it: the mapping test file was still named `PoracleMappingProfileTests.cs` while the class inside it had been renamed to `MappingExtensionTests`, so the file is renamed to match. No behaviour change. - **Localized the external SSO / OIDC strings** across all bundled locales. The SSO login feature added 30 i18n keys to English only, so every non-English locale fell back to English for the "Sign in with {provider}" button, the signed-out panel, the OIDC error messages, and the admin Authentication / External SSO settings group. These are now translated into Danish, German, Spanish, French, Italian, Dutch, Polish, Portuguese (PT & BR), and Swedish. Translation-only — no code or behavior change. - **Admin Server Settings page UX overhaul.** Adds a live **search/filter** (sticky bar, match highlighting, `/` or Ctrl/Cmd+K to focus), a **sticky save + discard bar** so saving is always reachable on the long page, **sign-in providers grouped under Authentication** (Telegram/Discord moved up), and **collapsible sections** (persisted) with per-section "unsaved" chips and state summaries (e.g. "7 of 9 enabled"). Headline fix: the alarm-type/feature toggles were a confusing **double negative** ("Disable X", ON = feature off) mixed with positive `enable_*` toggles; they are now **uniformly positive** (ON = enabled, labels are the feature name, descriptions are "Let users …"). The stored `disable_*` keys are **unchanged** — a presentation-only inversion — so backend feature-gating is unaffected. New UX i18n keys and the reframed positive labels/descriptions are translated across all 11 locales. diff --git a/Tests/Pgan.PoracleWebNet.Tests/Mappings/PoracleMappingProfileTests.cs b/Tests/Pgan.PoracleWebNet.Tests/Mappings/MappingExtensionTests.cs similarity index 100% rename from Tests/Pgan.PoracleWebNet.Tests/Mappings/PoracleMappingProfileTests.cs rename to Tests/Pgan.PoracleWebNet.Tests/Mappings/MappingExtensionTests.cs diff --git a/docs/architecture/backend.md b/docs/architecture/backend.md index 5b82a2b3..644c4d57 100644 --- a/docs/architecture/backend.md +++ b/docs/architecture/backend.md @@ -29,15 +29,23 @@ PoracleNG's tracking POST endpoint handles both creates and updates. When the re !!! note "`BaseRepository` removed" The generic `BaseRepository` and all alarm repository classes have been removed. `EnsureNotNullDefaults()` is no longer needed -- PoracleNG handles NULL defaults for alarm writes, and the remaining repositories handle null normalization as needed. -## AutoMapper (non-alarm entities only) +## Mapping extensions -AutoMapper is used for `humans` and `profiles` entities. Alarm tracking data flows as raw JSON through the PoracleNG API proxy and does not use AutoMapper. +Mapping is done with static extension methods in `Core.Mappings/`. There is no AutoMapper dependency. -All `*Update` models for non-alarm entities use **nullable `int?`** properties so partial updates don't zero out unset fields. +`AlarmMappingExtensions` covers the alarm DTOs: `To*()` builds a model from a `*Create` DTO (`create.ToMonster()`), and `ApplyUpdate()` merges a `*Update` DTO onto an existing model (`update.ApplyUpdate(existing)`). + +`EntityMappingExtensions` covers `Human`, `Profile`, and the `poracle_web`-owned entities (user geofences, site settings, webhook delegates, quick picks) with `ToModel()`, `ToEntity()`, and `ApplyTo()`. + +All `*Update` models use **nullable** properties so partial updates don't zero out unset fields. `ApplyUpdate` skips nulls explicitly: ```csharp -// The mapping profile skips null properties -.ForAllMembers(opts => opts.Condition((_, _, srcMember) => srcMember != null)) +public static void ApplyUpdate(this MonsterUpdate src, Monster dest) +{ + if (src.Ping != null) dest.Ping = src.Ping; + if (src.Distance != null) dest.Distance = src.Distance.Value; + // ... one guarded assignment per field +} ``` ## Alarm field defaults diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 67ace4b2..a649d60b 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -19,7 +19,7 @@ Pgan.PoracleWebNet.slnx ├── Core/ │ ├── Core.Abstractions/ Interfaces (IService, IPoracleTrackingProxy, IPoracleHumanProxy) │ ├── Core.Models/ DTOs passed between layers -│ ├── Core.Mappings/ AutoMapper profiles (Human, Profile, PoracleWeb.NET tables) +│ ├── Core.Mappings/ Mapping extension methods (alarm DTOs, Human, Profile, PoracleWeb.NET tables) │ ├── Core.Repositories/ Data access (Human, Profile, PoracleWeb-owned tables) │ └── Core.Services/ Business logic + PoracleNG API proxies ├── Data/ @@ -91,8 +91,8 @@ PoracleWeb.NET does **not** modify the Poracle DB schema. The Poracle database i ### Unified geofence feed PoracleWeb.NET acts as the single geofence source for PoracleJS. It fetches admin geofences from Koji, merges them with user-drawn geofences, and serves everything via one endpoint (`GET /api/geofence-feed`). No custom code needed in PoracleJS or Koji. User geofences support GeoJSON import/export for interoperability with external mapping tools. -### AutoMapper for partial updates -All update models use nullable `int?` properties so partial updates don't zero out unset fields. The mapping profile skips null properties automatically. Note: AutoMapper is now only used for non-alarm entities (humans, profiles). Alarm data flows as raw JSON through the PoracleNG API proxy. +### Manual mapping extensions +Mapping lives in static extension methods under `Core.Mappings/` -- there is no AutoMapper dependency. `AlarmMappingExtensions` provides `To*()` for `*Create` DTOs and `ApplyUpdate()` for `*Update` DTOs; `EntityMappingExtensions` provides `ToModel()`, `ToEntity()`, and `ApplyTo()` for Human, Profile, and the `poracle_web`-owned tables. Update models use nullable properties and `ApplyUpdate` skips nulls, so partial updates don't zero out unset fields. Alarm data itself flows as raw JSON through the PoracleNG API proxy. See [Backend Patterns](backend.md). ### Gym picker The `GymPickerComponent` (shared) lets users search for specific gyms when creating team, raid, or egg alarms. It calls the `ScannerService` (frontend) which hits scanner gym search endpoints on the backend (`ScannerController`). Search results use the `GymSearchResult` model and include photo thumbnails and area names resolved via the `PointInPolygon` geo utility. The scanner DB is optional — when not configured, the gym picker is hidden. diff --git a/docs/architecture/poracleng-proxy.md b/docs/architecture/poracleng-proxy.md index d4889006..5ae9bab8 100644 --- a/docs/architecture/poracleng-proxy.md +++ b/docs/architecture/poracleng-proxy.md @@ -145,7 +145,7 @@ See [PoracleNG Enhancement Requests](../poracleng-enhancement-requests.md) for t 3. Register the service in `ServiceCollectionExtensions.cs`. 4. Create the corresponding controller under `Controllers/`. -No repository, entity, or AutoMapper mapping is needed for alarm types -- the proxy handles all database interaction through PoracleNG. +No repository or entity is needed for alarm types -- the proxy handles all database interaction through PoracleNG. ## Registration diff --git a/docs/development/testing.md b/docs/development/testing.md index b9f09c70..e49c7ecb 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -31,7 +31,7 @@ Uses xUnit with Moq. Tests cover: - Human/profile services (`HumanServiceTests`, `ProfileServiceTests`) -- mock `IPoracleHumanProxy` for single-user ops, `IHumanRepository` for admin bulk ops - Active hours validation (`ActiveHoursValidationTests`) -- 17 tests for server-side active hours validation rules - Other services (`UserGeofenceServiceTests`, `CleaningServiceTests`, `DashboardServiceTests`, `SiteSettingServiceTests`, `WebhookDelegateServiceTests`, `SettingsMigrationServiceTests`, `QuickPickServiceSecurityTests`, `PokemonAvailabilityServiceTests`) -- AutoMapper mappings (non-alarm entities) +- Mapping extensions (`MappingExtensionTests`) -- alarm DTO `To*()` / `ApplyUpdate()` and entity `ToModel()` / `ToEntity()` / `ApplyTo()` !!! info "Alarm service tests mock IPoracleTrackingProxy" Since alarm services no longer use repositories, their tests mock `IPoracleTrackingProxy` instead of `IRepository`. The mock returns `JsonElement` values matching PoracleNG's snake_case JSON format.