Add credentials store to the CRM, docs cleanup, deploy pipeline TODO
Adds a `credentials` entity to the back office (never mirrored to Sheets, gated by the CRM token even to read) so client logins like the auto-generated Easy!Appointments provider password can be viewed and copied from the dashboard instead of getting lost — the actual cause of the happynails password going missing. Onboarding now saves that generated password instead of discarding it. Also adds Documentation.md, brings README/TODO in line with the current Postgres-first architecture, and tidies the backlog. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+12
-3
@@ -53,9 +53,9 @@ def _cell(v):
|
||||
|
||||
|
||||
def mirror_entity(entity):
|
||||
if SH is None:
|
||||
return 0
|
||||
spec = db.TABLES[entity]
|
||||
if SH is None or spec.get("mirror") is False:
|
||||
return 0
|
||||
cols = spec["cols"]
|
||||
order = LIST_ORDER.get(entity, spec["pk"])
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
@@ -87,7 +87,7 @@ threading.Thread(target=_mirror_worker, daemon=True).start()
|
||||
|
||||
|
||||
def mirror_async(entity):
|
||||
if SH is not None:
|
||||
if SH is not None and db.TABLES[entity].get("mirror") is not False:
|
||||
_mirror_q.put(entity)
|
||||
|
||||
# Static dashboard, with the CRM token injected so the (basic-auth-gated)
|
||||
@@ -113,6 +113,7 @@ LIST_ORDER = {
|
||||
"bookings": "start_time DESC NULLS LAST",
|
||||
"invoices": "issued_date DESC NULLS LAST",
|
||||
"activity_log": "ts DESC NULLS LAST",
|
||||
"credentials": "client_id",
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +144,11 @@ def healthz():
|
||||
def list_entity(entity):
|
||||
if entity not in db.TABLES:
|
||||
return jsonify({"error": "unknown entity"}), 404
|
||||
# Credentials carry secrets, not just business metadata — require the
|
||||
# mutation-level token even to read, on top of the Caddy basic-auth every
|
||||
# other entity's (read-only) list relies on alone.
|
||||
if entity == "credentials" and not authed():
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
order = LIST_ORDER.get(entity, db.TABLES[entity]["pk"])
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
cur.execute(f"SELECT * FROM {entity} ORDER BY {order}")
|
||||
@@ -198,6 +204,9 @@ def add_entity(entity):
|
||||
if not row.get("renewal_date") and row.get("start_date"):
|
||||
row["renewal_date"] = compute_renewal(
|
||||
row["start_date"], row.get("billing_cycle"))
|
||||
elif entity == "credentials":
|
||||
row["cred_id"] = row.get("cred_id") or "CR-" + str(int(time.time() * 1000))
|
||||
row["created_at"] = row.get("created_at") or datetime.now(timezone.utc)
|
||||
elif not row.get(pk):
|
||||
return jsonify({"error": f"{pk} required"}), 400
|
||||
cols = spec["cols"]
|
||||
|
||||
Reference in New Issue
Block a user