Skip to content

Commit

Permalink
WIP - Refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
elias-ba committed Feb 25, 2025
1 parent b849767 commit a99d4ef
Show file tree
Hide file tree
Showing 8 changed files with 487 additions and 83 deletions.
184 changes: 132 additions & 52 deletions lib/lightning/credentials.ex
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,48 @@ defmodule Lightning.Credentials do
[%Credential{user_id: 123}, %Credential{user_id: 123}]
"""
def list_credentials(%Project{} = project) do
Ecto.assoc(project, :credentials)
|> preload([:user, :project_credentials, :projects, :oauth_client])
|> Repo.all()
credentials =
Ecto.assoc(project, :credentials)
|> preload([
:user,
:project_credentials,
:projects,
oauth_token: :oauth_client
])
|> Repo.all()

# Set virtual fields for each credential
Enum.map(credentials, &set_oauth_virtual_fields/1)
end

def list_credentials(%User{id: user_id}) do
from(c in Credential,
where: c.user_id == ^user_id,
preload: [:projects, :oauth_client]
)
|> Repo.all()
credentials =
from(c in Credential,
where: c.user_id == ^user_id,
preload: [:projects, :user, oauth_token: :oauth_client]
)
|> Repo.all()

# Set virtual fields for each credential
Enum.map(credentials, &set_oauth_virtual_fields/1)
end

defp set_oauth_virtual_fields(%Credential{} = credential) do
if Ecto.assoc_loaded?(credential.oauth_token) && credential.oauth_token do
# Update both body and virtual fields for all credentials with oauth_token
%{
credential
| body: credential.oauth_token.body,
oauth_client_id: credential.oauth_token.oauth_client_id,
oauth_client:
if(Ecto.assoc_loaded?(credential.oauth_token.oauth_client),
do: credential.oauth_token.oauth_client,
else: nil
)
}
else
credential
end
end

@doc """
Expand Down Expand Up @@ -134,70 +165,119 @@ defmodule Lightning.Credentials do
|> Repo.preload([:project_credentials, :projects])
end

