# `PhoenixKitWeb.Users.Auth`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.14.2/lib/phoenix_kit_web/users/auth.ex#L1)

Authentication and authorization plugs for PhoenixKit user management.

This module provides plugs and functions for handling user authentication,
session management, and access control in Phoenix applications using PhoenixKit.

## Key Features

- User authentication with email and password
- Remember me functionality with secure cookies
- Session-based authentication
- Route protection and access control
- Module-level permission enforcement via on_mount hooks
- Integration with Phoenix LiveView on_mount callbacks

## on_mount Hooks

- `:phoenix_kit_ensure_admin` — Requires Owner/Admin role, or a custom role
  with at least one permission. Every role is then checked against the
  permission key mapped to the current admin view: Owner passes because its
  scope holds every key by construction; Admin holds keys as real,
  Owner-revocable rows (seeded/auto-granted by default). Disabled modules
  block everyone. Views that resolve to no permission key allow only a scope
  holding every enabled permission — role-agnostic, so a named Admin whose
  keys an Owner has partially revoked is denied too. The first mount of such a
  view logs a warning, whoever mounts it, so the missing mapping surfaces
  before a colleague hits the 403.
- `:phoenix_kit_ensure_module_access` — Checks that the feature module is
  permitted for the scope and (for custom roles) enabled.

## Usage

The plugs in this module are automatically configured when using
`PhoenixKitWeb.Integration.phoenix_kit_routes/0` macro in your router.

# `assign_embedded_current_user`

```elixir
@spec assign_embedded_current_user(Phoenix.LiveView.Socket.t(), map()) ::
  Phoenix.LiveView.Socket.t()
```

Reconstructs and assigns the current user + scope on an **embedded**
LiveView mount from a host-supplied `session["current_user_uuid"]`.

A LiveView rendered via `live_render/3` mounts with
`:not_mounted_at_router`, so it never runs a router `live_session`'s
`on_mount` hook (e.g. `:phoenix_kit_ensure_admin`) — leaving
`:phoenix_kit_current_scope` / `:phoenix_kit_current_user` absent, which
blinds any user-aware embedded UI (comment composers, activity-actor
attribution). The host bridges identity across the `live_render`
process boundary by passing its own authenticated user's UUID as
`session["current_user_uuid"]` — a **string**, never the `%User{}`
struct (a signed-but-not-encrypted `live_render` session would expose
it to the client). This helper reloads that user and assigns both
`:phoenix_kit_current_user` and `:phoenix_kit_current_scope`.

  * No-op when `:phoenix_kit_current_scope` is already assigned (router
    mount — the `on_mount` hook ran, before `mount/3`). Never clobbers
    the canonical scope.
  * An active user → assigns it + `Scope.for_user(user)`.
  * Absent / unknown / inactive uuid, or a transient DB error →
    anonymous (`nil` user + `Scope.for_user(nil)`), never raising.

> This reconstructs **identity** (audit, comment authorship), not
> **authorization** — it performs no role check. The UUID must come
> from the host's trusted server-side scope, never request params; and
> a host embedding an admin LiveView must gate the embedding page
> itself (the `on_mount` admin gate does not run for embeds).

Generic across embeddable feature modules — `phoenix_kit_projects` is
the reference consumer.

# `can_access_admin_view?`

```elixir
@spec can_access_admin_view?(PhoenixKit.Users.Auth.Scope.t() | nil, module()) ::
  boolean()
```

Whether `scope` may open the admin LiveView `view_module` — the SAME decision
`:phoenix_kit_ensure_admin` enforces on mount, exposed as a pure boolean.

Use it to decide whether to *render* a link, card or nav entry pointing at an
admin view. "A card is visible iff the visitor can open what it links to"
then holds by construction: both the card and its destination route through
this one function, so the two cannot drift.

Pure — no flash, no redirect, no logging. The on_mount hook keeps those side
effects (including the one-time "unmapped admin view" warning) and asks this
same decision for the verdict.

## The decision, branch by branch

1. **Admin-area gate** — `Scope.can_access_admin_area?/1` must hold (Owner,
   Admin, or any holder of at least one permission). Every `/admin` page
   mounts through `:phoenix_kit_ensure_admin`, which bounces a failing scope
   before any per-view check — every page but the landing, which is the one
   deliberate difference between this function and the gate (see below). So a
   card must respect it too: a `nil` scope, and an authenticated user with no
   permissions at all, stop here.
2. **Personal admin views** — the views in `@personal_admin_views` (your own
   notification inbox / preferences) show a user their OWN data rather than
   granting an administrative capability, so branch 1 is the whole gate.
