Skip to content

Feat/certificate page redesign#28

Merged
emilwojtaszek merged 10 commits into
mainfrom
feat/certificate-page-redesign
Feb 10, 2026
Merged

Feat/certificate page redesign#28
emilwojtaszek merged 10 commits into
mainfrom
feat/certificate-page-redesign

Conversation

@emilwojtaszek

@emilwojtaszek emilwojtaszek commented Feb 10, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Certificate metadata now displays subject, expiration date, and upload timestamp
    • Added certificate upload mode selection between PKCS12 and Key/Certificate file pairs
    • Enhanced UI with toggle to show/hide upload form and Cancel/Replace controls
    • Ability to remove or replace uploaded certificates
    • Automatic authentication queued after successful certificate upload
    • Expanded accepted certificate file formats (pem, key, crt, cer, p12, pfx)
  • Bug Fixes / UX

    • Improved user-facing error messages for certificate upload and parsing failures

emilwojtaszek and others added 7 commits February 10, 2026 01:53
Extract certificate subject (CN, O, etc.) and expiry date from PKCS12
files using openssl CLI + Erlang :public_key module. Follows the same
secure temp file pattern as Pkcs12Converter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Call CertificateInfo.extract/2 after receiving PKCS12 data to populate
certificate_subject and certificate_expires_at fields. Extraction is
best-effort — failures are logged but don't block the upload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace cryptic openssl exit codes with user-friendly messages.
Specific message for exit code 1 (wrong passphrase / mismatched files).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace accept: :any with specific extensions (.key, .pem for private
keys and .crt, .pem, .cer for certificates). Register MIME types for
these extensions in config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change default upload_mode from :p12 to :key_crt and swap button
order so .key + .crt appears first. Most users have separate key
and certificate files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the dual Active Certificate / All Certificates sections with a
single "Current Certificate" card showing NIP, Subject, Expires, Last
Sync, and Uploaded date. Add empty state when no certificate exists.

The upload form is now toggleable via "Replace Certificate" / "Cancel"
buttons and auto-hides after successful upload. The "Remove" button
replaces the old "Deactivate" action.

Remove: stream(:credentials), has_credentials assign, active_badge
component, All Certificates table.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace fragile elem/2 tuple access with proper Record.defrecord
macros for OTPCertificate and OTPTBSCertificate. Simplify dead
branch in extract_pem retry logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 10, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds certificate metadata extraction (behaviour + OpenSSL implementation), registers additional certificate MIME types, introduces an Oban Auth worker to perform post-upload XADES authentication, refactors the certificate LiveView for upload/remove/metadata display, and adds tests and mocks for these flows.

Changes

Cohort / File(s) Summary
Configuration
config/config.exs, config/test.exs
Registered additional certificate-related MIME types and set test certificate_info provider to a mock.
Certificate Info (behaviour & impl)
lib/ksef_hub/credentials/certificate_info/behaviour.ex, lib/ksef_hub/credentials/certificate_info/openssl.ex
Added behaviour extract/2 and an OpenSSL-based implementation that extracts subject and expiry from PKCS#12, with secure temp files, retries, parsing, and robust error handling.
LiveView (UI & flows)
lib/ksef_hub_web/live/certificate_live.ex
Refactored certificate LiveView: upload mode toggle (p12 vs key/crt), form toggling, removal action, metadata extraction on save, enqueueing of AuthWorker, updated IDs/labels and improved error messages.
Auth Worker
lib/ksef_hub/ksef_client/auth_worker.ex
New Oban worker that loads active credential, decrypts certificate and password, performs XADES authentication, and stores tokens; includes error handling for missing credentials and failures.
Tests & Mocks
test/test_helper.exs, test/ksef_hub/credentials/certificate_info/openssl_test.exs, test/ksef_hub_web/live/certificate_live_test.exs, test/ksef_hub/ksef_client/auth_worker_test.exs
Added Mox mock for CertificateInfo, OpenSSL extraction tests, extensive LiveView tests for upload/remove/toggle flows, and AuthWorker tests covering success, missing credential, and error paths.
Misc
mix.lock
Lockfile updated to reflect added/changed dependencies in tests and new modules.

Sequence Diagram(s)

