Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/algora/organizations/organizations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ defmodule Algora.Organizations do
def get_org(id), do: Repo.get(User, id)
def get_org!(id), do: Repo.get!(User, id)

@spec fetch_org_by(clauses :: Keyword.t() | map()) ::
{:ok, User.t()} | {:error, :not_found}
def fetch_org_by(clauses) do
Repo.fetch_by(User, clauses)
end

def list_orgs(opts) do
Repo.all(
from u in User,
Expand Down
25 changes: 15 additions & 10 deletions lib/algora/workspace/schemas/installation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,29 @@ defmodule Algora.Workspace.Installation do

@derive {Inspect, except: [:provider_meta]}
typed_schema "installations" do
field :provider, :string
field :provider_id, :string
field :provider_login, :string
field :provider_meta, :map
field :provider, :string, null: false
field :provider_id, :string, null: false
field :provider_meta, :map, null: false
field :provider_user_id, :string, null: false

field :avatar_url, :string
field :repository_selection, :string

belongs_to :owner, User
belongs_to :connected_user, User
belongs_to :owner, User, null: false
belongs_to :connected_user, User, null: false

timestamps()
end

def changeset(installation, :github, user, org, data) do
def github_changeset(installation, user, provider_user, org, data) do
params = %{
owner_id: user.id,
connected_user_id: org.id,
avatar_url: data["account"]["avatar_url"],
repository_selection: data["repository_selection"],
provider_id: to_string(data["id"]),
provider_login: data["account"]["login"]
provider_user_id: to_string(provider_user.id),
provider_meta: data
}

installation
Expand All @@ -37,10 +38,14 @@ defmodule Algora.Workspace.Installation do
:avatar_url,
:repository_selection,
:provider_id,
:provider_login
:provider_user_id,
:provider_meta
])
|> validate_required([:owner_id, :connected_user_id, :provider_id, :provider_login])
|> validate_required([:owner_id, :connected_user_id, :provider_id, :provider_user_id, :provider_meta])
|> generate_id()
|> foreign_key_constraint(:owner_id)
|> foreign_key_constraint(:connected_user_id)
|> unique_constraint([:provider, :provider_id])
|> put_change(:provider, "github")
|> put_change(:provider_meta, data)
end
Expand Down
20 changes: 16 additions & 4 deletions lib/algora/workspace/workspace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,28 @@ defmodule Algora.Workspace do
end
end

def create_installation(:github, user, org, data) do
def create_installation(user, provider_user, org, data) do
%Installation{}
|> Installation.changeset(:github, user, org, data)
|> Installation.github_changeset(user, provider_user, org, data)
|> Repo.insert()
end

def update_installation(:github, user, org, installation, data) do
def update_installation(installation, user, provider_user, org, data) do
installation
|> Installation.changeset(:github, user, org, data)
|> Installation.github_changeset(user, provider_user, org, data)
|> Repo.update()
end

def upsert_installation(installation, user, org, provider_user) do
case get_installation_by_provider_id("github", installation["id"]) do
nil ->
create_installation(user, provider_user, org, installation)

existing_installation ->
update_installation(existing_installation, user, provider_user, org, installation)
end
end

def get_installation_by(fields), do: Repo.get_by(Installation, fields)
def get_installation_by!(fields), do: Repo.get_by!(Installation, fields)

Expand All @@ -132,6 +142,8 @@ defmodule Algora.Workspace do
def get_installation(id), do: Repo.get(Installation, id)
def get_installation!(id), do: Repo.get!(Installation, id)

def list_installations_by(fields), do: Repo.all(from(i in Installation, where: ^fields))

def list_user_installations(user_id) do
Repo.all(from(i in Installation, where: i.owner_id == ^user_id, preload: [:connected_user]))
end
Expand Down
124 changes: 34 additions & 90 deletions lib/algora_web/controllers/installation_callback_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,128 +10,72 @@ defmodule AlgoraWeb.InstallationCallbackController do

def new(conn, params) do
case validate_query_params(params) do
{:ok, %{setup_action: "install", installation_id: installation_id}} ->
handle_installation(conn, installation_id)
{:ok, %{setup_action: :install, installation_id: installation_id}} ->
handle_installation(conn, :install, installation_id)

