Feat/user scoped certificates#30
Conversation
Create user_certificates table to store certificate data at the user level (per ADR 0012). Certificates belong to a person (PESEL), not a company. Includes unique partial index for one active cert per user. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add get_active_user_certificate/1, create_user_certificate/2, replace_active_user_certificate/2, and get_certificate_for_company/1. The latter joins through memberships to find the company owner's cert. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Data migration: copies cert data from ksef_credentials to user_certificates via owner memberships - Remove cert columns from ksef_credentials table - Strip cert fields from Credential schema (now sync config only) - Update AuthWorker to load cert via get_certificate_for_company - Update DashboardLive to show cert expiry from user_certificates - Update auth_worker tests and credentials tests CertificateLive tests still failing — will be fixed in next commit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CertificateLive now writes to user_certificates instead of ksef_credentials. Display shows cert info (subject, expiry) from user_certificate and sync info (NIP, last_sync) from credential. Remove action deactivates the user_certificate. Make deactivate_user_certificate public in context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SyncWorker now verifies that the company owner has an active user certificate before attempting to sync. Skips gracefully if no cert is found (sync can't succeed without cert for XADES auth). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughMoves certificate storage from company-level credentials to per-user UserCertificate records: adds schema + migrations, migrates data, removes certificate columns from credentials, updates credentials API, LiveViews, auth/sync workers, certificate parsing, factories, and tests to operate on user-scoped certificates. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant LiveView as Certificate LiveView
participant Credentials as Credentials Module
participant DB as Database
participant AuthWorker as AuthWorker
rect rgba(150,200,100,0.5)
Note over User,AuthWorker: Per-user certificate flow
User->>LiveView: Upload certificate (p12 / key+crt)
LiveView->>Credentials: create_user_certificate(user, attrs)
Credentials->>DB: INSERT user_certificates (user_id, encrypted data, is_active=true)
DB-->>Credentials: OK
LiveView->>LiveView: ensure placeholder exists for company
AuthWorker->>Credentials: get_certificate_for_company(company_id)
Credentials->>DB: join memberships -> user_certificates where is_active=true
DB-->>Credentials: user_certificate
Credentials-->>AuthWorker: user_certificate (encrypted)
AuthWorker->>AuthWorker: decrypt certificate and obtain token from KSeF
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@lib/ksef_hub/credentials.ex`:
- Around line 198-205: The get_certificate_for_company/1 query can raise
Ecto.MultipleResultsError when multiple owner memberships exist; modify the
query in get_certificate_for_company to defensively limit results (e.g., add
limit(1)) and add a deterministic order_by (for example order_by: [desc:
uc.inserted_at] or uc.id) before calling Repo.one(), so it always returns a
single, predictable UserCertificate even if multiple owner memberships exist.
In
`@priv/repo/migrations/20260210100002_migrate_cert_data_to_user_certificates.exs`:
- Around line 10-31: The migration's up/execute SQL inserts
certificate_password_encrypted (non-nullable) from ksef_credentials which is
nullable; add a NULL-guard by extending the WHERE clause in the execute block
(inside the up function) to include "AND kc.certificate_password_encrypted IS
NOT NULL" so only rows with both certificate_data_encrypted and
certificate_password_encrypted are inserted and the INSERT into
user_certificates won't fail.
In
`@priv/repo/migrations/20260210100003_remove_cert_columns_from_credentials.exs`:
- Around line 10-16: The migration's change/0 is irreversible because the remove
calls inside alter table(:ksef_credentials) don't provide full schema info for
rollback; update each remove invocation to the three-arity form that includes
the column type and options so Ecto can recreate them on rollback (e.g. replace
remove :certificate_data_encrypted, :binary with
remove(:certificate_data_encrypted, :binary, null: true) and do the same for
:certificate_password_encrypted, :certificate_subject, and
:certificate_expires_at), leaving the change/0 and alter block intact.
In `@test/ksef_hub/ksef_client/auth_worker_test.exs`:
- Around line 22-39: Replace inline maps passed to Credentials.create_credential
in this test with factory-generated params via params_for(:credential, ...) so
credential attributes follow factory defaults; specifically, build params using
params_for(:credential, nip: company.nip, company_id: company.id, is_active:
true) and pass that to Credentials.create_credential where the current inline
map is used (this applies to the three create_credential calls in the file),
leaving the insert(:user_certificate, ...) usage intact.
🧹 Nitpick comments (5)
docs/implementation-plan.md (1)
152-157: Refine LiveView test plan into smaller, staged files.The plan calls out a single LiveView test file; consider splitting into smaller, isolated files and sequencing content-only checks before interaction tests to match the project’s LiveView testing strategy.
Based on learnings: "Come up with a step-by-step test plan for LiveView tests that splits major test cases into small, isolated files, starting with simpler tests that verify content exists before adding interaction tests".
lib/ksef_hub_web/live/dashboard_live.ex (1)
53-72: Consider aligningcert_activewith certificate existence.Currently
cert_activeis based solely oncredential.is_active, but certificate data now comes fromuser_cert. If a credential exists without an owner certificate uploaded, the dashboard will show "Active" but no expiration date, which may confuse users.Should
cert_activealso verify thatuser_certexists?cert_active: credential != nil && credential.is_active && user_cert != niltest/ksef_hub/credentials_test.exs (1)
151-312: Use factory-driven test data for user certificate attributes.Several tests build inline attribute maps; using
params_for(:user_certificate)keeps test data consistent with the factory definition and reduces maintenance burden when defaults change.Example refactor
- attrs = %{ - certificate_data_encrypted: "enc-data", - certificate_password_encrypted: "enc-pass", - certificate_subject: "CN=Test", - not_before: ~D[2026-01-01], - not_after: ~D[2028-01-01], - fingerprint: "AA:BB", - is_active: true - } + attrs = + :user_certificate + |> params_for() + |> Map.drop([:user_id]) + |> Map.merge(%{ + certificate_subject: "CN=Test", + not_before: ~D[2026-01-01], + not_after: ~D[2028-01-01], + fingerprint: "AA:BB", + is_active: true + })test/ksef_hub/credentials/user_certificate_test.exs (1)
1-135: Use factory params to reduce inline test data duplication.The
user_certificate_factoryexists and provides all needed fields. Replace inline maps withparams_for(:user_certificate)to align with testing standards and keep test data centralized. Note: These tests validate changeset behavior with minimal required fields, so inline maps are understandable for clarity—refactoring is optional but recommended.♻️ Example refactor
- changeset = - %UserCertificate{user_id: user.id} - |> UserCertificate.changeset(%{ - certificate_data_encrypted: "encrypted-data", - certificate_password_encrypted: "encrypted-pass", - is_active: true - }) + changeset = + %UserCertificate{user_id: user.id} + |> UserCertificate.changeset( + params_for(:user_certificate) + |> Map.drop([:user_id]) + )lib/ksef_hub_web/live/certificate_live.ex (1)
214-224: Consider handling failures inensure_credential_exists.The result of
Credentials.replace_active_credential/2is discarded. If credential creation fails, this will silently return:ok, potentially leaving the system in an inconsistent state where a user certificate exists but no company credential exists for sync operations.♻️ Suggested improvement: log on failure
`@spec` ensure_credential_exists(KsefHub.Companies.Company.t()) :: :ok defp ensure_credential_exists(company) do case Credentials.get_active_credential(company.id) do nil -> - Credentials.replace_active_credential(company.id, %{}) - :ok + case Credentials.replace_active_credential(company.id, %{}) do + {:ok, _credential} -> + :ok + + {:error, changeset} -> + Logger.warning("Failed to create credential for company #{company.id}: #{inspect(changeset.errors)}") + :ok + end _credential -> :ok end end
- Certificate tab: show "Issued To", "Valid From", "Valid Until" (with expiry color), and "Fingerprint" instead of NIP - Update header to "Certificate" with subtitle explaining it's personal - CertificateInfo behaviour and Openssl impl now return not_before date - Companies tab: rename "Certificate" column to "KSeF Sync" with "Configured"/"Not configured" labels since certs are now per-user Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add limit(1) + order_by(desc: inserted_at) to get_certificate_for_company to prevent Ecto.MultipleResultsError with multiple owner memberships - Add NULL guard for certificate_password_encrypted in data migration - Use 3-arity remove in column removal migration for rollback support - Make cert_active on dashboard also verify user_cert exists - Log failure in ensure_credential_exists instead of silently discarding - Replace inline attr maps with params_for(:credential) in auth_worker_test - Replace inline attr maps with params_for(:user_certificate) in credentials_test and user_certificate_test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Certificates uploaded before metadata extraction was added have nil subject/dates. Now always show "Issued To", "Valid From", "Valid Until" with em-dash fallback for nil values, and display a hint to re-upload when metadata is missing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/ksef_hub/credentials/certificate_info/openssl.ex (1)
99-117:⚠️ Potential issue | 🟡 MinorUpdate
extract/2docs/spec to includenot_before.
parse_certificate/1now returnsnot_before(Line 99-113), butextract/2still documents/specs onlyexpires_at, which can mislead callers.🛠️ Suggested update for `extract/2` `@doc/`@spec
- `@doc` """ - Extracts the subject and expiry date from a PKCS12 binary. + `@doc` """ + Extracts certificate metadata (subject, validity start, expiry) from a PKCS12 binary. @@ - Returns `{:ok, %{subject: String.t(), expires_at: Date.t()}}` on success, + Returns `{:ok, %{subject: String.t(), not_before: Date.t(), expires_at: Date.t()}}` on success, @@ - `@spec` extract(binary(), String.t()) :: - {:ok, %{subject: String.t(), expires_at: Date.t()}} | {:error, term()} + `@spec` extract(binary(), String.t()) :: + {:ok, %{subject: String.t(), not_before: Date.t(), expires_at: Date.t()}} | {:error, term()}
🤖 Fix all issues with AI agents
In
`@priv/repo/migrations/20260210100002_migrate_cert_data_to_user_certificates.exs`:
- Around line 36-38: The current down/0 performs a destructive "DELETE FROM
user_certificates" which will remove any post-migration user data; fix by either
making the migration irreversible (change down/0 in this migration to raise an
Ecto.MigrationError with a clear message stating the migration cannot be rolled
back) or by implementing safe rollbacks: add a tracking column (e.g.,
migrated_from_credential_id) during up/0, populate it for migrated rows, and
change down/0 to delete only rows where migrated_from_credential_id is NOT NULL
(or matches the source IDs), ensuring only originally migrated records are
removed; reference the migration's down/0 function and the user_certificates
table and migrated_from_credential_id when making the change.
🧹 Nitpick comments (2)
lib/ksef_hub/credentials.ex (1)
144-150: Consider adding defensive query hardening for consistency.Unlike
get_active_credential/1(lines 27-31) andget_certificate_for_company/1(lines 205-206), this function relies solely on the unique constraint without defensiveorder_by+limit(1). While the constraint should prevent duplicates, adding defensive hardening would provide consistency and guard against constraint violations or race conditions.♻️ Optional defensive hardening
def get_active_user_certificate(user_id) do UserCertificate |> where([uc], uc.user_id == ^user_id and uc.is_active == true) + |> order_by([uc], desc: uc.inserted_at) + |> limit(1) |> Repo.one() endtest/ksef_hub_web/live/certificate_live_test.exs (1)
80-81: Consider using element selectors instead of raw HTML matching.These assertions test text content via
render(view) =~, which works but is less resilient to markup changes. If possible, adddata-testidor ID attributes to the heading and subject elements, then usehas_element?/3with selectors.However, this is acceptable for verifying displayed text when specific element IDs aren't available.
- Update extract/2 @doc and @SPEC to include not_before in return type - Make data migration irreversible (raise on rollback) to prevent accidental deletion of post-migration user certificates - Add defensive order_by + limit(1) to get_active_user_certificate for consistency with get_certificate_for_company - Use element ID selectors in certificate live test instead of raw HTML matching for resilience to markup changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary by CodeRabbit
New Features
Bug Fixes & Improvements
Migrations
Tests
Documentation