March Docs

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) -> Conn

Standard 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()
end

Inside 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)
end

Functions

fnauthenticatedauthenticated(tc : TypedConn(WithSession), handler) : Conn#

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.
fnfull_pipelinefull_pipeline(conn, secret, pool)#

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.
fnget_body_stringget_body_string(conn)#

Returns the request body string stored by parse_body.

fnget_current_userget_current_user(tc)#

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.
fnget_dbget_db(conn)#

Retrieve the database pool handle attached by with_db.

fnget_session_valueget_session_value(conn, key)#

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.
fnis_form_requestis_form_request(conn)#

Returns true if the request Content-Type is application/x-www-form-urlencoded.

fnis_json_requestis_json_request(conn)#

Returns true if the request Content-Type is application/json.

fnis_multipart_requestis_multipart_request(conn)#

Returns true if the request Content-Type is multipart/form-data.

fnload_current_userload_current_user(tc : TypedConn(WithSession), loader) : TypedConn(WithSession)#

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.
fnload_sessionload_session(tc : TypedConn(Parsed), secret_key_base : String) : TypedConn(WithSession)#

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)
fnparse_bodyparse_body(tc : TypedConn(Raw)) : TypedConn(Parsed)#

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)
fnput_session_valueput_session_value(conn, key, value)#

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.
fnrequire_authrequire_auth(tc : TypedConn(WithSession)) : Result(TypedConn(Authenticated), Conn)#

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
  end
fnset_current_user_idset_current_user_id(conn, user_id)#

Set 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).

fnunwrapunwrap(tc)#

Exit the typed middleware pipeline back to a raw HttpServer.Conn. Works at any pipeline stage thanks to the polymorphic phantom s.

fnweb_pipelineweb_pipeline(conn, secret)#

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.
fnwith_dbwith_db(conn, pool)#

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)
fnwrapwrap(conn : Conn) : TypedConn(Raw)#

Enter the typed middleware pipeline. Tags a raw HttpServer.Conn with the initial Raw state so it can flow through the typed stages.