- Fix doctests for Accounts, Agents.Stats, Snmp to match actual behavior - Fix dynamic_extra_test vendor post-processing tests to seed sensor data - Fix activity_controller_test to seed devices for feed data - Fix session_manager_test to create browser session for test - Fix topology_test link creation for connection test - Fix device_monitor/driver_worker tests for unique job constraints - Fix accounts_test expired_tokens assertion (magic link token is expired) - Fix happy_path_test and show_events_test to seed monitor data - Fix admin user_live_test user.name -> user.email (no name field) - Fix schema_test to seed activity data - Fix mobile_qr_live_test to match actual template text - Fix SnmpKit.MIB doctests and tests for enriched return values - Fix onboarding_live, mobile_controller, mib_test weak assertions - Remove dead code and fix credo warnings
246 lines
5.6 KiB
Elixir
246 lines
5.6 KiB
Elixir
defmodule Towerops.MobileSessions do
|
|
@moduledoc """
|
|
Context for managing mobile app authentication sessions.
|
|
"""
|
|
|
|
import Ecto.Query
|
|
|
|
alias Towerops.MobileSessions.MobileSession
|
|
alias Towerops.MobileSessions.QRLoginToken
|
|
alias Towerops.Repo
|
|
|
|
@doc """
|
|
Creates a new mobile session for a user.
|
|
|
|
"""
|
|
def create_mobile_session(attrs) do
|
|
%MobileSession{}
|
|
|> MobileSession.create_changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Gets a mobile session by ID.
|
|
"""
|
|
def get_session(session_id) do
|
|
Repo.get(MobileSession, session_id)
|
|
end
|
|
|
|
@doc """
|
|
Gets a mobile session by token.
|
|
|
|
Returns nil if the session is expired or doesn't exist.
|
|
"""
|
|
def get_session_by_token(token) when is_binary(token) do
|
|
now = DateTime.utc_now()
|
|
hashed = MobileSession.hash_token(token)
|
|
|
|
MobileSession
|
|
|> where([s], s.token == ^hashed)
|
|
|> where([s], s.expires_at > ^now)
|
|
|> Repo.one()
|
|
end
|
|
|
|
def get_session_by_token(_), do: nil
|
|
|
|
@doc """
|
|
Updates the last_used_at timestamp for a mobile session.
|
|
"""
|
|
def touch_session(%MobileSession{} = session) do
|
|
session
|
|
|> MobileSession.touch_changeset()
|
|
|> Repo.update()
|
|
end
|
|
|
|
@doc """
|
|
Lists all active mobile sessions for a user.
|
|
"""
|
|
def list_user_sessions(user_id) do
|
|
now = DateTime.utc_now()
|
|
|
|
MobileSession
|
|
|> where([s], s.user_id == ^user_id)
|
|
|> where([s], s.expires_at > ^now)
|
|
|> order_by([s], desc: s.last_used_at)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Revokes a mobile session by ID.
|
|
"""
|
|
def revoke_session(session_id) do
|
|
case Repo.get(MobileSession, session_id) do
|
|
nil -> {:error, :not_found}
|
|
session -> Repo.delete(session)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates alert preferences for a mobile session.
|
|
"""
|
|
def update_alert_preferences(session_id, attrs) do
|
|
case Repo.get(MobileSession, session_id) do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
session ->
|
|
session
|
|
|> Ecto.Changeset.cast(attrs, [:alerts_enabled, :push_token, :push_platform])
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Lists all devices with alerts enabled for a user.
|
|
Used for sending push notifications.
|
|
"""
|
|
def list_alert_enabled_devices(user_id) do
|
|
now = DateTime.utc_now()
|
|
|
|
MobileSession
|
|
|> where([s], s.user_id == ^user_id)
|
|
|> where([s], s.alerts_enabled == true)
|
|
|> where([s], s.expires_at > ^now)
|
|
|> where([s], not is_nil(s.push_token))
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Revokes all mobile sessions for a user.
|
|
"""
|
|
def revoke_all_user_sessions(user_id) do
|
|
{count, _} =
|
|
MobileSession
|
|
|> where([s], s.user_id == ^user_id)
|
|
|> Repo.delete_all()
|
|
|
|
{:ok, count}
|
|
end
|
|
|
|
@doc """
|
|
Deletes expired mobile sessions.
|
|
|
|
Returns the number of sessions deleted.
|
|
"""
|
|
def delete_expired_sessions do
|
|
now = DateTime.utc_now()
|
|
|
|
{count, _} =
|
|
MobileSession
|
|
|> where([s], s.expires_at <= ^now)
|
|
|> Repo.delete_all()
|
|
|
|
count
|
|
end
|
|
|
|
# QR Login Token functions
|
|
|
|
@doc """
|
|
Creates a new QR login token for a user.
|
|
|
|
Token expires in 5 minutes.
|
|
"""
|
|
def create_qr_login_token(user_id) do
|
|
%QRLoginToken{}
|
|
|> QRLoginToken.create_changeset(%{user_id: user_id})
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Gets a QR login token by token string.
|
|
|
|
Returns nil if the token is expired, already used, or doesn't exist.
|
|
"""
|
|
def get_qr_login_token(token) when is_binary(token) do
|
|
now = DateTime.utc_now()
|
|
|
|
QRLoginToken
|
|
|> where([t], t.token == ^token)
|
|
|> where([t], t.expires_at > ^now)
|
|
|> where([t], is_nil(t.completed_at))
|
|
|> Repo.one()
|
|
end
|
|
|
|
def get_qr_login_token(_), do: nil
|
|
|
|
@doc """
|
|
Completes a QR login token by creating a mobile session.
|
|
|
|
Returns {:ok, mobile_session} on success.
|
|
"""
|
|
def complete_qr_login(token, device_attrs) do
|
|
token
|
|
|> do_complete_qr_login(device_attrs)
|
|
|> Repo.transaction()
|
|
|> unwrap_transaction_result()
|
|
end
|
|
|
|
defp do_complete_qr_login(token, device_attrs) do
|
|
fn ->
|
|
with {:ok, qr_token} <- fetch_qr_token(token),
|
|
session_attrs = Map.put(device_attrs, :user_id, qr_token.user_id),
|
|
{:ok, session} <- create_mobile_session(session_attrs) do
|
|
# Mark QR token as completed
|
|
qr_token
|
|
|> QRLoginToken.complete_changeset(session.id)
|
|
|> Repo.update!()
|
|
|
|
{:ok, session}
|
|
else
|
|
{:error, :invalid_token} -> {:error, :invalid_token}
|
|
{:error, changeset} -> {:error, changeset}
|
|
end
|
|
end
|
|
end
|
|
|
|
# Helper function to unwrap nested transaction results
|
|
defp unwrap_transaction_result(result) do
|
|
case result do
|
|
{:ok, {:ok, value}} -> {:ok, value}
|
|
{:ok, {:error, reason}} -> {:error, reason}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp fetch_qr_token(token) do
|
|
case get_qr_login_token(token) do
|
|
nil -> {:error, :invalid_token}
|
|
qr_token -> {:ok, qr_token}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Deletes expired QR login tokens.
|
|
|
|
Returns the number of tokens deleted.
|
|
"""
|
|
def delete_expired_qr_tokens do
|
|
now = DateTime.utc_now()
|
|
|
|
{count, _} =
|
|
QRLoginToken
|
|
|> where([t], t.expires_at <= ^now)
|
|
|> Repo.delete_all()
|
|
|
|
count
|
|
end
|
|
|
|
@doc """
|
|
Checks if a QR login token has been completed.
|
|
|
|
Returns the mobile session if completed, nil otherwise.
|
|
"""
|
|
def check_qr_login_completed(token) when is_binary(token) do
|
|
QRLoginToken
|
|
|> where([t], t.token == ^token)
|
|
|> where([t], not is_nil(t.completed_at))
|
|
|> preload(:mobile_session)
|
|
|> Repo.one()
|
|
|> case do
|
|
%QRLoginToken{mobile_session: session} when not is_nil(session) -> session
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
def check_qr_login_completed(_), do: nil
|
|
end
|