Client-facing surface: self-serve funding and work submission #41

Closed
opened 2026-09-01 10:36:53 +02:00 by pbwraith · 1 comment
Owner

Outcome

A client can fund an account and submit work without the operator running
curl for them
. Today every route is operator/admin behind a shared
SAAS_API_KEY -- verified on main. There is no signup, no per-client
credential, no checkout initiation, and no client-visible status. That means the
service cannot have a second user.

Dependency

Dependency satisfied: app assembly #38 merged via PR #43. Verified current main contains gitea_saas/app.py:create_app.

Scope

  • Per-client API keys. The current SAAS_API_KEY is a single shared operator
    secret. Add per-client keys: hashed at rest (never stored plaintext), scoped so
    a client can only read and act on its own client_id. Issued on provision,
    revocable.
  • Checkout. POST /api/checkout returns a Stripe Checkout URL for a top-up.
    The existing webhook already credits idempotently (#3) -- reuse it, do not
    write a second credit path.
  • Client status. GET /api/me -- balance, queued items, in-flight items,
    delivered count. Scoped to the caller's client_id.

Security notes -- this is the first genuinely externally-reachable surface

  • A client key must never reach an operator route. Add a test that asserts a
    client key gets 403 on /api/clients and on another client's /api/factory/*.
  • Rate-limit /api/checkout per client.
  • Never log a key, a Stripe secret, or a full webhook payload.

Files this issue owns

gitea_saas/client_api.py (new), gitea_saas/auth.py (extend -- do not fork),
gitea_saas/schema.sql, tests/test_client_api.py (new).

Acceptance

  1. Cross-tenant test: client A's key returns 403 on every client B resource.
  2. Keys are stored hashed -- a test asserts the plaintext key is not in the DB.
  3. Checkout -> webhook -> balance credit works against the existing mockable
    Stripe seam, and a replayed webhook credits exactly once.
  4. Full suite green.

Integration clarification (Vale)

Also allow gitea_saas/app.py and tests/test_app_assembly.py solely to mount the client routes and verify they are reachable through the real assembled app. Existing operator before-request guards must not make scoped client endpoints unreachable. Add a real assembled-app test for the authenticated client path and the operator/cross-tenant denial cases. Never expose operator secrets to clients. All other files remain forbidden. Required verification: python -m pytest -q; use mock Stripe/Gitea, no real charges.

## Outcome A client can fund an account and submit work **without the operator running curl for them**. Today every route is operator/admin behind a shared `SAAS_API_KEY` -- verified on `main`. There is no signup, no per-client credential, no checkout initiation, and no client-visible status. That means the service cannot have a second user. ## Dependency **Dependency satisfied:** app assembly #38 merged via PR #43. Verified current main contains `gitea_saas/app.py:create_app`. ## Scope - **Per-client API keys.** The current `SAAS_API_KEY` is a single shared operator secret. Add per-client keys: hashed at rest (never stored plaintext), scoped so a client can only read and act on **its own** client_id. Issued on provision, revocable. - **Checkout.** `POST /api/checkout` returns a Stripe Checkout URL for a top-up. The existing webhook already credits idempotently (#3) -- reuse it, do not write a second credit path. - **Client status.** `GET /api/me` -- balance, queued items, in-flight items, delivered count. Scoped to the caller's client_id. ## Security notes -- this is the first genuinely externally-reachable surface - A client key must never reach an operator route. Add a test that asserts a client key gets 403 on `/api/clients` and on another client's `/api/factory/*`. - Rate-limit `/api/checkout` per client. - Never log a key, a Stripe secret, or a full webhook payload. ## Files this issue owns `gitea_saas/client_api.py` (new), `gitea_saas/auth.py` (extend -- do not fork), `gitea_saas/schema.sql`, `tests/test_client_api.py` (new). ## Acceptance 1. Cross-tenant test: client A's key returns 403 on every client B resource. 2. Keys are stored hashed -- a test asserts the plaintext key is not in the DB. 3. Checkout -> webhook -> balance credit works against the existing mockable Stripe seam, and a replayed webhook credits exactly once. 4. Full suite green. ## Integration clarification (Vale) Also allow `gitea_saas/app.py` and `tests/test_app_assembly.py` solely to mount the client routes and verify they are reachable through the real assembled app. Existing operator before-request guards must not make scoped client endpoints unreachable. Add a real assembled-app test for the authenticated client path and the operator/cross-tenant denial cases. Never expose operator secrets to clients. All other files remain forbidden. Required verification: `python -m pytest -q`; use mock Stripe/Gitea, no real charges.
pbwraith added the
blocked
p2
labels 2026-09-01 10:36:54 +02:00
Author
Owner

[owner-decision] Euro requested execution of the income methods. Dependency verified on current main: PR #43 is merged and app.py/create_app plus wsgi.py exist. Removing stale dependency block; implement the scoped ticket, test, and open one PR. No public launch, real payments, or production ledger changes under this ticket without separate approval.

[owner-decision] Euro requested execution of the income methods. Dependency verified on current main: PR #43 is merged and app.py/create_app plus wsgi.py exist. Removing stale dependency block; implement the scoped ticket, test, and open one PR. No public launch, real payments, or production ledger changes under this ticket without separate approval.
pbwraith removed the
blocked
label 2026-09-05 11:08:43 +02:00
pbwraith added the
in-progress
label 2026-09-05 11:16:23 +02:00
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: pbwraith/gitea-saas-core#41
No description provided.