Tango.Connection (tango v0.1.2)

Copy Markdown View Source

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 all connections for a provider across all tenants.

Revokes all connections for a tenant.

Functions

cleanup_expired_connections()

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

get_connection(connection_id, tenant_id)

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}

get_connection_for_provider(provider_slug, tenant_id)

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}

get_connection_for_provider(provider_slug, tenant_id, opts)

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

get_connection_stats(tenant_id)

Gets connection statistics for a tenant.

Examples

iex> get_connection_stats("user-123")
%{
  active: 3,
  expired: 1,
  revoked: 2,
  total: 6,
  providers: ["github", "google"]
}

list_connections(tenant_id, opts \\ [])

Lists active connections for a tenant.

Examples

iex> list_connections("user-123")
[%Connection{}, ...]

iex> list_connections("user-123", provider: "github")
[%Connection{provider: %{name: "github"}}, ...]

mark_connection_used(connection)

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]}}

record_auth_failure(connection, error_reason, opts \\ [])

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}}

record_auth_success(connection, opts \\ [])

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

refresh_connection(connection)

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}

refresh_expiring_connections()

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

revoke_connection(connection, tenant_id)

Revokes a connection.

Marks connection as revoked and logs the action.

Examples

iex> revoke_connection(connection, "user-123")
{:ok, %Connection{status: "revoked"}}

revoke_provider_connections(provider_name)

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

revoke_tenant_connections(tenant_id)

Revokes all connections for a tenant.

Examples

iex> revoke_tenant_connections("user-123")
{:ok, 3}  # 3 connections revoked