Context module for OAuth connection management with multi-tenant isolation.
Provides connection lifecycle management, token refresh, and multi-tenant isolation with comprehensive audit logging and automatic token refresh.
Summary
Functions
Cleans up expired connections.
Gets a connection by ID with tenant isolation.
Gets an active connection for provider and tenant.
Gets an active connection for provider and tenant with optional auto-refresh.
Gets connection statistics for a tenant.
Lists active connections for a tenant.
Updates connection usage timestamp.
Records the provider rejecting a connection's credentials.
Clears a connection's rejection streak after its credentials authenticate again.
Refreshes an OAuth connection's access token.
Refreshes connections that are about to expire.
Revokes a connection.
Revokes all connections for a provider across all tenants.
Revokes all connections for a tenant.
Functions
Cleans up expired connections.
Removes connections that have been expired for more than 30 days.
Examples
iex> cleanup_expired_connections()
{:ok, 12} # 12 connections cleaned up
Gets a connection by ID with tenant isolation.
Examples
iex> get_connection("conn-123", "user-123")
{:ok, %Connection{}}
iex> get_connection("conn-123", "wrong-tenant")
{:error, :not_found}
Gets an active connection for provider and tenant.
Provider is identified by slug, not name.
Examples
iex> get_connection_for_provider("github", "user-123")
{:ok, %Connection{}}
iex> get_connection_for_provider("google", "user-123")
{:ok, %Connection{}}
iex> get_connection_for_provider("nonexistent", "user-123")
{:error, :not_found}
Gets an active connection for provider and tenant with optional auto-refresh.
Provider is identified by slug, not name. When auto_refresh is true, automatically refreshes the token if it's about to expire (within 5 minutes).
Examples
iex> get_connection_for_provider("github", "user-123", auto_refresh: true)
{:ok, %Connection{}} # Token automatically refreshed if needed
iex> get_connection_for_provider("google", "user-123", auto_refresh: false)
{:ok, %Connection{}} # Token returned as-is, even if expired
Gets connection statistics for a tenant.
Examples
iex> get_connection_stats("user-123")
%{
active: 3,
expired: 1,
revoked: 2,
total: 6,
providers: ["github", "google"]
}
Lists active connections for a tenant.
Examples
iex> list_connections("user-123")
[%Connection{}, ...]
iex> list_connections("user-123", provider: "github")
[%Connection{provider: %{name: "github"}}, ...]
Updates connection usage timestamp.
Should be called when connection is used for API requests.
Examples
iex> mark_connection_used(connection)
{:ok, %Connection{last_used_at: ~U[2023-01-01 12:00:00Z]}}
Records the provider rejecting a connection's credentials.
Consumers call this when a provider answers an API call with HTTP 401 or 403.
Tango issues tokens but never sees the calls made with them, so a connection
holding credentials the provider has revoked stays :active here forever
unless the consumer reports it. Once the rejections reach the threshold the
connection is expired, get_connection_for_provider/2 stops handing it out,
and the consumer sees {:error, :not_found} — its cue to treat the
integration as disconnected and prompt the tenant to reconnect.
opts[:max_failures] sets the expiry threshold (default 3), per call rather
than library-wide: one Tango deployment serves many providers, each called on
its own cadence, and the count is a number of rejections rather than a
duration. Callers hitting a provider every few seconds pass a higher ceiling
than one sweeping twice an hour, or the same transient outage expires far
more of their connections.
This is not a refresh failure. A connection carrying no refresh token or no
expiry is never eligible for refresh, so refresh_connection/1 never runs and
its counters never move, no matter how long the credentials have been dead.
Only active connections accept a report: a connection revoked or expired
between the failing API call and the report is left exactly as it is —
:revoked and :expired age out on different schedules, so a late report
must not convert one into the other. The count is read from a freshly loaded
row locked for the update, never from the caller's copy, so concurrent
reporters serialize instead of clobbering each other or raising
Ecto.StaleEntryError on a stale struct.
Accepts a %Connection{}, or a connection id together with opts[:tenant_id]
(a bare id is not proof of access, so id-based reports carry the tenant scope
every other lookup in this module requires). Returns {:error, :not_found}
when the connection does not exist, is not active, or belongs to another
tenant, and {:error, :tenant_id_required} for an id without a tenant.
Examples
iex> record_auth_failure(connection, "http_401")
{:ok, %Connection{auth_failures: 1}}
iex> record_auth_failure(connection_id, "http_401", tenant_id: "user-123", max_failures: 10)
{:ok, %Connection{auth_failures: 1}}
Clears a connection's rejection streak after its credentials authenticate again.
Keeps record_auth_failure/3 counting consecutive rejections: without this,
isolated failures spread over months would accumulate to the threshold and
expire a connection that works. The streak is checked in the database rather
than on the caller's copy — a struct loaded before another process recorded a
failure would otherwise skip the clear and leave a phantom strike behind. When
there is no streak the update matches no row and the call is a single no-op
statement, so reporting success after every provider call stays cheap.
Accepts a %Connection{}, or a connection id together with opts[:tenant_id].
Returns :ok whether or not anything needed clearing (an inactive or already
clean connection has no streak worth reporting), and
{:error, :tenant_id_required} for an id without a tenant.
Examples
iex> record_auth_success(connection)
:ok
iex> record_auth_success(connection_id, tenant_id: "user-123")
:ok
Refreshes an OAuth connection's access token.
Attempts to refresh the access token using the refresh token. Updates connection with new token data or marks as expired on failure.
Examples
iex> refresh_connection(connection)
{:ok, %Connection{access_token: "new_token"}}
iex> refresh_connection(expired_connection)
{:error, :refresh_failed}
Refreshes connections that are about to expire.
Should be called periodically by a background job.
Examples
iex> refresh_expiring_connections()
{:ok, 3} # 3 connections refreshed
Revokes a connection.
Marks connection as revoked and logs the action.
Examples
iex> revoke_connection(connection, "user-123")
{:ok, %Connection{status: "revoked"}}
Revokes all connections for a provider across all tenants.
Should be used when a provider is deactivated or has security issues.
Examples
iex> revoke_provider_connections("github")
{:ok, 15} # 15 connections revoked
Revokes all connections for a tenant.
Examples
iex> revoke_tenant_connections("user-123")
{:ok, 3} # 3 connections revoked