3. **Mapped view** — `permission_key_for_admin_view/1` resolved a key:
   * the module behind the key must be enabled
     (`PhoenixKit.Users.Permissions.feature_enabled?/1`) — a disabled module
     is blocked for EVERYONE, Owner included;
   * a dotted SUB-permission key is checked with `Scope.can?/2` (which also
     requires the base key — a raw sub-key without its base is an orphan);
     a flat key with `Scope.has_module_access?/2`.
4. **Unmapped view** — the key resolved to `nil`: allowed ONLY for a scope
   that holds every enabled permission (`Scope.holds_all_enabled_permissions?/1`).
   This is the fail-CLOSED branch, and it is role-agnostic: Owner passes (it
   holds every key by construction), so does the `"*"` superadmin key and any
   role granted the whole operator baseline; a partially-revoked Admin or a
   narrow custom role does not. Hiding such a card from everyone but a
   full-access scope is exactly right — anyone else is redirected on arrival.

What this deliberately does NOT answer: authentication, email confirmation
and the account gate (`require_authenticated_live/2` and `live_account_gate/2`
run earlier in the hook). Those bounce a visitor before any view-permission
question is asked; a page deciding what to render has already passed them.

## The one view where this is stricter than the mount gate

`landing_view?/1` — the `/admin` index — is admitted by
`:phoenix_kit_ensure_admin` for EVERY authenticated visitor, because it is the
destination `PhoenixKit.Utils.Routes.safe_destination/2` guarantees. This
function still answers `false` for a scope that fails branch 1, and that is
deliberate: the landing is somewhere a visitor is *sent*, not somewhere a menu
should *offer* them. A permission-less visitor who lands there is greeted and
shown no navigation at all
(`PhoenixKitWeb.Components.Dashboard.AdminSidebar.reachable_tabs/2` returns
`[]` for exactly the same scopes), so answering `true` would render a link to
a page they are already on, inside a shell built for operators.

Every other view answers identically here and at the gate, which is what makes
"a card is visible iff its destination admits you" true — a visible card is
never a redirect, and the only page that admits more than it advertises is the
one no card points at.

## Examples

    # compute in the LiveView, keep HEEx declarative
    assign(socket,
      show_users_card?:
        Auth.can_access_admin_view?(scope, PhoenixKitWeb.Live.Users.Users)
    )

# `fetch_phoenix_kit_current_scope`

Fetches the current user and creates a scope for authentication context.

This plug combines user fetching with scope creation, providing a
structured way to handle authentication state in your application.

The scope is assigned to `:phoenix_kit_current_scope` and includes
both the user and authentication status.

Also verifies session fingerprints if enabled to detect session hijacking attempts.

# `fetch_phoenix_kit_current_user`

Authenticates the user by looking into the session
and remember me token.

Also verifies session fingerprints if enabled to detect session hijacking attempts.

This plug is idempotent - if the user has already been fetched, it returns early
to avoid duplicate database queries.

# `get_endpoint`

# `landing_view?`

```elixir
@spec landing_view?(module() | nil) :: boolean()
```

Whether `view` is the guaranteed landing — the one admin LiveView that
`:phoenix_kit_ensure_admin` opens to EVERY authenticated visitor, whatever
permissions they hold.

That view is `PhoenixKitWeb.Live.Dashboard`, the `/admin` index. It is the
terminal of `PhoenixKit.Utils.Routes.safe_destination/2`: the page core
promises any signed-in visitor can be redirected to. A terminal that rejects
its own visitor is an infinite redirect rather than a fallback, so the gate
has to admit them — and the page is built for it, gating every operator block
behind `can_access_admin_view?/2` and showing a permission-less visitor the
welcome block and nothing else.

Admission is NOT unconditional: authentication, the account gate (email
confirmation, maintenance mode, blocked accounts) and the locale hook all run
first, exactly as for any other admin view. Only the admin-area gate and the
per-view permission check are skipped.

    iex> PhoenixKitWeb.Users.Auth.landing_view?(PhoenixKitWeb.Live.Dashboard)
    true

    iex> PhoenixKitWeb.Users.Auth.landing_view?(PhoenixKitWeb.Live.Users.Users)
    false

# `log_in_user`

Logs the user in.

It renews the session ID and clears the whole session
to avoid fixation attacks. See the renew_session
function to customize this behaviour.

It also sets a `:live_socket_id` key in the session,
so LiveView sessions are identified and automatically
disconnected on log out. The line can be safely removed
if you are not using LiveView.

## Session Fingerprinting

When session fingerprinting is enabled, this function captures the user's
IP address and user agent to create a session fingerprint. This helps
detect session hijacking attempts.

# `log_out_user`

