berrypod/lib/berrypod_web/controllers/cart_controller.ex
jamey 0c2d4ac406
Some checks failed
deploy / deploy (push) Failing after 8m33s
add rate limiting and HSTS for security hardening
- Add Hammer library for rate limiting with ETS backend
- Rate limit login (5/min), magic link (3/min), newsletter (10/min), API (60/min)
- Add themed 429 error page using bare shop styling
- Enable HSTS in production with rewrite_on for Fly proxy
- Add security hardening plan to docs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-08 08:58:43 +00:00

111 lines
2.8 KiB
Elixir

defmodule BerrypodWeb.CartController do
@moduledoc """
Cart controller handling both JSON API persistence and HTML form fallbacks.
The JSON `update/2` action is called by a JS hook after each LiveView cart
change. The HTML actions (`add/2`, `remove/2`, `update_item/2`) provide
no-JS fallbacks via plain form POST + redirect.
"""
use BerrypodWeb, :controller
alias Berrypod.Cart
plug BerrypodWeb.Plugs.RateLimit, [type: :api] when action == :update
@doc """
Updates the cart in session (JSON API for JS hook).
"""
def update(conn, %{"items" => items}) when is_list(items) do
cart_items = Cart.deserialize(items)
conn
|> Cart.put_in_session(cart_items)
|> json(%{ok: true})
end
def update(conn, _params) do
conn
|> put_status(:bad_request)
|> json(%{error: "Invalid cart data"})
end
@doc """
Adds an item to cart via form POST (no-JS fallback).
"""
def add(conn, %{"variant_id" => variant_id, "quantity" => qty_str}) do
quantity = parse_quantity(qty_str)
cart = Cart.get_from_session(get_session(conn))
cart = Cart.add_item(cart, variant_id, quantity)
conn
|> Cart.put_in_session(cart)
|> put_flash(:info, "Added to basket")
|> redirect(to: ~p"/cart")
end
def add(conn, _params) do
conn
|> put_flash(:error, "Could not add item to basket")
|> redirect(to: ~p"/cart")
end
@doc """
Removes an item from cart via form POST (no-JS fallback).
"""
def remove(conn, %{"variant_id" => variant_id}) do
cart = Cart.get_from_session(get_session(conn))
cart = Cart.remove_item(cart, variant_id)
conn
|> Cart.put_in_session(cart)
|> put_flash(:info, "Removed from basket")
|> redirect(to: ~p"/cart")
end
@doc """
Updates item quantity via form POST (no-JS fallback).
"""
def update_item(conn, %{"variant_id" => variant_id, "quantity" => qty_str}) do
quantity = parse_update_quantity(qty_str)
cart = Cart.get_from_session(get_session(conn))
cart = Cart.update_quantity(cart, variant_id, quantity)
conn
|> Cart.put_in_session(cart)
|> redirect(to: ~p"/cart")
end
@doc """
Updates delivery country via form POST (no-JS fallback).
"""
def update_country(conn, %{"country" => code}) when is_binary(code) and code != "" do
conn
|> put_session("country_code", code)
|> redirect(to: ~p"/cart")
end
def update_country(conn, _params) do
redirect(conn, to: ~p"/cart")
end
defp parse_quantity(str) when is_binary(str) do
case Integer.parse(str) do
{qty, _} when qty > 0 -> qty
_ -> 1
end
end
defp parse_quantity(_), do: 1
# Allows 0 and negative values so Cart.update_quantity can remove items
defp parse_update_quantity(str) when is_binary(str) do
case Integer.parse(str) do
{qty, _} -> qty
:error -> 1
end
end
defp parse_update_quantity(_), do: 1
end