sequenceDiagram
    participant LiveView as "LiveView\n(Certificate UI)"
    participant DB as "Credential Store"
    participant AuthWorker as "AuthWorker (Oban)"
    participant Decrypt as "Decryption (local)"
    participant KsefAuth as "KsefClient.Auth"
    participant TokenMgr as "TokenManager"

    LiveView->>DB: save credential (encrypted p12/password) + metadata
    LiveView->>AuthWorker: enqueue job(company_id)
    AuthWorker->>DB: load_credential(company_id)
    DB-->>AuthWorker: credential (encrypted)
    AuthWorker->>Decrypt: decrypt p12_data
    AuthWorker->>Decrypt: decrypt password
    Decrypt-->>AuthWorker: p12_data, password
    AuthWorker->>KsefAuth: authenticate(p12_data, password)
    KsefAuth-->>AuthWorker: challenge/sign/poll -> tokens
    AuthWorker->>TokenMgr: store_tokens(tokens, validity)
    TokenMgr-->>AuthWorker: :ok
    AuthWorker-->>AuthWorker: return :ok
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰
I hopped in with a pem and a p12 key,
OpenSSL whispered secrets to me,
Oban queued a dance in the night,
Tokens tucked safely out of sight,
Hooray — certificates snug as can be!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/certificate page redesign' is directly related to the main changes, which involve extensive UI/UX updates to the certificate management page, including new upload modes, certificate display, and interaction patterns.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/certificate-page-redesign

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

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_web/live/certificate_live.ex (1)

261-266: ⚠️ Potential issue | 🔴 Critical

Wrap template content with <Layouts.app> to render sidebar, navigation, and flash messages.

The template must begin with <Layouts.app flash={@flash} current_user={@current_user} current_company={@current_company} companies={@companies} current_path={@current_path}> instead of starting directly with <.header>. The root layout only provides the HTML document structure; the application layout component provides the sidebar navigation, flash message display, and main content container. All required assigns are available from the LiveAuth on_mount hook.

🤖 Fix all issues with AI agents
In `@lib/ksef_hub/credentials/certificate_info/openssl.ex`:
- Around line 95-97: The Logger.warning call currently logs the full OpenSSL
output in the clause matching {:ok, {output, exit_code}}; change it to avoid
logging any certificate or error output by removing `output` from the log and
only logging the exit_code (or a sanitized/static message) in the Logger.warning
invocation (i.e., update the clause with {:ok, {output, exit_code}} -> ... so
Logger.warning only includes exit_code and not the `output` variable).
- Around line 15-16: The formatting in the OpenSSL credential file is off
causing CI to fail; run `mix format` for the module (file containing
Record.defrecord(:otp_certificate, ...) and
Record.defrecord(:otp_tbscertificate, ...)) or manually reformat so the two
Record.defrecord lines conform to Elixir formatter style (proper
spacing/indentation and line breaks) and then commit the formatted file so CI
passes.
🧹 Nitpick comments (1)
test/ksef_hub/credentials/certificate_info_test.exs (1)

1-7: Consider renaming test file to mirror source path.

The test file is at test/ksef_hub/credentials/certificate_info_test.exs but tests KsefHub.Credentials.CertificateInfo.Openssl located at lib/ksef_hub/credentials/certificate_info/openssl.ex. Per coding guidelines, test files should mirror source paths.

Consider renaming to test/ksef_hub/credentials/certificate_info/openssl_test.exs for consistency, or update the module name to reflect it tests multiple implementations.

