PhoenixKit.Users.Auth.Scope (phoenix_kit v2.0.1)

Copy Markdown View Source

Scope module for encapsulating PhoenixKit authentication state.

This module provides a structured way to handle user authentication context throughout your Phoenix application, similar to Phoenix's built-in authentication patterns but with PhoenixKit prefixing to avoid conflicts.

Nil scopes

Every predicate and accessor here tolerates a nil scope and answers as if the visitor were unauthenticated. Host layouts render on kit auth pages with phoenix_kit_current_scope unset, so authenticated?(nil) and anonymous?(nil) are ordinary calls, not programmer errors — and "no scope" and "a scope with no user" mean the same thing to a caller deciding what to render.

The exception is to_map/1, which deliberately does not accept nil. There is nothing meaningful to serialize for a non-scope, and returning an empty map would fabricate data that looks real; it is a debug/serialization helper, so failing surfaces the bug where permissiveness would bury it. Its spec keeps t(), so the type checker rejects to_map(nil) at compile time rather than leaving it to blow up at runtime.

Apply the same rule to anything added here: if nil has a correct answer, give it one; if answering would mean inventing data, let it raise.

Usage

# Create scope for authenticated user
scope = Scope.for_user(user)

# Create scope for anonymous user
scope = Scope.for_user(nil)

# Check authentication status
Scope.authenticated?(scope)  # true or false

# Get user information
Scope.user(scope)        # %User{} or nil
Scope.user_uuid(scope)   # user.uuid or nil
Scope.user_email(scope)  # user.email or nil

Role & State Checks

Scope.has_role?(scope, "Admin")  # true/false
Scope.owner?(scope)             # Owner role?
Scope.can_access_admin_area?(scope)  # Owner, Admin, or custom role with permissions?
Scope.system_role?(scope)       # Strictly Owner or Admin (not custom roles)?
Scope.anonymous?(scope)         # Not authenticated?
Scope.user_roles(scope)         # ["Admin", "User"]
Scope.user_full_name(scope)     # "John Doe" or nil
Scope.user_active?(scope)       # true/false
Scope.to_map(scope)             # Debug-friendly map of all fields

Module-Level Permissions

Permissions are cached in the scope when it is built via for_user/1 (on mount and on PubSub-triggered refresh). Owner gets every key automatically. Admin defaults to all keys via seeding/auto-grant but is genuinely gated by its rows — the full-access fallback applies only on an unseeded install (no permission rows exist at all).

Scope.has_module_access?(scope, "billing")          # Single key check (pure cache)
Scope.can?(scope, "calendar.view_others")           # Key held AND module enabled
Scope.has_any_module_access?(scope, ["billing", "shop"])  # Any of these?
Scope.has_all_module_access?(scope, ["billing", "shop"])  # All of these?
Scope.accessible_modules(scope)                     # MapSet of granted keys
Scope.permission_count(scope)                       # Number of granted keys

Struct Fields

  • :user - The current user struct or nil
  • :authenticated? - Boolean indicating if user is authenticated
  • :cached_roles - List of role name strings, loaded at scope creation
  • :cached_permissions - MapSet of granted permission keys, loaded at scope creation

Summary

Functions

Returns the set of module keys the user can access.

admin?(scope) deprecated

Deprecated alias for can_access_admin_area?/1.

Checks if the scope represents an anonymous (non-authenticated) user.

Checks if the scope represents an authenticated user.

Checks whether the user holds a permission key AND that key is currently effective — the module behind it (or behind its parent, for sub-permission keys like "calendar.view_others") is enabled.

Checks if the user can access the admin AREA — the /admin shell entry gate.

Creates a new scope for the given user.

Checks if the user has access to all of the given module keys.

Checks if the user has access to at least one of the given module keys.

Checks if the user has access to a specific admin module/section.

Checks if the user has a specific role.

Whether this scope holds EVERY currently-grantable permission key — i.e. it can reach everything the permission system exposes right now, exactly like Owner. Purely permission-based and role-AGNOSTIC: a custom role granted all permissions returns true, with no role-name special-casing.

Checks if the user is an owner.

Returns the number of module permissions the user has been granted.

