feat(guildhall): minimum viable guildhall — OIDC, guilds, schematics, members
Implements the full founding-guild onboarding stack across four phases: Phase A — Keycloak OIDC auth pipeline (oidcc) + guild registration with ceremony-engine approval (SingleApproval, hub operator approves via gRPC). Phase B — Founding schematic templates (MSP/ISV/NSP TOML), gRPC clients for ceremony-service and ffc-schematic-server, schematic fork/bind/realize LiveView with DB audit trail in guild_schematics. Phase C — RealizationPoller GenServer polling realization status every 5s, PubSub broadcast, live realization dashboard showing 7 reconciler sections. Phase D — Self-service member onboarding (join request → guild master approval via ceremony), member management LiveView, auto-create guild master on guild approval via Ecto.Multi transaction. Includes K8s manifests for ceremony-service (port 50053) and ffc-schematic-server (port 9091) as ClusterIP services, plus updated guildhall deployment with OIDC and gRPC service URL env vars. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tyler J King <tking@guildhouse.dev>
This commit is contained in:
parent
7730bf3818
commit
c0959a5376
44 changed files with 4234 additions and 30 deletions
57
apps/guildhall_ops_db/lib/guildhall/ops_db/guild.ex
Normal file
57
apps/guildhall_ops_db/lib/guildhall/ops_db/guild.ex
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
defmodule Guildhall.OpsDb.Guild do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@min_guild_id 0x010
|
||||
@max_guild_id 0x3FF
|
||||
|
||||
schema "guilds" do
|
||||
field :guild_id, :integer
|
||||
field :name, :string
|
||||
field :slug, :string
|
||||
field :guild_type, :string
|
||||
field :description, :string
|
||||
field :trust_domain, :string
|
||||
field :contact_did, :string
|
||||
field :registrant_did, :string
|
||||
field :registration_ceremony_id, :string
|
||||
field :status, :string, default: "pending_approval"
|
||||
field :enrollment_accord_ref, :string
|
||||
field :metadata, :map, default: %{}
|
||||
|
||||
has_many :guild_schematics, Guildhall.OpsDb.GuildSchematic
|
||||
has_many :guild_memberships, Guildhall.OpsDb.GuildMembership
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
def changeset(guild, attrs) do
|
||||
guild
|
||||
|> cast(attrs, [
|
||||
:guild_id,
|
||||
:name,
|
||||
:slug,
|
||||
:guild_type,
|
||||
:description,
|
||||
:trust_domain,
|
||||
:contact_did,
|
||||
:registrant_did,
|
||||
:registration_ceremony_id,
|
||||
:status,
|
||||
:enrollment_accord_ref,
|
||||
:metadata
|
||||
])
|
||||
|> validate_required([:guild_id, :name, :slug, :guild_type, :contact_did, :registrant_did])
|
||||
|> validate_inclusion(:guild_type, ~w(msp isv nsp))
|
||||
|> validate_inclusion(:status, ~w(pending_approval approved denied active suspended))
|
||||
|> validate_number(:guild_id,
|
||||
greater_than_or_equal_to: @min_guild_id,
|
||||
less_than_or_equal_to: @max_guild_id
|
||||
)
|
||||
|> unique_constraint(:guild_id)
|
||||
|> unique_constraint(:slug)
|
||||
|> unique_constraint(:name)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
defmodule Guildhall.OpsDb.GuildMembership do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
schema "guild_memberships" do
|
||||
belongs_to :guild, Guildhall.OpsDb.Guild
|
||||
|
||||
field :user_did, :string
|
||||
field :user_email, :string
|
||||
field :display_name, :string
|
||||
field :keycloak_sub, :string
|
||||
field :role, :string, default: "apprentice"
|
||||
field :status, :string, default: "pending"
|
||||
field :membership_ceremony_id, :string
|
||||
field :approved_by_did, :string
|
||||
field :approved_at, :utc_datetime_usec
|
||||
field :metadata, :map, default: %{}
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
def changeset(membership, attrs) do
|
||||
membership
|
||||
|> cast(attrs, [
|
||||
:guild_id,
|
||||
:user_did,
|
||||
:user_email,
|
||||
:display_name,
|
||||
:keycloak_sub,
|
||||
:role,
|
||||
:status,
|
||||
:membership_ceremony_id,
|
||||
:approved_by_did,
|
||||
:approved_at,
|
||||
:metadata
|
||||
])
|
||||
|> validate_required([:guild_id, :user_did, :user_email, :keycloak_sub])
|
||||
|> validate_inclusion(:role, ~w(apprentice journeyman master))
|
||||
|> validate_inclusion(:status, ~w(pending approved denied active suspended removed))
|
||||
|> foreign_key_constraint(:guild_id)
|
||||
|> unique_constraint([:guild_id, :user_did])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
defmodule Guildhall.OpsDb.GuildMemberships do
|
||||
import Ecto.Query, only: [from: 2]
|
||||
alias Guildhall.OpsDb.{Repo, GuildMembership}
|
||||
|
||||
def list_memberships(guild_id) do
|
||||
Repo.all(from(m in GuildMembership, where: m.guild_id == ^guild_id, order_by: [asc: m.inserted_at]))
|
||||
end
|
||||
|
||||
def get_membership!(id), do: Repo.get!(GuildMembership, id)
|
||||
|
||||
def active_members(guild_id) do
|
||||
Repo.all(from(m in GuildMembership, where: m.guild_id == ^guild_id and m.status == "active"))
|
||||
end
|
||||
|
||||
def masters_for_guild(guild_id) do
|
||||
Repo.all(
|
||||
from(m in GuildMembership,
|
||||
where: m.guild_id == ^guild_id and m.role == "master" and m.status == "active"
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def find_membership(guild_id, user_did) do
|
||||
Repo.one(from(m in GuildMembership, where: m.guild_id == ^guild_id and m.user_did == ^user_did))
|
||||
end
|
||||
|
||||
def request_membership(attrs) do
|
||||
%GuildMembership{}
|
||||
|> GuildMembership.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def approve_membership(%GuildMembership{} = membership, approver_did) do
|
||||
membership
|
||||
|> GuildMembership.changeset(%{
|
||||
status: "active",
|
||||
approved_by_did: approver_did,
|
||||
approved_at: DateTime.utc_now()
|
||||
})
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def deny_membership(%GuildMembership{} = membership, approver_did) do
|
||||
membership
|
||||
|> GuildMembership.changeset(%{
|
||||
status: "denied",
|
||||
approved_by_did: approver_did,
|
||||
approved_at: DateTime.utc_now()
|
||||
})
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def update_role(%GuildMembership{} = membership, new_role) do
|
||||
membership
|
||||
|> GuildMembership.changeset(%{role: new_role})
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
defmodule Guildhall.OpsDb.GuildSchematic do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
schema "guild_schematics" do
|
||||
belongs_to :guild, Guildhall.OpsDb.Guild
|
||||
|
||||
field :template_name, :string
|
||||
field :schematic_name, :string
|
||||
field :schematic_version, :string
|
||||
field :tree_hash, :string
|
||||
field :binding_id, :string
|
||||
field :realization_id, :string
|
||||
field :status, :string, default: "pending"
|
||||
field :customization_params, :map, default: %{}
|
||||
field :realization_snapshot, :map, default: %{}
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
def changeset(schematic, attrs) do
|
||||
schematic
|
||||
|> cast(attrs, [
|
||||
:guild_id,
|
||||
:template_name,
|
||||
:schematic_name,
|
||||
:schematic_version,
|
||||
:tree_hash,
|
||||
:binding_id,
|
||||
:realization_id,
|
||||
:status,
|
||||
:customization_params,
|
||||
:realization_snapshot
|
||||
])
|
||||
|> validate_required([:guild_id, :template_name, :schematic_name, :schematic_version])
|
||||
|> validate_inclusion(:status, ~w(pending forked binding_created realizing realized partially_realized failed))
|
||||
|> foreign_key_constraint(:guild_id)
|
||||
|> unique_constraint([:schematic_name, :schematic_version])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
defmodule Guildhall.OpsDb.GuildSchematics do
|
||||
import Ecto.Query, only: [from: 2]
|
||||
alias Guildhall.OpsDb.{Repo, GuildSchematic}
|
||||
|
||||
def get_for_guild(guild_id) do
|
||||
Repo.one(
|
||||
from(gs in GuildSchematic,
|
||||
where: gs.guild_id == ^guild_id,
|
||||
order_by: [desc: gs.inserted_at],
|
||||
limit: 1
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def create(attrs) do
|
||||
%GuildSchematic{}
|
||||
|> GuildSchematic.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def update_schematic(%GuildSchematic{} = gs, attrs) do
|
||||
gs
|
||||
|> GuildSchematic.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def update_realization_snapshot(%GuildSchematic{} = gs, snapshot) do
|
||||
update_schematic(gs, %{realization_snapshot: snapshot})
|
||||
end
|
||||
end
|
||||
63
apps/guildhall_ops_db/lib/guildhall/ops_db/guilds.ex
Normal file
63
apps/guildhall_ops_db/lib/guildhall/ops_db/guilds.ex
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
defmodule Guildhall.OpsDb.Guilds do
|
||||
import Ecto.Query
|
||||
alias Guildhall.OpsDb.{Repo, Guild, GuildMembership}
|
||||
alias Ecto.Multi
|
||||
|
||||
def list_guilds(filters \\ []) do
|
||||
Guild
|
||||
|> maybe_filter_status(filters[:status])
|
||||
|> order_by([g], desc: g.inserted_at)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def get_guild!(id), do: Repo.get!(Guild, id)
|
||||
|
||||
def get_guild_by_slug(slug), do: Repo.get_by(Guild, slug: slug)
|
||||
|
||||
def create_guild(attrs) do
|
||||
%Guild{}
|
||||
|> Guild.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def update_guild(%Guild{} = guild, attrs) do
|
||||
guild
|
||||
|> Guild.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def next_guild_id do
|
||||
case Repo.one(from(g in Guild, select: max(g.guild_id))) do
|
||||
nil -> 0x010
|
||||
max_id -> max_id + 1
|
||||
end
|
||||
end
|
||||
|
||||
def approve_guild(%Guild{} = guild) do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
Multi.new()
|
||||
|> Multi.update(:guild, Guild.changeset(guild, %{status: "approved"}))
|
||||
|> Multi.insert(:master_membership, GuildMembership.changeset(%GuildMembership{}, %{
|
||||
guild_id: guild.id,
|
||||
user_did: guild.registrant_did,
|
||||
user_email: guild.contact_did,
|
||||
display_name: "Guild Registrant",
|
||||
keycloak_sub: "",
|
||||
role: "master",
|
||||
status: "active",
|
||||
approved_by_did: "system",
|
||||
approved_at: now
|
||||
}), on_conflict: :nothing, conflict_target: [:guild_id, :user_did])
|
||||
|> Repo.transaction()
|
||||
end
|
||||
|
||||
def guild_count(status \\ nil) do
|
||||
Guild
|
||||
|> maybe_filter_status(status)
|
||||
|> Repo.aggregate(:count)
|
||||
end
|
||||
|
||||
defp maybe_filter_status(query, nil), do: query
|
||||
defp maybe_filter_status(query, status), do: where(query, [g], g.status == ^status)
|
||||
end
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
defmodule Guildhall.OpsDb.Repo.Migrations.CreateGuilds do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:guilds) do
|
||||
add :guild_id, :integer, null: false
|
||||
add :name, :string, null: false
|
||||
add :slug, :string, null: false
|
||||
add :guild_type, :string, null: false
|
||||
add :description, :text
|
||||
add :trust_domain, :string
|
||||
add :contact_did, :string, null: false
|
||||
add :registrant_did, :string, null: false
|
||||
add :registration_ceremony_id, :string
|
||||
add :status, :string, null: false, default: "pending_approval"
|
||||
add :enrollment_accord_ref, :string
|
||||
add :metadata, :map, default: %{}
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
create unique_index(:guilds, [:guild_id])
|
||||
create unique_index(:guilds, [:slug])
|
||||
create unique_index(:guilds, [:name])
|
||||
create index(:guilds, [:status])
|
||||
create index(:guilds, [:guild_type])
|
||||
create index(:guilds, [:registrant_did])
|
||||
|
||||
execute(
|
||||
"ALTER TABLE guilds ADD CONSTRAINT guilds_guild_id_range CHECK (guild_id >= 16 AND guild_id <= 1023)",
|
||||
"ALTER TABLE guilds DROP CONSTRAINT guilds_guild_id_range"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
defmodule Guildhall.OpsDb.Repo.Migrations.CreateGuildSchematics do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:guild_schematics) do
|
||||
add :guild_id, references(:guilds, on_delete: :restrict), null: false
|
||||
add :template_name, :string, null: false
|
||||
add :schematic_name, :string, null: false
|
||||
add :schematic_version, :string, null: false
|
||||
add :tree_hash, :string
|
||||
add :binding_id, :string
|
||||
add :realization_id, :string
|
||||
add :status, :string, null: false, default: "pending"
|
||||
add :customization_params, :map, default: %{}
|
||||
add :realization_snapshot, :map, default: %{}
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
create index(:guild_schematics, [:guild_id])
|
||||
create index(:guild_schematics, [:status])
|
||||
create unique_index(:guild_schematics, [:schematic_name, :schematic_version])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
defmodule Guildhall.OpsDb.Repo.Migrations.CreateGuildMemberships do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:guild_memberships) do
|
||||
add :guild_id, references(:guilds, on_delete: :restrict), null: false
|
||||
add :user_did, :string, null: false
|
||||
add :user_email, :string, null: false
|
||||
add :display_name, :string
|
||||
add :keycloak_sub, :string, null: false
|
||||
add :role, :string, null: false, default: "apprentice"
|
||||
add :status, :string, null: false, default: "pending"
|
||||
add :membership_ceremony_id, :string
|
||||
add :approved_by_did, :string
|
||||
add :approved_at, :utc_datetime_usec
|
||||
add :metadata, :map, default: %{}
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
create unique_index(:guild_memberships, [:guild_id, :user_did])
|
||||
create index(:guild_memberships, [:guild_id])
|
||||
create index(:guild_memberships, [:user_did])
|
||||
create index(:guild_memberships, [:status])
|
||||
create index(:guild_memberships, [:keycloak_sub])
|
||||
end
|
||||
end
|
||||
|
|
@ -5,7 +5,8 @@ defmodule Guildhall.Orchestrator.Application do
|
|||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
Guildhall.Orchestrator.CeremonyOrchestrator
|
||||
Guildhall.Orchestrator.CeremonyOrchestrator,
|
||||
Guildhall.Orchestrator.RealizationPoller
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: Guildhall.Orchestrator.Supervisor]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
defmodule Guildhall.Orchestrator.CeremonyClient do
|
||||
@moduledoc false
|
||||
|
||||
alias Ceremony.V1.{
|
||||
CeremonyService.Stub,
|
||||
CreateCeremonyRequest,
|
||||
ApproveCeremonyRequest,
|
||||
DenyCeremonyRequest,
|
||||
GetCeremonyRequest,
|
||||
ListPendingCeremoniesRequest,
|
||||
CeremonySubjectMsg
|
||||
}
|
||||
|
||||
def create_guild_registration_ceremony(guild_name, registrant_did, approver_did) do
|
||||
request = %CreateCeremonyRequest{
|
||||
ceremony_type: "single_approval",
|
||||
subject: %CeremonySubjectMsg{
|
||||
subject_type: "custom",
|
||||
reference_id: guild_name,
|
||||
description: "Guild registration: #{guild_name}",
|
||||
metadata: %{"registrant_did" => registrant_did, "approver_did" => approver_did}
|
||||
},
|
||||
required_approvals: 1,
|
||||
approver_roles: ["hub_operator"],
|
||||
ttl_hours: 168
|
||||
}
|
||||
|
||||
with {:ok, channel} <- connect(),
|
||||
{:ok, response} <- Stub.create_ceremony(channel, request) do
|
||||
GRPC.Stub.disconnect(channel)
|
||||
|
||||
if response.error != "" do
|
||||
{:error, response.error}
|
||||
else
|
||||
{:ok, response}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def approve_ceremony(ceremony_id, approver_did, role \\ "hub_operator") do
|
||||
request = %ApproveCeremonyRequest{
|
||||
ceremony_id: ceremony_id,
|
||||
approver_identity: approver_did,
|
||||
approver_role: role,
|
||||
comment: ""
|
||||
}
|
||||
|
||||
with {:ok, channel} <- connect(),
|
||||
{:ok, response} <- Stub.approve_ceremony(channel, request) do
|
||||
GRPC.Stub.disconnect(channel)
|
||||
|
||||
if response.error != "" do
|
||||
{:error, response.error}
|
||||
else
|
||||
{:ok, response}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def deny_ceremony(ceremony_id, approver_did, role, comment) do
|
||||
request = %DenyCeremonyRequest{
|
||||
ceremony_id: ceremony_id,
|
||||
approver_identity: approver_did,
|
||||
approver_role: role,
|
||||
comment: comment
|
||||
}
|
||||
|
||||
with {:ok, channel} <- connect(),
|
||||
{:ok, response} <- Stub.deny_ceremony(channel, request) do
|
||||
GRPC.Stub.disconnect(channel)
|
||||
|
||||
if response.error != "" do
|
||||
{:error, response.error}
|
||||
else
|
||||
{:ok, response}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_ceremony(ceremony_id) do
|
||||
request = %GetCeremonyRequest{ceremony_id: ceremony_id}
|
||||
|
||||
with {:ok, channel} <- connect(),
|
||||
{:ok, response} <- Stub.get_ceremony(channel, request) do
|
||||
GRPC.Stub.disconnect(channel)
|
||||
|
||||
if response.error != "" do
|
||||
{:error, response.error}
|
||||
else
|
||||
{:ok, response}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def list_pending_ceremonies(intent_id \\ "") do
|
||||
request = %ListPendingCeremoniesRequest{intent_id: intent_id}
|
||||
|
||||
with {:ok, channel} <- connect(),
|
||||
{:ok, response} <- Stub.list_pending_ceremonies(channel, request) do
|
||||
GRPC.Stub.disconnect(channel)
|
||||
{:ok, response.ceremonies}
|
||||
end
|
||||
end
|
||||
|
||||
defp connect do
|
||||
url = Application.get_env(:guildhall_orchestrator, :ceremony_service_url, "localhost:50053")
|
||||
GRPC.Stub.connect(url)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
defmodule Ceremony.V1.CreateCeremonyRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.CreateCeremonyRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :ceremony_type, 1, type: :string, json_name: "ceremonyType"
|
||||
field :subject, 2, type: Ceremony.V1.CeremonySubjectMsg
|
||||
field :required_approvals, 3, type: :uint32, json_name: "requiredApprovals"
|
||||
field :approver_roles, 4, repeated: true, type: :string, json_name: "approverRoles"
|
||||
field :ttl_hours, 5, type: :uint32, json_name: "ttlHours"
|
||||
field :intent_id, 6, type: :string, json_name: "intentId"
|
||||
field :run_id, 7, type: :string, json_name: "runId"
|
||||
field :pr_number, 8, type: :uint64, json_name: "prNumber"
|
||||
field :remote_name, 9, type: :string, json_name: "remoteName"
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.CreateCeremonyResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.CreateCeremonyResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :ceremony_id, 1, type: :string, json_name: "ceremonyId"
|
||||
field :status, 2, type: :string
|
||||
field :expires_at, 3, type: Google.Protobuf.Timestamp, json_name: "expiresAt"
|
||||
field :error, 4, type: :string
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.ApproveCeremonyRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.ApproveCeremonyRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :ceremony_id, 1, type: :string, json_name: "ceremonyId"
|
||||
field :approver_identity, 2, type: :string, json_name: "approverIdentity"
|
||||
field :approver_role, 3, type: :string, json_name: "approverRole"
|
||||
field :comment, 4, type: :string
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.ApproveCeremonyResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.ApproveCeremonyResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :success, 1, type: :bool
|
||||
field :status, 2, type: :string
|
||||
field :error, 3, type: :string
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.DenyCeremonyRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.DenyCeremonyRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :ceremony_id, 1, type: :string, json_name: "ceremonyId"
|
||||
field :approver_identity, 2, type: :string, json_name: "approverIdentity"
|
||||
field :approver_role, 3, type: :string, json_name: "approverRole"
|
||||
field :comment, 4, type: :string
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.DenyCeremonyResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.DenyCeremonyResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :success, 1, type: :bool
|
||||
field :status, 2, type: :string
|
||||
field :error, 3, type: :string
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.CancelCeremonyRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.CancelCeremonyRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :ceremony_id, 1, type: :string, json_name: "ceremonyId"
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.CancelCeremonyResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.CancelCeremonyResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :success, 1, type: :bool
|
||||
field :error, 2, type: :string
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.GetCeremonyRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.GetCeremonyRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :ceremony_id, 1, type: :string, json_name: "ceremonyId"
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.GetCeremonyResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.GetCeremonyResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :ceremony_id, 1, type: :string, json_name: "ceremonyId"
|
||||
field :ceremony_type, 2, type: :string, json_name: "ceremonyType"
|
||||
field :subject, 3, type: Ceremony.V1.CeremonySubjectMsg
|
||||
field :status, 4, type: :string
|
||||
field :required_approvals, 5, type: :uint32, json_name: "requiredApprovals"
|
||||
field :current_approvals, 6, type: :uint32, json_name: "currentApprovals"
|
||||
field :approvals, 7, repeated: true, type: Ceremony.V1.CeremonyApprovalMsg
|
||||
field :created_at, 8, type: Google.Protobuf.Timestamp, json_name: "createdAt"
|
||||
field :expires_at, 9, type: Google.Protobuf.Timestamp, json_name: "expiresAt"
|
||||
field :intent_id, 10, type: :string, json_name: "intentId"
|
||||
field :run_id, 11, type: :string, json_name: "runId"
|
||||
field :pr_number, 12, type: :uint64, json_name: "prNumber"
|
||||
field :remote_name, 13, type: :string, json_name: "remoteName"
|
||||
field :error, 14, type: :string
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.ListPendingCeremoniesRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.ListPendingCeremoniesRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :intent_id, 1, type: :string, json_name: "intentId"
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.ListPendingCeremoniesResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.ListPendingCeremoniesResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :ceremonies, 1, repeated: true, type: Ceremony.V1.GetCeremonyResponse
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.GetCeremonyProofRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.GetCeremonyProofRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :ceremony_id, 1, type: :string, json_name: "ceremonyId"
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.GetCeremonyProofResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.GetCeremonyProofResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :ceremony_id, 1, type: :string, json_name: "ceremonyId"
|
||||
field :status, 2, type: :string
|
||||
field :proof_hash, 3, type: :string, json_name: "proofHash"
|
||||
field :approvals, 4, repeated: true, type: Ceremony.V1.CeremonyApprovalMsg
|
||||
field :resolved_at, 5, type: Google.Protobuf.Timestamp, json_name: "resolvedAt"
|
||||
field :error, 6, type: :string
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.CheckCeremonyRequirementRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.CheckCeremonyRequirementRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :classification, 1, type: :string
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.CheckCeremonyRequirementResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.CheckCeremonyRequirementResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :outcome, 1, type: :string
|
||||
field :ceremony_type, 2, type: :string, json_name: "ceremonyType"
|
||||
field :approver_roles, 3, repeated: true, type: :string, json_name: "approverRoles"
|
||||
field :required_approvals, 4, type: :uint32, json_name: "requiredApprovals"
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.CeremonySubjectMsg.MetadataEntry do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.CeremonySubjectMsg.MetadataEntry",
|
||||
map: true,
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :key, 1, type: :string
|
||||
field :value, 2, type: :string
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.CeremonySubjectMsg do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.CeremonySubjectMsg",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :subject_type, 1, type: :string, json_name: "subjectType"
|
||||
field :reference_id, 2, type: :string, json_name: "referenceId"
|
||||
field :description, 3, type: :string
|
||||
|
||||
field :metadata, 4,
|
||||
repeated: true,
|
||||
type: Ceremony.V1.CeremonySubjectMsg.MetadataEntry,
|
||||
map: true
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.CeremonyApprovalMsg do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "ceremony.v1.CeremonyApprovalMsg",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :approver_identity, 1, type: :string, json_name: "approverIdentity"
|
||||
field :approver_role, 2, type: :string, json_name: "approverRole"
|
||||
field :decision, 3, type: :string
|
||||
field :comment, 4, type: :string
|
||||
field :decided_at, 5, type: Google.Protobuf.Timestamp, json_name: "decidedAt"
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.CeremonyService.Service do
|
||||
@moduledoc false
|
||||
|
||||
use GRPC.Service, name: "ceremony.v1.CeremonyService", protoc_gen_elixir_version: "0.16.0"
|
||||
|
||||
rpc :CreateCeremony, Ceremony.V1.CreateCeremonyRequest, Ceremony.V1.CreateCeremonyResponse
|
||||
|
||||
rpc :ApproveCeremony, Ceremony.V1.ApproveCeremonyRequest, Ceremony.V1.ApproveCeremonyResponse
|
||||
|
||||
rpc :DenyCeremony, Ceremony.V1.DenyCeremonyRequest, Ceremony.V1.DenyCeremonyResponse
|
||||
|
||||
rpc :CancelCeremony, Ceremony.V1.CancelCeremonyRequest, Ceremony.V1.CancelCeremonyResponse
|
||||
|
||||
rpc :GetCeremony, Ceremony.V1.GetCeremonyRequest, Ceremony.V1.GetCeremonyResponse
|
||||
|
||||
rpc :ListPendingCeremonies,
|
||||
Ceremony.V1.ListPendingCeremoniesRequest,
|
||||
Ceremony.V1.ListPendingCeremoniesResponse
|
||||
|
||||
rpc :GetCeremonyProof, Ceremony.V1.GetCeremonyProofRequest, Ceremony.V1.GetCeremonyProofResponse
|
||||
|
||||
rpc :CheckCeremonyRequirement,
|
||||
Ceremony.V1.CheckCeremonyRequirementRequest,
|
||||
Ceremony.V1.CheckCeremonyRequirementResponse
|
||||
end
|
||||
|
||||
defmodule Ceremony.V1.CeremonyService.Stub do
|
||||
@moduledoc false
|
||||
|
||||
use GRPC.Stub, service: Ceremony.V1.CeremonyService.Service
|
||||
end
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,452 @@
|
|||
defmodule Schematic.V1.CreateSchematicRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.CreateSchematicRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :name, 1, type: :string
|
||||
field :version, 2, type: :string
|
||||
field :manifest_yaml, 3, type: :bytes, json_name: "manifestYaml"
|
||||
field :files, 4, repeated: true, type: Schematic.V1.SchematicFile
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.SchematicFile do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.SchematicFile",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :path, 1, type: :string
|
||||
field :content, 2, type: :bytes
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.CreateSchematicResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.CreateSchematicResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :name, 1, type: :string
|
||||
field :version, 2, type: :string
|
||||
field :tree_hash, 3, type: :string, json_name: "treeHash"
|
||||
field :status, 4, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.GetSchematicRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.GetSchematicRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :name, 1, type: :string
|
||||
field :version, 2, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.GetSchematicResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.GetSchematicResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :name, 1, type: :string
|
||||
field :version, 2, type: :string
|
||||
field :tree_hash, 3, type: :string, json_name: "treeHash"
|
||||
field :status, 4, type: :string
|
||||
field :parent_hash, 5, type: :string, json_name: "parentHash"
|
||||
field :stakeholders, 6, repeated: true, type: Schematic.V1.StakeholderInfo
|
||||
field :created_at, 7, type: Google.Protobuf.Timestamp, json_name: "createdAt"
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.StakeholderInfo do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.StakeholderInfo",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :role, 1, type: :string
|
||||
field :identity, 2, type: :string
|
||||
field :required, 3, type: :bool
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.ListVersionsRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.ListVersionsRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :name, 1, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.ListVersionsResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.ListVersionsResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :versions, 1, repeated: true, type: Schematic.V1.VersionInfo
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.VersionInfo do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.VersionInfo",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :version, 1, type: :string
|
||||
field :tree_hash, 2, type: :string, json_name: "treeHash"
|
||||
field :status, 3, type: :string
|
||||
field :parent_hash, 4, type: :string, json_name: "parentHash"
|
||||
field :created_at, 5, type: Google.Protobuf.Timestamp, json_name: "createdAt"
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.UpdateSchematicRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.UpdateSchematicRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :name, 1, type: :string
|
||||
field :version, 2, type: :string
|
||||
field :files, 3, repeated: true, type: Schematic.V1.SchematicFile
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.UpdateSchematicResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.UpdateSchematicResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :tree_hash, 1, type: :string, json_name: "treeHash"
|
||||
field :status, 2, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.ValidateSchematicRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.ValidateSchematicRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :name, 1, type: :string
|
||||
field :version, 2, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.ValidateSchematicResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.ValidateSchematicResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :valid, 1, type: :bool
|
||||
field :error_count, 2, type: :int32, json_name: "errorCount"
|
||||
field :warning_count, 3, type: :int32, json_name: "warningCount"
|
||||
field :results, 4, repeated: true, type: Schematic.V1.ValidationResultProto
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.ValidationResultProto do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.ValidationResultProto",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :validator, 1, type: :string
|
||||
field :passed, 2, type: :bool
|
||||
field :message, 3, type: :string
|
||||
field :severity, 4, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.ApproveSchematicRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.ApproveSchematicRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :name, 1, type: :string
|
||||
field :version, 2, type: :string
|
||||
field :role, 3, type: :string
|
||||
field :identity, 4, type: :string
|
||||
field :tree_hash, 5, type: :string, json_name: "treeHash"
|
||||
field :comment, 6, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.ApproveSchematicResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.ApproveSchematicResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :accepted, 1, type: :bool
|
||||
field :approval_status, 2, type: :string, json_name: "approvalStatus"
|
||||
field :message, 3, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.GetApprovalStatusRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.GetApprovalStatusRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :name, 1, type: :string
|
||||
field :version, 2, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.GetApprovalStatusResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.GetApprovalStatusResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :status, 1, type: :string
|
||||
field :approved_roles, 2, repeated: true, type: :string, json_name: "approvedRoles"
|
||||
field :remaining_roles, 3, repeated: true, type: :string, json_name: "remainingRoles"
|
||||
field :approvals, 4, repeated: true, type: Schematic.V1.ApprovalInfo
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.ApprovalInfo do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.ApprovalInfo",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :role, 1, type: :string
|
||||
field :identity, 2, type: :string
|
||||
field :approved_at, 3, type: Google.Protobuf.Timestamp, json_name: "approvedAt"
|
||||
field :comment, 4, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.PublishSchematicRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.PublishSchematicRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :name, 1, type: :string
|
||||
field :version, 2, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.PublishSchematicResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.PublishSchematicResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :tree_hash, 1, type: :string, json_name: "treeHash"
|
||||
field :status, 2, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.CreateNextVersionRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.CreateNextVersionRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :name, 1, type: :string
|
||||
field :from_version, 2, type: :string, json_name: "fromVersion"
|
||||
field :bump, 3, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.CreateNextVersionResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.CreateNextVersionResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :version, 1, type: :string
|
||||
field :tree_hash, 2, type: :string, json_name: "treeHash"
|
||||
field :parent_hash, 3, type: :string, json_name: "parentHash"
|
||||
field :status, 4, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.ForkSchematicRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.ForkSchematicRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :source_name, 1, type: :string, json_name: "sourceName"
|
||||
field :source_version, 2, type: :string, json_name: "sourceVersion"
|
||||
field :new_name, 3, type: :string, json_name: "newName"
|
||||
field :new_version, 4, type: :string, json_name: "newVersion"
|
||||
field :operations, 5, repeated: true, type: Schematic.V1.TemplateOperation
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.TemplateOperation do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.TemplateOperation",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :op_type, 1, type: :string, json_name: "opType"
|
||||
field :path, 2, type: :string
|
||||
field :content, 3, type: :bytes
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.ForkSchematicResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.ForkSchematicResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :name, 1, type: :string
|
||||
field :version, 2, type: :string
|
||||
field :tree_hash, 3, type: :string, json_name: "treeHash"
|
||||
field :status, 4, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.CreateDeploymentBindingRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.CreateDeploymentBindingRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :schematic_name, 1, type: :string, json_name: "schematicName"
|
||||
field :schematic_version, 2, type: :string, json_name: "schematicVersion"
|
||||
field :pipeline_name, 3, type: :string, json_name: "pipelineName"
|
||||
field :target_env, 4, type: :string, json_name: "targetEnv"
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.CreateDeploymentBindingResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.CreateDeploymentBindingResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :binding_id, 1, type: :string, json_name: "bindingId"
|
||||
field :status, 2, type: :string
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.GetDeploymentBindingRequest do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.GetDeploymentBindingRequest",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :binding_id, 1, type: :string, json_name: "bindingId"
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.GetDeploymentBindingResponse do
|
||||
@moduledoc false
|
||||
|
||||
use Protobuf,
|
||||
full_name: "schematic.v1.GetDeploymentBindingResponse",
|
||||
protoc_gen_elixir_version: "0.16.0",
|
||||
syntax: :proto3
|
||||
|
||||
field :binding_id, 1, type: :string, json_name: "bindingId"
|
||||
field :schematic_name, 2, type: :string, json_name: "schematicName"
|
||||
field :schematic_version, 3, type: :string, json_name: "schematicVersion"
|
||||
field :tree_hash, 4, type: :string, json_name: "treeHash"
|
||||
field :pipeline_name, 5, type: :string, json_name: "pipelineName"
|
||||
field :target_env, 6, type: :string, json_name: "targetEnv"
|
||||
field :status, 7, type: :string
|
||||
field :run_id, 8, type: :string, json_name: "runId"
|
||||
field :created_at, 9, type: Google.Protobuf.Timestamp, json_name: "createdAt"
|
||||
field :deployed_at, 10, type: Google.Protobuf.Timestamp, json_name: "deployedAt"
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.SchematicsService.Service do
|
||||
@moduledoc false
|
||||
|
||||
use GRPC.Service, name: "schematic.v1.SchematicsService", protoc_gen_elixir_version: "0.16.0"
|
||||
|
||||
rpc :CreateSchematic, Schematic.V1.CreateSchematicRequest, Schematic.V1.CreateSchematicResponse
|
||||
|
||||
rpc :GetSchematic, Schematic.V1.GetSchematicRequest, Schematic.V1.GetSchematicResponse
|
||||
|
||||
rpc :ListVersions, Schematic.V1.ListVersionsRequest, Schematic.V1.ListVersionsResponse
|
||||
|
||||
rpc :UpdateSchematic, Schematic.V1.UpdateSchematicRequest, Schematic.V1.UpdateSchematicResponse
|
||||
|
||||
rpc :ValidateSchematic,
|
||||
Schematic.V1.ValidateSchematicRequest,
|
||||
Schematic.V1.ValidateSchematicResponse
|
||||
|
||||
rpc :ApproveSchematic,
|
||||
Schematic.V1.ApproveSchematicRequest,
|
||||
Schematic.V1.ApproveSchematicResponse
|
||||
|
||||
rpc :GetApprovalStatus,
|
||||
Schematic.V1.GetApprovalStatusRequest,
|
||||
Schematic.V1.GetApprovalStatusResponse
|
||||
|
||||
rpc :PublishSchematic,
|
||||
Schematic.V1.PublishSchematicRequest,
|
||||
Schematic.V1.PublishSchematicResponse
|
||||
|
||||
rpc :CreateNextVersion,
|
||||
Schematic.V1.CreateNextVersionRequest,
|
||||
Schematic.V1.CreateNextVersionResponse
|
||||
|
||||
rpc :ForkSchematic, Schematic.V1.ForkSchematicRequest, Schematic.V1.ForkSchematicResponse
|
||||
|
||||
rpc :CreateDeploymentBinding,
|
||||
Schematic.V1.CreateDeploymentBindingRequest,
|
||||
Schematic.V1.CreateDeploymentBindingResponse
|
||||
|
||||
rpc :GetDeploymentBinding,
|
||||
Schematic.V1.GetDeploymentBindingRequest,
|
||||
Schematic.V1.GetDeploymentBindingResponse
|
||||
end
|
||||
|
||||
defmodule Schematic.V1.SchematicsService.Stub do
|
||||
@moduledoc false
|
||||
|
||||
use GRPC.Stub, service: Schematic.V1.SchematicsService.Service
|
||||
end
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
defmodule Guildhall.Orchestrator.RealizationPoller do
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
alias Guildhall.Orchestrator.SchematicClient
|
||||
|
||||
@poll_interval_ms 5_000
|
||||
|
||||
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
|
||||
def watch(realization_id, guild_slug) do
|
||||
GenServer.cast(__MODULE__, {:watch, realization_id, guild_slug})
|
||||
end
|
||||
|
||||
def unwatch(realization_id) do
|
||||
GenServer.cast(__MODULE__, {:unwatch, realization_id})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
Logger.info("RealizationPoller started")
|
||||
schedule_poll()
|
||||
{:ok, %{watches: %{}}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:watch, realization_id, guild_slug}, state) do
|
||||
{:noreply, put_in(state, [:watches, realization_id], guild_slug)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:unwatch, realization_id}, state) do
|
||||
{:noreply, %{state | watches: Map.delete(state.watches, realization_id)}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:poll, %{watches: watches} = state) when map_size(watches) == 0 do
|
||||
schedule_poll()
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:poll, state) do
|
||||
new_watches =
|
||||
Enum.reduce(state.watches, state.watches, fn {realization_id, guild_slug}, acc ->
|
||||
case SchematicClient.get_realization_status(realization_id) do
|
||||
{:ok, response} ->
|
||||
snapshot = build_snapshot(response)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
Guildhall.PubSub,
|
||||
"realization:#{guild_slug}",
|
||||
{:realization_update, snapshot}
|
||||
)
|
||||
|
||||
if terminal_status?(response.overall_status) do
|
||||
Map.delete(acc, realization_id)
|
||||
else
|
||||
acc
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Realization poll failed for #{realization_id}: #{inspect(reason)}")
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
schedule_poll()
|
||||
{:noreply, %{state | watches: new_watches}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(_msg, state), do: {:noreply, state}
|
||||
|
||||
defp schedule_poll, do: Process.send_after(self(), :poll, @poll_interval_ms)
|
||||
|
||||
defp build_snapshot(response) do
|
||||
%{
|
||||
overall_status: to_string(response.overall_status),
|
||||
sections:
|
||||
Enum.map(response.per_section, fn s ->
|
||||
%{name: s.section_name, status: s.status, message: s.message}
|
||||
end)
|
||||
}
|
||||
end
|
||||
|
||||
defp terminal_status?(:FFC_SCHEMATIC_STATUS_REALIZED), do: true
|
||||
defp terminal_status?(:FFC_SCHEMATIC_STATUS_ARCHIVED), do: true
|
||||
defp terminal_status?(:FFC_SCHEMATIC_STATUS_WITHDRAWN), do: true
|
||||
defp terminal_status?(_), do: false
|
||||
end
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
defmodule Guildhall.Orchestrator.SchematicClient do
|
||||
@moduledoc false
|
||||
|
||||
alias Schematic.V1.{
|
||||
SchematicsService.Stub,
|
||||
ForkSchematicRequest,
|
||||
TemplateOperation,
|
||||
CreateDeploymentBindingRequest,
|
||||
GetDeploymentBindingRequest
|
||||
}
|
||||
|
||||
alias Schematic.V1.RealizeFfcSchematicRequest
|
||||
alias Schematic.V1.GetRealizationStatusRequest
|
||||
|
||||
def fork_schematic(source_name, source_version, new_name, new_version, operations) do
|
||||
ops =
|
||||
Enum.map(operations, fn op ->
|
||||
%TemplateOperation{
|
||||
op_type: op[:op_type] || "replace",
|
||||
path: op[:path],
|
||||
content: op[:content] || ""
|
||||
}
|
||||
end)
|
||||
|
||||
request = %ForkSchematicRequest{
|
||||
source_name: source_name,
|
||||
source_version: source_version,
|
||||
new_name: new_name,
|
||||
new_version: new_version,
|
||||
operations: ops
|
||||
}
|
||||
|
||||
with {:ok, channel} <- connect_schematic(),
|
||||
{:ok, response} <- Stub.fork_schematic(channel, request) do
|
||||
GRPC.Stub.disconnect(channel)
|
||||
{:ok, response}
|
||||
end
|
||||
end
|
||||
|
||||
def create_deployment_binding(schematic_name, version, pipeline_name, target_env) do
|
||||
request = %CreateDeploymentBindingRequest{
|
||||
schematic_name: schematic_name,
|
||||
schematic_version: version,
|
||||
pipeline_name: pipeline_name,
|
||||
target_env: target_env
|
||||
}
|
||||
|
||||
with {:ok, channel} <- connect_schematic(),
|
||||
{:ok, response} <- Stub.create_deployment_binding(channel, request) do
|
||||
GRPC.Stub.disconnect(channel)
|
||||
{:ok, response}
|
||||
end
|
||||
end
|
||||
|
||||
def get_deployment_binding(binding_id) do
|
||||
request = %GetDeploymentBindingRequest{binding_id: binding_id}
|
||||
|
||||
with {:ok, channel} <- connect_schematic(),
|
||||
{:ok, response} <- Stub.get_deployment_binding(channel, request) do
|
||||
GRPC.Stub.disconnect(channel)
|
||||
{:ok, response}
|
||||
end
|
||||
end
|
||||
|
||||
def realize_ffc_schematic(name, version) do
|
||||
request = %RealizeFfcSchematicRequest{name: name, version: version}
|
||||
|
||||
with {:ok, channel} <- connect_ffc(),
|
||||
{:ok, response} <- Schematic.V1.FfcSchematicService.Stub.realize_ffc_schematic(channel, request) do
|
||||
GRPC.Stub.disconnect(channel)
|
||||
{:ok, response}
|
||||
end
|
||||
end
|
||||
|
||||
def get_realization_status(realization_id) do
|
||||
request = %GetRealizationStatusRequest{realization_id: realization_id}
|
||||
|
||||
with {:ok, channel} <- connect_ffc(),
|
||||
{:ok, response} <- Schematic.V1.FfcSchematicService.Stub.get_realization_status(channel, request) do
|
||||
GRPC.Stub.disconnect(channel)
|
||||
{:ok, response}
|
||||
end
|
||||
end
|
||||
|
||||
defp connect_schematic do
|
||||
url =
|
||||
Application.get_env(:guildhall_orchestrator, :schematic_service_url, "localhost:9091")
|
||||
|
||||
GRPC.Stub.connect(url)
|
||||
end
|
||||
|
||||
defp connect_ffc do
|
||||
url =
|
||||
Application.get_env(:guildhall_orchestrator, :ffc_schematic_service_url, "localhost:9091")
|
||||
|
||||
GRPC.Stub.connect(url)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
defmodule Guildhall.Orchestrator.SchematicTemplate do
|
||||
@moduledoc false
|
||||
|
||||
@type_to_template %{
|
||||
"msp" => "msp-founding.toml",
|
||||
"isv" => "isv-founding.toml",
|
||||
"nsp" => "nsp-founding.toml"
|
||||
}
|
||||
|
||||
def load_template(guild_type) do
|
||||
filename = Map.fetch!(@type_to_template, guild_type)
|
||||
path = Path.join(template_dir(), filename)
|
||||
|
||||
case Toml.decode_file(path) do
|
||||
{:ok, data} -> {:ok, data}
|
||||
{:error, reason} -> {:error, {:template_load_failed, reason}}
|
||||
end
|
||||
end
|
||||
|
||||
def render_template(template, params) when is_map(template) and is_map(params) do
|
||||
deep_substitute(template, params)
|
||||
end
|
||||
|
||||
def to_fork_operations(rendered_template) do
|
||||
rendered_template
|
||||
|> Map.drop(["meta"])
|
||||
|> Enum.flat_map(fn {section, content} ->
|
||||
yaml = to_yaml_string(content)
|
||||
|
||||
[
|
||||
%{
|
||||
op_type: "replace",
|
||||
path: "#{section}.yaml",
|
||||
content: yaml
|
||||
}
|
||||
]
|
||||
end)
|
||||
end
|
||||
|
||||
def source_schematic(template) do
|
||||
meta = Map.get(template, "meta", %{})
|
||||
{Map.get(meta, "source_schematic", ""), Map.get(meta, "source_version", "1.0.0")}
|
||||
end
|
||||
|
||||
defp deep_substitute(value, params) when is_binary(value) do
|
||||
Enum.reduce(params, value, fn {key, val}, acc ->
|
||||
String.replace(acc, "{{#{key}}}", to_string(val))
|
||||
end)
|
||||
end
|
||||
|
||||
defp deep_substitute(value, params) when is_map(value) do
|
||||
Map.new(value, fn {k, v} -> {k, deep_substitute(v, params)} end)
|
||||
end
|
||||
|
||||
defp deep_substitute(value, params) when is_list(value) do
|
||||
Enum.map(value, &deep_substitute(&1, params))
|
||||
end
|
||||
|
||||
defp deep_substitute(value, _params), do: value
|
||||
|
||||
defp to_yaml_string(map) when is_map(map) do
|
||||
Jason.encode!(map)
|
||||
end
|
||||
|
||||
defp to_yaml_string(other), do: to_string(other)
|
||||
|
||||
defp template_dir do
|
||||
Application.app_dir(:guildhall_orchestrator, "priv/schematic_templates")
|
||||
end
|
||||
end
|
||||
|
|
@ -28,7 +28,11 @@ defmodule Guildhall.Orchestrator.MixProject do
|
|||
[
|
||||
{:guildhall_ops_db, in_umbrella: true},
|
||||
{:phoenix_pubsub, "~> 2.1"},
|
||||
{:jason, "~> 1.4"}
|
||||
{:jason, "~> 1.4"},
|
||||
{:grpc, "~> 0.9"},
|
||||
{:protobuf, "~> 0.13"},
|
||||
{:google_protos, "~> 0.4"},
|
||||
{:toml, "~> 0.7"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
[meta]
|
||||
template_name = "isv-founding"
|
||||
description = "Independent Software Vendor founding schematic"
|
||||
source_schematic = "guildhouse-isv-base"
|
||||
source_version = "1.0.0"
|
||||
|
||||
[consortium]
|
||||
realm = "{{guild_slug}}"
|
||||
description = "{{guild_name}} ISV Consortium"
|
||||
|
||||
[trust_domain]
|
||||
spiffe_trust_domain = "{{trust_domain}}"
|
||||
attestation_tier = 1
|
||||
|
||||
[identity_authority]
|
||||
provider = "keycloak"
|
||||
url = "https://auth.guildhouse.dev"
|
||||
realm = "{{guild_slug}}"
|
||||
trust_level = "federated"
|
||||
mfa_required = false
|
||||
|
||||
[ceremonies.code_change]
|
||||
type = "single_approval"
|
||||
eligible_roles = ["master", "journeyman"]
|
||||
quorum = 1
|
||||
|
||||
[ceremonies.governance_change]
|
||||
type = "single_approval"
|
||||
eligible_roles = ["master"]
|
||||
quorum = 1
|
||||
|
||||
[members]
|
||||
initial_roles = ["master"]
|
||||
|
||||
[infrastructure]
|
||||
compute_attestation_tier = 1
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
[meta]
|
||||
template_name = "msp-founding"
|
||||
description = "Managed Service Provider founding schematic"
|
||||
source_schematic = "guildhouse-msp-base"
|
||||
source_version = "1.0.0"
|
||||
|
||||
[consortium]
|
||||
realm = "{{guild_slug}}"
|
||||
description = "{{guild_name}} MSP Consortium"
|
||||
|
||||
[trust_domain]
|
||||
spiffe_trust_domain = "{{trust_domain}}"
|
||||
attestation_tier = 2
|
||||
|
||||
[identity_authority]
|
||||
provider = "keycloak"
|
||||
url = "https://auth.guildhouse.dev"
|
||||
realm = "{{guild_slug}}"
|
||||
trust_level = "federated"
|
||||
mfa_required = true
|
||||
|
||||
[ceremonies.code_change]
|
||||
type = "single_approval"
|
||||
eligible_roles = ["master", "journeyman"]
|
||||
quorum = 1
|
||||
|
||||
[ceremonies.governance_change]
|
||||
type = "single_approval"
|
||||
eligible_roles = ["master"]
|
||||
quorum = 1
|
||||
|
||||
[members]
|
||||
initial_roles = ["master"]
|
||||
|
||||
[infrastructure]
|
||||
compute_attestation_tier = 2
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
[meta]
|
||||
template_name = "nsp-founding"
|
||||
description = "Network Service Provider founding schematic"
|
||||
source_schematic = "guildhouse-nsp-base"
|
||||
source_version = "1.0.0"
|
||||
|
||||
[consortium]
|
||||
realm = "{{guild_slug}}"
|
||||
description = "{{guild_name}} NSP Consortium"
|
||||
|
||||
[trust_domain]
|
||||
spiffe_trust_domain = "{{trust_domain}}"
|
||||
attestation_tier = 3
|
||||
|
||||
[identity_authority]
|
||||
provider = "keycloak"
|
||||
url = "https://auth.guildhouse.dev"
|
||||
realm = "{{guild_slug}}"
|
||||
trust_level = "federated"
|
||||
mfa_required = true
|
||||
hardware_credential_required = true
|
||||
|
||||
[ceremonies.code_change]
|
||||
type = "single_approval"
|
||||
eligible_roles = ["master", "journeyman"]
|
||||
quorum = 1
|
||||
|
||||
[ceremonies.governance_change]
|
||||
type = "multi_party"
|
||||
eligible_roles = ["master"]
|
||||
quorum = 2
|
||||
|
||||
[members]
|
||||
initial_roles = ["master"]
|
||||
|
||||
[infrastructure]
|
||||
compute_attestation_tier = 3
|
||||
wireguard_tunnel = true
|
||||
vpp_dataplane = true
|
||||
|
|
@ -7,15 +7,31 @@ defmodule Guildhall.Application do
|
|||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
GuildhallWeb.Telemetry,
|
||||
{DNSCluster, query: Application.get_env(:guildhall_web, :dns_cluster_query) || :ignore},
|
||||
{Phoenix.PubSub, name: Guildhall.PubSub},
|
||||
# Start a worker by calling: Guildhall.Worker.start_link(arg)
|
||||
# {Guildhall.Worker, arg},
|
||||
# Start to serve requests, typically the last entry
|
||||
GuildhallWeb.Endpoint
|
||||
]
|
||||
oidc_children =
|
||||
case Application.get_env(:guildhall_web, :oidc) do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
config ->
|
||||
[
|
||||
{Oidcc.ProviderConfiguration.Worker,
|
||||
%{
|
||||
issuer: config[:issuer],
|
||||
name: GuildhallWeb.OidcProvider
|
||||
}}
|
||||
]
|
||||
end
|
||||
|
||||
children =
|
||||
[
|
||||
GuildhallWeb.Telemetry,
|
||||
{DNSCluster, query: Application.get_env(:guildhall_web, :dns_cluster_query) || :ignore},
|
||||
{Phoenix.PubSub, name: Guildhall.PubSub}
|
||||
] ++
|
||||
oidc_children ++
|
||||
[
|
||||
GuildhallWeb.Endpoint
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
defmodule GuildhallWeb.AuthController do
|
||||
use GuildhallWeb, :controller
|
||||
|
||||
@provider_name GuildhallWeb.OidcProvider
|
||||
|
||||
def login(conn, _params) do
|
||||
oidc_config = Application.fetch_env!(:guildhall_web, :oidc)
|
||||
client_id = oidc_config[:client_id]
|
||||
client_secret = oidc_config[:client_secret]
|
||||
redirect_uri = oidc_config[:redirect_uri]
|
||||
|
||||
nonce = :crypto.strong_rand_bytes(16) |> Base.url_encode64(padding: false)
|
||||
state = :crypto.strong_rand_bytes(16) |> Base.url_encode64(padding: false)
|
||||
|
||||
case Oidcc.create_redirect_url(@provider_name, client_id, client_secret, %{
|
||||
redirect_uri: redirect_uri,
|
||||
scopes: ["openid", "profile", "email"],
|
||||
state: state,
|
||||
nonce: nonce
|
||||
}) do
|
||||
{:ok, url} ->
|
||||
conn
|
||||
|> put_session(:oidc_state, state)
|
||||
|> put_session(:oidc_nonce, nonce)
|
||||
|> redirect(external: url)
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_flash(:error, "Failed to initiate login: #{inspect(reason)}")
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
end
|
||||
|
||||
def callback(conn, %{"code" => code, "state" => state}) do
|
||||
saved_state = get_session(conn, :oidc_state)
|
||||
|
||||
if state != saved_state do
|
||||
conn
|
||||
|> put_flash(:error, "Invalid state parameter.")
|
||||
|> redirect(to: "/")
|
||||
else
|
||||
oidc_config = Application.fetch_env!(:guildhall_web, :oidc)
|
||||
client_id = oidc_config[:client_id]
|
||||
client_secret = oidc_config[:client_secret]
|
||||
redirect_uri = oidc_config[:redirect_uri]
|
||||
nonce = get_session(conn, :oidc_nonce)
|
||||
|
||||
case Oidcc.retrieve_token(code, @provider_name, client_id, client_secret, %{
|
||||
redirect_uri: redirect_uri,
|
||||
nonce: nonce
|
||||
}) do
|
||||
{:ok, token} ->
|
||||
claims = extract_claims(token)
|
||||
|
||||
preferred_username =
|
||||
claims["preferred_username"] || claims["email"] || claims["sub"]
|
||||
|
||||
current_user = %{
|
||||
"sub" => claims["sub"],
|
||||
"email" => claims["email"],
|
||||
"name" => claims["name"] || preferred_username,
|
||||
"preferred_username" => preferred_username,
|
||||
"did" => "did:web:guildhouse.dev:user:#{preferred_username}"
|
||||
}
|
||||
|
||||
conn
|
||||
|> delete_session(:oidc_state)
|
||||
|> delete_session(:oidc_nonce)
|
||||
|> put_session(:current_user, current_user)
|
||||
|> put_flash(:info, "Welcome, #{current_user["name"]}.")
|
||||
|> redirect(to: "/")
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_flash(:error, "Authentication failed: #{inspect(reason)}")
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def callback(conn, _params) do
|
||||
conn
|
||||
|> put_flash(:error, "Missing authorization code.")
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
|
||||
def logout(conn, _params) do
|
||||
conn
|
||||
|> configure_session(drop: true)
|
||||
|> put_flash(:info, "Signed out.")
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
|
||||
defp extract_claims(%Oidcc.Token{id: %Oidcc.Token.Id{claims: claims}}), do: claims
|
||||
defp extract_claims(_), do: %{}
|
||||
end
|
||||
16
apps/guildhall_web/lib/guildhall_web_web/live/auth_hooks.ex
Normal file
16
apps/guildhall_web/lib/guildhall_web_web/live/auth_hooks.ex
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
defmodule GuildhallWeb.AuthHooks do
|
||||
@moduledoc false
|
||||
import Phoenix.LiveView
|
||||
import Phoenix.Component
|
||||
|
||||
def on_mount(:require_auth, _params, session, socket) do
|
||||
case session["current_user"] do
|
||||
nil -> {:halt, redirect(socket, to: "/auth/login")}
|
||||
user -> {:cont, assign(socket, :current_user, user)}
|
||||
end
|
||||
end
|
||||
|
||||
def on_mount(:fetch_user, _params, session, socket) do
|
||||
{:cont, assign(socket, :current_user, session["current_user"])}
|
||||
end
|
||||
end
|
||||
|
|
@ -5,7 +5,7 @@ defmodule GuildhallWeb.DashboardLive do
|
|||
"""
|
||||
use GuildhallWeb, :live_view
|
||||
|
||||
alias Guildhall.OpsDb.{Repo, GovernedArtifact, DeploymentState, VerificationResult}
|
||||
alias Guildhall.OpsDb.{Repo, GovernedArtifact, DeploymentState, VerificationResult, Guilds}
|
||||
import Ecto.Query
|
||||
|
||||
@impl true
|
||||
|
|
@ -35,6 +35,9 @@ defmodule GuildhallWeb.DashboardLive do
|
|||
|> assign(:healthy_count, count_by_drift("match"))
|
||||
|> assign(:drifted_count, count_by_drift("drift"))
|
||||
|> assign(:recent_verifications, recent_verifications(5))
|
||||
|> assign(:guild_total, Guilds.guild_count())
|
||||
|> assign(:guild_pending, Guilds.guild_count("pending_approval"))
|
||||
|> assign(:guild_active, Guilds.guild_count("active"))
|
||||
end
|
||||
|
||||
defp count_by_drift(status) do
|
||||
|
|
@ -77,7 +80,23 @@ defmodule GuildhallWeb.DashboardLive do
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-3 gap-4">
|
||||
<div class="rounded-lg border border-zinc-200 p-4">
|
||||
<div class="text-xs uppercase text-zinc-500">Guilds</div>
|
||||
<div class="text-3xl font-semibold">{@guild_total}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-zinc-200 p-4">
|
||||
<div class="text-xs uppercase text-zinc-500">Pending</div>
|
||||
<div class="text-3xl font-semibold text-amber-600">{@guild_pending}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-zinc-200 p-4">
|
||||
<div class="text-xs uppercase text-zinc-500">Active</div>
|
||||
<div class="text-3xl font-semibold text-emerald-600">{@guild_active}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav class="flex gap-3 text-sm">
|
||||
<.link navigate={~p"/guilds"} class="text-blue-600 underline">Guilds</.link>
|
||||
<.link navigate={~p"/ceremonies"} class="text-blue-600 underline">Ceremonies</.link>
|
||||
<.link navigate={~p"/artifacts"} class="text-blue-600 underline">Artifacts</.link>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
defmodule GuildhallWeb.GuildLive.Index do
|
||||
use GuildhallWeb, :live_view
|
||||
|
||||
alias Guildhall.OpsDb.Guilds
|
||||
alias Guildhall.Orchestrator.CeremonyClient
|
||||
|
||||
@hub_operator_did "did:web:guildhouse.dev:user:tking"
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(Guildhall.PubSub, "guild:*")
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Guilds")
|
||||
|> assign(:guilds, Guilds.list_guilds())
|
||||
|> assign(:is_hub_operator, socket.assigns.current_user["did"] == @hub_operator_did)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("approve_guild", %{"id" => id}, socket) do
|
||||
guild = Guilds.get_guild!(id)
|
||||
|
||||
case CeremonyClient.approve_ceremony(
|
||||
guild.registration_ceremony_id,
|
||||
socket.assigns.current_user["did"],
|
||||
"hub_operator"
|
||||
) do
|
||||
{:ok, %{status: "approved"}} ->
|
||||
{:ok, _} = Guilds.update_guild(guild, %{status: "approved"})
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "#{guild.name} approved.")
|
||||
|> assign(:guilds, Guilds.list_guilds())}
|
||||
|
||||
{:ok, _} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Approval recorded for #{guild.name}.")
|
||||
|> assign(:guilds, Guilds.list_guilds())}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Approval failed: #{inspect(reason)}")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("deny_guild", %{"id" => id}, socket) do
|
||||
guild = Guilds.get_guild!(id)
|
||||
|
||||
case CeremonyClient.deny_ceremony(
|
||||
guild.registration_ceremony_id,
|
||||
socket.assigns.current_user["did"],
|
||||
"hub_operator",
|
||||
"Denied by hub operator"
|
||||
) do
|
||||
{:ok, _} ->
|
||||
{:ok, _} = Guilds.update_guild(guild, %{status: "denied"})
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "#{guild.name} denied.")
|
||||
|> assign(:guilds, Guilds.list_guilds())}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Denial failed: #{inspect(reason)}")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:guild_updated, _}, socket) do
|
||||
{:noreply, assign(socket, :guilds, Guilds.list_guilds())}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(_msg, socket), do: {:noreply, socket}
|
||||
|
||||
defp status_class("pending_approval"), do: "text-amber-600"
|
||||
defp status_class("approved"), do: "text-emerald-600"
|
||||
defp status_class("active"), do: "text-emerald-600"
|
||||
defp status_class("denied"), do: "text-red-600"
|
||||
defp status_class("suspended"), do: "text-zinc-500"
|
||||
defp status_class(_), do: "text-zinc-500"
|
||||
|
||||
defp type_label("msp"), do: "MSP"
|
||||
defp type_label("isv"), do: "ISV"
|
||||
defp type_label("nsp"), do: "NSP"
|
||||
defp type_label(other), do: String.upcase(other)
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="mx-auto max-w-4xl p-6 space-y-4">
|
||||
<header class="flex items-center justify-between">
|
||||
<div>
|
||||
<.link navigate={~p"/"} class="text-sm text-blue-600 underline">← Dashboard</.link>
|
||||
<h1 class="text-2xl font-semibold mt-2">Guilds</h1>
|
||||
<p class="text-sm text-zinc-500">{length(@guilds)} registered.</p>
|
||||
</div>
|
||||
<.link navigate={~p"/guilds/register"} class="rounded bg-blue-600 px-4 py-2 text-sm text-white">
|
||||
Register Guild
|
||||
</.link>
|
||||
</header>
|
||||
|
||||
<table class="w-full text-sm">
|
||||
<thead class="text-left text-zinc-500">
|
||||
<tr>
|
||||
<th class="py-1">Name</th>
|
||||
<th class="py-1">Type</th>
|
||||
<th class="py-1">Guild ID</th>
|
||||
<th class="py-1">Status</th>
|
||||
<th class="py-1">Registrant</th>
|
||||
<th :if={@is_hub_operator} class="py-1">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr :for={g <- @guilds} class="border-t border-zinc-100">
|
||||
<td class="py-2">
|
||||
<.link navigate={~p"/guilds/#{g.slug}"} class="text-blue-600 underline">{g.name}</.link>
|
||||
</td>
|
||||
<td class="py-2">{type_label(g.guild_type)}</td>
|
||||
<td class="py-2 font-mono text-xs">0x{Integer.to_string(g.guild_id, 16) |> String.pad_leading(3, "0")}</td>
|
||||
<td class="py-2"><span class={status_class(g.status)}>{g.status}</span></td>
|
||||
<td class="py-2 text-xs font-mono">{g.registrant_did}</td>
|
||||
<td :if={@is_hub_operator} class="py-2">
|
||||
<div :if={g.status == "pending_approval"} class="flex gap-2">
|
||||
<button phx-click="approve_guild" phx-value-id={g.id} class="rounded bg-emerald-600 px-2 py-1 text-xs text-white">
|
||||
Approve
|
||||
</button>
|
||||
<button phx-click="deny_guild" phx-value-id={g.id} class="rounded bg-red-600 px-2 py-1 text-xs text-white">
|
||||
Deny
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr :if={@guilds == []}>
|
||||
<td colspan="6" class="py-6 text-center text-zinc-400">No guilds registered yet.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
100
apps/guildhall_web/lib/guildhall_web_web/live/guild_live/join.ex
Normal file
100
apps/guildhall_web/lib/guildhall_web_web/live/guild_live/join.ex
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
defmodule GuildhallWeb.GuildLive.Join do
|
||||
use GuildhallWeb, :live_view
|
||||
|
||||
alias Guildhall.OpsDb.{Guilds, GuildMemberships}
|
||||
alias Guildhall.Orchestrator.CeremonyClient
|
||||
|
||||
@impl true
|
||||
def mount(%{"slug" => slug}, _session, socket) do
|
||||
guild = Guilds.get_guild_by_slug(slug) || raise Ecto.NoResultsError, queryable: Guildhall.OpsDb.Guild
|
||||
user = socket.assigns.current_user
|
||||
existing = GuildMemberships.find_membership(guild.id, user["did"])
|
||||
|
||||
if existing do
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:info, "You already have a membership in #{guild.name}.")
|
||||
|> push_navigate(to: ~p"/guilds/#{slug}")}
|
||||
else
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Join #{guild.name}")
|
||||
|> assign(:guild, guild)
|
||||
|> assign(:submitted, false)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("request_membership", _params, socket) do
|
||||
guild = socket.assigns.guild
|
||||
user = socket.assigns.current_user
|
||||
|
||||
case GuildMemberships.request_membership(%{
|
||||
guild_id: guild.id,
|
||||
user_did: user["did"],
|
||||
user_email: user["email"],
|
||||
display_name: user["name"],
|
||||
keycloak_sub: user["sub"],
|
||||
role: "apprentice",
|
||||
status: "pending"
|
||||
}) do
|
||||
{:ok, membership} ->
|
||||
masters = GuildMemberships.masters_for_guild(guild.id)
|
||||
approver_did = if masters != [], do: hd(masters).user_did, else: guild.registrant_did
|
||||
|
||||
case CeremonyClient.create_guild_registration_ceremony(
|
||||
"membership:#{guild.slug}:#{user["preferred_username"]}",
|
||||
user["did"],
|
||||
approver_did
|
||||
) do
|
||||
{:ok, response} ->
|
||||
GuildMemberships.get_membership!(membership.id)
|
||||
|> Guildhall.OpsDb.GuildMembership.changeset(%{
|
||||
membership_ceremony_id: response.ceremony_id
|
||||
})
|
||||
|> Guildhall.OpsDb.Repo.update()
|
||||
|
||||
{:error, _} ->
|
||||
:ok
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:submitted, true)
|
||||
|> put_flash(:info, "Membership request submitted.")}
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to submit membership request.")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="mx-auto max-w-lg p-6 space-y-6">
|
||||
<header>
|
||||
<.link navigate={~p"/guilds/#{@guild.slug}"} class="text-sm text-blue-600 underline">← {@guild.name}</.link>
|
||||
<h1 class="text-2xl font-semibold mt-2">Join {@guild.name}</h1>
|
||||
</header>
|
||||
|
||||
<section class="text-sm space-y-2">
|
||||
<p>{@guild.description || "No description."}</p>
|
||||
<p class="text-zinc-500">Type: {String.upcase(@guild.guild_type)}</p>
|
||||
</section>
|
||||
|
||||
<div :if={!@submitted}>
|
||||
<button
|
||||
phx-click="request_membership"
|
||||
class="rounded bg-blue-600 px-4 py-2 text-sm text-white"
|
||||
>
|
||||
Request Membership
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div :if={@submitted} class="border border-emerald-200 bg-emerald-50 rounded p-4 text-sm text-emerald-700">
|
||||
Your membership request has been submitted. A guild master will review it.
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
defmodule GuildhallWeb.GuildLive.Members do
|
||||
use GuildhallWeb, :live_view
|
||||
|
||||
alias Guildhall.OpsDb.{Guilds, GuildMemberships}
|
||||
alias Guildhall.Orchestrator.CeremonyClient
|
||||
|
||||
@impl true
|
||||
def mount(%{"slug" => slug}, _session, socket) do
|
||||
guild = Guilds.get_guild_by_slug(slug) || raise Ecto.NoResultsError, queryable: Guildhall.OpsDb.Guild
|
||||
user = socket.assigns.current_user
|
||||
user_membership = GuildMemberships.find_membership(guild.id, user["did"])
|
||||
|
||||
is_master = user_membership && user_membership.role == "master" && user_membership.status == "active"
|
||||
|
||||
unless is_master do
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, "Only guild masters can manage members.")
|
||||
|> push_navigate(to: ~p"/guilds/#{slug}")}
|
||||
else
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "#{guild.name} — Members")
|
||||
|> assign(:guild, guild)
|
||||
|> assign(:memberships, GuildMemberships.list_memberships(guild.id))}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("approve_member", %{"id" => id}, socket) do
|
||||
membership = GuildMemberships.get_membership!(id)
|
||||
|
||||
if membership.membership_ceremony_id do
|
||||
CeremonyClient.approve_ceremony(
|
||||
membership.membership_ceremony_id,
|
||||
socket.assigns.current_user["did"],
|
||||
"guild_master"
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, _} = GuildMemberships.approve_membership(membership, socket.assigns.current_user["did"])
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Member approved.")
|
||||
|> assign(:memberships, GuildMemberships.list_memberships(socket.assigns.guild.id))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("deny_member", %{"id" => id}, socket) do
|
||||
membership = GuildMemberships.get_membership!(id)
|
||||
|
||||
if membership.membership_ceremony_id do
|
||||
CeremonyClient.deny_ceremony(
|
||||
membership.membership_ceremony_id,
|
||||
socket.assigns.current_user["did"],
|
||||
"guild_master",
|
||||
"Denied by guild master"
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, _} = GuildMemberships.deny_membership(membership, socket.assigns.current_user["did"])
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Member denied.")
|
||||
|> assign(:memberships, GuildMemberships.list_memberships(socket.assigns.guild.id))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("update_role", %{"id" => id, "role" => role}, socket) do
|
||||
membership = GuildMemberships.get_membership!(id)
|
||||
{:ok, _} = GuildMemberships.update_role(membership, role)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Role updated.")
|
||||
|> assign(:memberships, GuildMemberships.list_memberships(socket.assigns.guild.id))}
|
||||
end
|
||||
|
||||
defp status_class("pending"), do: "text-amber-600"
|
||||
defp status_class("active"), do: "text-emerald-600"
|
||||
defp status_class("denied"), do: "text-red-600"
|
||||
defp status_class("suspended"), do: "text-zinc-500"
|
||||
defp status_class(_), do: "text-zinc-500"
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="mx-auto max-w-4xl p-6 space-y-4">
|
||||
<header>
|
||||
<.link navigate={~p"/guilds/#{@guild.slug}"} class="text-sm text-blue-600 underline">← {@guild.name}</.link>
|
||||
<h1 class="text-2xl font-semibold mt-2">Members</h1>
|
||||
<p class="text-sm text-zinc-500">{length(@memberships)} member(s).</p>
|
||||
</header>
|
||||
|
||||
<table class="w-full text-sm">
|
||||
<thead class="text-left text-zinc-500">
|
||||
<tr>
|
||||
<th class="py-1">Name</th>
|
||||
<th class="py-1">Email</th>
|
||||
<th class="py-1">Role</th>
|
||||
<th class="py-1">Status</th>
|
||||
<th class="py-1">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr :for={m <- @memberships} class="border-t border-zinc-100">
|
||||
<td class="py-2">{m.display_name || "—"}</td>
|
||||
<td class="py-2 text-xs">{m.user_email}</td>
|
||||
<td class="py-2">{m.role}</td>
|
||||
<td class="py-2"><span class={status_class(m.status)}>{m.status}</span></td>
|
||||
<td class="py-2">
|
||||
<div :if={m.status == "pending"} class="flex gap-2">
|
||||
<button phx-click="approve_member" phx-value-id={m.id} class="rounded bg-emerald-600 px-2 py-1 text-xs text-white">
|
||||
Approve
|
||||
</button>
|
||||
<button phx-click="deny_member" phx-value-id={m.id} class="rounded bg-red-600 px-2 py-1 text-xs text-white">
|
||||
Deny
|
||||
</button>
|
||||
</div>
|
||||
<div :if={m.status == "active"}>
|
||||
<form phx-change="update_role" phx-value-id={m.id}>
|
||||
<input type="hidden" name="_member_id" value={m.id} />
|
||||
<select name="role" class="text-xs border border-zinc-300 rounded px-1 py-0.5">
|
||||
<option value="apprentice" selected={m.role == "apprentice"}>Apprentice</option>
|
||||
<option value="journeyman" selected={m.role == "journeyman"}>Journeyman</option>
|
||||
<option value="master" selected={m.role == "master"}>Master</option>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr :if={@memberships == []}>
|
||||
<td colspan="5" class="py-6 text-center text-zinc-400">No members yet.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
defmodule GuildhallWeb.GuildLive.Realization do
|
||||
use GuildhallWeb, :live_view
|
||||
|
||||
alias Guildhall.OpsDb.{Guilds, GuildSchematics}
|
||||
|
||||
@impl true
|
||||
def mount(%{"slug" => slug}, _session, socket) do
|
||||
guild = Guilds.get_guild_by_slug(slug) || raise Ecto.NoResultsError, queryable: Guildhall.OpsDb.Guild
|
||||
schematic = GuildSchematics.get_for_guild(guild.id)
|
||||
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(Guildhall.PubSub, "realization:#{slug}")
|
||||
end
|
||||
|
||||
snapshot = if schematic, do: schematic.realization_snapshot, else: %{}
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "#{guild.name} — Realization")
|
||||
|> assign(:guild, guild)
|
||||
|> assign(:schematic, schematic)
|
||||
|> assign(:snapshot, snapshot)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:realization_update, snapshot}, socket) do
|
||||
{:noreply, assign(socket, :snapshot, snapshot)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(_msg, socket), do: {:noreply, socket}
|
||||
|
||||
@reconciler_sections ~w(trust_domain identity_authority members infrastructure ceremonies federation_peers attestation)
|
||||
|
||||
defp section_status(snapshot, name) do
|
||||
sections = Map.get(snapshot, "sections", Map.get(snapshot, :sections, []))
|
||||
|
||||
case Enum.find(sections, fn s -> (s["name"] || s[:name]) == name end) do
|
||||
nil -> %{status: "pending", message: "Not started"}
|
||||
s -> %{status: s["status"] || s[:status], message: s["message"] || s[:message] || ""}
|
||||
end
|
||||
end
|
||||
|
||||
defp status_icon("succeeded"), do: "text-emerald-600"
|
||||
defp status_icon("in_progress"), do: "text-amber-600"
|
||||
defp status_icon("failed"), do: "text-red-600"
|
||||
defp status_icon("pending"), do: "text-zinc-400"
|
||||
defp status_icon(_), do: "text-zinc-400"
|
||||
|
||||
defp status_symbol("succeeded"), do: "✓"
|
||||
defp status_symbol("in_progress"), do: "⟳"
|
||||
defp status_symbol("failed"), do: "✗"
|
||||
defp status_symbol(_), do: "○"
|
||||
|
||||
defp overall_class(snapshot) do
|
||||
overall = Map.get(snapshot, "overall_status", Map.get(snapshot, :overall_status, "pending"))
|
||||
|
||||
cond do
|
||||
String.contains?(to_string(overall), "REALIZED") -> "bg-emerald-100 text-emerald-700"
|
||||
String.contains?(to_string(overall), "PUBLISHED") -> "bg-amber-100 text-amber-700"
|
||||
true -> "bg-zinc-100 text-zinc-700"
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
assigns = assign(assigns, :sections, @reconciler_sections)
|
||||
|
||||
~H"""
|
||||
<div class="mx-auto max-w-2xl p-6 space-y-6">
|
||||
<header>
|
||||
<.link navigate={~p"/guilds/#{@guild.slug}"} class="text-sm text-blue-600 underline">← {@guild.name}</.link>
|
||||
<h1 class="text-2xl font-semibold mt-2">Realization Dashboard</h1>
|
||||
<div :if={@snapshot != %{}} class={"inline-block rounded-full px-3 py-1 text-xs font-medium mt-2 #{overall_class(@snapshot)}"}>
|
||||
{Map.get(@snapshot, "overall_status", Map.get(@snapshot, :overall_status, "pending"))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section :if={@schematic == nil} class="text-zinc-500 text-sm">
|
||||
No schematic deployed for this guild yet.
|
||||
</section>
|
||||
|
||||
<section :if={@schematic} class="space-y-2">
|
||||
<div class="text-sm text-zinc-500 mb-3">
|
||||
Template: <span class="font-mono">{@schematic.template_name}</span> ·
|
||||
Schematic: <span class="font-mono">{@schematic.schematic_name} v{@schematic.schematic_version}</span>
|
||||
</div>
|
||||
|
||||
<div :for={name <- @sections} class="flex items-center gap-3 rounded border border-zinc-200 p-3">
|
||||
<% s = section_status(@snapshot, name) %>
|
||||
<span class={"text-lg font-bold #{status_icon(s.status)}"}>{status_symbol(s.status)}</span>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium">{String.replace(name, "_", " ") |> String.capitalize()}</div>
|
||||
<div class="text-xs text-zinc-500">{s.message}</div>
|
||||
</div>
|
||||
<span class={"text-xs #{status_icon(s.status)}"}>{s.status}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
defmodule GuildhallWeb.GuildLive.Register do
|
||||
use GuildhallWeb, :live_view
|
||||
|
||||
alias Guildhall.OpsDb.{Guild, Guilds}
|
||||
alias Guildhall.Orchestrator.CeremonyClient
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
changeset = Guild.changeset(%Guild{}, %{})
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Register Guild")
|
||||
|> assign(:form, to_form(changeset))
|
||||
|> assign(:submitting, false)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"guild" => params}, socket) do
|
||||
changeset =
|
||||
%Guild{}
|
||||
|> Guild.changeset(params)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
{:noreply, assign(socket, :form, to_form(changeset))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("register", %{"guild" => params}, socket) do
|
||||
user = socket.assigns.current_user
|
||||
guild_id = Guilds.next_guild_id()
|
||||
slug = slugify(params["name"] || "")
|
||||
|
||||
attrs =
|
||||
params
|
||||
|> Map.put("guild_id", guild_id)
|
||||
|> Map.put("slug", slug)
|
||||
|> Map.put("registrant_did", user["did"])
|
||||
|> Map.put("contact_did", user["did"])
|
||||
|> Map.put("trust_domain", "#{slug}.guildhouse.dev")
|
||||
|> Map.put("status", "pending_approval")
|
||||
|
||||
case Guilds.create_guild(attrs) do
|
||||
{:ok, guild} ->
|
||||
ceremony_result =
|
||||
CeremonyClient.create_guild_registration_ceremony(
|
||||
guild.name,
|
||||
user["did"],
|
||||
"did:web:guildhouse.dev:user:tking"
|
||||
)
|
||||
|
||||
case ceremony_result do
|
||||
{:ok, response} ->
|
||||
Guilds.update_guild(guild, %{registration_ceremony_id: response.ceremony_id})
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Guild registration submitted for approval.")
|
||||
|> push_navigate(to: ~p"/guilds/#{guild.slug}")}
|
||||
|
||||
{:error, changeset} ->
|
||||
{:noreply, assign(socket, :form, to_form(changeset))}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="mx-auto max-w-lg p-6 space-y-4">
|
||||
<header>
|
||||
<.link navigate={~p"/guilds"} class="text-sm text-blue-600 underline">← Guilds</.link>
|
||||
<h1 class="text-2xl font-semibold mt-2">Register Guild</h1>
|
||||
<p class="text-sm text-zinc-500">Submit a new guild for hub operator approval.</p>
|
||||
</header>
|
||||
|
||||
<.form for={@form} phx-change="validate" phx-submit="register" class="space-y-4">
|
||||
<div>
|
||||
<label for="guild_name" class="block text-sm font-medium text-zinc-700">Guild Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="guild_name"
|
||||
name="guild[name]"
|
||||
value={@form[:name].value}
|
||||
class="mt-1 block w-full rounded border border-zinc-300 px-3 py-2 text-sm"
|
||||
placeholder="e.g. Gator Guild"
|
||||
required
|
||||
/>
|
||||
<p :if={@form[:name].errors != []} class="mt-1 text-xs text-red-600">
|
||||
{error_to_string(@form[:name].errors)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="guild_type" class="block text-sm font-medium text-zinc-700">Guild Type</label>
|
||||
<select
|
||||
id="guild_type"
|
||||
name="guild[guild_type]"
|
||||
class="mt-1 block w-full rounded border border-zinc-300 px-3 py-2 text-sm"
|
||||
required
|
||||
>
|
||||
<option value="">Select type...</option>
|
||||
<option value="msp" selected={@form[:guild_type].value == "msp"}>MSP — Managed Service Provider</option>
|
||||
<option value="isv" selected={@form[:guild_type].value == "isv"}>ISV — Independent Software Vendor</option>
|
||||
<option value="nsp" selected={@form[:guild_type].value == "nsp"}>NSP — Network Service Provider</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="guild_description" class="block text-sm font-medium text-zinc-700">Description</label>
|
||||
<textarea
|
||||
id="guild_description"
|
||||
name="guild[description]"
|
||||
class="mt-1 block w-full rounded border border-zinc-300 px-3 py-2 text-sm"
|
||||
rows="3"
|
||||
placeholder="What does this guild do?"
|
||||
>{@form[:description].value}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={@submitting}
|
||||
class="rounded bg-blue-600 px-4 py-2 text-sm text-white disabled:opacity-50"
|
||||
>
|
||||
Submit Registration
|
||||
</button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp error_to_string(errors) do
|
||||
Enum.map_join(errors, ", ", fn {msg, _opts} -> msg end)
|
||||
end
|
||||
|
||||
defp slugify(name) do
|
||||
name
|
||||
|> String.downcase()
|
||||
|> String.replace(~r/[^a-z0-9\s-]/, "")
|
||||
|> String.replace(~r/[\s]+/, "-")
|
||||
|> String.trim("-")
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
defmodule GuildhallWeb.GuildLive.Schematic do
|
||||
use GuildhallWeb, :live_view
|
||||
|
||||
alias Guildhall.OpsDb.{Guilds, GuildSchematics}
|
||||
alias Guildhall.Orchestrator.{SchematicClient, SchematicTemplate, RealizationPoller}
|
||||
|
||||
@impl true
|
||||
def mount(%{"slug" => slug}, _session, socket) do
|
||||
guild = Guilds.get_guild_by_slug(slug) || raise Ecto.NoResultsError, queryable: Guildhall.OpsDb.Guild
|
||||
|
||||
unless guild.status in ["approved", "active"] do
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, "Guild must be approved before deploying a schematic.")
|
||||
|> push_navigate(to: ~p"/guilds/#{slug}")}
|
||||
else
|
||||
existing = GuildSchematics.get_for_guild(guild.id)
|
||||
|
||||
if existing do
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:info, "Schematic already deployed.")
|
||||
|> push_navigate(to: ~p"/guilds/#{slug}/realization")}
|
||||
else
|
||||
template_result = SchematicTemplate.load_template(guild.guild_type)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "#{guild.name} — Deploy Schematic")
|
||||
|> assign(:guild, guild)
|
||||
|> assign(:template, elem_ok(template_result))
|
||||
|> assign(:template_error, elem_error(template_result))
|
||||
|> assign(:deploying, false)
|
||||
|> assign(:deploy_error, nil)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("deploy_schematic", _params, socket) do
|
||||
guild = socket.assigns.guild
|
||||
template = socket.assigns.template
|
||||
socket = assign(socket, :deploying, true)
|
||||
|
||||
params = %{
|
||||
"guild_slug" => guild.slug,
|
||||
"guild_name" => guild.name,
|
||||
"trust_domain" => guild.trust_domain || "#{guild.slug}.guildhouse.dev",
|
||||
"registrant_did" => guild.registrant_did
|
||||
}
|
||||
|
||||
rendered = SchematicTemplate.render_template(template, params)
|
||||
operations = SchematicTemplate.to_fork_operations(rendered)
|
||||
{source_name, source_version} = SchematicTemplate.source_schematic(template)
|
||||
schematic_name = "#{guild.slug}-#{guild.guild_type}-founding"
|
||||
schematic_version = "1.0.0"
|
||||
|
||||
with {:ok, fork_resp} <-
|
||||
SchematicClient.fork_schematic(source_name, source_version, schematic_name, schematic_version, operations),
|
||||
{:ok, binding_resp} <-
|
||||
SchematicClient.create_deployment_binding(schematic_name, schematic_version, "founding", "production"),
|
||||
{:ok, realize_resp} <-
|
||||
SchematicClient.realize_ffc_schematic(schematic_name, schematic_version) do
|
||||
{:ok, gs} =
|
||||
GuildSchematics.create(%{
|
||||
guild_id: guild.id,
|
||||
template_name: "#{guild.guild_type}-founding",
|
||||
schematic_name: schematic_name,
|
||||
schematic_version: schematic_version,
|
||||
tree_hash: Map.get(fork_resp, :tree_hash, ""),
|
||||
binding_id: Map.get(binding_resp, :binding_id, ""),
|
||||
realization_id: Map.get(realize_resp, :realization_id, ""),
|
||||
status: "realizing"
|
||||
})
|
||||
|
||||
if gs.realization_id && gs.realization_id != "" do
|
||||
RealizationPoller.watch(gs.realization_id, guild.slug)
|
||||
end
|
||||
|
||||
{:ok, _} = Guilds.update_guild(guild, %{status: "active"})
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Schematic deployed. Realization in progress.")
|
||||
|> push_navigate(to: ~p"/guilds/#{guild.slug}/realization")}
|
||||
else
|
||||
{:error, reason} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:deploying, false)
|
||||
|> assign(:deploy_error, inspect(reason))}
|
||||
end
|
||||
end
|
||||
|
||||
defp elem_ok({:ok, val}), do: val
|
||||
defp elem_ok(_), do: nil
|
||||
|
||||
defp elem_error({:error, reason}), do: inspect(reason)
|
||||
defp elem_error(_), do: nil
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="mx-auto max-w-2xl p-6 space-y-6">
|
||||
<header>
|
||||
<.link navigate={~p"/guilds/#{@guild.slug}"} class="text-sm text-blue-600 underline">← {@guild.name}</.link>
|
||||
<h1 class="text-2xl font-semibold mt-2">Deploy Founding Schematic</h1>
|
||||
</header>
|
||||
|
||||
<div :if={@template_error} class="border border-red-200 bg-red-50 rounded p-4 text-sm text-red-700">
|
||||
Failed to load template: {@template_error}
|
||||
</div>
|
||||
|
||||
<section :if={@template} class="space-y-4">
|
||||
<div class="text-sm space-y-2">
|
||||
<p>
|
||||
Template: <span class="font-mono font-semibold">{@guild.guild_type}-founding</span>
|
||||
</p>
|
||||
<div class="rounded border border-zinc-200 p-3 text-xs font-mono bg-zinc-50 max-h-64 overflow-y-auto">
|
||||
<pre>{inspect(@template, pretty: true, limit: :infinity)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-zinc-500 space-y-1">
|
||||
<p>This will:</p>
|
||||
<ol class="list-decimal ml-5 space-y-1">
|
||||
<li>Fork the founding schematic template for {@guild.name}</li>
|
||||
<li>Create a deployment binding (production)</li>
|
||||
<li>Trigger realization across all reconciler sections</li>
|
||||
<li>Transition guild to <span class="font-semibold">active</span> status</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div :if={@deploy_error} class="border border-red-200 bg-red-50 rounded p-4 text-sm text-red-700">
|
||||
Deploy failed: {@deploy_error}
|
||||
</div>
|
||||
|
||||
<button
|
||||
phx-click="deploy_schematic"
|
||||
disabled={@deploying}
|
||||
class={"rounded px-4 py-2 text-sm text-white #{if @deploying, do: "bg-zinc-400 cursor-not-allowed", else: "bg-blue-600 hover:bg-blue-700"}"}
|
||||
>
|
||||
{if @deploying, do: "Deploying…", else: "Deploy Schematic"}
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
227
apps/guildhall_web/lib/guildhall_web_web/live/guild_live/show.ex
Normal file
227
apps/guildhall_web/lib/guildhall_web_web/live/guild_live/show.ex
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
defmodule GuildhallWeb.GuildLive.Show do
|
||||
use GuildhallWeb, :live_view
|
||||
|
||||
alias Guildhall.OpsDb.{Guilds, GuildSchematics, GuildMemberships}
|
||||
alias Guildhall.Orchestrator.CeremonyClient
|
||||
|
||||
@hub_operator_did "did:web:guildhouse.dev:user:tking"
|
||||
@poll_interval_ms 5_000
|
||||
|
||||
@impl true
|
||||
def mount(%{"slug" => slug}, _session, socket) do
|
||||
guild = Guilds.get_guild_by_slug(slug) || raise Ecto.NoResultsError, queryable: Guildhall.OpsDb.Guild
|
||||
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(Guildhall.PubSub, "guild:#{slug}")
|
||||
|
||||
if guild.status == "pending_approval" && guild.registration_ceremony_id do
|
||||
Process.send_after(self(), :poll_ceremony, @poll_interval_ms)
|
||||
end
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, guild.name)
|
||||
|> assign(:guild, guild)
|
||||
|> assign(:ceremony_status, nil)
|
||||
|> assign(:is_hub_operator, socket.assigns.current_user["did"] == @hub_operator_did)
|
||||
|> assign(:schematic, GuildSchematics.get_for_guild(guild.id))
|
||||
|> assign(:member_count, length(GuildMemberships.active_members(guild.id)))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:poll_ceremony, socket) do
|
||||
guild = socket.assigns.guild
|
||||
|
||||
case CeremonyClient.get_ceremony(guild.registration_ceremony_id) do
|
||||
{:ok, response} ->
|
||||
socket = assign(socket, :ceremony_status, response.status)
|
||||
|
||||
socket =
|
||||
case response.status do
|
||||
"approved" ->
|
||||
{:ok, %{guild: updated}} = Guilds.approve_guild(guild)
|
||||
assign(socket, :guild, updated)
|
||||
|
||||
"denied" ->
|
||||
{:ok, updated} = Guilds.update_guild(guild, %{status: "denied"})
|
||||
assign(socket, :guild, updated)
|
||||
|
||||
_ ->
|
||||
Process.send_after(self(), :poll_ceremony, @poll_interval_ms)
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, _} ->
|
||||
Process.send_after(self(), :poll_ceremony, @poll_interval_ms)
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:guild_updated, _}, socket) do
|
||||
guild = Guilds.get_guild_by_slug(socket.assigns.guild.slug)
|
||||
{:noreply, assign(socket, :guild, guild)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(_msg, socket), do: {:noreply, socket}
|
||||
|
||||
@impl true
|
||||
def handle_event("approve_guild", _params, socket) do
|
||||
guild = socket.assigns.guild
|
||||
|
||||
case CeremonyClient.approve_ceremony(
|
||||
guild.registration_ceremony_id,
|
||||
socket.assigns.current_user["did"],
|
||||
"hub_operator"
|
||||
) do
|
||||
{:ok, %{status: "approved"}} ->
|
||||
{:ok, %{guild: updated}} = Guilds.approve_guild(guild)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:guild, updated)
|
||||
|> assign(:ceremony_status, "approved")
|
||||
|> put_flash(:info, "#{guild.name} approved.")}
|
||||
|
||||
{:ok, _} ->
|
||||
{:noreply, put_flash(socket, :info, "Approval recorded.")}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Approval failed: #{inspect(reason)}")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("deny_guild", _params, socket) do
|
||||
guild = socket.assigns.guild
|
||||
|
||||
case CeremonyClient.deny_ceremony(
|
||||
guild.registration_ceremony_id,
|
||||
socket.assigns.current_user["did"],
|
||||
"hub_operator",
|
||||
"Denied by hub operator"
|
||||
) do
|
||||
{:ok, _} ->
|
||||
{:ok, updated} = Guilds.update_guild(guild, %{status: "denied"})
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:guild, updated)
|
||||
|> assign(:ceremony_status, "denied")
|
||||
|> put_flash(:info, "#{guild.name} denied.")}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Denial failed: #{inspect(reason)}")}
|
||||
end
|
||||
end
|
||||
|
||||
defp status_class("pending_approval"), do: "bg-amber-100 text-amber-700"
|
||||
defp status_class("approved"), do: "bg-emerald-100 text-emerald-700"
|
||||
defp status_class("active"), do: "bg-emerald-100 text-emerald-700"
|
||||
defp status_class("denied"), do: "bg-red-100 text-red-700"
|
||||
defp status_class("suspended"), do: "bg-zinc-100 text-zinc-700"
|
||||
defp status_class(_), do: "bg-zinc-100 text-zinc-700"
|
||||
|
||||
defp schematic_status_class("realized"), do: "text-emerald-600"
|
||||
defp schematic_status_class("realizing"), do: "text-amber-600"
|
||||
defp schematic_status_class("failed"), do: "text-red-600"
|
||||
defp schematic_status_class(_), do: "text-zinc-500"
|
||||
|
||||
defp type_label("msp"), do: "Managed Service Provider"
|
||||
defp type_label("isv"), do: "Independent Software Vendor"
|
||||
defp type_label("nsp"), do: "Network Service Provider"
|
||||
defp type_label(other), do: other
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="mx-auto max-w-2xl p-6 space-y-6">
|
||||
<header>
|
||||
<.link navigate={~p"/guilds"} class="text-sm text-blue-600 underline">← Guilds</.link>
|
||||
<h1 class="text-2xl font-semibold mt-2">{@guild.name}</h1>
|
||||
<span class={"inline-block rounded-full px-3 py-1 text-xs font-medium #{status_class(@guild.status)}"}>
|
||||
{@guild.status}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<section class="space-y-3 text-sm">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="text-zinc-500">Guild Type</div>
|
||||
<div>{type_label(@guild.guild_type)}</div>
|
||||
|
||||
<div class="text-zinc-500">Guild ID</div>
|
||||
<div class="font-mono">0x{Integer.to_string(@guild.guild_id, 16) |> String.pad_leading(3, "0")}</div>
|
||||
|
||||
<div class="text-zinc-500">Trust Domain</div>
|
||||
<div class="font-mono">{@guild.trust_domain || "—"}</div>
|
||||
|
||||
<div class="text-zinc-500">Registrant</div>
|
||||
<div class="font-mono text-xs">{@guild.registrant_did}</div>
|
||||
|
||||
<div class="text-zinc-500">Contact</div>
|
||||
<div class="font-mono text-xs">{@guild.contact_did}</div>
|
||||
</div>
|
||||
|
||||
<div :if={@guild.description} class="border-t border-zinc-100 pt-3">
|
||||
<div class="text-zinc-500 mb-1">Description</div>
|
||||
<p>{@guild.description}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section :if={@guild.status == "pending_approval" && @guild.registration_ceremony_id} class="border border-amber-200 bg-amber-50 rounded p-4 text-sm">
|
||||
<h2 class="font-semibold text-amber-800 mb-2">Awaiting Approval</h2>
|
||||
<p class="text-amber-700 mb-1">
|
||||
Ceremony: <span class="font-mono text-xs">{@guild.registration_ceremony_id}</span>
|
||||
</p>
|
||||
<p :if={@ceremony_status} class="text-amber-700">
|
||||
Status: <span class="font-semibold">{@ceremony_status}</span>
|
||||
</p>
|
||||
|
||||
<div :if={@is_hub_operator} class="flex gap-2 mt-3">
|
||||
<button phx-click="approve_guild" class="rounded bg-emerald-600 px-3 py-1.5 text-xs text-white">
|
||||
Approve
|
||||
</button>
|
||||
<button phx-click="deny_guild" class="rounded bg-red-600 px-3 py-1.5 text-xs text-white">
|
||||
Deny
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section :if={@guild.status == "denied"} class="border border-red-200 bg-red-50 rounded p-4 text-sm">
|
||||
<p class="text-red-700">This guild registration was denied.</p>
|
||||
</section>
|
||||
|
||||
<section :if={@guild.status in ["approved", "active"]} class="space-y-3">
|
||||
<div class="border border-emerald-200 bg-emerald-50 rounded p-4 text-sm text-emerald-700">
|
||||
Guild is active and operational. {@member_count} active member(s).
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 text-sm">
|
||||
<.link :if={@schematic == nil} navigate={~p"/guilds/#{@guild.slug}/schematic"} class="rounded bg-blue-600 px-3 py-1.5 text-white">
|
||||
Deploy Schematic
|
||||
</.link>
|
||||
<.link :if={@schematic} navigate={~p"/guilds/#{@guild.slug}/realization"} class="rounded bg-zinc-700 px-3 py-1.5 text-white">
|
||||
Realization Dashboard
|
||||
</.link>
|
||||
<.link navigate={~p"/guilds/#{@guild.slug}/join"} class="rounded border border-blue-600 px-3 py-1.5 text-blue-600">
|
||||
Join Guild
|
||||
</.link>
|
||||
<.link navigate={~p"/guilds/#{@guild.slug}/members"} class="rounded border border-zinc-300 px-3 py-1.5 text-zinc-700">
|
||||
Manage Members
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<div :if={@schematic} class="rounded border border-zinc-200 p-3 text-sm">
|
||||
<div class="text-zinc-500 text-xs mb-1">Schematic</div>
|
||||
<span class="font-mono">{@schematic.schematic_name} v{@schematic.schematic_version}</span>
|
||||
<span class={"ml-2 text-xs #{schematic_status_class(@schematic.status)}"}>{@schematic.status}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
24
apps/guildhall_web/lib/guildhall_web_web/plugs/auth.ex
Normal file
24
apps/guildhall_web/lib/guildhall_web_web/plugs/auth.ex
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
defmodule GuildhallWeb.Plugs.Auth do
|
||||
@moduledoc false
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, _opts) do
|
||||
case get_session(conn, :current_user) do
|
||||
nil ->
|
||||
conn
|
||||
|> put_flash(:error, "Please sign in to continue.")
|
||||
|> redirect(to: "/auth/login")
|
||||
|> halt()
|
||||
|
||||
user ->
|
||||
assign(conn, :current_user, user)
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_current_user(conn, _opts) do
|
||||
assign(conn, :current_user, get_session(conn, :current_user))
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
defmodule GuildhallWeb.Router do
|
||||
use GuildhallWeb, :router
|
||||
|
||||
import GuildhallWeb.Plugs.Auth, only: [fetch_current_user: 2]
|
||||
|
||||
pipeline :browser do
|
||||
plug :accepts, ["html"]
|
||||
plug :fetch_session
|
||||
|
|
@ -8,24 +10,41 @@ defmodule GuildhallWeb.Router do
|
|||
plug :put_root_layout, html: {GuildhallWeb.Layouts, :root}
|
||||
plug :protect_from_forgery
|
||||
plug :put_secure_browser_headers
|
||||
plug :fetch_current_user
|
||||
end
|
||||
|
||||
pipeline :api do
|
||||
plug :accepts, ["json"]
|
||||
end
|
||||
|
||||
# Public auth routes (no auth required)
|
||||
scope "/auth", GuildhallWeb do
|
||||
pipe_through :browser
|
||||
|
||||
get "/login", AuthController, :login
|
||||
get "/callback", AuthController, :callback
|
||||
get "/logout", AuthController, :logout
|
||||
end
|
||||
|
||||
# Authenticated LiveView routes
|
||||
scope "/", GuildhallWeb do
|
||||
pipe_through :browser
|
||||
|
||||
live "/", DashboardLive, :index
|
||||
live "/ceremonies", CeremonyLive.Index, :index
|
||||
live "/artifacts", ArtifactLive.Index, :index
|
||||
live_session :authenticated, on_mount: {GuildhallWeb.AuthHooks, :require_auth} do
|
||||
live "/", DashboardLive, :index
|
||||
live "/ceremonies", CeremonyLive.Index, :index
|
||||
live "/artifacts", ArtifactLive.Index, :index
|
||||
live "/guilds", GuildLive.Index, :index
|
||||
live "/guilds/register", GuildLive.Register, :new
|
||||
live "/guilds/:slug", GuildLive.Show, :show
|
||||
live "/guilds/:slug/schematic", GuildLive.Schematic, :schematic
|
||||
live "/guilds/:slug/realization", GuildLive.Realization, :realization
|
||||
live "/guilds/:slug/join", GuildLive.Join, :join
|
||||
live "/guilds/:slug/members", GuildLive.Members, :members
|
||||
end
|
||||
end
|
||||
|
||||
# Health check endpoint for Kubernetes probes + LB targets.
|
||||
# GET /health — returns 200 when Phoenix is up AND the Ecto pool
|
||||
# can query the DB; 503 otherwise. Unauthenticated (the whole point
|
||||
# is that it's reachable without credentials).
|
||||
scope "/health", GuildhallWeb do
|
||||
pipe_through :api
|
||||
get "/", HealthController, :check
|
||||
|
|
|
|||
|
|
@ -65,7 +65,9 @@ defmodule GuildhallWeb.MixProject do
|
|||
{:gettext, "~> 1.0"},
|
||||
{:jason, "~> 1.2"},
|
||||
{:dns_cluster, "~> 0.2.0"},
|
||||
{:bandit, "~> 1.5"}
|
||||
{:bandit, "~> 1.5"},
|
||||
{:oidcc, "~> 3.2"},
|
||||
{:req, "~> 0.5"}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -44,3 +44,14 @@ config :phoenix_live_view,
|
|||
debug_heex_annotations: true,
|
||||
debug_attributes: true,
|
||||
enable_expensive_runtime_checks: true
|
||||
|
||||
config :guildhall_web, :oidc,
|
||||
issuer: "https://auth.guildhouse.dev/realms/guildhouse",
|
||||
client_id: "guildhall-web",
|
||||
client_secret: System.get_env("OIDC_CLIENT_SECRET"),
|
||||
redirect_uri: "http://localhost:4000/auth/callback"
|
||||
|
||||
config :guildhall_orchestrator,
|
||||
ceremony_service_url: "localhost:50053",
|
||||
schematic_service_url: "localhost:9091",
|
||||
ffc_schematic_service_url: "localhost:9091"
|
||||
|
|
|
|||
|
|
@ -43,14 +43,20 @@ if config_env() == :prod do
|
|||
http: [ip: {0, 0, 0, 0, 0, 0, 0, 0}],
|
||||
secret_key_base: secret_key_base
|
||||
|
||||
# K8s cluster connection (future — orchestrator will use this)
|
||||
# config :guildhall_orchestrator,
|
||||
# kubeconfig: System.get_env("KUBECONFIG") || "~/.kube/config",
|
||||
# context: System.get_env("K8S_CONTEXT")
|
||||
config :guildhall_web, :oidc,
|
||||
issuer:
|
||||
System.get_env("OIDC_ISSUER") || "https://auth.guildhouse.dev/realms/guildhouse",
|
||||
client_id: System.get_env("OIDC_CLIENT_ID") || "guildhall-web",
|
||||
client_secret: System.get_env("OIDC_CLIENT_SECRET"),
|
||||
redirect_uri:
|
||||
System.get_env("OIDC_REDIRECT_URI") ||
|
||||
"https://guildhall.guildhouse.dev/auth/callback"
|
||||
|
||||
# Keycloak OIDC (future — auth)
|
||||
# config :guildhall_web, :oidc,
|
||||
# issuer: System.get_env("OIDC_ISSUER") || "https://auth.guildhouse.dev/realms/guildhouse",
|
||||
# client_id: System.get_env("OIDC_CLIENT_ID"),
|
||||
# client_secret: System.get_env("OIDC_CLIENT_SECRET")
|
||||
config :guildhall_orchestrator,
|
||||
ceremony_service_url:
|
||||
System.get_env("CEREMONY_SERVICE_URL") || "localhost:50053",
|
||||
schematic_service_url:
|
||||
System.get_env("SCHEMATIC_SERVICE_URL") || "localhost:9091",
|
||||
ffc_schematic_service_url:
|
||||
System.get_env("FFC_SCHEMATIC_SERVICE_URL") || "localhost:9091"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,9 +37,12 @@
|
|||
#
|
||||
# SECRET_KEY_BASE="$(cd /home/tking/projects/substrate-project/guildhall && mix phx.gen.secret)"
|
||||
#
|
||||
# OIDC_CLIENT_SECRET="<from Keycloak guildhall-web client credentials>"
|
||||
#
|
||||
# kubectl create secret generic guildhall-app-secrets \
|
||||
# --from-literal=SECRET_KEY_BASE="$SECRET_KEY_BASE" \
|
||||
# --from-literal=DATABASE_URL="ecto://guildhall:$DB_PASSWORD@guildhall-postgres:5432/guildhall" \
|
||||
# --from-literal=OIDC_CLIENT_SECRET="$OIDC_CLIENT_SECRET" \
|
||||
# --namespace=guildhall
|
||||
#
|
||||
# Note: `ecto://` scheme, not `postgres://` — `config/runtime.exs`
|
||||
|
|
@ -55,5 +58,20 @@
|
|||
# namespace: guildhall
|
||||
# type: Opaque
|
||||
# data:
|
||||
# SECRET_KEY_BASE: <b64 "<64-byte-base64-session-key>">
|
||||
# DATABASE_URL: <b64 "ecto://guildhall:<pw>@guildhall-postgres:5432/guildhall">
|
||||
# SECRET_KEY_BASE: <b64 "<64-byte-base64-session-key>">
|
||||
# DATABASE_URL: <b64 "ecto://guildhall:<pw>@guildhall-postgres:5432/guildhall">
|
||||
# OIDC_CLIENT_SECRET: <b64 "<keycloak-client-secret>">
|
||||
#
|
||||
# ---------- ceremony-service-secrets ----------
|
||||
# Consumed by the ceremony-service Deployment.
|
||||
#
|
||||
# kubectl create secret generic ceremony-service-secrets \
|
||||
# --from-literal=DATABASE_URL="postgres://ceremony:$CEREMONY_DB_PW@guildhall-postgres:5432/ceremony" \
|
||||
# --namespace=guildhall
|
||||
#
|
||||
# ---------- schematic-server-secrets ----------
|
||||
# Consumed by the ffc-schematic-server Deployment.
|
||||
#
|
||||
# kubectl create secret generic schematic-server-secrets \
|
||||
# --from-literal=DATABASE_URL="postgres://schematic:$SCHEMATIC_DB_PW@guildhall-postgres:5432/schematic" \
|
||||
# --namespace=guildhall
|
||||
|
|
|
|||
|
|
@ -54,6 +54,25 @@ spec:
|
|||
secretKeyRef:
|
||||
name: guildhall-app-secrets
|
||||
key: SECRET_KEY_BASE
|
||||
# OIDC (Keycloak)
|
||||
- name: OIDC_ISSUER
|
||||
value: "https://auth.guildhouse.dev/realms/guildhouse"
|
||||
- name: OIDC_CLIENT_ID
|
||||
value: guildhall-web
|
||||
- name: OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: guildhall-app-secrets
|
||||
key: OIDC_CLIENT_SECRET
|
||||
- name: OIDC_REDIRECT_URI
|
||||
value: "https://guildhall.guildhouse.dev/auth/callback"
|
||||
# gRPC service URLs (in-cluster ClusterIP DNS)
|
||||
- name: CEREMONY_SERVICE_URL
|
||||
value: "ceremony-service.guildhall.svc.cluster.local:50053"
|
||||
- name: SCHEMATIC_SERVICE_URL
|
||||
value: "ffc-schematic-server.guildhall.svc.cluster.local:9091"
|
||||
- name: FFC_SCHEMATIC_SERVICE_URL
|
||||
value: "ffc-schematic-server.guildhall.svc.cluster.local:9091"
|
||||
# Ecto
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
|
|
|
|||
72
k8s/90-ceremony-service-deployment.yaml
Normal file
72
k8s/90-ceremony-service-deployment.yaml
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ceremony-service
|
||||
namespace: guildhall
|
||||
labels:
|
||||
app.kubernetes.io/name: ceremony-service
|
||||
app.kubernetes.io/part-of: guildhouse
|
||||
app.kubernetes.io/component: ceremony-engine
|
||||
app.kubernetes.io/managed-by: manual
|
||||
app.kubernetes.io/version: v0.1.0
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ceremony-service
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ceremony-service
|
||||
app.kubernetes.io/name: ceremony-service
|
||||
app.kubernetes.io/part-of: guildhouse
|
||||
app.kubernetes.io/component: ceremony-engine
|
||||
app.kubernetes.io/version: v0.1.0
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: guildhall-registry
|
||||
containers:
|
||||
- name: ceremony-service
|
||||
image: git.guildhouse.dev/tking/ceremony-service:v0.1.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 50053
|
||||
name: grpc
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: RUST_LOG
|
||||
value: info
|
||||
- name: LISTEN_ADDR
|
||||
value: "0.0.0.0:50053"
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ceremony-service-secrets
|
||||
key: DATABASE_URL
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
readinessProbe:
|
||||
grpc:
|
||||
port: 50053
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
grpc:
|
||||
port: 50053
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
terminationGracePeriodSeconds: 15
|
||||
19
k8s/91-ceremony-service-service.yaml
Normal file
19
k8s/91-ceremony-service-service.yaml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ceremony-service
|
||||
namespace: guildhall
|
||||
labels:
|
||||
app.kubernetes.io/name: ceremony-service
|
||||
app.kubernetes.io/part-of: guildhouse
|
||||
app.kubernetes.io/component: ceremony-engine
|
||||
app.kubernetes.io/managed-by: manual
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 50053
|
||||
targetPort: grpc
|
||||
protocol: TCP
|
||||
name: grpc
|
||||
selector:
|
||||
app: ceremony-service
|
||||
72
k8s/92-schematic-server-deployment.yaml
Normal file
72
k8s/92-schematic-server-deployment.yaml
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ffc-schematic-server
|
||||
namespace: guildhall
|
||||
labels:
|
||||
app.kubernetes.io/name: ffc-schematic-server
|
||||
app.kubernetes.io/part-of: guildhouse
|
||||
app.kubernetes.io/component: schematic-engine
|
||||
app.kubernetes.io/managed-by: manual
|
||||
app.kubernetes.io/version: v0.1.0
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ffc-schematic-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ffc-schematic-server
|
||||
app.kubernetes.io/name: ffc-schematic-server
|
||||
app.kubernetes.io/part-of: guildhouse
|
||||
app.kubernetes.io/component: schematic-engine
|
||||
app.kubernetes.io/version: v0.1.0
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: guildhall-registry
|
||||
containers:
|
||||
- name: ffc-schematic-server
|
||||
image: git.guildhouse.dev/tking/ffc-schematic-server:v0.1.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 9091
|
||||
name: grpc
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: RUST_LOG
|
||||
value: info
|
||||
- name: LISTEN_ADDR
|
||||
value: "0.0.0.0:9091"
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: schematic-server-secrets
|
||||
key: DATABASE_URL
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
readinessProbe:
|
||||
grpc:
|
||||
port: 9091
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
grpc:
|
||||
port: 9091
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
terminationGracePeriodSeconds: 15
|
||||
19
k8s/93-schematic-server-service.yaml
Normal file
19
k8s/93-schematic-server-service.yaml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ffc-schematic-server
|
||||
namespace: guildhall
|
||||
labels:
|
||||
app.kubernetes.io/name: ffc-schematic-server
|
||||
app.kubernetes.io/part-of: guildhouse
|
||||
app.kubernetes.io/component: schematic-engine
|
||||
app.kubernetes.io/managed-by: manual
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 9091
|
||||
targetPort: grpc
|
||||
protocol: TCP
|
||||
name: grpc
|
||||
selector:
|
||||
app: ffc-schematic-server
|
||||
19
mix.lock
19
mix.lock
|
|
@ -1,6 +1,8 @@
|
|||
%{
|
||||
"bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||
"cowboy": {:hex, :cowboy, "2.15.0", "9cfe86ed7117bf045e10adbedb0170af7be57f2a3637e7be143433d8dd267396", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "179fb65140fb440a17b767ad53b755081506f9596c4db5c49c0396d8c8643668"},
|
||||
"cowlib": {:hex, :cowlib, "2.16.1", "318d385d55f657e9a5005838c4e426e13dcd724a691438384b6165a69687e531", [:make, :rebar3], [], "hexpm", "58f1e425a9e04176f1d30e20116f57c4e90ef0e187552e9741c465bdf4044f70"},
|
||||
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
|
||||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
||||
|
|
@ -10,13 +12,25 @@
|
|||
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
|
||||
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
|
||||
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
|
||||
"finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"},
|
||||
"fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"},
|
||||
"flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"},
|
||||
"gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"},
|
||||
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
|
||||
"google_protos": {:hex, :google_protos, "0.4.0", "93e1be2c1a07517ffed761f69047776caf35e4acd385aac4f5ce4fedd07f3660", [:mix], [{:protobuf, "~> 0.10", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "4c54983d78761a3643e2198adf0f5d40a5a8b08162f3fc91c50faa257f3fa19f"},
|
||||
"googleapis": {:hex, :googleapis, "0.1.0", "13770f3f75f5b863fb9acf41633c7bc71bad788f3f553b66481a096d083ee20e", [:mix], [{:protobuf, "~> 0.12", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1989a7244fd17d3eb5f3de311a022b656c3736b39740db46506157c4604bd212"},
|
||||
"grpc": {:hex, :grpc, "0.11.5", "5dbde9420718b58712779ad98fff1ef50349ca0fa7cc0858ae0f826015068654", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:cowboy, "~> 2.10", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowlib, "~> 2.12", [hex: :cowlib, repo: "hexpm", optional: false]}, {:flow, "~> 1.2", [hex: :flow, repo: "hexpm", optional: false]}, {:googleapis, "~> 0.1.0", [hex: :googleapis, repo: "hexpm", optional: false]}, {:gun, "~> 2.0", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, ">= 0.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mint, "~> 1.5", [hex: :mint, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.14", [hex: :protobuf, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0a5d8673ef16649bef0903bca01c161acfc148e4d269133b6834b2af1f07f45e"},
|
||||
"gun": {:hex, :gun, "2.3.0", "c1eb7be3b5178f6e67edd373f954360de7d7933f2d5a57686affd3b279d76cdf", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "c3bfbbb8f146a6c5ffb2c487f06a3ca4a57e90220b07a1f97eb69a4e7b176035"},
|
||||
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
|
||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"},
|
||||
"lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mint": {:hex, :mint, "1.8.0", "b964eaf4416f2dee2ba88968d52239fca5621b0402b9c95f55a08eb9d74803e9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"oidcc": {:hex, :oidcc, "3.7.2", "2047949832ca7984d6d9c218cc5f23e8096bf50ebb809124d3a01673ee2bfe12", [:mix, :rebar3], [{:igniter, "~> 0.6.3 or ~> 0.7.0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.1", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "e3f1ed91509fdeb31ec8b9de4ecda0e80cb68b463a9f5b7a9ee1ee40e521e445"},
|
||||
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"},
|
||||
|
|
@ -26,12 +40,17 @@
|
|||
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||
"postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"},
|
||||
"protobuf": {:hex, :protobuf, "0.16.0", "d1878725105d49162977cf3408ccc3eac4f3532e26e5a9e250f2c624175d10f6", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "f0d0d3edd8768130f24cc2cfc41320637d32c80110e80d13f160fa699102c828"},
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
|
||||
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
|
||||
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
|
||||
"telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"},
|
||||
"tesla": {:hex, :tesla, "1.16.0", "de77d083aea08ebd1982600693ff5d779d68a4bb835d136a0394b08f69714660", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "eb3bdfc0c6c8a23b4e3d86558e812e3577acff1cb4acb6cfe2da1985a1035b89"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
|
||||
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue