Commit graph

250 commits

Author SHA1 Message Date
le king fu
3b70abdb9e feat: propagate right-click "add as keyword" to transactions page and highlights list (#75)
Some checks failed
PR Check / rust (push) Has been cancelled
PR Check / frontend (push) Has been cancelled
PR Check / rust (pull_request) Has been cancelled
PR Check / frontend (pull_request) Has been cancelled
Wire the ContextMenu + AddKeywordDialog pair onto the remaining per-transaction
surfaces. No new business logic — pure composition of #69 / #74 pieces.

- HighlightsTopTransactionsList: optional onContextMenuRow prop, ReportsHighlightsPage
  renders ContextMenu + AddKeywordDialog on right-click
- TransactionTable: optional onRowContextMenu prop on each <tr>; TransactionsPage
  handles it and opens the dialog pre-filled with the row description + current
  category
- Aggregate tables (HighlightsTopMoversTable, ComparePeriodTable, MonthlyTrendsTable,
  CategoryOverTimeTable) are intentionally NOT wired: they show category / month
  aggregates, not individual transactions, so there is no keyword to extract from
  a row — the dialog would be nonsensical there

Fixes #75

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:15:33 -04:00
334f975deb Merge pull request 'feat: category zoom + secure AddKeywordDialog (#74)' (#93) from issue-74-zoom-add-keyword into main 2026-04-14 19:11:54 +00:00
le king fu
62430c63dc feat: category zoom + secure AddKeywordDialog with context menu (#74)
Some checks failed
PR Check / rust (push) Has been cancelled
PR Check / frontend (push) Has been cancelled
PR Check / rust (pull_request) Has been cancelled
PR Check / frontend (pull_request) Has been cancelled
Service layer
- New reportService.getCategoryZoom(categoryId, from, to, includeChildren) —
  bounded recursive CTE (WHERE ct.depth < 5) protects against parent_id cycles;
  direct-only path skips the CTE; every binding is parameterised
- Export categorizationService helpers normalizeDescription / buildKeywordRegex /
  compileKeywords so the dialog can reuse them
- New validateKeyword() enforces 2–64 char length (anti-ReDoS), whitespace-only
  rejection, returns discriminated result
- New previewKeywordMatches(keyword, limit=50) uses parameterised LIKE + regex
  filter in memory; caps candidate scan at 1000 rows to protect against
  catastrophic backtracking
- New applyKeywordWithReassignment wraps INSERT (or UPDATE-reassign) +
  per-transaction UPDATEs in an explicit BEGIN/COMMIT/ROLLBACK; rejects
  existing keyword reassignment unless allowReplaceExisting is set; never
  recategorises historical transactions beyond the ids the caller supplied

Hook
- Flesh out useCategoryZoom with reducer + fetch + refetch hook

Components (flat under src/components/reports/)
- CategoryZoomHeader — category combobox + include/direct toggle
- CategoryDonutChart — template'd from dashboard/CategoryPieChart with
  innerRadius=55 and ChartPatternDefs for SVG patterns
- CategoryEvolutionChart — AreaChart with Intl-formatted axes
- CategoryTransactionsTable — sortable table with per-row onContextMenu
  → ContextMenu → "Add as keyword" action

AddKeywordDialog — src/components/categories/AddKeywordDialog.tsx
- Lives in categories/ (not reports/) because it is a keyword-editing widget
  consumed from multiple sections
- Renders transaction descriptions as React children only (no
  dangerouslySetInnerHTML); CSS truncation (CWE-79 safe)
- Per-row checkboxes for applying recategorisation; cap visible rows at 50;
  explicit opt-in checkbox to extend to N-50 non-displayed matches
- Surfaces apply errors + "keyword already exists" replace prompt
- Re-runs category zoom fetch on success so the zoomed view updates

Page
- ReportsCategoryPage composes header + donut + evolution + transactions
  + AddKeywordDialog, fetches from useCategoryZoom, preserves query string
  for back navigation

i18n
- New keys reports.category.* and reports.keyword.* in FR + EN
- Plural forms use i18next v25 _one / _other suffixes (nMatches)

Tests
- 3 reportService tests cover bounded CTE, cycle-guard depth check, direct-only fallthrough
- New categorizationService.test.ts: 13 tests covering validation boundaries,
  parameterised LIKE preview, regex word-boundary filter, explicit BEGIN/COMMIT
  wrapping, rollback on failure, existing keyword reassignment policy
- 62 total tests passing

Fixes #74

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:09:17 -04:00
b3b832650f Merge pull request 'feat: compare report — MoM / YoY / Actual vs Budget (#73)' (#92) from issue-73-compare into main 2026-04-14 19:00:32 +00:00
le king fu
ff350d75e7 feat: compare report — MoM / YoY / budget with view toggle (#73)
Some checks failed
PR Check / rust (push) Successful in 23m24s
PR Check / frontend (push) Successful in 2m17s
PR Check / frontend (pull_request) Has been cancelled
PR Check / rust (pull_request) Has been cancelled
- Services: getCompareMonthOverMonth(year, month) and getCompareYearOverYear(year)
  return CategoryDelta[] (expense-side, ABS aggregates, parameterised SQL only)
- Shared CategoryDelta type with HighlightMover now aliased to it
- Flesh out useCompare hook: reducer + fetch + automatic year/month inference
  from the shared useReportsPeriod `to` date; budget mode skips fetch and
  delegates to CompareBudgetView which wraps the existing BudgetVsActualTable
- Components: CompareModeTabs (MoM/YoY/Budget tabs), ComparePeriodTable (sortable
  table with signed delta coloring), ComparePeriodChart (diverging horizontal
  bar chart with ChartPatternDefs for SVG patterns), CompareBudgetView
  (fetches budget rows for the current target year/month)
- ReportsComparePage wires everything with PeriodSelector + ViewModeToggle
  (storage key reports-viewmode-compare); chart/table toggle is hidden in budget
  mode since the budget table has its own presentation
- i18n keys: reports.compare.modeMoM / modeYoY / modeBudget in FR + EN
- 4 new vitest cases for the compare services: parameterised boundaries,
  January wrap-around to December previous year, delta conversion with
  previous=0 fallback to null pct, year-over-year spans

Fixes #73

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:57:13 -04:00
fae76a6b82 Merge pull request 'feat: trends report — global flow + by category (#72)' (#91) from issue-72-trends into main 2026-04-14 18:53:24 +00:00
le king fu
d06dd7a858 feat: trends report — global flow + by category with view toggle (#72)
All checks were successful
PR Check / rust (push) Successful in 23m52s
PR Check / frontend (push) Successful in 2m20s
PR Check / rust (pull_request) Successful in 23m55s
PR Check / frontend (pull_request) Successful in 2m15s
- Flesh out ReportsTrendsPage with internal subview toggle
  (global / byCategory) and ViewModeToggle (storage key reports-viewmode-trends)
- Reuse existing MonthlyTrendsChart/Table and CategoryOverTimeChart/Table
  without modification; wire them through useTrends + useReportsPeriod so the
  URL period is respected
- Add reports.trends.subviewGlobal / subviewByCategory keys in FR + EN

Fixes #72

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:52:34 -04:00
5d206d5faf Merge pull request 'feat: reports hub + highlights panel + detailed highlights page (#71)' (#90) from issue-71-highlights-hub into main 2026-04-14 18:50:40 +00:00
le king fu
ac9c8afc4a feat: reports hub with highlights panel and detailed highlights page (#71)
All checks were successful
PR Check / rust (pull_request) Successful in 24m54s
PR Check / frontend (pull_request) Successful in 2m32s
PR Check / rust (push) Successful in 24m14s
PR Check / frontend (push) Successful in 2m26s
- Transform /reports into a hub: highlights panel + 4 nav cards
- New service: reportService.getHighlights (parameterised SQL, deterministic
  via referenceDate argument for tests, computes current-month balance, YTD,
  12-month sparkline series, top expense movers vs previous month, top recent
  transactions within configurable 30/60/90 day window)
- Extended types: HighlightsData, HighlightMover, MonthBalance
- Wired useHighlights hook with reducer + window-days state
- Hub tiles (flat naming under src/components/reports):
  HubNetBalanceTile, HubTopMoversTile, HubTopTransactionsTile,
  HubHighlightsPanel, HubReportNavCard
- Detailed ReportsHighlightsPage: balance tiles, sortable top movers table,
  diverging bar chart (Recharts + patterns SVG), top transactions list with
  30/60/90 window toggle; ViewModeToggle persistence keyed as
  reports-viewmode-highlights
- New i18n keys: reports.hub.*, reports.highlights.*
- 5 new vitest cases: empty profile, parameterised queries, window sizing,
  delta computation, zero-previous divisor handling

Fixes #71

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:47:55 -04:00
a26d642b1b Merge pull request 'refactor: split useReports into per-domain hooks + URL period (#70)' (#89) from issue-70-hooks-per-domain into main 2026-04-14 18:40:16 +00:00
le king fu
6a6a196467 refactor: split useReports into per-domain hooks + URL-bookmarked period (#70)
All checks were successful
PR Check / rust (push) Successful in 24m38s
PR Check / frontend (push) Successful in 2m22s
PR Check / rust (pull_request) Successful in 24m56s
PR Check / frontend (pull_request) Successful in 2m31s
- New useReportsPeriod hook reads/writes period via ?from=&to=&period= URL params,
  default civil year, pure resolver exported for tests
- New per-domain hooks: useHighlights, useTrends, useCompare, useCategoryZoom
  (stubs wired to useReportsPeriod, to be fleshed out in #71-#74)
- Rewire legacy useReports to consume useReportsPeriod; keep backward-compat
  state shape (period/customDateFrom/customDateTo) so /reports tabs keep working
- Mark useReports @deprecated pending removal in #76
- Tests: 7 new cases covering resolveReportsPeriod defaults, bookmarks,
  invalid inputs, preset resolution

Fixes #70

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:37:33 -04:00
a50be5caf6 Merge pull request 'refactor: pivot removal + sub-route skeletons + shared components (#69)' (#88) from issue-69-foundation-pivot-removal into main 2026-04-14 18:33:17 +00:00
le king fu
91430e994a refactor: remove pivot report, add sub-route skeletons and shared components (#69)
All checks were successful
PR Check / rust (push) Successful in 24m21s
PR Check / frontend (push) Successful in 2m12s
PR Check / rust (pull_request) Successful in 23m5s
PR Check / frontend (pull_request) Successful in 2m16s
- Delete DynamicReport* components and pivot types (PivotConfig, PivotResult, PivotFieldId, etc.)
- Remove getDynamicReportData/getDynamicFilterValues from reportService
- Strip pivotConfig/pivotResult from useReports hook and ReportsPage
- Drop "dynamic" from ReportTab union
- Remove reports.pivot.* and reports.dynamic i18n keys in FR and EN
- Add skeletons for /reports/highlights, /trends, /compare, /category pages
- Register the 4 new sub-routes in App.tsx
- Add reports.hub, reports.viewMode, reports.empty, common.underConstruction keys
- New shared ContextMenu component with click-outside + Escape handling
- Refactor ChartContextMenu to compose generic ContextMenu
- New ViewModeToggle with localStorage persistence via storageKey
- New Sparkline (Recharts LineChart) for compact trends
- Unit tests for readViewMode helper

Fixes #69

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:25:38 -04:00
le king fu
cab4cc174a chore: release v0.7.4
All checks were successful
Release / build-and-release (push) Successful in 26m7s
Wraps up the spec-oauth-keychain milestone: OAuth tokens in OS keychain,
HMAC-signed account cache, fallback banner, and Argon2id PIN hashing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:24:40 -04:00
ba5257791f Merge pull request 'fix: migrate PIN hashing from SHA-256 to Argon2id (#54)' (#55) from fix/simpl-resultat-54-argon2id-pin into main 2026-04-14 12:49:05 +00:00
440a43683d Merge pull request 'fix(deps): bump vite to 6.4.2 (GHSA-4w7w-66w2-5vf9, GHSA-p9ff-h696-f583)' (#77) from issue-59-bump-vite into main 2026-04-14 12:29:08 +00:00
9ccfc7a9d9 Merge pull request 'docs: ADR 0006 + changelog + architecture for OAuth keychain (#82)' (#87) from issue-82-wrap-up into main 2026-04-14 12:28:07 +00:00
le king fu
65bc7f5130 docs: ADR 0006 + changelog + architecture for OAuth keychain (#82)
All checks were successful
PR Check / rust (push) Successful in 22m44s
PR Check / frontend (push) Successful in 2m19s
PR Check / rust (pull_request) Successful in 22m25s
PR Check / frontend (pull_request) Successful in 2m19s
- New ADR-0006 documenting the OS keychain migration: context,
  options considered (keyring vs stronghold vs AES-from-PIN), the
  backend choice rationale (sync-secret-service vs async-secret-
  service), anti-downgrade design, migration semantics, and the
  subscription-tampering fix via account_cache.
- architecture.md updated: new token_store / account_cache module
  entries, auth_commands descriptions now point at the keychain-
  backed API, OAuth2 + deep-link flow diagram mentions the HMAC
  step, command count bumped to 35.
- CHANGELOG.md + CHANGELOG.fr.md under Unreleased:
  - Changed: tokens moved to keychain with transparent migration
    and Settings banner on fallback.
  - Changed: account cache is now HMAC-signed.
  - Security: CWE-312 and CWE-345 explicitly closed.

Manual test matrix (pop-os + Windows) is tracked in issue #82 and
will be run by the release gatekeeper before the next tag.

Refs #66, #78, #79, #80, #81

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:26:21 -04:00
745f71782f Merge pull request 'feat: settings banner when OAuth tokens use file fallback (#81)' (#86) from issue-81-fallback-banner into main 2026-04-14 12:21:37 +00:00
le king fu
9a9d3c89b9 feat: dismissable banner with session-storage memory (#81)
All checks were successful
PR Check / rust (push) Successful in 22m28s
PR Check / frontend (push) Successful in 2m17s
PR Check / rust (pull_request) Successful in 22m30s
PR Check / frontend (pull_request) Successful in 2m18s
Adds a close button and session-scoped dismissal flag so the banner
can be acknowledged for the current run but reappears on the next
app launch if the fallback is still active — matches the #81
acceptance criterion.

- sessionStorage key survives page navigation within the run, is
  cleared on app restart.
- Graceful on storage quota errors.
- New `common.close` i18n key (FR: "Fermer", EN: "Close") used as
  the aria-label of the close button.
2026-04-14 08:20:20 -04:00
le king fu
3b1c41c48e feat: settings banner when OAuth tokens fall back to file store (#81)
Some checks are pending
PR Check / rust (push) Waiting to run
PR Check / frontend (push) Waiting to run
PR Check / rust (pull_request) Successful in 22m28s
PR Check / frontend (pull_request) Successful in 2m19s
Adds a visible warning in the Settings page when `token_store` has
landed in the file fallback instead of the OS keychain. Without this,
a user on a keychain-less system would silently lose the security
benefit introduced in #78 and never know.

- New `get_token_store_mode` service wrapper in authService.ts.
- New `TokenStoreFallbackBanner` component: fetches the mode on mount,
  renders nothing when mode is `keychain` or null, renders an
  amber warning card when mode is `file`.
- Mounted in SettingsPage right after AccountCard so it sits next to
  the account state the user can fix (log out + log back in once the
  keychain is available).
- i18n keys under `account.tokenStore.fallback.*` in fr/en.

Refs #66

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:18:41 -04:00
cf31666c35 Merge pull request 'feat: HMAC-verified account cache (#80)' (#85) from issue-80-subscription-integrity into main 2026-04-14 12:12:16 +00:00
le king fu
2d7d1e05d2 feat: HMAC-sign cached account info to close subscription tampering (#80)
All checks were successful
PR Check / rust (push) Successful in 26m11s
PR Check / frontend (push) Successful in 2m20s
PR Check / rust (pull_request) Successful in 22m22s
PR Check / frontend (pull_request) Successful in 2m18s
Before this change, `license_commands::check_account_edition` read
`account.json` directly and granted Premium when `subscription_status`
was `"active"`. Any local process could write that JSON and bypass
the paywall without ever touching the Logto session.

Introduce `account_cache` with:
- `save(app, &AccountInfo)` — signs the serialised AccountInfo with
  HMAC-SHA256 and writes a `{"data", "sig"}` envelope. The 32-byte
  key lives in the OS keychain (service `com.simpl.resultat`, user
  `account-hmac-key`) alongside the OAuth tokens from #78.
- `load_unverified` — accepts both signed and legacy payloads for UI
  display (name, email, picture). The license path must never use
  this.
- `load_verified` — requires a valid HMAC signature; returns None for
  legacy payloads, missing keychain, tampered data. Used by
  `check_account_edition` so Premium stays locked until the next
  token refresh re-signs the cache.
- `delete` — wipes both the file and the keychain key on logout so
  the next session generates a fresh cryptographic anchor.

`auth_commands::handle_auth_callback` and `refresh_auth_token` now
call `account_cache::save` instead of writing the file directly.
`logout` clears both stores. `get_account_info` delegates to
`load_unverified` so upgraded users see their profile immediately.

Trust boundary: the HMAC key lives in the keychain and shares its
security model with the OAuth tokens. If the keychain is unreachable,
the gating path refuses to grant Premium (fail-closed), which matches
the store_mode policy introduced in #78.

Refs #66, CWE-345

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:07:47 -04:00
b684c88d2b Merge pull request 'ci: libdbus-1-dev for keyring build, drop appimage target (#79)' (#84) from issue-79-ci-libdbus into main 2026-04-14 00:35:36 +00:00
le king fu
481018e1e3 ci: install libdbus-1-dev for keyring build, drop appimage target (#79)
All checks were successful
PR Check / rust (push) Successful in 23m16s
PR Check / frontend (push) Successful in 2m17s
PR Check / rust (pull_request) Successful in 21m37s
PR Check / frontend (pull_request) Successful in 2m10s
The new token_store module (#78) depends on `sync-secret-service` via
`dbus-secret-service`, which in turn links to libdbus-1 at build time
through the `dbus` crate. Add `libdbus-1-dev` to:

- `check.yml` rust job (alongside the existing webkit/appindicator
  system deps), so every PR run compiles the keyring backend.
- `release.yml` Linux deps step, so tagged builds link correctly.

Runtime requires `libdbus-1-3`, which is present on every desktop
Linux distro by default, so `.deb` / `.rpm` depends stay unchanged.

Also add a non-blocking `cargo audit` step to check.yml to surface
advisories across the transitive dep graph (zbus, dbus-secret-service,
etc.) without failing unrelated PRs.

Drop `appimage` from `bundle.targets` in tauri.conf.json: the release
workflow explicitly builds `--bundles deb,rpm` so AppImage was never
shipped, and its presence in the config risks a silent fallback to
plaintext token storage for anyone running `tauri build` locally
without libsecret/libdbus bundled into the AppImage. No behaviour
change for CI; follow-up to re-enable AppImage properly would need a
linuxdeploy workflow that bundles the backend.

Refs #66

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:27:14 -04:00
e331217c14 Merge pull request 'feat: OAuth token storage via OS keychain (#78)' (#83) from issue-78-token-store into main 2026-04-14 00:17:41 +00:00
le king fu
feaed4058d feat: migrate OAuth tokens to OS keychain via token_store (#78)
All checks were successful
PR Check / rust (push) Successful in 17m25s
PR Check / frontend (push) Successful in 2m31s
PR Check / rust (pull_request) Successful in 18m14s
PR Check / frontend (pull_request) Successful in 2m14s
Introduce a new token_store module that persists OAuth tokens in the OS
keychain (Credential Manager on Windows, Secret Service on Linux through
sync-secret-service + crypto-rust, both pure-Rust backends).

- Keychain service name matches the Tauri bundle identifier
  (com.simpl.resultat) so credentials are scoped to the real app
  identity.
- Transparent migration on first load: a legacy tokens.json is copied
  into the keychain, then zeroed and unlinked before removal to reduce
  refresh-token recoverability from unallocated disk blocks.
- Store-mode flag (keychain|file) persisted next to the auth dir.
  After a successful keychain write the store refuses to silently
  downgrade to the file fallback, so a subsequent failure forces
  re-authentication instead of leaking plaintext.
- New get_token_store_mode command exposes the current mode to the
  frontend so a settings banner can warn users running on the file
  fallback.
- auth_commands.rs refactored: all tokens.json read/write/delete paths
  go through token_store; check_subscription_status now uses
  token_store::load().is_some() to trigger migration even when the
  24h throttle would early-return.

Refs #66

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:41:54 -04:00
le king fu
813d29e38a fix(deps): bump vite to 6.4.2 to resolve GHSA-4w7w-66w2-5vf9 and GHSA-p9ff-h696-f583
All checks were successful
PR Check / rust (push) Successful in 17m28s
PR Check / frontend (push) Successful in 2m15s
PR Check / rust (pull_request) Successful in 17m33s
PR Check / frontend (pull_request) Successful in 2m17s
Closes #59

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:05:28 -04:00
le king fu
43c5be0c84 docs(architecture): update for v0.7.3 OAuth2 and single-instance wiring
- Bump header date/version to 2026-04-13 / v0.7.3
- Correct Tauri command count (25 → 34) and add the missing commands
- Add `auth_commands.rs` section (5 commands) and expand `license_commands.rs`
  with the 4 activation commands that already existed
- New "Plugins Tauri" section documenting init order constraints
  (single-instance must be first, deep-link before setup)
- New "OAuth2 et deep-link" section explaining the end-to-end flow,
  why single-instance is required, and why `on_open_url` is used
  instead of `app.listen()`
- Note the temporary auto-update gate opening in entitlements
- Update CI/CD: GitHub Actions → Forgejo Actions, add check.yml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:25:57 -04:00
le king fu
f5d74b4664 fix: use on_open_url for OAuth deep-link callback
All checks were successful
Release / build-and-release (push) Successful in 27m50s
The listener `app.listen("deep-link://new-url", ...)` did not reliably
fire when tauri-plugin-single-instance (deep-link feature) forwarded a
simpl-resultat://auth/callback URL to the running instance. The user
saw the browser complete the OAuth flow, the app regain focus, and
then sit in "loading" forever because the listener never received the
URL.

Switch to the canonical Tauri v2 API — `app.deep_link().on_open_url()`
via DeepLinkExt — which is directly coupled to the deep-link plugin
and catches URLs from both initial launch and single-instance forwards.

Also surface OAuth error responses: if the callback URL contains an
`error` parameter instead of a `code`, emit `auth-callback-error` so
the UI can show the error instead of staying stuck in "loading".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:26:17 -04:00
le king fu
f14ac3c6f8 fix: temporarily open auto-update to Free edition
All checks were successful
Release / build-and-release (push) Successful in 25m59s
The auto-update gate added in #48 requires the Base edition, but the
license server (#49) needed to grant Base does not exist yet. This
chicken-and-egg left the only current user — myself — unable to
receive the critical v0.7.1 OAuth callback fix via auto-update.

Add EDITION_FREE to the auto-update feature tiers as a temporary
measure. The gate will be restored to [BASE, PREMIUM] once paid
activation works end-to-end via the Phase 2 license server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:11:16 -04:00
le king fu
88e1fff253 fix: wire single-instance plugin for OAuth deep-link callback
All checks were successful
Release / build-and-release (push) Successful in 26m52s
The Maximus Account sign-in flow was broken in v0.7.0: clicking "Sign in"
opened Logto in the browser, but when the OAuth2 callback fired
simpl-resultat://auth/callback?code=..., the OS launched a second app
instance instead of routing the URL to the running one. The second
instance had no PKCE verifier in memory, and the original instance
never received the deep-link event, leaving it stuck in "loading".

Fix: register tauri-plugin-single-instance (with the deep-link feature)
as the first plugin. It forwards the callback URL to the existing
process, which triggers the existing deep-link://new-url listener and
completes the token exchange.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:37:09 -04:00
le king fu
93fd60bf41 chore: release v0.7.0
All checks were successful
Release / build-and-release (push) Successful in 27m50s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:58:34 -04:00
le king fu
324436c0f1 fix: set Logto app ID to sr-desktop-native
Update the default LOGTO_APP_ID to match the Native App registered
in the Logto instance at auth.lacompagniemaximus.com.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:53:53 -04:00
4b42d53659 Merge pull request 'feat: Maximus Account OAuth2 + machine activation (#51, #53)' (#65) from issue-51-compte-maximus-oauth into main 2026-04-10 19:38:27 +00:00
le king fu
e314bbe1e3 fix: remove handle_auth_callback from invoke_handler
All checks were successful
PR Check / rust (push) Successful in 17m12s
PR Check / frontend (push) Successful in 2m12s
PR Check / rust (pull_request) Successful in 16m56s
PR Check / frontend (pull_request) Successful in 2m14s
The auth callback is handled exclusively via the deep-link handler in
lib.rs — exposing it as a JS-invocable command is unnecessary attack
surface. The frontend listens for auth-callback-success/error events
instead.

Plaintext token storage documented as known limitation (see #66).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:35:10 -04:00
le king fu
60b995394e fix: tighten CSP img-src, show initials instead of external avatar
Some checks are pending
PR Check / rust (push) Waiting to run
PR Check / frontend (push) Waiting to run
PR Check / rust (pull_request) Successful in 17m9s
PR Check / frontend (pull_request) Successful in 2m15s
Privacy-first: remove 'https:' from img-src CSP directive to prevent
IP leaks via external avatar URLs (Google/Gravatar). AccountCard now
shows user initials instead of loading a remote image.

Also remove .keys-temp/ from .gitignore (not relevant to this PR).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:24:52 -04:00
le king fu
4e92882724 fix: restrict last_check file perms + add useAuth to architecture docs
Some checks are pending
PR Check / rust (push) Waiting to run
PR Check / frontend (push) Waiting to run
PR Check / rust (pull_request) Successful in 17m24s
PR Check / frontend (pull_request) Successful in 2m14s
- Use write_restricted() for auth/last_check file (consistent 0600)
- Add useAuth hook to the hooks table in docs/architecture.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:14:31 -04:00
le king fu
ca3005bc0e fix: use write_restricted for account.json (0600 perms)
Some checks are pending
PR Check / rust (push) Waiting to run
PR Check / frontend (push) Waiting to run
PR Check / rust (pull_request) Successful in 17m0s
PR Check / frontend (pull_request) Successful in 2m12s
account.json contains PII and subscription_status — apply the same
restricted file permissions as tokens.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:04:05 -04:00
le king fu
9e26ad58d1 fix: use base64 crate, restrict token file perms, safer chrono_now
Some checks are pending
PR Check / rust (push) Waiting to run
PR Check / frontend (push) Waiting to run
PR Check / rust (pull_request) Successful in 17m32s
PR Check / frontend (pull_request) Successful in 2m15s
- Replace hand-rolled base64 encoder with base64::URL_SAFE_NO_PAD crate
- Set 0600 permissions on tokens.json via write_restricted() helper (Unix)
- Replace chrono_now() .unwrap() with .unwrap_or_default()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:58:10 -04:00
le king fu
be5f6a55c5 fix: URL-decode auth code + replace Mutex unwrap with map_err
Some checks failed
PR Check / rust (push) Has been cancelled
PR Check / frontend (push) Has been cancelled
PR Check / rust (pull_request) Successful in 17m21s
PR Check / frontend (pull_request) Successful in 2m21s
- extract_auth_code now URL-decodes the code parameter to handle
  percent-encoded characters from the OAuth provider
- Replace Mutex::lock().unwrap() with .lock().map_err() in start_oauth
  and handle_auth_callback to avoid panics on poisoned mutex

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:43:18 -04:00
le king fu
b53a902f11 feat: Maximus Account OAuth2 PKCE + machine activation + subscription check (#51, #53)
All checks were successful
PR Check / rust (push) Successful in 16m34s
PR Check / frontend (push) Successful in 2m14s
PR Check / rust (pull_request) Successful in 16m31s
PR Check / frontend (pull_request) Successful in 2m13s
- Add auth_commands.rs: OAuth2 PKCE flow (start_oauth, handle_auth_callback,
  refresh_auth_token, get_account_info, check_subscription_status, logout)
- Add deep-link handler in lib.rs for simpl-resultat://auth/callback
- Add AccountCard.tsx + useAuth hook + authService.ts
- Add machine activation commands (activate, deactivate, list, get_activation_status)
- Extend LicenseCard with machine management UI
- get_edition() now checks account subscription for Premium detection
- Daily subscription status check (refresh token if last check > 24h)
- Configure CSP for API/auth endpoints
- Configure tauri-plugin-deep-link for desktop
- Update i18n (FR/EN), changelogs, and architecture docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:18:51 -04:00
877ace370f Merge pull request 'feat: license system (UI card + auto-update gating) (#47, #48)' (#64) from issue-46-license-commands-entitlements into main 2026-04-10 14:28:27 +00:00
dd106a1df6 Merge pull request 'feat: gate auto-updates behind license entitlement (#48)' (#58) from issue-48-gate-auto-updates into issue-46-license-commands-entitlements
All checks were successful
PR Check / rust (push) Successful in 16m9s
PR Check / frontend (push) Successful in 2m11s
PR Check / rust (pull_request) Successful in 16m10s
PR Check / frontend (pull_request) Successful in 2m14s
2026-04-10 13:55:39 +00:00
44f98549b5 Merge pull request 'feat: license UI card in settings (#47)' (#57) from issue-47-license-ui-card into issue-46-license-commands-entitlements
Some checks failed
PR Check / rust (push) Has been cancelled
PR Check / frontend (push) Has been cancelled
2026-04-10 13:54:59 +00:00
le king fu
6d67ab8935 feat: gate auto-updates behind license entitlement (#48)
All checks were successful
PR Check / rust (push) Successful in 16m6s
PR Check / frontend (push) Successful in 2m15s
Both code paths that touch the updater now consult `check_entitlement`
from the Rust entitlements module before calling `check()`:

- `useUpdater.ts` adds a `notEntitled` status; on Free, the check
  short-circuits and the Settings page displays an upgrade hint instead
  of fetching update metadata.
- `ErrorPage.tsx` (recovery screen) does the same so the error path
  matches the main path; users on Free no longer see network errors when
  the updater would have run.

The gate name (`auto-update`) is the same string consumed by
`commands/entitlements.rs::FEATURE_TIERS`, so changing which tier
unlocks updates is a one-line edit in that file.

Bilingual i18n keys for the new messages are added to both `fr.json`
and `en.json`. CHANGELOG entries in both languages.
2026-04-09 15:52:59 -04:00
le king fu
2da2de183a feat: license card in settings (#47)
All checks were successful
PR Check / rust (push) Successful in 16m19s
PR Check / frontend (push) Successful in 2m14s
Adds the user-facing layer on top of the Rust license commands shipped
in #46.

- `licenseService.ts` thin wrapper around the new Tauri commands
- `useLicense` hook follows the project's useReducer pattern (idle,
  loading, ready, validating, error) and exposes `submitKey`,
  `refresh`, and `checkEntitlement` for cross-component use
- `LicenseCard` shows the current edition, the expiry date when set,
  accepts a license key with inline validation feedback, and links to
  the purchase page via `openUrl` from `@tauri-apps/plugin-opener`
- Card is inserted at the top of `SettingsPage` so the edition is the
  first thing users see when looking for license-related actions
- i18n: new `license.*` keys in both `fr.json` and `en.json`
- Bilingual CHANGELOG entries
2026-04-09 15:47:04 -04:00
escouade-bot
e5be6f5a56 fix: wrap rehash updateProfile in try/catch for best-effort (#54)
All checks were successful
PR Check / rust (push) Successful in 16m33s
PR Check / frontend (push) Successful in 2m14s
PR Check / rust (pull_request) Successful in 16m33s
PR Check / frontend (pull_request) Successful in 2m15s
Both handlePinSuccess handlers (ProfileSwitcher and ProfileSelectionPage)
now catch updateProfile errors so that a failed rehash persistence does
not block switchProfile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:46:27 -04:00
escouade-bot
2f610bf10a fix: make legacy PIN rehash non-blocking in verify_pin (#54)
Replace hash_pin(pin)? with hash_pin(pin).ok() so that a rehash
failure does not propagate as an error. The user can now switch
profiles even if the Argon2id re-hashing step fails — the PIN
is still correctly verified, and the legacy hash remains until
the next successful login.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:46:27 -04:00
escouade-bot
34626711eb fix: address reviewer feedback (#54)
- Add automatic re-hashing of legacy SHA-256 PINs to Argon2id on
  successful verification, returning new hash to frontend for persistence
- Use constant-time comparison (subtle::ConstantTimeEq) for both
  Argon2id and legacy SHA-256 hash verification
- Add unit tests for hash_pin, verify_pin (Argon2id and legacy paths),
  re-hashing flow, error cases, and hex encoding roundtrip
- Update frontend to handle VerifyPinResult struct and save rehashed
  PIN hash via profile update

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:46:27 -04:00