Whether the scope holds the wildcard superadmin key ("*") — a blanket, drift-immune grant to every permission-gated FEATURE/VIEW. Owner holds it by construction; a host can grant it to a custom role to make that role Owner-equivalent for feature access with one grant, and unlike a "grant every current key" role it stays complete as new modules are added.

Checks if the user holds the Owner or Admin system role.

Converts scope to a map for debugging or logging purposes.

Gets the user from the scope.

Checks if the user is active.

Gets the user email from the scope.

Gets the user's full name.

Gets all roles for the user.

Gets the user ID (UUID) from the scope.

Types

t()

@type t() :: %PhoenixKit.Users.Auth.Scope{
  authenticated?: boolean(),
  cached_permissions: MapSet.t() | nil,
  cached_roles: [String.t()] | nil,
  multi_session_accounts: list(),
  multi_session_allowed?: boolean(),
  user: PhoenixKit.Users.Auth.User.t() | nil
}

Functions

accessible_modules(scope)

@spec accessible_modules(t()) :: MapSet.t()

Returns the set of module keys the user can access.

admin?(scope)

This function is deprecated. Use can_access_admin_area?/1 — `admin?` is true for ANY permission holder, not just the Admin role..
@spec admin?(t()) :: boolean()

Deprecated alias for can_access_admin_area?/1.

The name misleads: it returns true for ANY permission holder, not just the Admin role. Call can_access_admin_area?/1 instead.

anonymous?(arg1)

@spec anonymous?(t() | nil) :: boolean()

Checks if the scope represents an anonymous (non-authenticated) user.

Examples

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.anonymous?(scope)
true

iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.anonymous?(scope)
false

authenticated?(arg1)

@spec authenticated?(t() | nil) :: boolean()

Checks if the scope represents an authenticated user.

Examples

iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.authenticated?(scope)
true

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.authenticated?(scope)
false

can?(arg1, key)

@spec can?(t(), String.t()) :: boolean()

Checks whether the user holds a permission key AND that key is currently effective — the module behind it (or behind its parent, for sub-permission keys like "calendar.view_others") is enabled.

This is the check modules should use for fine-grained, in-page authorization. Unlike has_module_access?/2 (a pure cache lookup used on hot paths where enablement is enforced separately at mount), can?/2 consults live module-enablement state, so a scope snapshotted before a module was disabled cannot keep authorizing its actions.

Examples

Scope.can?(scope, "calendar.edit_others")
Scope.can?(scope, "calendar")

can_access_admin_area?(arg1)

@spec can_access_admin_area?(t()) :: boolean()

Checks if the user can access the admin AREA — the /admin shell entry gate.

Returns true when the user holds the Admin or Owner role, OR has been explicitly granted any module-level permission (via RolePermission) — so a custom role (e.g. "Editor", "Support") holding at least one permission can enter the admin area.

This is a COARSE entry gate only. It does NOT mean the user is a privileged operator: holding a single grant is enough. Which pages and actions are actually allowed is enforced per-view by has_module_access?/2 / can?/2. For a "can do everything, like Owner" check use holds_all_enabled_permissions?/1 or superadmin?/1 instead — never this.

Examples

iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.can_access_admin_area?(scope)
true

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.can_access_admin_area?(scope)
false

for_user(user)

@spec for_user(PhoenixKit.Users.Auth.User.t() | nil) :: t()

Creates a new scope for the given user.

Examples

iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001", email: "user@example.com"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> scope.authenticated?
true
iex> scope.user.email
"user@example.com"

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> scope.authenticated?
false
iex> scope.user
nil

has_all_module_access?(arg1, keys)

@spec has_all_module_access?(t(), [String.t()]) :: boolean()

Checks if the user has access to all of the given module keys.

Examples

Scope.has_all_module_access?(scope, ["billing", "shop"])

has_any_module_access?(arg1, keys)

@spec has_any_module_access?(t(), [String.t()]) :: boolean()

Checks if the user has access to at least one of the given module keys.

Examples

Scope.has_any_module_access?(scope, ["billing", "shop"])

has_module_access?(arg1, module_key)

@spec has_module_access?(t(), String.t()) :: boolean()

Checks if the user has access to a specific admin module/section.

Looks up module_key in cached_permissions. Owner access works because for_user/1 pre-populates all keys for owners; this function itself does not special-case roles.

has_role?(arg1, role_name)

@spec has_role?(t(), String.t()) :: boolean()

Checks if the user has a specific role.

Examples

iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.has_role?(scope, "Admin")
true

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.has_role?(scope, "Admin")
false

holds_all_enabled_permissions?(scope)

@spec holds_all_enabled_permissions?(t()) :: boolean()

Whether this scope holds EVERY currently-grantable permission key — i.e. it can reach everything the permission system exposes right now, exactly like Owner. Purely permission-based and role-AGNOSTIC: a custom role granted all permissions returns true, with no role-name special-casing.

Compared against Permissions.enabled_module_keys/0 (the grantable set the permissions matrix produces), NOT all_module_keys/0. Disabled modules expose no reachable admin surface and their keys can't be granted via the UI, so a grant-all custom role legitimately lacks them — including them would re-introduce the Owner-vs-custom asymmetry this is meant to remove (Owner's set is a superset that trivially satisfies the subset test either way). The size > 0 guard stops an empty grantable set from making subset?/2 vacuously true (a fail-open) for every scope; in practice the 5 core + 2 integration keys are a non-disableable floor, but the guard never rots.

owner?(arg1)

@spec owner?(t()) :: boolean()

Checks if the user is an owner.

Examples

iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.owner?(scope)
true

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.owner?(scope)
false

permission_count(arg1)

@spec permission_count(t()) :: non_neg_integer()

Returns the number of module permissions the user has been granted.

superadmin?(arg1)

@spec superadmin?(t()) :: boolean()

Whether the scope holds the wildcard superadmin key ("*") — a blanket, drift-immune grant to every permission-gated FEATURE/VIEW. Owner holds it by construction; a host can grant it to a custom role to make that role Owner-equivalent for feature access with one grant, and unlike a "grant every current key" role it stays complete as new modules are added.

Scope: feature/view access only. The Owner-only role-management safety rails (editing the Admin role, assigning Owner/Admin, the last-Owner guard) remain role-name-based by design and are NOT unlocked by "*".

system_role?(arg1)

@spec system_role?(t()) :: boolean()

Checks if the user holds the Owner or Admin system role.

Unlike admin?/1 which also returns true for custom roles with permissions, this strictly checks for the two built-in system roles.

to_map(scope)

@spec to_map(t()) :: map()

Converts scope to a map for debugging or logging purposes.

Examples

iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001", email: "user@example.com"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.to_map(scope)
%{
  authenticated?: true,
  user_uuid: "019...",
  user_email: "user@example.com",
  user_roles: ["Admin", "User"],
  owner?: false,
  admin?: true
}

user(arg1)

@spec user(t() | nil) :: PhoenixKit.Users.Auth.User.t() | nil

Gets the user from the scope.

Examples

iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001", email: "user@example.com"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.user(scope)
%PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001", email: "user@example.com"}

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.user(scope)
nil

user_active?(arg1)

@spec user_active?(t() | nil) :: boolean()

Checks if the user is active.

Examples

iex> user = %PhoenixKit.Users.Auth.User{is_active: true}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.user_active?(scope)
true

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.user_active?(scope)
false

user_email(arg1)

@spec user_email(t() | nil) :: String.t() | nil

Gets the user email from the scope.

Examples

iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001", email: "user@example.com"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.user_email(scope)
"user@example.com"

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.user_email(scope)
nil

user_full_name(arg1)

@spec user_full_name(t() | nil) :: String.t() | nil

Gets the user's full name.

Examples

iex> user = %PhoenixKit.Users.Auth.User{first_name: "John", last_name: "Doe"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.user_full_name(scope)
"John Doe"

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.user_full_name(scope)
nil

user_roles(arg1)

@spec user_roles(t()) :: [String.t()]

Gets all roles for the user.

Examples

iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.user_roles(scope)
["Admin", "User"]

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.user_roles(scope)
[]

user_uuid(arg1)

@spec user_uuid(t() | nil) :: String.t() | nil

Gets the user ID (UUID) from the scope.

Examples

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.user_uuid(scope)
"0193a5e4-..."

iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.user_uuid(scope)
nil