TypedMiddleware
Bastion.TypedMiddleware — typed middleware pipeline for Bastion.
Each middleware function transforms a TypedConn(s) from one state to the next. The phantom type parameter s makes pipeline ordering a compile-time guarantee: passing TypedConn(Raw) where TypedConn(WithSession) is expected is a type error caught by the March typechecker.
Enforced type signatures:
wrap : HttpServer.Conn -> TypedConn(Raw)
parse_body : TypedConn(Raw) -> TypedConn(Parsed)
load_session : TypedConn(Parsed) -> TypedConn(WithSession)
load_current_user : TypedConn(WithSession) -> TypedConn(WithSession) (soft, never halts)
unwrap : TypedConn(s) -> HttpServer.Conn
with_db : HttpServer.Conn -> HttpServer.Conn (WithDB
compositionality deferred: needs intersection/row types)Auth is a gate, not a pipeline step. require_auth is called inside route handlers and returns Result — callers handle both branches:
require_auth : TypedConn(WithSession) -> Result(TypedConn(Authenticated), Conn)
authenticated : TypedConn(WithSession) -> (TypedConn(Authenticated) -> Conn) -> ConnStandard endpoint pipeline:
fn call(conn) do
wrap(conn)
|> parse_body()
|> load_session(secret)
|> load_current_user(fn id -> MyApp.Accounts.get_user(db, id) end)
|> unwrap()
|> MyApp.Router.route()
endInside a handler that requires authentication:
fn handle_dashboard(conn) do
let tc = wrap(conn) |> parse_body() |> load_session(secret) |> load_current_user(loader)
authenticated(tc, fn tc ->
Dashboard.render(tc) |> unwrap()
end)
endFunctions
Sugar for the common require_auth pattern.
Calls handler with a TypedConn(Authenticated) if auth passes, or returns
the halted redirect conn directly if not. The handler is only ever
called with a genuinely authenticated conn.Full pipeline: parse body, load session, attach DB pool.
Takes and returns a raw HttpServer.Conn. with_db/2 is applied to the
raw conn after unwrapping because WithDB composes at any stage and
intersection/row types are not yet supported.Returns the request body string stored by parse_body.
Get the current user assigned by load_current_user.
Returns None if load_current_user was not called or no user was found.
Callable at any pipeline stage but only meaningful after WithSession.Retrieve the database pool handle attached by with_db.
Look up a value in the loaded session by key.
Session data is encoded as "key=value;key2=value2;..." in assigns.
Returns None if the key is not present or load_session has not run.Returns true if the request Content-Type is application/x-www-form-urlencoded.
Returns true if the request Content-Type is application/json.
Returns true if the request Content-Type is multipart/form-data.
Soft auth check: load the current user from the session into conn assigns.
Reads current_user_id from the session, calls loader(conn, id) to fetch
the user, and assigns the result under "current_user". If no session
user ID is present, or the loader returns an error (e.g. deleted user),
assigns None. Never halts.
Enforced type: TypedConn(WithSession) -> TypedConn(WithSession)
This belongs in the pipeline. For a hard auth gate (halt on failure),
call require_auth/1 inside the route handler after the pipeline runs.Load and decode the session from the signed "_session" cookie.
Stores the raw session payload in assigns under "_session".
If the cookie is absent or tampered with, an empty value is stored.
The secret_key_base is used to verify the cookie signature. A real
implementation uses HMAC-SHA256; this stub extracts the payload after
the first "." separator (sig.payload format).
Enforced type: TypedConn(Parsed) -> TypedConn(WithSession)Parse the request body based on the Content-Type header.
Stores the raw body in assigns under "_body". The parsed form is also
available via get_body_string/1. For JSON bodies, use Request.json_body/1.
Enforced type: TypedConn(Raw) -> TypedConn(Parsed)Store a value in the current session payload string.
Returns the conn with the updated session in assigns.
Call this before writing the session cookie in the response.Auth gate: require an authenticated user. Call inside route handlers, not in the pipeline.
Returns Ok(TypedConn(Authenticated)) if a current user is loaded, or
Error(Conn) with a halted redirect to /login if not.
The Error branch carries a plain Conn (not TypedConn) because it is a
finished response that needs no further type tracking.
Use with the `with` expression or the `authenticated/2` helper:
with Ok(tc) <- require_auth(tc) do
...
else
Error(conn) -> conn
endSet the current user ID in conn assigns (used by auth generators and log_in helpers to mark a user as logged in for the current request).
Exit the typed middleware pipeline back to a raw HttpServer.Conn. Works at any pipeline stage thanks to the polymorphic phantom s.
Standard web pipeline: parse body then load session.
Takes and returns a raw HttpServer.Conn so existing call sites keep
working. Internally runs the typed pipeline and unwraps at the end.Attach a Depot database pool handle to the conn.
Stores the pool reference under "_db_pool" in assigns so downstream
handlers can retrieve it with TypedMiddleware.get_db/1.
Intended type: Conn(a) -> Conn(a & WithDB)
Example:
conn |> TypedMiddleware.with_db(MyApp.Repo.pool())
-- later in handler:
let pool = TypedMiddleware.get_db(conn)