Comment thread lib/ksef_hub/credentials/certificate_info/openssl.ex Outdated
Comment thread lib/ksef_hub/credentials/certificate_info/openssl.ex Outdated
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/ksef_hub/credentials/certificate_info/openssl.ex`:
- Around line 134-137: The rescue block currently logs exception details via
Logger.warning("Failed to parse certificate: #{inspect(e)}") which can leak
certificate data; change the log to a static message (e.g.
Logger.warning("Failed to parse certificate")) and preserve the existing return
{:error, :parse_failed} so no exception/binary contents are logged; update the
rescue in the same function where Logger.warning and {:error, :parse_failed}
appear (the certificate parse/rescue block) to remove inspect(e) from the log.
🧹 Nitpick comments (1)
lib/ksef_hub/credentials/certificate_info/openssl.ex (1)

55-100: Reduce duplication in OpenSSL invocation helpers.

extract_pem/2 and retry_without_legacy/2 duplicate the same Task/timeout boilerplate, inflating both functions beyond the small-function guideline. Consider extracting a shared helper to keep each function focused.

♻️ Suggested refactor
+  defp run_openssl(args) do
+    task = Task.async(fn -> System.cmd("openssl", args, stderr_to_stdout: true) end)
+
+    case Task.yield(task, 30_000) || Task.shutdown(task, :brutal_kill) do
+      {:ok, {output, 0}} -> {:ok, output}
+      {:ok, {output, exit_code}} -> {:error, {:openssl_failed, output, exit_code}}
+      nil -> {:error, :timeout}
+    end
+  end
+
   defp extract_pem(p12_path, pass_path) do
     args = [
       "pkcs12",
       "-in",
       p12_path,
       "-passin",
       "file:#{pass_path}",
       "-clcerts",
       "-nokeys",
       "-legacy"
     ]
 
-    task = Task.async(fn -> System.cmd("openssl", args, stderr_to_stdout: true) end)
-
-    case Task.yield(task, 30_000) || Task.shutdown(task, :brutal_kill) do
-      {:ok, {output, 0}} ->
-        {:ok, output}
-
-      {:ok, {_output, _exit_code}} ->
-        # Retry without -legacy flag for older OpenSSL versions
-        retry_without_legacy(p12_path, pass_path)
-
-      nil ->
-        Logger.warning("openssl pkcs12 info extraction timed out")
-        {:error, :timeout}
-    end
+    case run_openssl(args) do
+      {:ok, output} ->
+        {:ok, output}
+
+      {:error, {:openssl_failed, _output, _exit_code}} ->
+        retry_without_legacy(p12_path, pass_path)
+
+      {:error, :timeout} ->
+        Logger.warning("openssl pkcs12 info extraction timed out")
+        {:error, :timeout}
+    end
   end
 
   defp retry_without_legacy(p12_path, pass_path) do
     args = [
       "pkcs12",
       "-in",
       p12_path,
       "-passin",
       "file:#{pass_path}",
       "-clcerts",
       "-nokeys"
     ]
 
-    task = Task.async(fn -> System.cmd("openssl", args, stderr_to_stdout: true) end)
-
-    case Task.yield(task, 30_000) || Task.shutdown(task, :brutal_kill) do
-      {:ok, {output, 0}} ->
-        {:ok, output}
-
-      {:ok, {output, exit_code}} ->
-        Logger.warning("openssl pkcs12 info extraction failed (exit #{exit_code}): #{output}")
-        {:error, {:openssl_failed, exit_code}}
-
-      nil ->
-        Logger.warning("openssl pkcs12 info extraction timed out")
-        {:error, :timeout}
-    end
+    case run_openssl(args) do
+      {:ok, output} ->
+        {:ok, output}
+
+      {:error, {:openssl_failed, output, exit_code}} ->
+        Logger.warning("openssl pkcs12 info extraction failed (exit #{exit_code}): #{output}")
+        {:error, {:openssl_failed, exit_code}}
+
+      {:error, :timeout} ->
+        Logger.warning("openssl pkcs12 info extraction timed out")
+        {:error, :timeout}
+    end
   end

As per coding guidelines, "Keep functions small and focused (< 15 lines ideally) in Elixir."

Comment thread lib/ksef_hub/credentials/certificate_info/openssl.ex
After a certificate upload, the SyncWorker would fail with
reauth_required because no initial XADES authentication was
triggered. Add AuthWorker (Oban job) that runs the full auth
flow after upload and stores tokens via TokenManager.

Also: stop logging openssl output in cert info extraction,
rename certificate_info test to mirror source path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/ksef_hub_web/live/certificate_live.ex`:
- Around line 225-235: In extract_certificate_info, avoid logging the
potentially sensitive reason returned from certificate_info().extract; replace
Logger.warning("Failed to extract certificate info: #{inspect(reason)}") with a
static, non-sensitive message such as Logger.warning("Failed to extract
certificate info") so extract_certificate_info and any callers of
certificate_info().extract no longer emit certificate or parsing details to
logs.
🧹 Nitpick comments (2)
lib/ksef_hub_web/live/certificate_live.ex (1)

247-252: Consider handling enqueue_auth failures.

The return value of enqueue_auth/1 is ignored. If Oban insertion fails, the user receives a success flash but authentication won't be attempted. Consider logging or notifying on failure.

💡 Suggested improvement
-  `@spec` enqueue_auth(Ecto.UUID.t()) :: {:ok, Oban.Job.t()} | {:error, term()}
-  defp enqueue_auth(company_id) do
-    %{company_id: company_id}
-    |> AuthWorker.new()
-    |> Oban.insert()
+  `@spec` enqueue_auth(Ecto.UUID.t()) :: :ok
+  defp enqueue_auth(company_id) do
+    case %{company_id: company_id} |> AuthWorker.new() |> Oban.insert() do
+      {:ok, _job} -> :ok
+      {:error, reason} ->
+        Logger.error("Failed to enqueue auth job for company #{company_id}")
+        :ok
+    end
   end
test/ksef_hub/credentials/certificate_info/openssl_test.exs (1)

1-8: Consider async: false for integration tests using external processes.

This test spawns OpenSSL processes via Pkcs12Converter and Openssl.extract. While SecureTemp generates unique paths, running multiple integration tests concurrently could cause resource contention or flaky behavior.

💡 Suggested change
-  use ExUnit.Case, async: true
+  use ExUnit.Case, async: false

Comment thread lib/ksef_hub_web/live/certificate_live.ex
Remove potentially sensitive data from log messages in certificate
parsing and extraction. Extract shared run_openssl/1 helper to
eliminate Task/timeout boilerplate duplication. Handle enqueue_auth
failures with explicit error logging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_web/live/certificate_live.ex (1)

85-101: ⚠️ Potential issue | 🟠 Major

Verify authorization before removing credential.

The remove_certificate handler validates the UUID and fetches the credential, but doesn't verify the credential belongs to current_company. A user could potentially remove credentials from other companies by manipulating the id parameter.

🛡️ Proposed fix to add authorization check
 def handle_event("remove_certificate", %{"id" => id}, socket) do
+  company_id = socket.assigns.current_company && socket.assigns.current_company.id
+
   with {:ok, uuid} <- Ecto.UUID.cast(id),
-       %{} = credential <- Credentials.get_credential(uuid) do
+       %{} = credential <- Credentials.get_credential(uuid),
+       true <- credential.company_id == company_id do
     case Credentials.deactivate_credential(credential) do
       {:ok, _} ->
         {:noreply,
          socket
          |> put_flash(:info, "Certificate removed.")
          |> load_credentials()}

       {:error, _} ->
         {:noreply, put_flash(socket, :error, "Failed to remove certificate.")}
     end
   else
     _ -> {:noreply, put_flash(socket, :error, "Certificate not found.")}
   end
 end

Based on learnings: "Validate all state changes in LiveView events" and "Validate all user input in event handlers."

🧹 Nitpick comments (2)
lib/ksef_hub/credentials/certificate_info/openssl.ex (1)

99-116: Consider returning more specific error information.

The rescue block catches all exceptions and returns a generic :parse_failed error. While this is safe from a logging perspective, it may make debugging difficult. Consider matching specific exception types if there are known failure modes that would benefit from distinct error atoms.

lib/ksef_hub_web/live/certificate_live.ex (1)

247-257: Silent failure in enqueue_auth may hide issues.

The function logs an error but always returns :ok, making the failure invisible to the caller. Consider whether the caller should be informed of enqueueing failures, or at minimum ensure monitoring/alerting catches these logged errors.

♻️ Alternative: Return the error tuple for caller awareness
 `@spec` enqueue_auth(Ecto.UUID.t()) :: :ok | {:error, term()}
 defp enqueue_auth(company_id) do
   case %{company_id: company_id} |> AuthWorker.new() |> Oban.insert() do
     {:ok, _job} ->
       :ok

     {:error, reason} ->
       Logger.error("Failed to enqueue auth job for company #{company_id}")
-      :ok
+      {:error, reason}
   end
 end

Then handle in save_credential:

case Credentials.replace_active_credential(company.id, attrs) do
  {:ok, _credential} ->
    case enqueue_auth(company.id) do
      :ok -> :ok
      {:error, _} -> :ok  # Log already happened, upload succeeded
    end
    # ... rest of success handling

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant