Packages
phoenix_kit
1.7.121
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/users/oauth.ex
if Code.ensure_loaded?(Ueberauth) do
defmodule PhoenixKit.Users.OAuth do
@moduledoc """
OAuth authentication context for PhoenixKit.
Handles OAuth authentication flows for external providers like Google, Apple, GitHub.
This module requires the Ueberauth library to be installed. If Ueberauth is not available,
a fallback module with basic functionality will be used instead.
"""
import Ecto.Query, warn: false
alias PhoenixKit.RepoHelper, as: Repo
alias PhoenixKit.Modules.Referrals
alias PhoenixKit.Users.Auth
alias PhoenixKit.Users.Auth.User
alias PhoenixKit.Users.OAuthProvider
@doc """
Handles OAuth callback from Ueberauth.
"""
def handle_oauth_callback(%Ueberauth.Auth{} = auth, opts \\ []) do
oauth_data = extract_oauth_data(auth)
track_geolocation = Keyword.get(opts, :track_geolocation, false)
ip_address = Keyword.get(opts, :ip_address)
referral_code = Keyword.get(opts, :referral_code)
Repo.transaction(fn ->
with {:ok, user, _status} <-
find_or_create_user(oauth_data, track_geolocation, ip_address),
{:ok, _provider} <- link_oauth_provider(user, oauth_data),
{:ok, user} <- maybe_save_oauth_avatar(user, oauth_data),
:ok <- maybe_process_referral_code(user, referral_code) do
user
else
{:error, reason} -> Repo.rollback(reason)
end
end)
end
@doc """
Finds an existing user by email or creates a new one from OAuth data.
"""
def find_or_create_user(oauth_data, track_geolocation \\ false, ip_address \\ nil) do
case Auth.get_user_by_email(oauth_data.email) do
%User{} = user ->
# Auto-confirm email for existing users logging in via OAuth
{:ok, confirmed_user} = maybe_confirm_user(user)
{:ok, confirmed_user, :found}
nil ->
case register_oauth_user(oauth_data, track_geolocation, ip_address) do
{:ok, user} -> {:ok, user, :created}
{:error, reason} -> {:error, reason}
end
end
end
# Auto-confirm email for unconfirmed users logging in via OAuth.
# OAuth providers verify email ownership, so we can trust it.
defp maybe_confirm_user(%User{confirmed_at: nil} = user) do
case Auth.admin_confirm_user(user) do
{:ok, confirmed_user} ->
PhoenixKit.Activity.log(%{
action: "user.email_confirmed",
module: "users",
mode: "auto",
actor_uuid: confirmed_user.uuid,
resource_type: "user",
resource_uuid: confirmed_user.uuid,
metadata: %{"method" => "oauth", "actor_role" => "user"}
})
{:ok, confirmed_user}
{:error, _changeset} ->
{:ok, user}
end
end
defp maybe_confirm_user(%User{} = user), do: {:ok, user}
# Save OAuth avatar URL to user's custom_fields if available
# This enables displaying the OAuth provider's avatar in the UI
defp maybe_save_oauth_avatar(user, %{image: image}) when is_binary(image) and image != "" do
# Only update if user doesn't already have a custom avatar
case user.custom_fields do
%{"avatar_file_uuid" => file_uuid} when is_binary(file_uuid) and file_uuid != "" ->
# User has a custom avatar, don't override
{:ok, user}
_ ->
# Save OAuth avatar URL for fallback display
Auth.set_user_custom_field(user, "oauth_avatar_url", image)
end
end
defp maybe_save_oauth_avatar(user, _oauth_data), do: {:ok, user}
@doc """
Links an OAuth provider to a user account.
"""
def link_oauth_provider(%User{} = user, oauth_data) when is_map(oauth_data) do
attrs = %{
user_uuid: user.uuid,
provider: oauth_data.provider,
provider_uid: oauth_data.provider_uid,
provider_email: oauth_data.email,
access_token: oauth_data[:access_token],
refresh_token: oauth_data[:refresh_token],
token_expires_at: oauth_data[:token_expires_at],
raw_data: build_raw_data(oauth_data)
}
%OAuthProvider{}
|> OAuthProvider.changeset(attrs)
|> Repo.insert(
on_conflict: {:replace_all_except, [:uuid, :user_uuid, :provider, :inserted_at]},
conflict_target: [:user_uuid, :provider]
)
end
@doc """
Gets all OAuth providers for a user.
"""
def get_user_oauth_providers(user_uuid) when is_binary(user_uuid) do
from(p in OAuthProvider,
where: p.user_uuid == ^user_uuid,
order_by: [desc: p.inserted_at]
)
|> Repo.all()
end
@doc """
Unlinks an OAuth provider from a user.
"""
def unlink_oauth_provider(user_uuid, provider)
when is_binary(user_uuid) and is_binary(provider) do
case from(p in OAuthProvider, where: p.user_uuid == ^user_uuid and p.provider == ^provider)
|> Repo.one() do
nil -> {:error, :not_found}
provider_record -> Repo.delete(provider_record)
end
end
# Private functions
defp extract_oauth_data(%Ueberauth.Auth{} = auth) do
%{
provider: to_string(auth.provider),
provider_uid: to_string(auth.uid),
email: auth.info.email,
first_name: auth.info.first_name,
last_name: auth.info.last_name,
image: auth.info.image,
# FIXED: Use dot notation for struct fields (structs don't implement Access behaviour)
access_token: auth.credentials.token,
refresh_token: auth.credentials.refresh_token,
token_expires_at: get_token_expires_at(auth.credentials),
raw_info: get_raw_info(auth.extra)
}
end
# Helper to safely extract raw_info from extra struct
defp get_raw_info(%{raw_info: raw_info}), do: raw_info
defp get_raw_info(_), do: %{}
defp get_token_expires_at(%{expires_at: expires_at}) when is_integer(expires_at) do
DateTime.from_unix!(expires_at)
end
defp get_token_expires_at(_), do: nil
defp register_oauth_user(oauth_data, track_geolocation, ip_address) do
attrs = %{
email: oauth_data.email,
password: generate_random_password(),
first_name: oauth_data.first_name,
last_name: oauth_data.last_name
}
result =
if track_geolocation && ip_address do
Auth.register_user_with_geolocation(attrs, ip_address)
else
Auth.register_user(attrs, ip_address)
end
# Auto-confirm email for OAuth users (providers verify email ownership)
case result do
{:ok, user} -> maybe_confirm_user(user)
error -> error
end
end
defp generate_random_password do
:crypto.strong_rand_bytes(32)
|> Base.url_encode64(padding: false)
|> binary_part(0, 32)
end
defp build_raw_data(oauth_data) do
%{
image: oauth_data[:image],
raw_info: serialize_raw_info(oauth_data[:raw_info])
}
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|> Enum.into(%{})
end
defp serialize_raw_info(nil), do: %{}
defp serialize_raw_info(raw_info) when is_map(raw_info) do
Enum.into(raw_info, %{}, fn {key, value} ->
{key, serialize_value(value)}
end)
end
defp serialize_raw_info(raw_info), do: raw_info
# Serialize OAuth2.AccessToken if present
defp serialize_value(%OAuth2.AccessToken{} = token) do
%{
access_token: token.access_token,
refresh_token: token.refresh_token,
expires_at: token.expires_at,
token_type: token.token_type,
other_params: token.other_params
}
end
defp serialize_value(value), do: value
defp maybe_process_referral_code(_user, nil), do: :ok
defp maybe_process_referral_code(user, referral_code) when is_binary(referral_code) do
if Code.ensure_loaded?(Referrals) do
case Referrals.get_code_by_string(referral_code) do
nil -> :ok
code -> Referrals.use_code(code.code, user.uuid)
end
end
:ok
end
end
else
# Fallback module when Ueberauth is not loaded
defmodule PhoenixKit.Users.OAuth do
@moduledoc """
Fallback OAuth context module when Ueberauth is not installed.
This module provides basic OAuth provider management without authentication capabilities.
To enable full OAuth authentication, install the required dependencies.
"""
import Ecto.Query, warn: false
alias PhoenixKit.RepoHelper, as: Repo
alias PhoenixKit.Users.Auth.User
alias PhoenixKit.Users.OAuthProvider
@doc """
Returns an error when OAuth callback is attempted without Ueberauth.
"""
def handle_oauth_callback(_auth, _opts \\ []) do
{:error, :ueberauth_not_loaded}
end
@doc """
Gets all OAuth providers for a user.
"""
def get_user_oauth_providers(user_uuid) when is_binary(user_uuid) do
from(p in OAuthProvider,
where: p.user_uuid == ^user_uuid,
order_by: [desc: p.inserted_at]
)
|> Repo.all()
end
@doc """
Unlinks an OAuth provider from a user.
"""
def unlink_oauth_provider(user_uuid, provider)
when is_binary(user_uuid) and is_binary(provider) do
case from(p in OAuthProvider, where: p.user_uuid == ^user_uuid and p.provider == ^provider)
|> Repo.one() do
nil -> {:error, :not_found}
provider_record -> Repo.delete(provider_record)
end
end
end
end