chore: upgrade to Elixir 1.19.5 / OTP 28, add Gleam to dev shell

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-09 20:01:38 +02:00
parent 35f29f42e3
commit afcbba3fc7
7 changed files with 80894 additions and 47260 deletions

View file

@ -0,0 +1,13 @@
defmodule CentralcloudMy.Application do
use Application
@impl true
def start(_type, _args) do
children = [
CentralcloudMy.Endpoint
]
opts = [strategy: :one_for_one, name: CentralcloudMy.Supervisor]
Supervisor.start_link(children, opts)
end
end

View file

@ -0,0 +1,88 @@
defmodule CentralcloudMy.SessionController do
use Phoenix.Controller, namespace: CentralcloudMy
import Plug.Conn
def new(conn, _params) do
conn
|> put_resp_content_type("text/html")
|> send_resp(200, login_page(conn))
end
def create(conn, %{"username" => _username, "password" => _password}) do
# TODO: validate against DR portal customer_users or Authentik
conn
|> put_flash(:error, "Invalid credentials")
|> redirect(to: "/login")
end
def delete(conn, _params) do
conn
|> clear_session()
|> redirect(to: "/login")
end
def oidc_callback(conn, params) do
# TODO: exchange code with Authentik, set session
_ = params
conn
|> put_flash(:info, "SSO login coming soon")
|> redirect(to: "/login")
end
defp login_page(conn) do
csrf = Phoenix.Controller.get_csrf_token()
flash_error = Phoenix.Flash.get(conn.assigns[:flash] || %{}, :error)
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="csrf-token" content="#{csrf}"/>
<title>Sign in CentralCloud</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0f172a; color: #e2e8f0; display: flex;
align-items: center; justify-content: center; min-height: 100vh; }
.card { background: #1e293b; border: 1px solid #334155; border-radius: 12px;
padding: 2.5rem; width: 100%; max-width: 380px; }
h1 { font-size: 1.4rem; font-weight: 700; color: #38bdf8; margin-bottom: 0.25rem; }
p.sub { color: #94a3b8; font-size: 0.85rem; margin-bottom: 2rem; }
label { display: block; font-size: 0.8rem; color: #94a3b8; margin-bottom: 0.3rem; }
input { width: 100%; padding: 0.6rem 0.8rem; background: #0f172a; border: 1px solid #475569;
border-radius: 6px; color: #e2e8f0; font-size: 0.9rem; margin-bottom: 1rem; }
button { width: 100%; padding: 0.7rem; background: #0ea5e9; border: none;
border-radius: 6px; color: #fff; font-weight: 600; cursor: pointer; font-size: 0.95rem; }
button:hover { background: #38bdf8; }
.divider { text-align: center; color: #475569; font-size: 0.8rem; margin: 1rem 0; }
.sso { width: 100%; padding: 0.7rem; background: #1e293b; border: 1px solid #475569;
border-radius: 6px; color: #e2e8f0; font-size: 0.9rem; cursor: pointer; text-align: center;
text-decoration: none; display: block; margin-top: 0.5rem; }
.sso:hover { border-color: #38bdf8; }
.error { background: #7f1d1d; border: 1px solid #dc2626; border-radius: 6px;
padding: 0.6rem 0.8rem; font-size: 0.85rem; margin-bottom: 1rem; }
</style>
</head>
<body>
<div class="card">
<h1>CentralCloud</h1>
<p class="sub">Sign in to your account</p>
#{if flash_error, do: "<div class='error'>#{flash_error}</div>", else: ""}
<form method="post" action="/login">
<input type="hidden" name="_csrf_token" value="#{csrf}"/>
<label>Username</label>
<input type="text" name="username" autocomplete="username" required/>
<label>Password</label>
<input type="password" name="password" autocomplete="current-password" required/>
<button type="submit">Sign in</button>
</form>
<div class="divider">or</div>
<a class="sso" href="/auth/callback">🔐 Sign in with CentralCloud SSO</a>
</div>
</body>
</html>
"""
end
end

View file

@ -0,0 +1,28 @@
defmodule CentralcloudMy.Endpoint do
use Phoenix.Endpoint, otp_app: :centralcloud_my
socket "/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [session: {CentralcloudMy.Router, :session_opts, []}]]
plug Plug.Static,
at: "/",
from: {:centralcloud_my, "priv/static"},
gzip: false
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, store: :cookie,
key: "_centralcloud_my_session",
signing_salt: "my_salt_2026",
same_site: "Lax"
plug CentralcloudMy.Router
end

View file

@ -0,0 +1,55 @@
defmodule CentralcloudMy.Layouts do
use Phoenix.Component
def render("root.html", assigns) do
~H"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="csrf-token" content={Phoenix.Controller.get_csrf_token()}/>
<title>CentralCloud</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0f172a; color: #e2e8f0; min-height: 100vh; }
nav { background: #1e293b; padding: 1rem 2rem; display: flex;
align-items: center; justify-content: space-between; border-bottom: 1px solid #334155; }
nav .brand { font-weight: 700; font-size: 1.1rem; color: #38bdf8; }
nav a { color: #94a3b8; text-decoration: none; margin-left: 1.5rem; font-size: 0.9rem; }
nav a:hover { color: #e2e8f0; }
main { padding: 2rem; max-width: 1200px; margin: 0 auto; }
.flash { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; }
.flash-info { background: #1d4ed8; }
.flash-error { background: #dc2626; }
</style>
{live_title_tag(assigns[:page_title] || "My CentralCloud")}
</head>
<body>
<nav>
<span class="brand"> CentralCloud</span>
<div>
<a href="/">Dashboard</a>
<a href="/dr">DR Status</a>
<a href="/billing">Billing</a>
<a href="/support">Support</a>
<a href="/logout" data-method="delete">Sign out</a>
</div>
</nav>
<main>
<p :if={msg = Phoenix.Flash.get(@flash, :info)} class="flash flash-info">{msg}</p>
<p :if={msg = Phoenix.Flash.get(@flash, :error)} class="flash flash-error">{msg}</p>
{@inner_content}
</main>
</body>
</html>
"""
end
def render("app.html", assigns) do
~H"""
{@inner_content}
"""
end
end

View file

@ -0,0 +1,40 @@
defmodule CentralcloudMy.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
@session_opts [store: :cookie, key: "_centralcloud_my_session", signing_salt: "my_salt_2026"]
def session_opts, do: @session_opts
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {CentralcloudMy.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :auth do
plug CentralcloudMy.Plugs.RequireAuth
end
# Public routes
scope "/", CentralcloudMy do
pipe_through :browser
get "/login", SessionController, :new
post "/login", SessionController, :create
delete "/logout", SessionController, :delete
get "/auth/callback", SessionController, :oidc_callback
end
# Authenticated customer routes
scope "/", CentralcloudMy do
pipe_through [:browser, :auth]
live "/", DashboardLive, :index
live "/dr", ReplicationLive, :index
live "/billing", BillingLive, :index
live "/support", SupportLive, :index
end
end

File diff suppressed because one or more lines are too long

View file

@ -13,8 +13,9 @@
in {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
elixir
erlang
elixir_1_19 # 1.19.5 — latest stable; upgrade to 1.20 when nixpkgs picks it up
erlang_28 # OTP 28.5
gleam # ready when we want type-safe core modules
nodejs_22 # for Phoenix asset pipeline
inotify-tools # LiveReload on Linux
];