Packages
phoenix_kit
1.7.113
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
Current section
Files
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 %>
<%!-- Sessions Table / Cards --%>
<.table_default
id="sessions-table"
variant="zebra"
class="w-full"
toggleable={true}
items={@sessions}
card_fields={
fn session ->
[
%{label: gettext("Token"), value: "#{session.token_preview}..."},
%{
label: gettext("Created"),
value:
"#{UtilsDate.format_date_with_user_format(session.created_at)} #{UtilsDate.format_time_with_user_format(session.created_at)}"
},
%{label: gettext("Age"), value: "#{session.age_in_days} #{gettext("days")}"},
%{
label: gettext("Expires"),
value:
"#{UtilsDate.format_date_with_user_format(session.expires_at)} #{UtilsDate.format_time_with_user_format(session.expires_at)}"
}
]
end
}
>
<:toolbar_title>
<form phx-change="search" class="contents">
<label class="input input-sm w-40 sm:w-52 lg:w-64 min-w-0">
<.icon name="hero-magnifying-glass" class="h-4 w-4 opacity-50" />
<input
type="search"
name="search"
value={@search_query}
placeholder="Search by email or token..."
class="grow"
phx-debounce="300"
/>
</label>
</form>
</:toolbar_title>
<:toolbar_actions>
<%!-- Filter by User Status --%>
<span class="text-sm text-base-content/70 whitespace-nowrap">
{gettext("Status:")}
</span>
<form phx-change="filter_by_user_status" class="contents">
<select class="select select-sm w-40 min-w-0" name="status">
<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>
</form>
<button phx-click="refresh_sessions" class="btn btn-sm btn-primary">
<.icon name="hero-arrow-path" class="w-4 h-4" /> {gettext("Refresh")}
</button>
</:toolbar_actions>
<:card_header :let={session}>
<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>
</:card_header>
<:card_actions :let={session}>
<.table_row_menu
id={"session-card-menu-#{session.token_uuid}"}
label={gettext("Session actions")}
>
<.table_row_menu_button
phx-click="show_revoke_session"
phx-value-token_uuid={session.token_uuid}
icon="hero-x-mark"
label={gettext("Revoke")}
variant="error"
/>
<.table_row_menu_button
phx-click="show_revoke_user_sessions"
phx-value-user_uuid={session.user_uuid}
icon="hero-exclamation-triangle"
label={gettext("Revoke all")}
variant="warning"
/>
</.table_row_menu>
</:card_actions>
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>{gettext("User")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Token")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Status")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Created")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Age")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Expires")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("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 %>
{gettext("No active sessions found.")}
<% else %>
{gettext("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>
<.table_row_menu
id={"session-menu-#{session.token_uuid}"}
label={gettext("Session actions")}
>
<.table_row_menu_button
phx-click="show_revoke_session"
phx-value-token_uuid={session.token_uuid}
icon="hero-x-mark"
label={gettext("Revoke")}
variant="error"
/>
<.table_row_menu_button
phx-click="show_revoke_user_sessions"
phx-value-user_uuid={session.user_uuid}
icon="hero-exclamation-triangle"
label={gettext("Revoke all")}
variant="warning"
/>
</.table_row_menu>
</.table_default_cell>
</.table_default_row>
<% end %>
<% end %>
</.table_default_body>
</.table_default>
<%!-- 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>