224 lines
6.2 KiB
Elixir
224 lines
6.2 KiB
Elixir
defmodule Plausible.Auth.UserSessions do
|
|
@moduledoc """
|
|
Functions for interacting with user sessions.
|
|
"""
|
|
|
|
use Plausible
|
|
|
|
import Ecto.Query
|
|
alias Plausible.Auth
|
|
alias Plausible.Repo
|
|
|
|
@socket_id_prefix "user_sessions:"
|
|
|
|
@spec list_for_user(Auth.User.t(), NaiveDateTime.t()) :: [Auth.UserSession.t()]
|
|
def list_for_user(user, now \\ NaiveDateTime.utc_now(:second)) do
|
|
Repo.all(
|
|
from us in Auth.UserSession,
|
|
where: us.user_id == ^user.id,
|
|
where: us.timeout_at >= ^now,
|
|
order_by: [desc: us.last_used_at, desc: us.id]
|
|
)
|
|
end
|
|
|
|
@spec count_for_users([Auth.User.t()], NaiveDateTime.t()) :: list()
|
|
def count_for_users(users, now \\ NaiveDateTime.utc_now(:second)) when is_list(users) do
|
|
Repo.all(
|
|
from(us in Auth.UserSession,
|
|
where: us.user_id in ^Enum.map(users, & &1.id),
|
|
where: us.timeout_at >= ^now,
|
|
group_by: us.user_id,
|
|
select: {us.user_id, count(us.id)},
|
|
order_by: [asc: us.user_id]
|
|
)
|
|
)
|
|
end
|
|
|
|
on_ee do
|
|
alias Plausible.Teams
|
|
|
|
@spec list_sso_for_team(Teams.Team.t(), NaiveDateTime.t()) :: [Auth.UserSession.t()]
|
|
def list_sso_for_team(team, now \\ NaiveDateTime.utc_now(:second)) do
|
|
user_ids =
|
|
Repo.all(
|
|
from t in Teams.Team,
|
|
inner_join: tm in assoc(t, :team_memberships),
|
|
inner_join: u in assoc(tm, :user),
|
|
where: t.id == ^team.id,
|
|
where: tm.role != :guest,
|
|
where: u.type == :sso,
|
|
select: u.id
|
|
)
|
|
|
|
Repo.all(
|
|
from us in Auth.UserSession,
|
|
inner_join: u in assoc(us, :user),
|
|
where: us.user_id in ^user_ids,
|
|
where: us.timeout_at >= ^now,
|
|
order_by: [desc: us.last_used_at, desc: us.id],
|
|
preload: [user: u]
|
|
)
|
|
end
|
|
|
|
@spec revoke_sso_by_id(Teams.Team.t(), pos_integer()) :: :ok
|
|
def revoke_sso_by_id(team, session_id) do
|
|
{_, tokens} =
|
|
Repo.delete_all(
|
|
from us in Auth.UserSession,
|
|
inner_join: u in assoc(us, :user),
|
|
inner_join: tm in assoc(u, :team_memberships),
|
|
where: u.type == :sso,
|
|
where: us.id == ^session_id,
|
|
where: tm.role != :guest,
|
|
where: tm.team_id == ^team.id,
|
|
select: us.token
|
|
)
|
|
|
|
case tokens do
|
|
[token] ->
|
|
disconnect_by_token(token)
|
|
|
|
_ ->
|
|
:pass
|
|
end
|
|
|
|
:ok
|
|
end
|
|
end
|
|
|
|
@spec last_used_humanize(Auth.UserSession.t(), NaiveDateTime.t()) :: String.t()
|
|
def last_used_humanize(user_session, now \\ NaiveDateTime.utc_now(:second)) do
|
|
diff = NaiveDateTime.diff(now, user_session.last_used_at, :hour)
|
|
diff_days = NaiveDateTime.diff(now, user_session.last_used_at, :day)
|
|
|
|
cond do
|
|
diff < 1 -> "Just recently"
|
|
diff == 1 -> "1 hour ago"
|
|
diff < 24 -> "#{diff} hours ago"
|
|
diff < 2 * 24 -> "Yesterday"
|
|
true -> "#{diff_days} days ago"
|
|
end
|
|
end
|
|
|
|
@spec get_by_token(String.t()) ::
|
|
{:ok, Auth.UserSession.t()}
|
|
| {:error, :not_found}
|
|
| {:error, :expired, Auth.UserSession.t()}
|
|
def get_by_token(token) do
|
|
now = NaiveDateTime.utc_now(:second)
|
|
|
|
last_team_subscription_query = Plausible.Teams.last_subscription_join_query()
|
|
|
|
token_query =
|
|
from(us in Auth.UserSession,
|
|
inner_join: u in assoc(us, :user),
|
|
as: :user,
|
|
left_join: tm in assoc(u, :team_memberships),
|
|
on: tm.role != :guest,
|
|
left_join: t in assoc(tm, :team),
|
|
as: :team,
|
|
left_join: o in assoc(t, :owners),
|
|
left_lateral_join: ts in subquery(last_team_subscription_query),
|
|
on: true,
|
|
where: us.token == ^token,
|
|
order_by: t.id,
|
|
preload: [user: {u, team_memberships: {tm, team: {t, subscription: ts, owners: o}}}]
|
|
)
|
|
|
|
case Repo.one(token_query) do
|
|
%Auth.UserSession{} = user_session ->
|
|
if NaiveDateTime.compare(user_session.timeout_at, now) == :gt do
|
|
{:ok, user_session}
|
|
else
|
|
{:error, :expired, user_session}
|
|
end
|
|
|
|
nil ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
@spec create!(Auth.User.t(), String.t(), Keyword.t()) :: Auth.UserSession.t()
|
|
def create!(user, device_name, opts \\ []) do
|
|
user
|
|
|> Auth.UserSession.new_session(device_name, opts)
|
|
|> Repo.insert!()
|
|
end
|
|
|
|
@spec remove_by_token(String.t()) :: :ok
|
|
def remove_by_token(token) do
|
|
Repo.delete_all(from us in Auth.UserSession, where: us.token == ^token)
|
|
:ok
|
|
end
|
|
|
|
@spec touch(Auth.UserSession.t(), NaiveDateTime.t()) :: Auth.UserSession.t()
|
|
def touch(user_session, now \\ NaiveDateTime.utc_now(:second)) do
|
|
if NaiveDateTime.diff(now, user_session.last_used_at, :hour) >= 1 do
|
|
Plausible.Users.bump_last_seen(user_session.user_id, now)
|
|
|
|
user_session
|
|
|> Repo.preload(:user)
|
|
|> Auth.UserSession.touch_session(now)
|
|
|> Repo.update!(allow_stale: true)
|
|
else
|
|
user_session
|
|
end
|
|
end
|
|
|
|
@spec revoke_by_id(Auth.User.t(), pos_integer()) :: :ok
|
|
def revoke_by_id(user, session_id) do
|
|
{_, tokens} =
|
|
Repo.delete_all(
|
|
from us in Auth.UserSession,
|
|
where: us.user_id == ^user.id and us.id == ^session_id,
|
|
select: us.token
|
|
)
|
|
|
|
case tokens do
|
|
[token] ->
|
|
disconnect_by_token(token)
|
|
|
|
_ ->
|
|
:pass
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@spec revoke_all(Auth.User.t(), Keyword.t()) :: :ok
|
|
def revoke_all(user, opts \\ []) do
|
|
except = Keyword.get(opts, :except)
|
|
|
|
delete_query = from us in Auth.UserSession, where: us.user_id == ^user.id, select: us.token
|
|
|
|
delete_query =
|
|
if except do
|
|
where(delete_query, [us], us.id != ^except.id)
|
|
else
|
|
delete_query
|
|
end
|
|
|
|
{_count, tokens} = Repo.delete_all(delete_query)
|
|
|
|
Enum.each(tokens, &disconnect_by_token/1)
|
|
end
|
|
|
|
@spec disconnect_by_token(String.t()) :: :ok
|
|
def disconnect_by_token(token_or_socket_id) do
|
|
socket_id =
|
|
if String.starts_with?(token_or_socket_id, @socket_id_prefix) do
|
|
token_or_socket_id
|
|
else
|
|
socket_id(token_or_socket_id)
|
|
end
|
|
|
|
PlausibleWeb.Endpoint.broadcast(socket_id, "disconnect", %{})
|
|
:ok
|
|
end
|
|
|
|
@spec socket_id(String.t()) :: String.t()
|
|
def socket_id(token) do
|
|
@socket_id_prefix <> Base.url_encode64(token)
|
|
end
|
|
end
|