fix(kol): Phase 1 review — ACL lineage, expiry filters, tighter tenant scope
Addresses findings from consolidated Phase 1 code review: - Add composite unique indexes + composite FKs so subscription_prompts and packages cannot reference mismatched (tenant_id, package_id, prompt_id) tuples, enforcing ACL lineage at the schema level. - Add status CHECK constraints on all KOL tables as defense-in-depth. - ListActiveSubscribersForPackage: drop meaningless tenant_id IS NOT NULL, add end_at expiry filter so fan-out skips expired subscribers. - ApproveKolSubscription: add status='pending' guard to prevent double-approval. - ListSubscriptionPromptsByTenant: join subscription/prompt/package/profile lifecycle so hidden/archived content no longer leaks; surface kol_display_name for UI. - ListPublishedKolPackages: exclude expired subscribers from marketplace count. - KolDashboardTrend: rewrite as CTE with day-series, returning both daily usage and new subscription counts. - Remove cosmetic tenant_id IS NOT NULL from dashboard overview queries (the column is NOT NULL by schema). - check_tenant_scope.sh: require tenant_id = sqlc.(n?)arg filter, reject tenant_id IS NOT NULL as a fake scope, maintain explicit exemption list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,8 @@ SELECT kp.id, kp.tenant_id, kp.kol_profile_id, kp.name, kp.description,
|
||||
(SELECT COUNT(*) FROM kol_prompts p
|
||||
WHERE p.package_id = kp.id AND p.status = 'active' AND p.deleted_at IS NULL) AS prompt_count,
|
||||
(SELECT COUNT(*) FROM kol_subscriptions s
|
||||
WHERE s.package_id = kp.id AND s.status = 'active') AS subscriber_count
|
||||
WHERE s.package_id = kp.id AND s.status = 'active'
|
||||
AND (s.end_at IS NULL OR s.end_at > NOW())) AS subscriber_count
|
||||
FROM kol_packages kp
|
||||
JOIN kol_profiles pf ON pf.id = kp.kol_profile_id
|
||||
AND pf.status = 'active'
|
||||
|
||||
@@ -23,11 +23,13 @@ WHERE tenant_id = sqlc.arg(tenant_id)::bigint
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: ListActiveSubscribersForPackage :many
|
||||
-- Cross-tenant read: used by platform admin for subscription fan-out.
|
||||
-- Excludes expired subscriptions so new prompts are not granted to them.
|
||||
SELECT id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at
|
||||
FROM kol_subscriptions
|
||||
WHERE package_id = sqlc.arg(package_id)
|
||||
AND tenant_id IS NOT NULL
|
||||
AND status = 'active';
|
||||
AND status = 'active'
|
||||
AND (end_at IS NULL OR end_at > NOW());
|
||||
|
||||
-- name: ApproveKolSubscription :exec
|
||||
UPDATE kol_subscriptions
|
||||
@@ -37,7 +39,8 @@ SET status = 'active',
|
||||
end_at = sqlc.arg(end_at),
|
||||
updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id)
|
||||
AND tenant_id = sqlc.arg(tenant_id)::bigint;
|
||||
AND tenant_id = sqlc.arg(tenant_id)::bigint
|
||||
AND status = 'pending';
|
||||
|
||||
-- name: RevokeKolSubscription :exec
|
||||
UPDATE kol_subscriptions
|
||||
@@ -55,10 +58,25 @@ ON CONFLICT (subscription_id, prompt_id) DO NOTHING;
|
||||
SELECT sp.id, sp.tenant_id, sp.subscription_id, sp.package_id, sp.prompt_id,
|
||||
sp.status, sp.granted_at, sp.revoked_at,
|
||||
p.name AS prompt_name, p.platform_hint, p.published_revision_no,
|
||||
k.name AS package_name
|
||||
k.name AS package_name,
|
||||
pf.display_name AS kol_display_name
|
||||
FROM kol_subscription_prompts sp
|
||||
JOIN kol_prompts p ON p.id = sp.prompt_id AND p.deleted_at IS NULL
|
||||
JOIN kol_packages k ON k.id = sp.package_id AND k.deleted_at IS NULL
|
||||
JOIN kol_subscriptions s ON s.id = sp.subscription_id
|
||||
AND s.tenant_id = sp.tenant_id
|
||||
AND s.package_id = sp.package_id
|
||||
AND s.status = 'active'
|
||||
AND (s.end_at IS NULL OR s.end_at > NOW())
|
||||
JOIN kol_prompts p ON p.id = sp.prompt_id
|
||||
AND p.package_id = sp.package_id
|
||||
AND p.status = 'active'
|
||||
AND p.published_revision_no IS NOT NULL
|
||||
AND p.deleted_at IS NULL
|
||||
JOIN kol_packages k ON k.id = sp.package_id
|
||||
AND k.status = 'published'
|
||||
AND k.deleted_at IS NULL
|
||||
JOIN kol_profiles pf ON pf.id = k.kol_profile_id
|
||||
AND pf.status = 'active'
|
||||
AND pf.deleted_at IS NULL
|
||||
WHERE sp.tenant_id = sqlc.arg(tenant_id)::bigint
|
||||
AND sp.status = 'active'
|
||||
ORDER BY sp.granted_at DESC;
|
||||
|
||||
@@ -44,6 +44,7 @@ WHERE generation_task_id = sqlc.arg(generation_task_id)
|
||||
AND tenant_id = sqlc.arg(tenant_id)::bigint;
|
||||
|
||||
-- name: KolDashboardOverview :one
|
||||
-- Cross-tenant aggregation: KOL owns packages across subscriber tenants.
|
||||
SELECT
|
||||
(SELECT COUNT(DISTINCT s.tenant_id) FROM kol_subscriptions s
|
||||
WHERE s.package_id = ANY(sqlc.arg(package_ids)::bigint[]) AND s.status='active'
|
||||
@@ -53,32 +54,53 @@ SELECT
|
||||
AND s.status='active'
|
||||
AND s.created_at >= date_trunc('month', NOW())) AS month_subs,
|
||||
(SELECT COUNT(*) FROM kol_usage_logs u
|
||||
WHERE u.package_id = ANY(sqlc.arg(package_ids)::bigint[])
|
||||
AND u.tenant_id IS NOT NULL) AS total_usage,
|
||||
WHERE u.package_id = ANY(sqlc.arg(package_ids)::bigint[])) AS total_usage,
|
||||
(SELECT COUNT(*) FROM kol_usage_logs u
|
||||
WHERE u.package_id = ANY(sqlc.arg(package_ids)::bigint[])
|
||||
AND u.tenant_id IS NOT NULL
|
||||
AND u.created_at >= date_trunc('month', NOW())) AS month_usage,
|
||||
(SELECT COUNT(*) FROM kol_usage_logs u
|
||||
WHERE u.package_id = ANY(sqlc.arg(package_ids)::bigint[])
|
||||
AND u.tenant_id IS NOT NULL
|
||||
AND u.status='failed') AS failure_count;
|
||||
|
||||
-- name: KolDashboardTrend :many
|
||||
SELECT date_trunc('day', u.created_at)::date AS d,
|
||||
COUNT(*) FILTER (WHERE u.status='completed') AS usages
|
||||
FROM kol_usage_logs u
|
||||
WHERE u.package_id = ANY(sqlc.arg(package_ids)::bigint[])
|
||||
AND u.tenant_id IS NOT NULL
|
||||
AND u.created_at >= NOW() - sqlc.arg(period_days)::int * INTERVAL '1 day'
|
||||
GROUP BY d
|
||||
ORDER BY d ASC;
|
||||
-- Returns a per-day series combining usage count and new subscription count for KOL dashboard.
|
||||
WITH days AS (
|
||||
SELECT generate_series(
|
||||
date_trunc('day', NOW()) - (sqlc.arg(period_days)::int - 1) * INTERVAL '1 day',
|
||||
date_trunc('day', NOW()),
|
||||
INTERVAL '1 day'
|
||||
)::date AS d
|
||||
),
|
||||
u AS (
|
||||
SELECT date_trunc('day', created_at)::date AS d,
|
||||
COUNT(*) FILTER (WHERE status = 'completed') AS usages
|
||||
FROM kol_usage_logs
|
||||
WHERE package_id = ANY(sqlc.arg(package_ids)::bigint[])
|
||||
AND created_at >= NOW() - sqlc.arg(period_days)::int * INTERVAL '1 day'
|
||||
GROUP BY 1
|
||||
),
|
||||
s AS (
|
||||
SELECT date_trunc('day', created_at)::date AS d,
|
||||
COUNT(*) AS subscriptions
|
||||
FROM kol_subscriptions
|
||||
WHERE package_id = ANY(sqlc.arg(package_ids)::bigint[])
|
||||
AND status IN ('active', 'expired', 'revoked')
|
||||
AND created_at >= NOW() - sqlc.arg(period_days)::int * INTERVAL '1 day'
|
||||
GROUP BY 1
|
||||
)
|
||||
SELECT days.d,
|
||||
COALESCE(u.usages, 0)::bigint AS usages,
|
||||
COALESCE(s.subscriptions, 0)::bigint AS subscriptions
|
||||
FROM days
|
||||
LEFT JOIN u ON u.d = days.d
|
||||
LEFT JOIN s ON s.d = days.d
|
||||
ORDER BY days.d ASC;
|
||||
|
||||
-- name: KolUsageCountByPackage :many
|
||||
-- Cross-tenant aggregation for KOL dashboard.
|
||||
SELECT package_id, COUNT(*) AS total,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
|
||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed
|
||||
FROM kol_usage_logs
|
||||
WHERE package_id = ANY(sqlc.arg(package_ids)::bigint[])
|
||||
AND tenant_id IS NOT NULL
|
||||
GROUP BY package_id;
|
||||
|
||||
Reference in New Issue
Block a user