# TODO: Implement update
{:ok, %{setup_action: "update"}} ->
redirect(conn, to: "/user/installations")
{:ok, %{setup_action: :update, installation_id: installation_id}} ->
handle_installation(conn, :update, installation_id)

# TODO: Implement request
{:ok, %{setup_action: :request}} ->
conn
|> put_flash(
:info,
"Installation request submitted! The Algora app will be activated upon approval from your organization administrator."
)
|> redirect(to: redirect_url(conn))

{:error, _reason} ->
redirect(conn, to: "/user/installations")
redirect(conn, to: redirect_url(conn))
end
end

defp validate_query_params(params) do
case params do
%{"setup_action" => "install", "installation_id" => installation_id} ->
{:ok, %{setup_action: "install", installation_id: String.to_integer(installation_id)}}
{:ok, %{setup_action: :install, installation_id: String.to_integer(installation_id)}}

%{"setup_action" => "update", "installation_id" => installation_id} ->
{:ok, %{setup_action: "update", installation_id: String.to_integer(installation_id)}}
{:ok, %{setup_action: :update, installation_id: String.to_integer(installation_id)}}

%{"setup_action" => "request"} ->
{:ok, %{setup_action: "request"}}
{:ok, %{setup_action: :request}}

_ ->
{:error, :invalid_params}
end
end

defp handle_installation(conn, installation_id) do
defp handle_installation(conn, setup_action, installation_id) do
user = conn.assigns.current_user

case do_handle_installation(conn, user, installation_id) do
{:ok, org} ->
# TODO: Trigger org joined event
# trigger_org_joined(org)

put_flash(conn, :info, "Organization created successfully: #{org.handle}")

# TODO: Redirect to the org dashboard and set the session context
redirect_url = determine_redirect_url(conn, org, user)
redirect(conn, to: redirect_url)
case do_handle_installation(user, installation_id) do
{:ok, _org} ->
conn
|> put_flash(:info, if(setup_action == :install, do: "Installation successful!", else: "Installation updated!"))
|> redirect(to: redirect_url(conn))

{:error, error} ->
Logger.error("❌ Installation callback failed: #{inspect(error)}")

put_flash(conn, :error, "#{inspect(error)}")
redirect(conn, to: "/user/installations")
conn
|> put_flash(:error, "#{inspect(error)}")
|> redirect(to: redirect_url(conn))
end
end

defp do_handle_installation(conn, user, installation_id) do
defp do_handle_installation(user, installation_id) do
# TODO: replace :last_context with a new :last_installation_target field
# TODO: handle nil user
# TODO: handle nil last_context
with {:ok, access_token} <- Accounts.get_access_token(user),
{:ok, installation} <- Github.find_installation(access_token, installation_id),
{:ok, github_handle} <- extract_github_handle(installation),
{:ok, account} <- Github.get_user_by_username(access_token, github_handle),
{:ok, org} <- upsert_org(conn, user, installation, account),
{:ok, _} <- upsert_installation(user, org, installation) do
{:ok, org}
end
end

defp extract_github_handle(%{"account" => %{"login" => login}}), do: {:ok, login}
defp extract_github_handle(_), do: {:error, 404}

defp upsert_installation(user, org, installation) do
case Workspace.get_installation_by_provider_id("github", installation["id"]) do
nil ->
Workspace.create_installation(:github, user, org, installation)

existing_installation ->
Workspace.update_installation(:github, user, org, existing_installation, installation)
end
end

defp upsert_org(conn, user, installation, account) do
attrs = %{
provider: "github",
provider_id: account["id"],
provider_login: account["login"],
provider_meta: account,
handle: account["login"],
name: account["name"],
description: account["bio"],
website_url: account["blog"],
twitter_url: get_twitter_url(account),
avatar_url: account["avatar_url"],
# TODO:
active: true,
featured: account["type"] != "User",
github_handle: account["login"]
}

case Organizations.get_org_by_handle(account["login"]) do
nil -> create_org(conn, user, attrs, installation)
existing_org -> update_org(conn, user, existing_org, attrs, installation)
end
end