Logs the user out.

It clears all session data for safety. See renew_session.

# `log_out_user_from_all_sessions`

Logs out a specific user by invalidating all their session tokens and broadcasting disconnect to their LiveView sessions.

This function is useful when user roles or permissions change and you need to force re-authentication
to ensure the user gets updated permissions in their session.

## Parameters

- `user`: The user to log out from all sessions

## Examples

    iex> log_out_user_from_all_sessions(user)
    :ok

# `magic_link_login_enabled?`

```elixir
@spec magic_link_login_enabled?() :: boolean()
```

Whether signing in with a magic link is available.

Gates the request page, and the token endpoint that emailed links point at.
Turning it off is a security-posture decision ("password and 2FA only"), so it
has to close the route rather than only hide the button — an admin who
switches it off on the Authorization settings page will reasonably believe
nobody can still sign in this way.

In-flight links stop working too. Disabling passwordless login means no more
magic-link sign-ins, not no new ones from today; the tokens are short-lived,
so the window this affects is small.

# `magic_link_registration_enabled?`

```elixir
@spec magic_link_registration_enabled?() :: boolean()
```

Whether registering via a magic link is available.

Gates the request page and the completion page, for the same reason as
`magic_link_login_enabled?/0`: the setting has to close the route, not just
hide the entry point.

# `maybe_redirect_authenticated`

Checks if the current user is authenticated and returns a redirect socket if so.

Used by auth pages (login, register, etc.) that should not be accessible
to already-authenticated users when placed in a shared public live_session.

Returns `{:redirect, redirected_socket}` if authenticated (caller should halt),
or `:cont` if not authenticated (caller should proceed with normal mount).

# `on_mount`

Handles mounting and authenticating the phoenix_kit_current_user in LiveViews.

## `on_mount` arguments

  * `:phoenix_kit_mount_current_user` - Assigns phoenix_kit_current_user
    to socket assigns based on user_token, or nil if
    there's no user_token or no matching user.

  * `:phoenix_kit_mount_current_scope` - Assigns both phoenix_kit_current_user
    and phoenix_kit_current_scope to socket assigns. The scope provides
    structured access to authentication state.

  * `:phoenix_kit_ensure_authenticated` - Authenticates the user from the session,
    and assigns the phoenix_kit_current_user to socket assigns based
    on user_token.
    Redirects to login page if there's no logged user.

  * `:phoenix_kit_ensure_authenticated_scope` - Authenticates the user via scope system,
    assigns both phoenix_kit_current_user and phoenix_kit_current_scope.

  * `:phoenix_kit_ensure_owner` - Ensures the user has owner role,
    and redirects to the home page if not.

  * `:phoenix_kit_ensure_admin` - Ensures the user has admin or owner role,
    and redirects to the home page if not.
    Redirects to login page if there's no logged user.

  * `:phoenix_kit_redirect_if_user_is_authenticated` - Authenticates the user from the session.
    Redirects to signed_in_path if there's a logged user.

  * `:phoenix_kit_redirect_if_authenticated_scope` - Checks authentication via scope system.
    Redirects to signed_in_path if there's a logged user.

## Examples

Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
the current_user:

    defmodule PhoenixKitWeb.PageLive do
      use PhoenixKitWeb, :live_view

      on_mount {PhoenixKitWeb.Users.Auth, :phoenix_kit_mount_current_user}
      ...
    end

Or use the scope system for better encapsulation:

    defmodule PhoenixKitWeb.PageLive do
      use PhoenixKitWeb, :live_view

      on_mount {PhoenixKitWeb.Users.Auth, :phoenix_kit_mount_current_scope}
      ...
    end

Or use the `live_session` of your router to invoke the on_mount callback:

    live_session :authenticated, on_mount: [{PhoenixKitWeb.Users.Auth, :phoenix_kit_ensure_authenticated_scope}] do
      live "/profile", ProfileLive, :index
    end

# `redirect_if_user_is_authenticated`

Used for routes that require the user to not be authenticated.

# `redirect_invalid_locale`

Redirects invalid locale URLs to the canonical default-locale shape.

Takes the current URL path and replaces the invalid locale segment so
the redirect target matches the rest of the app's URL emission:

- With `default_language_no_prefix?` ON → strip the segment entirely
  (the canonical primary shape is prefixless, e.g. `/phoenix_kit/admin`).
- With the setting OFF (default) → swap the invalid segment for the
  primary base code so the canonical prefixed shape is preserved
  (e.g. `/phoenix_kit/xx/admin` → `/phoenix_kit/en/admin`).

# `redirect_to_base_locale`

Redirects full dialect code URLs to base language URLs.

