Skip to content

Feat/email password auth#31

Merged
emilwojtaszek merged 10 commits into
mainfrom
feat/email-password-auth
Feb 10, 2026
Merged

Feat/email password auth#31
emilwojtaszek merged 10 commits into
mainfrom
feat/email-password-auth

Conversation

@emilwojtaszek

@emilwojtaszek emilwojtaszek commented Feb 10, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Email/password registration, login, confirmation and password-reset flows (with email delivery)
    • Session tokens and token-based session management; token-backed login/logout
    • Google account linking to existing users
    • New user-facing forms/components for consistent form UI
  • Improvements

    • Landing page updated to show Log in / Create an account
    • Authentication flows unified and simplified (token-centric)
  • Tests

    • Expanded coverage for auth flows, tokens, and email delivery
  • Chores

    • Removed explicit allowed-email allowlist from config and examples

emilwojtaszek and others added 6 commits February 10, 2026 20:19
Add bcrypt_elixir for password hashing. Create migration to add
hashed_password and confirmed_at fields to users table, and create
users_tokens table for session/email token management.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- UserToken: session and email token generation with SHA256 hashing
- User: registration_changeset, password_changeset, valid_password?
- UserNotifier: confirmation and password reset emails via Swoosh
- Accounts: register_user, session tokens, confirmation, password reset,
  get_or_create_google_user (with email-linking)
- UserAuth: log_in_user/log_out_user session helpers
- extract_user_token test helper in DataCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch all authentication from raw user_id in session to
token-based session management. This enables token revocation,
expiry, and is more secure.

- RequireAuth plug reads user_token, looks up via session token
- LiveAuth reads user_token, adds redirect_if_authenticated and
  mount_current_user on_mount callbacks
- AuthController uses get_or_create_google_user and UserAuth.log_in_user
- CompanySwitchController uses conn.assigns.current_user
- Layouts logout link points to /users/log-out
- All test files migrated to log_in_user helper
- ConnCase.log_in_user/3 helper with optional extra session params

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add registration, login, forgot password, reset password, confirmation,
and confirmation instructions LiveViews. Add UserSessionController for
email/password login/logout. Add simple_form and public error components
to CoreComponents. Add password_user factory and tests (22 new tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove allowed_email?/1, allowed_emails/0, parse_and_cache_allowed_emails/0,
clear_allowed_emails_cache/0, and find_or_create_user/1 from Accounts context.
Remove ALLOWED_EMAILS config from runtime.exs and test.exs. Update all tests
to use get_or_create_google_user/1. Update landing page with login/register
links instead of Google-only sign-in.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add mass-assignment security test for registration_changeset (Rule 2)
- Add data-testid attributes to all auth LiveView templates (Rule 5)
- Update LiveView tests to use has_element? with data-testid (Rule 4)
- Add @SPEC to simple_form/1 and error/1 in CoreComponents (Rule 7)

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

coderabbitai Bot commented Feb 10, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces the Google allowlist OAuth flow with full email/password auth and token-based sessions: adds bcrypt hashing, user confirmation and password-reset tokens, UserToken/UserNotifier/UserAuth modules, new LiveViews/controllers/routes, DB migration, tests, test helpers, and removes ALLOWED_EMAILS configuration.

Changes

Cohort / File(s) Summary
Config
config/runtime.exs, config/test.exs, .env.example
Removed ALLOWED_EMAILS allowlist configuration and example env entry.
Accounts core & schema
lib/ksef_hub/accounts.ex, lib/ksef_hub/accounts/user.ex
Added email/password registration, bcrypt hashing/validation, confirmation/reset changesets, session token APIs, get_or_create_google_user; added hashed_password, confirmed_at, and related helpers.
Tokens & mailer
lib/ksef_hub/accounts/user_token.ex, lib/ksef_hub/accounts/user_notifier.ex
New users_tokens schema and token builders/verifiers (session + email tokens); UserNotifier composes/sends confirmation and reset emails via Swoosh.
Web auth helpers & plugs
lib/ksef_hub_web/user_auth.ex, lib/ksef_hub_web/live/live_auth.ex, lib/ksef_hub_web/plugs/require_auth.ex
Added UserAuth (log_in_user/log_out_user/signed_in_path); LiveAuth and require_auth migrated to session_token flows and added on_mount variants.
Controllers & router
lib/ksef_hub_web/controllers/auth_controller.ex, lib/ksef_hub_web/controllers/user_session_controller.ex, lib/ksef_hub_web/router.ex
AuthController now uses get_or_create_google_user + UserAuth; new UserSessionController for email/password login/logout; router adds LiveView routes for register/login/confirm/reset and controller endpoints; removed old OAuth logout route.
LiveViews (auth UI)
lib/ksef_hub_web/live/...
Added LiveViews for registration, login, forgot-password, reset-password, confirmation instructions, and confirmation handling with token flows and validations.
UI components & pages
lib/ksef_hub_web/components/core_components.ex, lib/ksef_hub_web/components/layouts.ex, lib/ksef_hub_web/controllers/page_html/home.html.heex
Made error component public, added simple_form with slots, updated logout link to /users/log-out, replaced Google sign-in on home with login/register links.
Company/session & templates
lib/ksef_hub_web/controllers/company_switch_controller.ex, lib/ksef_hub_web/live/company_live/index.ex
Company switch now reads current_user from assigns; company name wrapped with data-testid="company-name" for tests.
DB migration & deps
priv/repo/migrations/...add_auth_fields_to_users.exs, mix.exs
Migration adds hashed_password and confirmed_at to users and creates users_tokens table; added bcrypt_elixir dependency.
Tests & test helpers
test/**/*, test/support/{conn_case.ex,data_case.ex,factory.ex}
Extensive tests added/updated for registration, login, tokens, confirmation, reset, notifier and LiveViews; added extract_user_token and log_in_user test helpers and password_user_factory.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser as Web Browser
    participant LiveView as Phoenix LiveView
    participant Accounts as Accounts Context
    participant DB as Database
    participant Mailer as Email Mailer

    User->>Browser: Open /users/register
    Browser->>LiveView: mount registration form
    LiveView->>Browser: render form
    User->>LiveView: submit email & password
    LiveView->>Accounts: register_user(attrs)
    Accounts->>Accounts: validate & bcrypt hash password
    Accounts->>DB: insert user (hashed_password, confirmed_at nil)
    Accounts->>Accounts: build confirmation token
    Accounts->>Mailer: deliver_user_confirmation_instructions(user, url)
    Mailer-->>User: email sent
    User->>Browser: click email link
    Browser->>LiveView: mount /users/confirm/:token
    LiveView->>Accounts: confirm_user(token)
    Accounts->>DB: verify token & set confirmed_at
    DB-->>Accounts: confirmation saved
    LiveView->>Browser: show success
Loading
sequenceDiagram
    participant User
    participant Browser as Web Browser
    participant LiveView as Phoenix LiveView
    participant Controller as UserSessionController
    participant Accounts as Accounts Context
    participant DB as Database

    User->>Browser: Open /users/log-in
    Browser->>LiveView: mount login form
    User->>LiveView: submit credentials (phx-trigger-action)
    Browser->>Controller: POST /users/log-in
    Controller->>Accounts: get_user_by_email_and_password(email, pass)
    Accounts->>DB: fetch user by email
    Accounts->>Accounts: verify password (bcrypt)
    alt valid
        Accounts-->>Controller: user
        Controller->>Accounts: generate_user_session_token(user)
        Accounts->>DB: insert session token
        Controller->>Browser: put session user_token & redirect /dashboard
    else invalid
        Controller->>Browser: flash error & redirect /users/log-in
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I nibble code and hop with glee,
Bcrypt crumbs and tokens set free,
Confirmation letters fly at night,
Sessions sparkle, tests all right,
I thump my foot — deploy with delight!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/email password auth' clearly summarizes the main change—the introduction of email/password authentication functionality—as evidenced by the comprehensive additions throughout the changeset including new User schema fields, authentication controllers, LiveView forms, session management, and supporting infrastructure.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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/email-password-auth

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
lib/ksef_hub/accounts.ex (2)

244-252: Add guard clause for consistency with session token functions.

confirm_user/1 lacks a guard clause on the token parameter, unlike get_user_by_session_token/1 and delete_user_session_token/1 which have when is_binary(token) guards and fallback clauses. If UserToken.verify_email_token_query/2 raises on non-binary input, the else clause won't catch it.

Suggested fix
-  def confirm_user(token) do
+  def confirm_user(token) when is_binary(token) do
     with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
          %User{} = user <- Repo.one(query),
          {:ok, %{user: user}} <- confirm_user_multi(user) do
       {:ok, user}
     else
       _ -> :error
     end
   end
+
+  def confirm_user(_), do: :error

280-288: Add guard clause for consistency with session token functions.

Similar to confirm_user/1, this function lacks a guard clause on the token parameter for consistency with the established pattern in session token functions.

Suggested fix
-  def get_user_by_reset_password_token(token) do
+  def get_user_by_reset_password_token(token) when is_binary(token) do
     with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"),
          %User{} = user <- Repo.one(query) do
       user
     else
       _ -> nil
     end
   end
+
+  def get_user_by_reset_password_token(_), do: nil

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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/ksef_hub_web/live/company_live_test.exs (1)

40-46: ⚠️ Potential issue | 🟡 Minor

Add data-testid attributes to template and use has_element?/2 for assertions. The current raw HTML assertions should be replaced with proper element selectors. First, add a data-testid="company-name" attribute to the company name cell in the template (line 152 of lib/ksef_hub_web/live/company_live/index.ex), then update the test assertions:

      {:ok, view, _html} =
         conn
         |> log_in_user(user, %{current_company_id: company.id})
         |> live("/companies")

-      assert html =~ "My Visible Co"
-      refute html =~ "Someone Else Co"
+      assert has_element?(view, "[data-testid='company-name']", "My Visible Co")
+      refute has_element?(view, "[data-testid='company-name']", "Someone Else Co")

Template change needed in lib/ksef_hub_web/live/company_live/index.ex line 152:

-        <:col :let={company} label="Name">{company.name}</:col>
+        <:col :let={company} label="Name"><span data-testid="company-name">{company.name}</span></:col>
🤖 Fix all issues with AI agents
In `@lib/ksef_hub_web/controllers/company_switch_controller.ex`:
- Around line 14-18: The update action currently reads user =
conn.assigns.current_user and then uses user.id which can be nil and crash;
change the with in update to pattern-match the user (e.g., {:ok, %User{} = user}
or user when not is_nil(user)) as the first clause so missing/unauthenticated
current_user is routed to the with's else branch instead of raising; ensure
subsequent clauses still use that matched user
(Companies.get_membership(user.id, uuid)) and add an appropriate else clause
handling the unauthenticated case.

In `@lib/ksef_hub_web/live/user_registration_live.ex`:
- Around line 30-35: The direct pattern match {:ok, _} =
Accounts.deliver_user_confirmation_instructions(...) can crash on delivery
failure; replace it with a safe branch (case or with) around
Accounts.deliver_user_confirmation_instructions in user_registration_live (the
block that builds url(~p"/users/confirm/#{&1}")) and handle {:ok, _} and
{:error, reason} explicitly — on success keep current flow, on error log the
reason and surface a user-friendly message via put_flash (or similar) so the
LiveView does not crash and the user sees a graceful failure response.

In `@lib/ksef_hub_web/user_auth.ex`:
- Around line 47-56: The docstring for signed_in_path/1 claims it redirects to
"/companies/new" when the user has no companies but the implementation always
returns ~p"/dashboard"; update the function signed_in_path(user) to inspect the
User struct (e.g., check user.companies or call Accounts.list_user_companies/1)
and return ~p"/companies/new" when none exist, otherwise ~p"/dashboard", or
alternatively change the `@doc` to state it always returns "/dashboard" so docs
and implementation match—ensure you modify the `@doc` or the function
signed_in_path/1 accordingly and keep the `@spec` signature unchanged.

In `@lib/ksef_hub/accounts.ex`:
- Around line 63-73: Normalize the incoming email in
get_user_by_email_and_password the same way registration does: trim whitespace
and downcase before calling get_user_by_email so lookups match stored normalized
emails; update get_user_by_email_and_password to transform the email (e.g.,
String.trim/1 |> String.downcase/1) prior to fetching the user and then continue
to call User.valid_password?/2 as before (keep function name
get_user_by_email_and_password and the call to get_user_by_email so only the
input is normalized).

In `@lib/ksef_hub/accounts/user_token.ex`:
- Around line 95-113: verify_email_token_query/2 can crash because
days_for_context/1 has no catch-all; add a default clause in days_for_context/1
that returns :error for unknown contexts (e.g., days_for_context(_)-> :error)
and then update verify_email_token_query/2 to pattern-match the days result (or
case on days_for_context(context)) and return :error when it receives :error
instead of calling ago/2 with an invalid value; reference functions
days_for_context/1 and verify_email_token_query/2 when making these changes
(also apply same pattern to the similar code around the other occurrence).

In `@test/support/conn_case.ex`:
- Around line 39-60: Update the `@doc` for the public functions to include
explicit parameter and return types: for defdelegate extract_user_token(fun)
document that fun is a zero-arity function or a function expected to send an
email (e.g., (-> any) or (() -> any)) and state the return type, and for
log_in_user(conn, user, extra_session \\ %{}) update its doc to specify types
for conn (Plug.Conn.t()), user (KsefHub.Accounts.User.t() or the appropriate
struct/type), extra_session (map()), and the return type (Plug.Conn.t() with
session containing :user_token). Keep the prose brief and place the type
annotations inside the `@doc` so readers can see parameter and return types for
extract_user_token and log_in_user.

In `@test/support/data_case.ex`:
- Around line 45-56: Update the public function doc for extract_user_token/1 to
include explicit parameter and return types: document the single parameter as a
function of type (String.t() -> any()) (or more specific if known) that receives
the URL, and document the return as a tuple {String.t(), any()} where the first
element is the encoded token; add a brief "Parameters" and "Returns" line in the
`@doc` for extract_user_token/1 reflecting these types and the example usage.
🧹 Nitpick comments (9)
test/support/data_case.ex (1)

57-62: Return {:ok, result}/{:error, reason} instead of raising on failures.

This helper assumes success and will crash if the email function fails or the token markers are missing.

♻️ Possible refactor
 def extract_user_token(fun) do
-  {:ok, captured} = fun.(&"[TOKEN]#{&1}[/TOKEN]")
-  %{text_body: body} = captured
-  [_, token_and_rest | _] = String.split(body, "[TOKEN]")
-  [token | _] = String.split(token_and_rest, "[/TOKEN]")
-  {token, captured}
+  case fun.(&"[TOKEN]#{&1}[/TOKEN]") do
+    {:ok, %{text_body: body} = captured} ->
+      with [_, token_and_rest | _] <- String.split(body, "[TOKEN]"),
+           [token | _] <- String.split(token_and_rest, "[/TOKEN]") do
+        {:ok, {token, captured}}
+      else
+        _ -> {:error, :token_not_found}
+      end
+
+    {:error, reason} ->
+      {:error, reason}
+  end
 end
Based on learnings “Use `{:ok, result}` and `{:error, reason}` tuples for operations that can fail”.
lib/ksef_hub_web/live/user_login_live.ex (1)

28-32: Minor: Redundant parameter access in "save" handler.

The user_params is already destructured from params["user"], but line 30 accesses params["user"] again. This could be simplified.

♻️ Suggested simplification
-  def handle_event("save", %{"user" => _user_params} = params, socket) do
-    # The form will POST to the session controller via phx-trigger-action
-    form = to_form(params["user"], as: "user")
+  def handle_event("save", %{"user" => user_params}, socket) do
+    # The form will POST to the session controller via phx-trigger-action
+    form = to_form(user_params, as: "user")
     {:noreply, assign(socket, form: form, trigger_submit: true)}
   end
test/ksef_hub_web/live/user_reset_password_live_test.exs (1)

50-59: Consider using element-based assertion for validation errors.

Line 58 asserts against raw HTML content (result =~ "should be at least 12 character"). Per coding guidelines, prefer testing for element presence rather than raw HTML content which can change. Consider asserting on an error element with a specific selector.

♻️ Alternative using element assertion
     test "renders errors for invalid password", %{conn: conn, token: token} do
       {:ok, view, _html} = live(conn, ~p"/users/reset-password/#{token}")

-      result =
-        view
-        |> form("#reset_password_form", user: %{password: "short"})
-        |> render_submit()
-
-      assert result =~ "should be at least 12 character"
+      view
+      |> form("#reset_password_form", user: %{password: "short"})
+      |> render_submit()
+
+      assert has_element?(view, "[data-testid='password-error']") or
+             has_element?(view, ".field-error")
     end

Note: The exact selector depends on how errors are rendered in your form component.

test/ksef_hub_web/live/user_registration_live_test.exs (1)

50-60: Consider element-based assertion for validation errors.

Similar to other tests, line 59 asserts against raw HTML content. Consider using an element selector for the error message if one exists.

test/ksef_hub/accounts/user_test.exs (1)

6-78: Prefer factory-built attrs over inline maps. This keeps tests consistent with the rest of the suite and reduces drift if defaults change.

♻️ Possible direction
-      attrs = %{email: "user@example.com", password: "valid_password123"}
+      attrs = params_for(:user, email: "user@example.com", password: "valid_password123")

(Apply similarly across other tests.)

As per coding guidelines "Use ExMachina factories for test data instead of inline valid_attrs maps".

test/ksef_hub_web/live/user_login_live_test.exs (1)

27-44: Consider using factory defaults for password.

The test manually hashes the password with Bcrypt.hash_pwd_salt(password). If the :password_user factory already handles password hashing, you could simplify this by passing the plaintext password to the factory and letting it hash internally.

However, this approach is acceptable if you need explicit control over the password value for testing.

test/ksef_hub_web/controllers/auth_controller_test.exs (1)

135-162: Consider using factory for consistency.

The test uses Accounts.register_user/1 directly instead of the :password_user factory. While this works, using factories provides consistency across tests.

♻️ Optional refactor using factory
    test "links Google account to existing email-registered user", %{conn: conn} do
-     # Register a user via email/password first
-     {:ok, email_user} =
-       Accounts.register_user(%{email: "link@example.com", password: "valid_password123"})
+     email_user = insert(:password_user, email: "link@example.com")

      auth = %Ueberauth.Auth{
        uid: "google-link-uid",
        info: %Ueberauth.Auth.Info{
          email: "link@example.com",
lib/ksef_hub/accounts/user_notifier.ex (1)

68-68: Consider making the sender address configurable.

The hardcoded "noreply@ksef-hub.com" may need to vary across environments (development, staging, production). Consider pulling this from application configuration:

♻️ Proposed refactor
+  `@from_email` Application.compile_env(:ksef_hub, :mailer_from, {"KSeF Hub", "noreply@ksef-hub.com"})
+
   `@spec` deliver(String.t(), String.t(), String.t()) ::
           {:ok, Swoosh.Email.t()} | {:error, term()}
   defp deliver(recipient, subject, body) do
     email =
       new()
       |> to(recipient)
-      |> from({"KSeF Hub", "noreply@ksef-hub.com"})
+      |> from(`@from_email`)
       |> subject(subject)
       |> text_body(body)
test/ksef_hub/accounts_test.exs (1)

219-225: Simplify the double-negative assertion.

refute updated.id != user.id is a double negative that can be confusing. Consider using a direct assertion for clarity.

♻️ Proposed refactor
       assert {:ok, updated} =
                Accounts.reset_user_password(user, %{password: "new_password12345"})

       assert Accounts.get_user_by_email_and_password("reset@example.com", "new_password12345")
-      refute updated.id != user.id
+      assert updated.id == user.id

Comment thread lib/ksef_hub_web/controllers/company_switch_controller.ex Outdated
Comment on lines +30 to +35
{:ok, user} ->
{:ok, _} =
Accounts.deliver_user_confirmation_instructions(
user,
&url(~p"/users/confirm/#{&1}")
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle email delivery failure gracefully.

The pattern match {:ok, _} = Accounts.deliver_user_confirmation_instructions(...) will crash if email delivery fails. Consider handling the error case to provide a better user experience.

🛡️ Proposed fix
       {:ok, user} ->
-        {:ok, _} =
-          Accounts.deliver_user_confirmation_instructions(
-            user,
-            &url(~p"/users/confirm/#{&1}")
-          )
+        case Accounts.deliver_user_confirmation_instructions(
+               user,
+               &url(~p"/users/confirm/#{&1}")
+             ) do
+          {:ok, _} -> :ok
+          {:error, _} -> :ok  # User created; email failure logged separately
+        end

         changeset = Accounts.change_registration(user)
         {:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{:ok, user} ->
{:ok, _} =
Accounts.deliver_user_confirmation_instructions(
user,
&url(~p"/users/confirm/#{&1}")
)
{:ok, user} ->
case Accounts.deliver_user_confirmation_instructions(
user,
&url(~p"/users/confirm/#{&1}")
) do
{:ok, _} -> :ok
{:error, _} -> :ok # User created; email failure logged separately
end
changeset = Accounts.change_registration(user)
{:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}
🤖 Prompt for AI Agents
In `@lib/ksef_hub_web/live/user_registration_live.ex` around lines 30 - 35, The
direct pattern match {:ok, _} =
Accounts.deliver_user_confirmation_instructions(...) can crash on delivery
failure; replace it with a safe branch (case or with) around
Accounts.deliver_user_confirmation_instructions in user_registration_live (the
block that builds url(~p"/users/confirm/#{&1}")) and handle {:ok, _} and
{:error, reason} explicitly — on success keep current flow, on error log the
reason and surface a user-friendly message via put_flash (or similar) so the
LiveView does not crash and the user sees a graceful failure response.

Comment thread lib/ksef_hub_web/user_auth.ex
Comment thread lib/ksef_hub/accounts/user_token.ex
Comment thread test/support/data_case.ex
- CompanySwitchController: guard against nil current_user with case
  instead of flat with, separate unauthenticated vs not-found paths
- UserRegistrationLive: handle email delivery failure gracefully
  with case instead of crash-on-error pattern match
- UserAuth: fix signed_in_path docs to match implementation (always
  returns /dashboard, company redirect handled by LiveAuth on_mount)
- Accounts: normalize email (trim + downcase) in
  get_user_by_email_and_password to match registration behavior
- UserToken: add catch-all to days_for_context returning :error,
  update verify_email_token_query to handle unknown contexts safely
- UserNotifier: make sender email configurable via :mailer_from
- UserLoginLive: fix redundant params access in save handler
- CompanyLive: add data-testid="company-name" to template
- Test improvements: fix double-negative assertion, use factories
  consistently, improve docs on extract_user_token and log_in_user,
  use has_element? for company list assertions

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: 5

🤖 Fix all issues with AI agents
In `@lib/ksef_hub_web/live/user_registration_live.ex`:
- Around line 31-41: The Logger.error call in the
Accounts.deliver_user_confirmation_instructions error branch exposes raw error
details via inspect(reason); update the error handling in
user_registration_live.ex to avoid logging tokens/credentials by removing
inspect(reason) and logging a non-sensitive, generic message (e.g., "Failed to
deliver confirmation email") or a sanitized error code/string instead, keeping
the surrounding structure (the case on
Accounts.deliver_user_confirmation_instructions and the require Logger) intact.

In `@lib/ksef_hub_web/user_auth.ex`:
- Around line 32-45: When logging out in log_out_user, broadcast a "disconnect"
to the user's LiveView topic so mounted LiveViews close immediately: after
reading user_token via get_session(conn, :user_token) (and before relying on
stale assigns), compose the live_socket_id string (e.g.
"users_sessions:#{user_token}") and call your endpoint's broadcast with event
"disconnect" and an empty payload, then proceed to call
Accounts.delete_user_session_token(user_token) and continue with
configure_session/clear_session/redirect; update lib/ksef_hub_web/user_auth.ex
around the log_out_user function to perform the broadcast using your app
Endpoint and the user_token-derived live_socket_id.

In `@lib/ksef_hub/accounts.ex`:
- Around line 110-135: In get_or_create_google_user/1, normalize the Google
email by downcasing it once and reuse that normalized value for lookups and
inserts to avoid case-mismatch duplicates; specifically, compute a
normalized_email = String.downcase(email || "") at the top of the function, call
get_user_by_email(normalized_email) (instead of the raw email), and when
building the User.changeset for the insert/update include the normalized_email
as the :email field so the behaviour matches User.changeset/2 which downcases
emails.
- Around line 185-197: Update the `@specs` for
deliver_user_confirmation_instructions/2 and
deliver_user_reset_password_instructions/2 to include mailer error returns:
change the return type from {:ok, Swoosh.Email.t()} | {:error,
:already_confirmed} (or solely {:ok, Swoosh.Email.t()}) to include {:error,
term()} (and keep the existing {:error, :already_confirmed} case if you want it
explicit), so the specs reflect that UserNotifier.deliver_* can return {:ok,
Swoosh.Email.t()} | {:error, term()} (and optionally | {:error,
:already_confirmed}) for the functions deliver_user_confirmation_instructions
and deliver_user_reset_password_instructions.

In `@test/support/conn_case.ex`:
- Around line 39-76: Add `@spec` annotations for the public helpers: declare
extract_user_token/1 with the type (fun :: (String.t() -> {:ok,
Swoosh.Email.t()})) :: {String.t(), Swoosh.Email.t()} placed immediately above
the defdelegate extract_user_token(fun) line, and declare log_in_user/3 with the
type (conn :: Plug.Conn.t(), user :: KsefHub.Accounts.User.t(), extra_session ::
map()) :: Plug.Conn.t() placed immediately above the def log_in_user(conn, user,
extra_session \\ %{}) definition; ensure the specs match the exported function
names extract_user_token/1 and log_in_user/3 and follow project formatting
conventions.

Comment thread lib/ksef_hub_web/live/user_registration_live.ex
Comment thread lib/ksef_hub_web/user_auth.ex
Comment thread lib/ksef_hub/accounts.ex Outdated
Comment thread lib/ksef_hub/accounts.ex
Comment thread test/support/conn_case.ex
- Remove inspect(reason) from Logger.error in UserRegistrationLive to
  avoid leaking sensitive data in logs
- Add LiveView broadcast disconnect on logout so mounted sessions close
  immediately
- Normalize Google email in get_or_create_google_user to prevent
  case-sensitive duplicates
- Update @specs for deliver_user_confirmation_instructions and
  deliver_user_reset_password_instructions to include {:error, term()}
- Add @SPEC annotations to ConnCase helpers

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: 3

🤖 Fix all issues with AI agents
In `@lib/ksef_hub/accounts.ex`:
- Around line 63-74: The current get_user_by_email_and_password/2 function has a
guard requiring binaries and will raise a FunctionClauseError for non-binary
inputs; add a fallback clause def get_user_by_email_and_password(_, _), do: nil
so calls with nil or non-binary email/password safely return nil, leaving the
existing guarded version (which normalizes the email, calls get_user_by_email/1
and uses User.valid_password?/2) intact.
- Around line 102-138: The current get_or_create_google_user performs separate
reads and writes which can race; convert it to an atomic Ecto transaction
(Ecto.Multi) inside get_or_create_google_user: start a Repo.transaction that (1)
attempts to fetch the user by google_uid (using get_user_by_google_uid or
Repo.get_by within the transaction) and returns it if found, (2) otherwise
attempts to fetch by normalized email and, if found, build a User.changeset to
set google_uid/name/avatar_url and call Repo.update as a Multi.run step, (3) if
no email user, build the new user changeset and call Repo.insert as a
Multi.insert step; ensure the User schema enforces unique_constraint on :email
and :google_uid and handle unique_constraint errors from the transaction result
by returning the existing user (pattern-match {:ok, result} vs {:error,
_changeset} and resolve conflicts by fetching the already-created record and
returning {:ok, user}). This replaces the plain read→update/insert flow with an
atomic Ecto.Multi/Repo.transaction using User.changeset, Repo.insert/update, and
unique_constraint handling.

In `@test/support/conn_case.ex`:
- Around line 39-55: Update the callback type for extract_user_token/1 to
reflect that the passed-in fun receives a URL-builder function and returns {:ok,
Swoosh.Email.t()}: change the `@doc` to state fun has the shape (String.t() ->
String.t()) -> {:ok, Swoosh.Email.t()} and update the `@spec` from (function() ->
{:ok, Swoosh.Email.t()}) to ((String.t() -> String.t()) -> {:ok,
Swoosh.Email.t()}) :: {String.t(), Swoosh.Email.t()}; reference the behavior in
KsefHub.DataCase.extract_user_token/1 (which calls
fun.(&"[TOKEN]#{&1}[/TOKEN]")) when making these edits so the doc and spec match
the actual callback signature.
🧹 Nitpick comments (1)
test/support/conn_case.ex (1)

72-79: Prevent extra_session from overriding :user_token.

Map.merge(%{user_token: token}, extra_session) lets callers overwrite :user_token, which can mask auth issues in tests. Consider ensuring the generated token always wins.

♻️ Suggested adjustment
-    session = Map.merge(%{user_token: token}, extra_session)
+    session = extra_session |> Map.put(:user_token, token)

Comment thread lib/ksef_hub/accounts.ex
Comment thread lib/ksef_hub/accounts.ex Outdated
Comment thread test/support/conn_case.ex
- Add fallback clause for get_user_by_email_and_password/2 to safely
  return nil for non-binary inputs instead of raising FunctionClauseError
- Convert get_or_create_google_user/1 to atomic Ecto.Multi transaction
  with unique constraint conflict resolution, preventing race conditions
  on concurrent Google sign-ins
- Fix extract_user_token @doc and @SPEC in ConnCase and DataCase to
  reflect actual callback signature: (String.t() -> String.t()) ->
  {:ok, Swoosh.Email.t()}
- Ensure log_in_user helper always wins over extra_session for
  :user_token key to prevent masking auth issues in tests

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/accounts.ex`:
- Around line 179-213: Add explicit guards to the session-token functions:
change get_user_by_session_token/1 to have a clause with when is_binary(token)
that runs the existing verification and Repo.one(query), and add a fallback
clause def get_user_by_session_token(_), do: nil; likewise add when
is_binary(token) to delete_user_session_token/1 for the existing code that
hashes and deletes, and add a fallback def delete_user_session_token(_), do:
:ok. Reference the existing functions get_user_by_session_token/1 and
delete_user_session_token/1 and keep generate_user_session_token/1 unchanged.

Comment thread lib/ksef_hub/accounts.ex
Add is_binary(token) guards to get_user_by_session_token/1 and
delete_user_session_token/1 with fallback clauses that safely return
nil/:ok for non-binary inputs instead of raising.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@emilwojtaszek emilwojtaszek merged commit 0d1d35c into main Feb 10, 2026
3 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Feb 10, 2026
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