marcus06 aa446cdfa5 feat(sca): add per-user spend cap override (#53)
* feat(sca): add per-user spend cap override

* feat(sca): add admin endpoint to set per-user order cap

- PATCH /api/v1/users/{id}/order-cap (RequireAdmin / X-Admin-Key) sets or
  clears User.MaxOrderCents; null clears (revert to global), >=0 sets,
  negative rejected 400 INVALID_CAP at the write boundary
- harden ConvertCart: a negative override falls back to the global cap with
  a slog.Warn (mirrors loadMaxOrderCents) so a bad DB write cannot silently
  void the fraud control
- emit user_order_cap_set audit event (set/cleared)
- tests: admin handler table + RequireAdmin gate router test + negative
  read-path fallback case

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(api): drop orphan comment referencing nonexistent request struct

The paragraph above SetUserOrderCap documented a setUserOrderCapRequest
struct that does not exist; the handler decodes into a raw map instead.
The raw-map rationale already lives inline at the decode site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:56:38 +02:00
2026-04-09 19:05:29 +02:00
2026-05-23 21:43:30 +02:00
2026-04-09 19:05:29 +02:00
2026-04-09 17:39:59 +02:00

Terminal Coffee Shop

ssh sshops.uk

demo

https://github.com/user-attachments/assets/daf895ac-2c01-4323-bf27-8d7829b9ddd5

it's a coffee shop but in your terminal. you ssh in, browse the menu, chuck stuff in your cart, put in your address and card, and buy coffee without ever touching a browser. your ssh key is your account so there's no login or signup or any of that.

made it because i thought it would be fun to see if you could build a real e-commerce flow that lives entirely in a terminal. turns out you can. heavily inspired by terminal.shop.

what it actually does

ssh in and you get a TUI. browse coffee, add to cart, save addresses, save cards. checkout charges your card via stripe and creates a real order. addresses get validated against shippo (US) or bring (Norway) before they're allowed in. cards are tokenized server side so the secret key never touches your terminal session. adding cards through the tui is coming soon, i just need to register a business and i have no money. webhooks from stripe reconcile order state if anything goes weird mid charge, and a cron job catches orders that got charged but never recorded because the server crashed.

it's a real shop. you can actually buy coffee.

the stack

TUI + SSH. Go with Bubbletea for the terminal UI, Wish for the ssh server. every ssh session spawns its own bubbletea program. lipgloss for styling.

API. Go again, Chi for routing. JWT auth where the token is exchanged for an ssh fingerprint, so the TUI authenticates by proving it has the same key the user ssh'd in with. no passwords anywhere.

Database. SQLite via GORM. yeah just a file. it's fine. i back it up nightly with restic and the SQLite backup API so it's WAL safe even if a write is in flight. when traffic actually demands postgres i'll switch. the DSN prefix logic in pkg/database/db.go already handles both drivers.

Payments. Stripe. cards are tokenized server side, never raw card data on disk. webhooks handle async confirmations like 3DS, deferred charges, refunds. idempotency keys tied to order IDs so retries can't double charge.

Address validation. Shippo for US addresses, Bring for Norway. invalid addresses get rejected before they hit the cart.

where it runs

production lives on a single Hetzner VPS in Helsinki, behind Caddy which handles auto TLS so there's no nginx config to babysit. everything in docker compose. the API, caddy, and the whole observability stack run as separate compose stacks on the same box. public DNS only resolves the API and the marketing site, everything else is tailnet only.

backups go to a second machine sitting in my house. that machine runs Linux From Scratch with a hand compiled kernel and a hand rolled ports repo i maintain myself with scratchpkg. no docker, no apt, every binary built from a recipe i wrote. it also runs my self hosted Gitea. it's got like 4 GB of RAM so it has to run lean. picked it because i wanted to learn how a linux box actually fits together when nobody's holding your hand.

backup pipeline runs nightly. cron on the Hetzner box snapshots the SQLite db via the backup API, ships it over Tailscale SFTP to the LFS box with restic, encrypted and deduped. dual dead man switches, healthchecks.io and a Uptime Kuma push monitor, both go off if the cron silently dies.

observability

probably the over engineered part. honestly built it because i wanted to learn the stack.

Prometheus scrapes the API for the usual RED metrics plus custom counters like cart conversions, auth attempts, stripe events, log levels, orders created.

Grafana sits in front of everything. tailnet only, no public DNS.

Loki + Promtail for logs. promtail auto discovers every container via docker labels so anything new on the box is logged for free.

Tempo for traces. the API exports OTLP via otelchi on the inbound side and otelhttp on outbound calls to stripe, shippo, bring. the GORM tracing plugin emits a span per SQL query.

Uptime Kuma for blackbox checks. API health, TLS, backup target reachable.

Alerts route through Grafana to a ntfy webhook to my phone.

end to end, a request to /api/v1/cart shows up as one trace with the chi handler as the root, gorm queries as children, and outbound HTTP to stripe as a sibling. all one tempo span tree.

local dev

git clone https://github.com/marcusDenslow/terminalShop
cd terminalShop
cp .env.example .env  # fill in stripe test keys
go run main.go        # ssh server on :23457
go run api/main.go    # API on :8080
ssh localhost -p 23457

uses SQLite by default so there's nothing to spin up.

inspiration

terminal.shop for the original idea and most of the patterns in pkg/tui/. their source is open and worth reading.

something

Description
No description provided
Readme 46 MiB
Languages
Go 94.7%
Shell 3.8%
JavaScript 1.2%
Makefile 0.3%