This function handles backward compatibility by redirecting old URLs with
full dialect codes (en-US, es-MX) to the new simplified base code URLs (en, es).

Sends a **302**. The docs here used to promise a 301, and the call passed
`status: 301` — but `Phoenix.Controller.redirect/2` sends
`conn.status || 302` and ignores an `:status` option, so no 301 was ever
emitted. The option has been removed rather than made real: a cached
permanent redirect off a dialect code is hard to walk back if the
dialect set changes, and nothing here needs permanence. Use
`put_status(:moved_permanently)` before `redirect/2` if that ever changes.

## Examples

    iex> redirect_to_base_locale(conn, "en-US")
    # /phoenix_kit/en-US/admin → /phoenix_kit/en/admin

    iex> redirect_to_base_locale(conn, "es-MX")
    # /phoenix_kit/es-MX/users?page=2 → /phoenix_kit/es/users?page=2

## Preservation

- Query parameters preserved (via `with_query_string/2` — they were not,
  before: this rebuilds a path, and `conn.request_path` stops at the "?")
- Request method unchanged (GET → GET)
- Full path structure maintained

URL fragments are NOT preserved, and cannot be: a fragment never leaves
the browser, so the server has nothing to copy. The client re-applies
its own fragment to the `Location` it follows.

## Notes

- Halts conn pipeline (no further processing)
- Logged for monitoring migration patterns

# `remember_me_default?`

```elixir
@spec remember_me_default?() :: boolean()
```

Whether the "remember me" checkbox starts checked.

Site-wide default via the `remember_me_default` setting (default `true`) —
users can still untick it per login. Flows with no UI to tick (magic-link
login, OAuth) follow this value directly. Always false when
`remember_me_enabled?/0` is false.

# `remember_me_enabled?`

```elixir
@spec remember_me_enabled?() :: boolean()
```

Whether the site allows persistent ("remember me") sessions at all.

Site-wide policy via the `remember_me_enabled` setting (default `true`).
When false the checkbox is hidden on every auth form and no flow can write
the persistent cookie — logins last only as long as the browser session.

# `remember_me_params`

```elixir
@spec remember_me_params() :: map()
```

The params a no-UI login flow (magic link, OAuth) should pass to
`log_in_user/3` to follow the site's persistence policy.

# `remembered?`

```elixir
@spec remembered?(Plug.Conn.t()) :: boolean()
```

Whether this request already carries a persistent remember-me cookie.

Lets a re-login flow that has no checkbox of its own (the password-change
handoff) preserve the choice the user already made, instead of silently
downgrading them to a session-only login.

# `require_admin`

Used for routes that require the user to be an admin or owner.

If you want to enforce the admin requirement without
redirecting to the login page, consider using
`:phoenix_kit_require_authenticated_scope` instead.

# `require_authenticated_scope`

Used for routes that require the user to be authenticated via scope.

This function checks authentication status through the scope system,
providing a more structured approach to authentication checks.

Enforces email confirmation before allowing access to the application.

# `require_authenticated_user`

Used for routes that require the user to be authenticated.

Enforces email confirmation before allowing access to the application.

# `require_module_access`

Used for routes that require the user to have module-level permission.

# `require_owner`

Used for routes that require the user to be an owner.

If you want to enforce the owner requirement without
redirecting to the login page, consider using
`:phoenix_kit_require_authenticated_scope` instead.

# `require_role`

# `validate_and_set_locale`

Validates and sets the locale for the current request.

This function is called as a plug in the router to validate locale codes in the URL path.
It implements PhoenixKit's simplified URL architecture:

- URLs use base language codes (en, es, fr) for simplicity
- Full dialect codes (en-US, es-MX) are redirected to base codes (301)
- User preferences determine which dialect variant to use for translations
- Translation system uses full dialect codes internally

## Data Flow

1. Check if URL contains full dialect code → redirect to base
2. Validate base code exists in predefined language list
3. Resolve to full dialect using user preference or default mapping
4. Set Gettext to full dialect for translations
5. Store both base code (for URLs) and full dialect (for translations)

## Examples

    # Base code in URL (preferred format)
    conn = validate_and_set_locale(conn, [])
    # Sets: current_locale_base="en", current_locale="en-US"

    # Full dialect in URL (legacy/bookmarks)
    conn = validate_and_set_locale(%{path_params: %{"locale" => "en-US"}}, [])
    # Redirects 301 to: /en/...

    # Invalid locale in URL
    conn = validate_and_set_locale(%{path_params: %{"locale" => "xx"}}, [])
    # Redirects to default locale URL

---

*Consult [api-reference.md](api-reference.md) for complete listing*
