Buyer self-service recovery + db-info admin endpoint
Two operator-facing additions, both addressing risks we'd flagged
earlier in the v0.2 plan but hadn't shipped.
**POST /v1/recover (+ GET /recover HTML form).** Lets a buyer who
lost their license key re-derive it themselves by presenting their
invoice id + the email they paid with. Until now, the recovery
flow was "DM the operator with your invoice id and they re-send" —
operator-time scaling badly. With this, the buyer self-serves and
the operator never has to know.
The endpoint takes (invoice_id, email), case-insensitive on email.
Returns a generic 404 on any mismatch — does NOT distinguish
"invoice not found" from "wrong email" so an attacker can't
brute-force email addresses against a known invoice id. Per-IP
rate limited at 10 requests / minute. Audit-logged as
license.recovered with the email's SHA-256 hash so PII isn't
written to the log.
The HTML form at GET /recover is server-rendered, no JS framework,
no cookies — designed for a customer who's just had a catastrophic
failure of their primary computer and reached us from whatever
device they could find.
Test in tests/api.rs:recover_returns_license_key_for_matching_pair
exercises the happy path (case-insensitive email match), the
generic-404 paths (wrong email, missing invoice), the round-trip
(recovered key validates via /v1/validate), and the audit-log
write.
**GET /v1/admin/db-info.** Cheap insurance against the
catastrophic-loss risk: /data/keysat.db is a single SQLite file,
losing it invalidates every license ever issued. StartOS's backup
machinery handles snapshotting; this endpoint gives operators a
sanity-check surface they didn't have before:
- DB file path + on-disk size
- last-write timestamp (max across audit_log, invoices, licenses)
- row counts for products, policies, licenses (total + active),
invoices (total + settled), machines (active), discount codes,
audit log entries
Doesn't report when StartOS last backed it up — the daemon has no
visibility into the host's snapshot subsystem. What it gives the
operator is a "I expected ~50 licenses and I see ~50 licenses; the
file is N MB; the last write was 6 hours ago" check.
Test count: 31 (was 30; +1 for the recover test).
This commit is contained in:
@@ -69,6 +69,8 @@ pub mod self_license;
|
||||
pub mod session_layer;
|
||||
pub mod tier;
|
||||
pub mod validate;
|
||||
pub mod db_info;
|
||||
pub mod recover;
|
||||
pub mod webhook;
|
||||
pub mod webhook_deliveries;
|
||||
pub mod webhook_endpoints;
|
||||
@@ -191,6 +193,10 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/v1/purchase/:invoice_id", get(purchase::status))
|
||||
.route("/v1/redeem", post(redeem::redeem))
|
||||
.route("/v1/validate", post(validate::validate))
|
||||
// Buyer self-service recovery (lost key → re-derive from
|
||||
// settled invoice + buyer email).
|
||||
.route("/recover", get(recover::page))
|
||||
.route("/v1/recover", post(recover::recover))
|
||||
// Client-facing machine endpoints.
|
||||
.route("/v1/machines/activate", post(machines::activate))
|
||||
.route("/v1/machines/heartbeat", post(machines::heartbeat))
|
||||
@@ -315,6 +321,9 @@ pub fn router(state: AppState) -> Router {
|
||||
"/v1/admin/webhook-deliveries/:id/retry",
|
||||
post(webhook_deliveries::retry),
|
||||
)
|
||||
// Database health snapshot — operator-facing sanity check
|
||||
// against the catastrophic-loss risk; see db_info.rs.
|
||||
.route("/v1/admin/db-info", get(db_info::get))
|
||||
// Discount / referral codes.
|
||||
.route(
|
||||
"/v1/admin/discount-codes",
|
||||
|
||||
Reference in New Issue
Block a user