mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
feat(coach,admin): finish the self-context and invite surfaces end to end
Invites move into their own admin section with a real ledger: status badges, expiry, uses, a redemption history of who joined when (new redemption table, revocation is now soft so history survives), and an honest one-time link/QR moment. The assistant self-context grows structured fields for conditions, allergies and focus — encrypted like the free text — merges age and gender from the profile into one fenced prompt block, and asks up to three targeted follow-up questions that surface as tappable chips above the coach composer. A fourth nudge trigger gently suggests a check-up when the self-context is stale and the coach is actually in use.
This commit is contained in:
1 parent
9fe5673251
commit
5b75184649
32 files changed
+2648
-423
No files matched your search
@@ -1,11 +1,14 @@
|
||||
/**
|
||||
* DELETE /api/admin/invites/{id} — revoke a registration invite.
|
||||
*
|
||||
* v1.15.20 — hard delete: an invite row carries no health data and no
|
||||
* plaintext secret, and the audit log already records mint + revoke, so
|
||||
* keeping a tombstone would only grow the table. Idempotent — deleting
|
||||
* an unknown id returns `{ deleted: false }` rather than 404 so a
|
||||
* double-tap in the admin UI never surfaces an error.
|
||||
* v1.16.0 — soft revocation (`revokedAt`), replacing the v1.15.20 hard
|
||||
* delete: the admin table now renders redemption history per invite,
|
||||
* and a deleted row would erase who was admitted through it. The
|
||||
* consume path refuses a revoked invite like an expired one.
|
||||
*
|
||||
* Idempotent — revoking an unknown or already-revoked id returns
|
||||
* `{ revoked: false }` rather than 404 so a double-tap in the admin UI
|
||||
* never surfaces an error.
|
||||
*/
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
||||
@@ -20,11 +23,14 @@ export const DELETE = apiHandler(
|
||||
const { user } = await requireAdmin();
|
||||
const { id } = await params;
|
||||
|
||||
const { count } = await prisma.inviteToken.deleteMany({ where: { id } });
|
||||
const deleted = count > 0;
|
||||
const { count } = await prisma.inviteToken.updateMany({
|
||||
where: { id, revokedAt: null },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
const revoked = count > 0;
|
||||
|
||||
if (deleted) {
|
||||
await auditLog("admin.invite.deleted", {
|
||||
if (revoked) {
|
||||
await auditLog("admin.invite.revoked", {
|
||||
userId: user.id,
|
||||
ipAddress: getClientIp(req),
|
||||
details: { inviteId: id },
|
||||
@@ -32,10 +38,10 @@ export const DELETE = apiHandler(
|
||||
}
|
||||
|
||||
annotate({
|
||||
action: { name: "admin.invite.deleted" },
|
||||
meta: { deleted },
|
||||
action: { name: "admin.invite.revoked" },
|
||||
meta: { revoked },
|
||||
});
|
||||
|
||||
return apiSuccess({ deleted });
|
||||
return apiSuccess({ revoked });
|
||||
},
|
||||
);
|
||||
@@ -40,10 +40,21 @@ export const GET = apiHandler(async () => {
|
||||
createdAt: true,
|
||||
expiresAt: true,
|
||||
usedAt: true,
|
||||
revokedAt: true,
|
||||
uses: true,
|
||||
maxUses: true,
|
||||
creator: { select: { id: true, username: true } },
|
||||
consumer: { select: { id: true, username: true } },
|
||||
// v1.16.0 — full per-signup history. `consumer` only carries the
|
||||
// LAST account; multi-use invites list every admitted signup here.
|
||||
redemptions: {
|
||||
select: {
|
||||
id: true,
|
||||
redeemedAt: true,
|
||||
user: { select: { id: true, username: true, email: true } },
|
||||
},
|
||||
orderBy: { redeemedAt: "desc" },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user