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