Current section

Files

Jump to
nex_core lib nex cookie.ex
Raw

lib/nex/cookie.ex

defmodule Nex.Cookie do
@moduledoc """
Cookie read/write helpers for Nex actions and API handlers.
Cookies are set by storing pending cookie operations in the process dictionary.
`Nex.Handler` applies them to the `Plug.Conn` response before sending.
## Usage
# In a page action
def login(req) do
token = req.body["token"]
Nex.Cookie.put(:session_id, token, max_age: 86_400, http_only: true)
Nex.redirect("/dashboard")
end
def logout(_req) do
Nex.Cookie.delete(:session_id)
Nex.redirect("/")
end
# Read cookies in mount/1 via params (cookies are in req.cookies for API,
# or use Nex.Cookie.get/1 in page mount)
def mount(params) do
session_id = Nex.Cookie.get(:session_id)
%{logged_in: session_id != nil}
end
## Options for `put/3`
* `:max_age` - Seconds until expiry (default: session cookie)
* `:http_only` - Prevent JS access (default: `true`)
* `:secure` - HTTPS only (default: `false`)
* `:same_site` - `"Strict"`, `"Lax"`, or `"None"` (default: `"Lax"`)
* `:path` - Cookie path (default: `"/"`)
* `:domain` - Cookie domain (default: not set)
"""
@pending_key :nex_pending_cookies
@incoming_key :nex_incoming_cookies
@doc """
Sets a cookie. The cookie is applied to the response by `Nex.Handler`.
"""
def put(name, value, opts \\ []) do
name_str = to_string(name)
value_str = to_string(value)
cookie = %{
name: name_str,
value: value_str,
max_age: Keyword.get(opts, :max_age),
http_only: Keyword.get(opts, :http_only, true),
secure: Keyword.get(opts, :secure, false),
same_site: Keyword.get(opts, :same_site, "Lax"),
path: Keyword.get(opts, :path, "/"),
domain: Keyword.get(opts, :domain),
delete: false
}
pending = Process.get(@pending_key, [])
Process.put(@pending_key, [cookie | pending])
value
end
@doc """
Deletes a cookie by setting its max_age to 0.
"""
def delete(name, opts \\ []) do
name_str = to_string(name)
cookie = %{
name: name_str,
value: "",
max_age: 0,
http_only: Keyword.get(opts, :http_only, true),
secure: Keyword.get(opts, :secure, false),
same_site: Keyword.get(opts, :same_site, "Lax"),
path: Keyword.get(opts, :path, "/"),
domain: Keyword.get(opts, :domain),
delete: true
}
pending = Process.get(@pending_key, [])
Process.put(@pending_key, [cookie | pending])
:ok
end
@doc """
Reads a cookie value from the current request.
Returns `nil` if the cookie is not present.
Cookies are populated by `Nex.Handler` at the start of each request.
"""
def get(name, default \\ nil) do
name_str = to_string(name)
cookies = Process.get(@incoming_key, %{})
Map.get(cookies, name_str, default)
end
@doc """
Returns all cookies from the current request as a map.
"""
def all do
Process.get(@incoming_key, %{})
end
@doc """
Stores incoming cookies from the request in the process dictionary.
Called by `Nex.Handler` at the start of each request.
"""
def load_from_conn(conn) do
conn = Plug.Conn.fetch_cookies(conn)
Process.put(@incoming_key, conn.cookies)
conn
end
@doc """
Applies all pending cookie operations to the `Plug.Conn`.
Called by `Nex.Handler` before sending the response.
"""
def apply_to_conn(conn) do
pending = Process.get(@pending_key, [])
Enum.reduce(pending, conn, fn cookie, conn ->
opts = build_plug_opts(cookie)
Plug.Conn.put_resp_cookie(conn, cookie.name, cookie.value, opts)
end)
end
@doc """
Clears pending cookie state from the process dictionary.
Called by `Nex.Handler` after the response is sent.
"""
def clear_process_state do
Process.delete(@pending_key)
Process.delete(@incoming_key)
end
# Build Plug.Conn.put_resp_cookie/4 options from our cookie map
defp build_plug_opts(cookie) do
opts = [
http_only: cookie.http_only,
secure: cookie.secure,
same_site: cookie.same_site,
path: cookie.path
]
opts = if cookie.max_age != nil, do: Keyword.put(opts, :max_age, cookie.max_age), else: opts
opts = if cookie.domain != nil, do: Keyword.put(opts, :domain, cookie.domain), else: opts
opts
end
end