# TODO: handle conflicting handles
defp create_org(_conn, user, attrs, _installation) do
# TODO: trigger org joined event
# trigger_org_joined(org)
with {:ok, org} <- Organizations.create_organization(attrs),
{:ok, _} <- Organizations.create_member(org, user, :admin) do
{:ok, provider_user} <- Workspace.ensure_user(access_token, installation["account"]["login"]),
{:ok, org} <- Organizations.fetch_org_by(handle: user.last_context),
{:ok, _} <- Workspace.upsert_installation(installation, user, org, provider_user) do
{:ok, org}
end
end

defp update_org(_conn, _user, existing_org, attrs, _installation) do
with {:ok, _} <- Organizations.update_organization(existing_org, attrs) do
{:ok, existing_org}
end
end

defp determine_redirect_url(_conn, _org, _user) do
# TODO: Implement
"/user/installations"
end

defp get_twitter_url(%{twitter_username: username}) when is_binary(username) do
"https://twitter.com/#{username}"
end

defp get_twitter_url(_), do: nil
defp redirect_url(conn), do: ~p"/org/#{conn.assigns.current_user.last_context}/settings"
end
45 changes: 44 additions & 1 deletion lib/algora_web/live/org/settings_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule AlgoraWeb.Org.SettingsLive do

alias Algora.Accounts
alias Algora.Accounts.User
alias AlgoraWeb.Components.Logos

def render(assigns) do
~H"""
Expand All @@ -13,6 +14,47 @@ defmodule AlgoraWeb.Org.SettingsLive do
<p class="text-muted-foreground">Update your settings and preferences</p>
</div>

<.card>
<.card_header>
<.card_title>GitHub Integration</.card_title>
<.card_description :if={@installations == []}>
Install the Algora app to enable slash commands in your GitHub repositories
</.card_description>
</.card_header>
<.card_content>
<div class="flex flex-col gap-3">
<%= if @installations != [] do %>
<%= for installation <- @installations do %>
<div class="flex items-center gap-2">
<img src={installation.avatar_url} class="w-9 h-9 rounded-lg" />
<div>
<p class="font-medium">{installation.provider_meta["account"]["login"]}</p>
<p class="text-sm text-muted-foreground">
Algora app is installed in <strong>{installation.repository_selection}</strong>
repositories
</p>
</div>
</div>
<% end %>
<.link href={Algora.Github.install_url()} rel="noopener" class="ml-auto gap-2">
<.button>
<Logos.github class="w-4 h-4 mr-2 -ml-1" />
Manage {ngettext("installation", "installations", length(@installations))}
</.button>
</.link>
<% else %>
<div class="flex flex-col gap-2">
<.link href={Algora.Github.install_url()} rel="noopener" class="ml-auto gap-2">
<.button>
<Logos.github class="w-4 h-4 mr-2 -ml-1" /> Install GitHub App
</.button>
</.link>
</div>
<% end %>
</div>
</.card_content>
</.card>

<.card>
<.card_header>
<.card_title>Account</.card_title>
Expand Down Expand Up @@ -63,10 +105,11 @@ defmodule AlgoraWeb.Org.SettingsLive do
%{current_org: current_org} = socket.assigns

changeset = User.settings_changeset(current_org, %{})
installations = Algora.Workspace.list_installations_by(connected_user_id: current_org.id, provider: "github")

{:ok,
socket
|> assign(current_org: current_org)
|> assign(:installations, installations)
|> assign_form(changeset)}
end

Expand Down
21 changes: 21 additions & 0 deletions priv/repo/migrations/20250110150238_update_installations.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule Algora.Repo.Migrations.UpdateInstallations do
use Ecto.Migration

def up do
alter table(:installations) do
modify :owner_id, :string, null: true
modify :connected_user_id, :string, null: true
add :provider_user_id, :string, null: false
remove :provider_login
end
end

def down do
alter table(:installations) do
modify :owner_id, :string, null: false
modify :connected_user_id, :string, null: false
remove :provider_user_id
add :provider_login, :string, null: false
end
end
end
Loading