feat: scaffold centralcloud Elixir umbrella (my. + ops. + core)
- Umbrella root with apps/centralcloud_{my,ops,core}
- centralcloud_core: HostBill Admin API client, DR Portal API client
- centralcloud_my: Phoenix LiveView app for my.centralcloud.com
- centralcloud_ops: Phoenix app for ops.centralcloud.com
- Shared config: Authentik OIDC, HostBill, DR Portal endpoints
- README with quick start and required env vars
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
commit
009d644a15
11 changed files with 305 additions and 0 deletions
49
README.md
Normal file
49
README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# centralcloud
|
||||
|
||||
Elixir umbrella app for CentralCloud customer and staff portals.
|
||||
|
||||
## Apps
|
||||
|
||||
| App | URL | Audience |
|
||||
|-----|-----|----------|
|
||||
| `centralcloud_my` | `my.centralcloud.com` | Customers — unified dashboard |
|
||||
| `centralcloud_ops` | `ops.centralcloud.com` | Staff — ops and on-call |
|
||||
| `centralcloud_core` | (library) | Shared: HostBill client, DR API client, OIDC |
|
||||
|
||||
`my.centralcloud.com` will also serve `www.centralcloud.com` (marketing pages) when DNS is repointed.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Install Elixir 1.17+ and Phoenix
|
||||
mix local.hex --force
|
||||
mix archive.install hex phx_new --force
|
||||
|
||||
# Install deps
|
||||
mix deps.get
|
||||
|
||||
# Run my. portal (port 4001)
|
||||
cd apps/centralcloud_my
|
||||
mix phx.server
|
||||
|
||||
# Run ops portal (port 4000)
|
||||
cd apps/centralcloud_ops
|
||||
mix phx.server
|
||||
```
|
||||
|
||||
## Required env vars
|
||||
|
||||
```bash
|
||||
MY_SECRET_KEY_BASE=... # mix phx.gen.secret
|
||||
OPS_SECRET_KEY_BASE=...
|
||||
HOSTBILL_API_ID=... # from HostBill admin API keys
|
||||
HOSTBILL_API_KEY=...
|
||||
DR_PORTAL_URL=https://dr.centralcloud.com
|
||||
DR_PORTAL_API_KEY=...
|
||||
OIDC_CLIENT_ID=... # Authentik application client ID
|
||||
OIDC_CLIENT_SECRET=...
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
See `../dr-repo/docs/adr/001-unified-portal-architecture.md`
|
||||
30
apps/centralcloud_core/lib/centralcloud_core/dr_portal.ex
Normal file
30
apps/centralcloud_core/lib/centralcloud_core/dr_portal.ex
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
defmodule CentralcloudCore.DrPortal do
|
||||
@moduledoc """
|
||||
DR Portal API client (Go/Gin backend at dr.centralcloud.com).
|
||||
Used by my.centralcloud.com to fetch VM/replication status for a customer.
|
||||
"""
|
||||
|
||||
@spec get_customer_vms(String.t()) :: {:ok, list(map())} | {:error, term()}
|
||||
def get_customer_vms(customer_id) do
|
||||
get("/api/v1/customers/#{customer_id}/vms")
|
||||
end
|
||||
|
||||
@spec get_replication_status(String.t()) :: {:ok, map()} | {:error, term()}
|
||||
def get_replication_status(customer_id) do
|
||||
get("/api/v1/customers/#{customer_id}/replication")
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Private
|
||||
|
||||
defp get(path) do
|
||||
cfg = Application.fetch_env!(:centralcloud_core, :dr_portal)
|
||||
url = cfg[:base_url] <> path
|
||||
|
||||
case Req.get(url, headers: [{"x-api-key", cfg[:api_key]}]) do
|
||||
{:ok, %{status: 200, body: body}} -> {:ok, body}
|
||||
{:ok, %{status: status}} -> {:error, {:http, status}}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
50
apps/centralcloud_core/lib/centralcloud_core/hostbill.ex
Normal file
50
apps/centralcloud_core/lib/centralcloud_core/hostbill.ex
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
defmodule CentralcloudCore.HostBill do
|
||||
@moduledoc """
|
||||
HostBill Admin API client.
|
||||
|
||||
Uses the Admin API at /admin/api.php (version 2026-04-21).
|
||||
All requests are authenticated with api_id + api_key query params.
|
||||
"""
|
||||
|
||||
@spec get_client(integer()) :: {:ok, map()} | {:error, term()}
|
||||
def get_client(client_id) do
|
||||
call("getClientsDetails", %{clientid: client_id})
|
||||
end
|
||||
|
||||
@spec get_clients() :: {:ok, list(map())} | {:error, term()}
|
||||
def get_clients do
|
||||
call("getClients", %{})
|
||||
end
|
||||
|
||||
@spec get_client_services(integer()) :: {:ok, list(map())} | {:error, term()}
|
||||
def get_client_services(client_id) do
|
||||
call("getClientsProducts", %{clientid: client_id})
|
||||
end
|
||||
|
||||
@spec get_invoices(integer()) :: {:ok, list(map())} | {:error, term()}
|
||||
def get_invoices(client_id) do
|
||||
call("getInvoices", %{clientid: client_id})
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Private
|
||||
|
||||
defp call(action, params) do
|
||||
cfg = Application.fetch_env!(:centralcloud_core, :hostbill)
|
||||
|
||||
query =
|
||||
Map.merge(params, %{
|
||||
api_id: cfg[:api_id],
|
||||
api_key: cfg[:api_key],
|
||||
call: action
|
||||
})
|
||||
|
||||
url = cfg[:base_url] <> "/admin/api.php"
|
||||
|
||||
case Req.post(url, form: query) do
|
||||
{:ok, %{status: 200, body: body}} -> {:ok, body}
|
||||
{:ok, %{status: status}} -> {:error, {:http, status}}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
29
apps/centralcloud_core/mix.exs
Normal file
29
apps/centralcloud_core/mix.exs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
defmodule CentralcloudCore.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :centralcloud_core,
|
||||
version: "0.1.0",
|
||||
build_path: "../../_build",
|
||||
config_path: "../../config/config.exs",
|
||||
deps_path: "../../deps",
|
||||
lockfile: "../../mix.lock",
|
||||
elixir: "~> 1.17",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[extra_applications: [:logger]]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:req, "~> 0.5"}, # HTTP client for HostBill, DR API, OnCall
|
||||
{:jason, "~> 1.4"}, # JSON
|
||||
{:jose, "~> 1.11"} # JWT / OIDC token verification
|
||||
]
|
||||
end
|
||||
end
|
||||
44
apps/centralcloud_my/mix.exs
Normal file
44
apps/centralcloud_my/mix.exs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
defmodule CentralcloudMy.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :centralcloud_my,
|
||||
version: "0.1.0",
|
||||
build_path: "../../_build",
|
||||
config_path: "../../config/config.exs",
|
||||
deps_path: "../../deps",
|
||||
lockfile: "../../mix.lock",
|
||||
elixir: "~> 1.17",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[
|
||||
mod: {CentralcloudMy.Application, []},
|
||||
extra_applications: [:logger, :runtime_tools]
|
||||
]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:centralcloud_core, in_umbrella: true},
|
||||
{:phoenix, "~> 1.7"},
|
||||
{:phoenix_live_view, "~> 1.0"},
|
||||
{:phoenix_html, "~> 4.0"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:bandit, "~> 1.2"},
|
||||
{:ecto_sql, "~> 3.10"},
|
||||
{:postgrex, "~> 0.17"},
|
||||
{:swoosh, "~> 1.3"}, # email
|
||||
{:finch, "~> 0.13"},
|
||||
{:telemetry_metrics, "~> 1.0"},
|
||||
{:telemetry_poller, "~> 1.0"},
|
||||
{:jason, "~> 1.4"},
|
||||
{:dns_cluster, "~> 0.1.1"},
|
||||
{:oidcc, "~> 3.2"} # OIDC client for Authentik SSO
|
||||
]
|
||||
end
|
||||
end
|
||||
37
apps/centralcloud_ops/mix.exs
Normal file
37
apps/centralcloud_ops/mix.exs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
defmodule CentralcloudOps.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :centralcloud_ops,
|
||||
version: "0.1.0",
|
||||
build_path: "../../_build",
|
||||
config_path: "../../config/config.exs",
|
||||
deps_path: "../../deps",
|
||||
lockfile: "../../mix.lock",
|
||||
elixir: "~> 1.17",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[
|
||||
mod: {CentralcloudOps.Application, []},
|
||||
extra_applications: [:logger, :runtime_tools]
|
||||
]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:centralcloud_core, in_umbrella: true},
|
||||
{:phoenix, "~> 1.7"},
|
||||
{:phoenix_live_view, "~> 1.0"},
|
||||
{:phoenix_html, "~> 4.0"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:bandit, "~> 1.2"},
|
||||
{:jason, "~> 1.4"},
|
||||
{:oidcc, "~> 3.2"}
|
||||
]
|
||||
end
|
||||
end
|
||||
30
config/config.exs
Normal file
30
config/config.exs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import Config
|
||||
|
||||
config :centralcloud_my, CentralcloudMy.Endpoint,
|
||||
url: [host: "my.centralcloud.com"],
|
||||
http: [port: 4001],
|
||||
secret_key_base: System.fetch_env!("MY_SECRET_KEY_BASE")
|
||||
|
||||
config :centralcloud_ops, CentralcloudOps.Endpoint,
|
||||
url: [host: "ops.centralcloud.com"],
|
||||
http: [port: 4000],
|
||||
secret_key_base: System.fetch_env!("OPS_SECRET_KEY_BASE")
|
||||
|
||||
# HostBill Admin API
|
||||
config :centralcloud_core, :hostbill,
|
||||
base_url: System.get_env("HOSTBILL_URL", "https://portal.centralcloud.com"),
|
||||
api_id: System.fetch_env!("HOSTBILL_API_ID"),
|
||||
api_key: System.fetch_env!("HOSTBILL_API_KEY")
|
||||
|
||||
# DR Portal API
|
||||
config :centralcloud_core, :dr_portal,
|
||||
base_url: System.get_env("DR_PORTAL_URL", "https://dr.centralcloud.com"),
|
||||
api_key: System.fetch_env!("DR_PORTAL_API_KEY")
|
||||
|
||||
# Authentik OIDC
|
||||
config :centralcloud_core, :oidc,
|
||||
issuer: System.get_env("AUTHENTIK_ISSUER", "https://sso.centralcloud.com/application/o/centralcloud/"),
|
||||
client_id: System.fetch_env!("OIDC_CLIENT_ID"),
|
||||
client_secret: System.fetch_env!("OIDC_CLIENT_SECRET")
|
||||
|
||||
import_config "#{config_env()}.exs"
|
||||
15
config/dev.exs
Normal file
15
config/dev.exs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import Config
|
||||
|
||||
config :centralcloud_my, CentralcloudMy.Endpoint,
|
||||
code_reloader: true,
|
||||
debug_errors: true,
|
||||
check_origin: false,
|
||||
watchers: []
|
||||
|
||||
config :centralcloud_ops, CentralcloudOps.Endpoint,
|
||||
code_reloader: true,
|
||||
debug_errors: true,
|
||||
check_origin: false
|
||||
|
||||
config :logger, :console, format: "[$level] $message\n"
|
||||
config :phoenix, :stacktrace_depth, 20
|
||||
4
config/prod.exs
Normal file
4
config/prod.exs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import Config
|
||||
|
||||
config :logger, level: :info
|
||||
config :phoenix, :serve_endpoints, true
|
||||
1
config/test.exs
Normal file
1
config/test.exs
Normal file
|
|
@ -0,0 +1 @@
|
|||
import Config
|
||||
16
mix.exs
Normal file
16
mix.exs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
defmodule Centralcloud.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
apps_path: "apps",
|
||||
version: "0.1.0",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[]
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue