Packages

phoenix_kit

1.7.65
1.7.210 1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit_web live users sessions.html.heex
Raw

lib/phoenix_kit_web/live/users/sessions.html.heex

<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title="Session Management"
current_path={@url_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="container flex-col mx-auto px-4 py-6">
<.admin_page_header
back={PhoenixKit.Utils.Routes.path("/admin/users")}
title="Session Management"
subtitle="Monitor and manage active user sessions"
/>
<%!-- Session Statistics --%>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={@stats.total_active}
title="Active Sessions"
subtitle="Currently logged in"
>
<:icon>
<.icon name="hero-bolt" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={@stats.unique_users}
title="Unique Users"
subtitle="With active sessions"
>
<:icon>
<.icon name="hero-user" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={@stats.sessions_today}
title="Today's Sessions"
subtitle="New login activity"
>
<:icon>
<.icon name="hero-clock" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={@stats.expired_sessions}
title="Expired Sessions"
subtitle="Need cleanup"
>
<:icon>
<.icon name="hero-no-symbol" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
</div>
<%!-- Revoke Session Modal --%>
<%= if @show_revoke_modal do %>
<div class="modal modal-open">
<div class="modal-box">
<%= if @revoke_type == :single and @selected_session do %>
<h3 class="font-bold text-lg mb-4">Revoke Session</h3>
<p class="mb-4">
Are you sure you want to revoke this session for <strong>{@selected_session.user_email}</strong>?
</p>
<div class="bg-base-200 rounded p-3 mb-4">
<div class="text-sm">
<div><strong>Token:</strong> {@selected_session.token_preview}...</div>
<div>
<strong>Created:</strong> {UtilsDate.format_datetime_with_user_format(
@selected_session.created_at
)}
</div>
<div><strong>Age:</strong> {@selected_session.age_in_days} days</div>
</div>
</div>
<% end %>
<%= if @revoke_type == :user_all and @selected_user do %>
<h3 class="font-bold text-lg mb-4">Revoke All User Sessions</h3>
<p class="mb-4">
Are you sure you want to revoke
<strong>all {@user_sessions_count} session(s)</strong>
for user <strong>{@selected_user.email}</strong>?
</p>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-6 w-6" />
<span>This will log out the user from all devices and sessions.</span>
</div>
<% end %>
<div class="modal-action">
<button type="button" phx-click="hide_revoke_modal" class="btn btn-outline">
Cancel
</button>
<button type="button" phx-click="confirm_revoke_session" class="btn btn-error">
Revoke Session{if @revoke_type == :user_all, do: "s", else: ""}
</button>
</div>
</div>
</div>
<% end %>
<%!-- Controls --%>
<div class="bg-base-200 rounded-lg p-6 mb-6">
<div class="flex flex-col lg:flex-row gap-4 items-end">
<%!-- Search --%>
<div class="flex-1">
<label class="label">
<span class="label-text">Search by email or token</span>
</label>
<input
type="text"
value={@search_query}
phx-change="search"
phx-debounce="300"
name="search"
placeholder="Enter email address or token prefix..."
class="input input-bordered w-full"
/>
</div>
<%!-- Filter by User Status --%>
<div>
<label class="label">
<span class="label-text">Filter by User Status</span>
</label>
<select
phx-change="filter_by_user_status"
name="status"
class="select select-bordered w-full max-w-xs"
>
<option value="all" selected={@filter_user_status == "all"}>All Users</option>
<option value="active" selected={@filter_user_status == "active"}>
Active Users
</option>
<option value="inactive" selected={@filter_user_status == "inactive"}>
Inactive Users
</option>
<option value="confirmed" selected={@filter_user_status == "confirmed"}>
Confirmed Email
</option>
<option value="pending" selected={@filter_user_status == "pending"}>
Pending Email
</option>
</select>
</div>
<%!-- Refresh Button --%>
<button
phx-click="refresh_sessions"
class="btn btn-primary"
>
<.icon name="hero-arrow-path" class="w-4 h-4 mr-2" /> Refresh
</button>
</div>
</div>
<%!-- Sessions Table --%>
<div class="hidden md:block bg-base-100 mb-6">
<.table_default variant="zebra" class="w-full">
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>User</.table_default_header_cell>
<.table_default_header_cell>Token</.table_default_header_cell>
<.table_default_header_cell>Status</.table_default_header_cell>
<.table_default_header_cell>Created</.table_default_header_cell>
<.table_default_header_cell>Age</.table_default_header_cell>
<.table_default_header_cell>Expires</.table_default_header_cell>
<.table_default_header_cell>Actions</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<%= if Enum.empty?(@sessions) do %>
<.table_default_row>
<.table_default_cell class="text-center py-8 text-base-content/70">
<%= if @total_count == 0 do %>
No active sessions found.
<% else %>
No sessions match your current filters.
<% end %>
</.table_default_cell>
</.table_default_row>
<% else %>
<%= for session <- @sessions do %>
<.table_default_row>
<%!-- User --%>
<.table_default_cell>
<div class="flex flex-col">
<span class="font-medium">{session.user_email}</span>
<span class="text-xs text-base-content/70">{session.user_uuid}</span>
</div>
</.table_default_cell>
<%!-- Token --%>
<.table_default_cell>
<code class="bg-base-200 px-2 py-1 rounded text-sm">
{session.token_preview}...
</code>
</.table_default_cell>
<%!-- User Status --%>
<.table_default_cell>
<.user_status_badge
is_active={session.user_is_active}
confirmed_at={session.user_confirmed_at}
size={:sm}
/>
</.table_default_cell>
<%!-- Created --%>
<.table_default_cell class="text-sm">
<div class="flex flex-col gap-1">
<div>{UtilsDate.format_date_with_user_format(session.created_at)}</div>
<div class="text-xs text-base-content/70">
{UtilsDate.format_time_with_user_format(session.created_at)}
</div>
</div>
</.table_default_cell>
<%!-- Age --%>
<.table_default_cell>
<.age_badge days={session.age_in_days} class="badge-sm h-auto" />
</.table_default_cell>
<%!-- Expires --%>
<.table_default_cell class="text-sm">
<div class="flex flex-col gap-1">
<div>{UtilsDate.format_date_with_user_format(session.expires_at)}</div>
<div class="text-xs text-base-content/70">
{UtilsDate.format_time_with_user_format(session.expires_at)}
</div>
</div>
</.table_default_cell>
<%!-- Actions --%>
<.table_default_cell>
<div class="flex gap-2">
<%!-- Revoke Single Session --%>
<button
phx-click="show_revoke_session"
phx-value-token_uuid={session.token_uuid}
class="btn btn-error btn-xs tooltip tooltip-bottom"
data-tip={gettext("Revoke")}
>
<.icon name="hero-x-mark" class="w-3 h-3 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Revoke")}</span>
</button>
<%!-- Revoke All User Sessions --%>
<button
phx-click="show_revoke_user_sessions"
phx-value-user_uuid={session.user_uuid}
class="btn btn-warning btn-xs tooltip tooltip-bottom"
data-tip={gettext("Revoke all")}
>
<.icon name="hero-exclamation-triangle" class="w-3 h-3 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Revoke all")}</span>
</button>
</div>
</.table_default_cell>
</.table_default_row>
<% end %>
<% end %>
</.table_default_body>
</.table_default>
</div>
<%!-- Sessions Cards (small screens only) --%>
<div class="md:hidden mb-6">
<%= if Enum.empty?(@sessions) do %>
<div class="text-center py-8 text-base-content/70">
<%= if @total_count == 0 do %>
No active sessions found.
<% else %>
No sessions match your current filters.
<% end %>
</div>
<% else %>
<div class="space-y-3">
<%= for session <- @sessions do %>
<div class={[
"card card-border card-sm bg-base-100",
session.is_expired && "opacity-60"
]}>
<div class="card-body gap-3">
<%!-- Header: email + status --%>
<div class="flex items-start justify-between gap-2">
<div class="flex flex-col min-w-0">
<span class="font-medium text-sm truncate">{session.user_email}</span>
<span class="text-xs text-base-content/50 truncate">{session.user_uuid}</span>
</div>
<.user_status_badge
is_active={session.user_is_active}
confirmed_at={session.user_confirmed_at}
size={:sm}
/>
</div>
<%!-- Body: 2-column key/value grid --%>
<div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
<div class="text-base-content/60">Token</div>
<div>
<code class="bg-base-200 px-1.5 py-0.5 rounded text-xs">
{session.token_preview}...
</code>
</div>
<div class="text-base-content/60">Created</div>
<div>
{UtilsDate.format_date_with_user_format(session.created_at)}
<span class="text-xs text-base-content/70">
{UtilsDate.format_time_with_user_format(session.created_at)}
</span>
</div>
<div class="text-base-content/60">Age</div>
<div>
<.age_badge days={session.age_in_days} class="badge-sm h-auto" />
</div>
<div class="text-base-content/60">Expires</div>
<div>
{UtilsDate.format_date_with_user_format(session.expires_at)}
<span class="text-xs text-base-content/70">
{UtilsDate.format_time_with_user_format(session.expires_at)}
</span>
</div>
</div>
<%!-- Footer: actions --%>
<div class="card-actions justify-end pt-1 border-t border-base-200">
<button
phx-click="show_revoke_session"
phx-value-token_uuid={session.token_uuid}
class="btn btn-error btn-xs"
>
<.icon name="hero-x-mark" class="w-3 h-3" /> {gettext("Revoke")}
</button>
<button
phx-click="show_revoke_user_sessions"
phx-value-user_uuid={session.user_uuid}
class="btn btn-warning btn-xs"
>
<.icon name="hero-exclamation-triangle" class="w-3 h-3" /> {gettext(
"Revoke all"
)}
</button>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
<%!-- Pagination --%>
<%= if @total_pages > 1 do %>
<div class="flex flex-col sm:flex-row justify-between items-center gap-4">
<div class="text-sm text-base-content/70">
Showing {(@page - 1) * @per_page + 1} to {min(@page * @per_page, @total_count)} of {@total_count} sessions
</div>
<div class="join">
<%= if @page > 1 do %>
<button
phx-click="change_page"
phx-value-page={@page - 1}
class="join-item btn btn-outline"
>
« Previous
</button>
<% end %>
<%= for page_num <- max(1, @page - 2)..min(@total_pages, @page + 2) do %>
<button
phx-click="change_page"
phx-value-page={page_num}
class={"join-item btn #{if page_num == @page, do: "btn-primary", else: "btn-outline"}"}
>
{page_num}
</button>
<% end %>
<%= if @page < @total_pages do %>
<button
phx-click="change_page"
phx-value-page={@page + 1}
class="join-item btn btn-outline"
>
Next »
</button>
<% end %>
</div>
</div>
<% end %>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>