@doc """
Creates a credential.
## Examples
iex> create_credential(%{field: value})
{:ok, %Credential{}}
iex> create_credential(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_credential(attrs \\ %{}) do
changeset =
%Credential{}
|> change_credential(attrs)

Multi.new()
|> Multi.insert(
:credential,
changeset
)
# Handle OAuth credentials
multi =
if attrs["schema"] == "oauth" do
user_id = attrs["user_id"]
oauth_client_id = attrs["oauth_client_id"]
body = attrs["body"]

Multi.new()
|> Multi.run(:oauth_token, fn _repo, _changes ->
with {:ok, scopes} <-
Lightning.Credentials.OauthToken.extract_scopes(body) do
Lightning.Credentials.OauthToken.find_or_create_for_scopes(
user_id,
oauth_client_id,
scopes,
body
)
else
:error -> {:error, "Could not extract scopes from OAuth token"}
end
end)
|> Multi.insert(:credential, fn %{oauth_token: token} ->
dbg(token)
# Create credential changeset with token reference and nil body
credential_params =
attrs
|> Map.put("oauth_token_id", token.id)
|> Map.put("body", %{})

%Credential{}
|> Credential.changeset(credential_params)
|> dbg()
end)
else
# Regular credential creation
Multi.new()
|> Multi.insert(:credential, changeset)
end

# Add events and execute transaction
multi
|> derive_events(changeset)
|> Repo.transaction()
|> case do
{:error, _op, changeset, _changes} ->
{:error, changeset}

{:ok, %{credential: credential}} ->
{:ok, credential}
{:error, _op, error, _changes} -> {:error, error}
{:ok, %{credential: credential}} -> {:ok, credential}
end
end

@doc """
Updates a credential.
## Examples
iex> update_credential(credential, %{field: new_value})
{:ok, %Credential{}}
iex> update_credential(credential, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_credential(%Credential{} = credential, attrs) do
changeset =
credential
|> change_credential(attrs)
|> cast_body_change()

Multi.new()
|> Multi.update(:credential, changeset)
# Handle OAuth credentials with body updates
multi =
if credential.schema == "oauth" && Map.has_key?(attrs, "body") do
# Load the oauth token if needed
credential = Repo.preload(credential, :oauth_token)
new_body = attrs["body"]

Multi.new()
|> Multi.run(:oauth_token, fn _repo, _changes ->
if credential.oauth_token_id do
# Update existing token
credential.oauth_token
|> Lightning.Credentials.OauthToken.update_tokens_changeset(new_body)
|> Repo.update()
else
# Find or create token
with {:ok, scopes} <-
Lightning.Credentials.OauthToken.extract_scopes(new_body),
oauth_client_id =
credential.oauth_client_id || attrs["oauth_client_id"] do
Lightning.Credentials.OauthToken.find_or_create_for_scopes(
credential.user_id,
oauth_client_id,
scopes,
new_body
)
else
:error -> {:error, "Could not extract scopes from OAuth token"}
end
end
end)
|> Multi.run(:credential, fn repo, %{oauth_token: token} ->
# Update credential to reference token
updated_changeset =
changeset
|> Ecto.Changeset.put_change(:oauth_token_id, token.id)
|> Ecto.Changeset.put_change(:body, nil)

repo.update(updated_changeset)
end)
else
# Regular credential update
Multi.new()
|> Multi.update(:credential, changeset)
end

# Add events and execute transaction
multi
|> derive_events(changeset)
|> Repo.transaction()
|> case do
{:error, :credential, changeset, _changes} ->
{:error, changeset}

{:ok, %{credential: credential}} ->
Lightning.Repo.get(Lightning.Credentials.Credential, credential.id)
|> Map.get(:body)

{:ok, credential}
{:error, _op, error, _changes} -> {:error, error}
{:ok, %{credential: credential}} -> {:ok, credential}
end
end

Expand Down
82 changes: 53 additions & 29 deletions lib/lightning/credentials/credential.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Lightning.Credentials.Credential do
use Lightning.Schema

alias Lightning.Accounts.User
alias Lightning.Credentials.OauthClient
alias Lightning.Credentials.OauthToken
alias Lightning.Projects.ProjectCredential

@type t :: %__MODULE__{
Expand All @@ -22,8 +22,11 @@ defmodule Lightning.Credentials.Credential do
field :scheduled_deletion, :utc_datetime
field :transfer_status, Ecto.Enum, values: [:pending, :completed]

field :oauth_client_id, :binary_id, virtual: true
field :oauth_client, :any, virtual: true

belongs_to :user, User
belongs_to :oauth_client, OauthClient
belongs_to :oauth_token, OauthToken

has_many :project_credentials, ProjectCredential
has_many :projects, through: [:project_credentials, :project]
Expand All @@ -39,7 +42,7 @@ defmodule Lightning.Credentials.Credential do
:body,
:production,
:user_id,
:oauth_client_id,
:oauth_token_id,
:schema,
:scheduled_deletion,
:transfer_status
Expand All @@ -50,36 +53,57 @@ defmodule Lightning.Credentials.Credential do
message: "you have another credential with the same name"
)
|> assoc_constraint(:user)
|> assoc_constraint(:oauth_client)
|> validate_format(:name, ~r/^[a-zA-Z0-9_\- ]*$/,
message: "credential name has invalid format"
)
|> validate_oauth()
end

defp validate_oauth(changeset) do
if get_field(changeset, :schema) == "oauth" do
body = get_field(changeset, :body) || %{}

body = Enum.into(body, %{}, fn {k, v} -> {to_string(k), v} end)

required_fields = ["access_token", "refresh_token"]
expires_fields = ["expires_in", "expires_at"]

has_required_fields? = Enum.all?(required_fields, &Map.has_key?(body, &1))
has_expires_field? = Enum.any?(expires_fields, &Map.has_key?(body, &1))

if has_required_fields? and has_expires_field? do
changeset
else
add_error(
changeset,
:body,
"Invalid OAuth token. Missing required fields: access_token, refresh_token, and either expires_in or expires_at."
)
end
else
changeset
end
@doc """
Gets the oauth_client_id for a credential by checking its oauth_token.
"""
def oauth_client_id(%__MODULE__{oauth_token: %OauthToken{oauth_client_id: id}}),
do: id

def oauth_client_id(%__MODULE__{} = credential) do
credential = Lightning.Repo.preload(credential, :oauth_token)
oauth_client_id(credential)
end

@doc """
Gets the oauth_client for a credential through its oauth_token.
"""
def oauth_client(%__MODULE__{oauth_token: %OauthToken{oauth_client: client}})
when not is_nil(client),
do: client

def oauth_client(%__MODULE__{} = credential) do
credential = Lightning.Repo.preload(credential, oauth_token: :oauth_client)
oauth_client(credential)
end

# defp validate_oauth(changeset) do
# if get_field(changeset, :schema) == "oauth" do
# body = get_field(changeset, :body) || %{}

# body = Enum.into(body, %{}, fn {k, v} -> {to_string(k), v} end)

# required_fields = ["access_token", "refresh_token"]
# expires_fields = ["expires_in", "expires_at"]

# has_required_fields? = Enum.all?(required_fields, &Map.has_key?(body, &1))
# has_expires_field? = Enum.any?(expires_fields, &Map.has_key?(body, &1))

# if has_required_fields? and has_expires_field? do
# changeset
# else
# add_error(
# changeset,
# :body,
# "Invalid OAuth token. Missing required fields: access_token, refresh_token, and either expires_in or expires_at."
# )
# end
# else
# changeset
# end
# end
end
4 changes: 2 additions & 2 deletions lib/lightning/credentials/oauth_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defmodule Lightning.Credentials.OauthClient do
use Lightning.Schema

alias Lightning.Accounts.User
alias Lightning.Credentials.Credential
alias Lightning.Credentials.OauthToken
alias Lightning.Projects.ProjectOauthClient
alias Lightning.Validators

Expand All @@ -35,7 +35,7 @@ defmodule Lightning.Credentials.OauthClient do

belongs_to :user, User

has_many :credentials, Credential
has_many :oauth_tokens, OauthToken
has_many :project_oauth_clients, ProjectOauthClient
has_many :projects, through: [:project_oauth_clients, :project]

Expand Down
Loading

0 comments on commit a99d4ef

Please sign in to comment.