Commit Diff


commit - 14823e8a735acd0afe8936642bc20c7d20904897
commit + b78885c4292ba35b9c15abc9f312edb1ffb2d927
blob - 43b9f8002f4fea4dd167cf6ad65cf6fb2d29a7a9
blob + 0397dccbafebf8d743df2307847609d503ff1f35
--- README.md
+++ README.md
@@ -15,7 +15,7 @@ mix release
 ### Create a short link
 
 ```sh
-curl -X POST http://localhost:8721/curtail \
+curl -X POST http://localhost:4000/curtail \
   -H "Authorization: Bearer <token>" \
   -H "Content-Type: application/json" \
   -d '{"url": "https://example.com/very/long/path"}'
@@ -24,7 +24,7 @@ curl -X POST http://localhost:8721/curtail \
 Optionally provide a custom code:
 
 ```sh
-curl -X POST http://localhost:8721/curtail \
+curl -X POST http://localhost:4000/curtail \
   -H "Authorization: Bearer <token>" \
   -H "Content-Type: application/json" \
   -d '{"url": "https://example.com/very/long/path", "code": "my-code"}'
@@ -33,14 +33,14 @@ curl -X POST http://localhost:8721/curtail \
 ### List all short links
 
 ```sh
-curl http://localhost:8721/history \
+curl http://localhost:4000/history \
   -H "Authorization: Bearer <token>"
 ```
 
 Returns a table ordered by creation time. Request JSON instead with an `Accept` header:
 
 ```sh
-curl http://localhost:8721/history \
+curl http://localhost:4000/history \
   -H "Authorization: Bearer <token>" \
   -H "Accept: application/json"
 ```
@@ -55,10 +55,50 @@ GET /s/<code>
 
 ## Configuration
 
-| Variable   | Description                          |
-|------------|--------------------------------------|
-| `PORT`     | Port to listen on (default: `8721`)  |
-| `HOST`     | Host to bind to (default: `127.0.0.1`) |
-| `BASE_URL` | Base URL used when generating links  |
-| `AUTH_TOKEN` | Bearer token for the create endpoint |
-| `DB_PATH`  | Path to the SQLite database file     |
+Runtime configuration is resolved with the following precedence:
+
+1. Environment variables
+2. `/etc/shirts.conf` (a native Elixir config file; see `config/shirts.conf.example`)
+3. Built-in defaults
+
+| Variable     | Description                          | Default        |
+|--------------|--------------------------------------|----------------|
+| `PORT`       | Port to listen on                    | `4000`         |
+| `HOST`       | Host/interface to bind to            | `127.0.0.1`    |
+| `BASE_URL`   | Base URL used when generating links   | `http://localhost` |
+| `AUTH_TOKEN` | Bearer token for the create endpoint | _required_     |
+| `DB_PATH`    | Path to the SQLite database file     | `data.db`      |
+
+In production, `AUTH_TOKEN` must be set either via the `AUTH_TOKEN` environment
+variable or in `/etc/shirts.conf`, otherwise the application refuses to start.
+
+### Config file
+
+`/etc/shirts.conf` is an optional native Elixir config file. Copy the example
+and edit it:
+
+```sh
+cp config/shirts.conf.example /etc/shirts.conf
+```
+
+```elixir
+import Config
+
+config :shirts,
+  port: 4000,
+  host: "127.0.0.1",
+  auth_token: "change-me",
+  db_path: "/var/db/shirts/data.db",
+  base_url: "https://shirts.example"
+```
+
+### Release settings
+
+The following are read natively by the Elixir release runtime (not by the
+config file above); set them as environment variables:
+
+| Variable              | Description                          |
+|-----------------------|--------------------------------------|
+| `RELEASE_COOKIE`      | Distribution cookie for the BEAM node |
+| `RELEASE_NODE`        | Node name, e.g. `shirts@127.0.0.1`   |
+| `RELEASE_DISTRIBUTION`| `name` / `sname` for node distribution |
blob - 4c71fded6ba028239409f1abb6fd337c85dd2d66 (mode 644)
blob + /dev/null
--- config/config.exs
+++ /dev/null
@@ -1,8 +0,0 @@
-import Config
-
-config :shirts,
-  port: System.get_env("PORT", "4000") |> String.to_integer(),
-  host: System.get_env("HOST", "127.0.0.1"),
-  auth_token: System.get_env("AUTH_TOKEN", "changeme"),
-  db_path: System.get_env("DB_PATH", "data.db"),
-  base_url: System.get_env("BASE_URL", "http://localhost")
blob - 1b7aebe30f53e1899976c373ef9a2ed6474097ec
blob + 42705acc3aa5190bce7f51bfd72122576495acdd
--- config/runtime.exs
+++ config/runtime.exs
@@ -1,40 +1,37 @@
 import Config
 
-if config_env() == :prod do
-  conf =
-    case File.read("/etc/shirts.conf") do
-      {:ok, contents} ->
-        contents
-        |> String.split("\n")
-        |> Enum.reduce(%{}, fn line, acc ->
-          line = String.trim(line)
+prod? = config_env() == :prod
 
-          case String.split(line, "=", parts: 2) do
-            [k, v] when byte_size(k) > 0 ->
-              Map.put(acc, k, v |> String.trim("\"") |> String.trim("'"))
-
-            _ ->
-              acc
-          end
-        end)
-
-      {:error, _} ->
-        %{}
-    end
-
-  get = fn key, default ->
-    case System.get_env(key) do
-      nil -> Map.get(conf, key, default)
-      val -> val
-    end
+shirts =
+  if prod? and File.exists?("/etc/shirts.conf") do
+    Access.get(Config.Reader.read!("/etc/shirts.conf"), :shirts, [])
+  else
+    []
   end
 
-  config :shirts,
-    port: get.("PORT", "4000") |> String.to_integer(),
-    host: get.("HOST", "127.0.0.1"),
-    auth_token:
-      System.get_env("AUTH_TOKEN") || Map.get(conf, "AUTH_TOKEN") ||
-        raise("AUTH_TOKEN not set in environment or /etc/shirts.conf"),
-    db_path: get.("DB_PATH", "data.db"),
-    base_url: get.("BASE_URL", "http://localhost")
+get = fn env_name, file_key, default ->
+  case System.get_env(env_name) do
+    nil -> Access.get(shirts, file_key, default)
+    val -> val
+  end
 end
+
+port =
+  case get.("PORT", :port, 4000) do
+    p when is_integer(p) -> p
+    p when is_binary(p) -> String.to_integer(p)
+  end
+
+auth_token =
+  case get.("AUTH_TOKEN", :auth_token, nil) do
+    nil when prod? -> raise "AUTH_TOKEN not set in environment or /etc/shirts.conf"
+    nil -> "changeme"
+    token -> token
+  end
+
+config :shirts,
+  port: port,
+  host: get.("HOST", :host, "127.0.0.1"),
+  auth_token: auth_token,
+  db_path: get.("DB_PATH", :db_path, "data.db"),
+  base_url: get.("BASE_URL", :base_url, "http://localhost")
blob - /dev/null
blob + 77a91511d57b69e03aaa2a1d662145cd81d167f0 (mode 644)
--- /dev/null
+++ config/shirts.conf.example
@@ -0,0 +1,8 @@
+import Config
+
+config :shirts,
+  port: 4000,
+  host: "127.0.0.1",
+  auth_token: "change-me",
+  db_path: "/var/db/shirts/data.db",
+  base_url: "https://shirts.example"
blob - 0f11e17583ed46319f6c76600318bd485344c0e0
blob + 836ad699c5e8d64f9e5d08637c4ca8bcfb34473f
--- justfile
+++ justfile
@@ -3,31 +3,24 @@ export BASE_URL := "http://localhost"
 export PORT := "4000"
 export DB_PATH := "shirts.db"
 
-# List available recipes
 default:
     @just --list
 
-# Fetch deps and compile
 build:
     mix deps.get
     mix compile
 
-# Run the server
 run:
     mix run --no-halt
 
-# Format sources
 format:
     mix format
 
-# Lint with credo
 lint:
     mix credo --strict
 
-# Type-check with dialyzer
 check:
     mix dialyzer
 
-# Remove build artifacts
 clean:
-    mix clean
+    mix clean
\ No newline at end of file
blob - 092700072553550e2204e3d3e869b0a773467f86
blob + deab86b4743b64738b1bafa0be05ffd30a70941b
--- lib/shortener/router.ex
+++ lib/shortener/router.ex
@@ -1,9 +1,9 @@
 defmodule Shirts.Router do
   @moduledoc """
   Plug router:
-    POST /curtail — create a short link
-    GET  /history — list all short links
-    GET  /s/:code — redirect to the original URL
+    POST /curtail - create a short link
+    GET  /history - list all short links
+    GET  /s/:code - redirect to the original URL
   """
 
   use Plug.Router
@@ -13,12 +13,6 @@ defmodule Shirts.Router do
   plug(Plug.Parsers, parsers: [:json], json_decoder: Jason)
   plug(:dispatch)
 
-  # ---------------------------------------------------------------------------
-  # POST /l/curtail
-  # Body (JSON): {"url": "https://example.com/very/long/path"}
-  # Optional:    {"url": "...", "code": "my-custom-code"}
-  # Header:      Authorization: Bearer <token>
-  # ---------------------------------------------------------------------------
   post "/curtail" do
     with :ok <- check_auth(conn),
          {:ok, url} <- fetch_url(conn),
@@ -43,9 +37,6 @@ defmodule Shirts.Router do
     end
   end
 
-  # ---------------------------------------------------------------------------
-  # GET /history  — list all short links
-  # ---------------------------------------------------------------------------
   get "/history" do
     with :ok <- check_auth(conn),
          {:ok, shirts} <- Shirts.DB.list() do
@@ -59,9 +50,6 @@ defmodule Shirts.Router do
     end
   end
 
-  # ---------------------------------------------------------------------------
-  # GET /s/:code  — redirect
-  # ---------------------------------------------------------------------------
   get "/s/:code" do
     case Shirts.DB.lookup(code) do
       {:ok, url} ->
@@ -77,17 +65,10 @@ defmodule Shirts.Router do
     end
   end
 
-  # ---------------------------------------------------------------------------
-  # Catch-all
-  # ---------------------------------------------------------------------------
   match _ do
     json(conn, 404, %{error: "not found"})
   end
 
-  # ---------------------------------------------------------------------------
-  # Helpers
-  # ---------------------------------------------------------------------------
-
   defp check_auth(conn) do
     expected = "Bearer #{Application.fetch_env!(:shirts, :auth_token)}"