| 1 | -- SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | -- |
| 3 | -- Admin-only query surface for /admin/*. Non-admin handlers should |
| 4 | -- use the per-domain packages (usersdb, reposdb, …) directly. This |
| 5 | -- file collects the cross-cutting reads + writes the admin UI needs: |
| 6 | -- * dashboard counts |
| 7 | -- * user/repo listings with admin-only filters (suspended, deleted) |
| 8 | -- * site-admin flag toggle |
| 9 | -- * transactional_email_log read/write |
| 10 | -- |
| 11 | -- Queries that already exist in their domain package (e.g. |
| 12 | -- users.SuspendUser) are reused directly — no duplication here. |
| 13 | |
| 14 | -- ─── dashboard counts ──────────────────────────────────────────── |
| 15 | |
| 16 | -- name: CountActiveUsers :one |
| 17 | SELECT COUNT(*) FROM users WHERE deleted_at IS NULL; |
| 18 | |
| 19 | -- name: CountSuspendedUsers :one |
| 20 | SELECT COUNT(*) FROM users WHERE suspended_at IS NOT NULL AND deleted_at IS NULL; |
| 21 | |
| 22 | -- name: CountSiteAdmins :one |
| 23 | SELECT COUNT(*) FROM users WHERE is_site_admin = true AND deleted_at IS NULL; |
| 24 | |
| 25 | -- name: CountActiveRepos :one |
| 26 | SELECT COUNT(*) FROM repos WHERE deleted_at IS NULL; |
| 27 | |
| 28 | -- name: CountActiveOrgs :one |
| 29 | SELECT COUNT(*) FROM orgs WHERE deleted_at IS NULL; |
| 30 | |
| 31 | -- name: CountJobsByStatus :many |
| 32 | -- Status is derived from the timestamp columns since the jobs table |
| 33 | -- doesn't carry an enum: completed > failed > running > queued. |
| 34 | SELECT |
| 35 | CASE |
| 36 | WHEN completed_at IS NOT NULL THEN 'completed' |
| 37 | WHEN failed_at IS NOT NULL THEN 'failed' |
| 38 | WHEN locked_at IS NOT NULL THEN 'running' |
| 39 | ELSE 'queued' |
| 40 | END AS status, |
| 41 | COUNT(*)::bigint AS n |
| 42 | FROM jobs |
| 43 | GROUP BY 1; |
| 44 | |
| 45 | -- ─── users list ────────────────────────────────────────────────── |
| 46 | |
| 47 | -- name: ListUsersForAdmin :many |
| 48 | -- Filters: optional username prefix, optional suspended/deleted-only. |
| 49 | -- All filters use sqlc.narg so the empty case is "no filter". |
| 50 | SELECT id, username, display_name, primary_email_id, |
| 51 | suspended_at, deleted_at, last_login_at, is_site_admin, |
| 52 | created_at |
| 53 | FROM users |
| 54 | WHERE |
| 55 | (sqlc.narg(username_prefix)::text IS NULL OR username::text ILIKE sqlc.narg(username_prefix)::text || '%') |
| 56 | AND (sqlc.narg(suspended_only)::bool IS NOT TRUE OR suspended_at IS NOT NULL) |
| 57 | AND (sqlc.narg(deleted_only)::bool IS NOT TRUE OR deleted_at IS NOT NULL) |
| 58 | ORDER BY id DESC |
| 59 | LIMIT $1 OFFSET $2; |
| 60 | |
| 61 | -- name: SetUserSiteAdmin :exec |
| 62 | UPDATE users |
| 63 | SET is_site_admin = $2, |
| 64 | updated_at = now() |
| 65 | WHERE id = $1; |
| 66 | |
| 67 | -- ─── repos list ────────────────────────────────────────────────── |
| 68 | |
| 69 | -- name: ListReposForAdmin :many |
| 70 | SELECT r.id, r.owner_user_id, r.owner_org_id, r.name, r.visibility, |
| 71 | r.is_archived, r.deleted_at, r.disk_used_bytes, r.created_at, |
| 72 | u.username AS owner_user_username, |
| 73 | o.slug AS owner_org_slug |
| 74 | FROM repos r |
| 75 | LEFT JOIN users u ON u.id = r.owner_user_id |
| 76 | LEFT JOIN orgs o ON o.id = r.owner_org_id |
| 77 | WHERE |
| 78 | (sqlc.narg(name_prefix)::text IS NULL OR r.name ILIKE sqlc.narg(name_prefix)::text || '%') |
| 79 | AND (sqlc.narg(deleted_only)::bool IS NOT TRUE OR r.deleted_at IS NOT NULL) |
| 80 | AND (sqlc.narg(archived_only)::bool IS NOT TRUE OR r.is_archived = true) |
| 81 | AND (sqlc.narg(visibility_filter)::repo_visibility IS NULL OR r.visibility = sqlc.narg(visibility_filter)::repo_visibility) |
| 82 | ORDER BY r.id DESC |
| 83 | LIMIT $1 OFFSET $2; |
| 84 | |
| 85 | -- ─── jobs queue inspector ──────────────────────────────────────── |
| 86 | |
| 87 | -- name: ListJobsForAdmin :many |
| 88 | -- The admin can filter by kind + status (computed from the timestamp |
| 89 | -- columns). Status filter values: queued | running | failed | completed. |
| 90 | SELECT id, kind, payload, run_at, attempts, max_attempts, |
| 91 | last_error, locked_by, locked_at, completed_at, failed_at, created_at, |
| 92 | CASE |
| 93 | WHEN completed_at IS NOT NULL THEN 'completed' |
| 94 | WHEN failed_at IS NOT NULL THEN 'failed' |
| 95 | WHEN locked_at IS NOT NULL THEN 'running' |
| 96 | ELSE 'queued' |
| 97 | END AS status |
| 98 | FROM jobs |
| 99 | WHERE |
| 100 | (sqlc.narg(kind)::text IS NULL OR kind = sqlc.narg(kind)::text) |
| 101 | AND ( |
| 102 | sqlc.narg(status_filter)::text IS NULL |
| 103 | OR (sqlc.narg(status_filter)::text = 'queued' AND completed_at IS NULL AND failed_at IS NULL AND locked_at IS NULL) |
| 104 | OR (sqlc.narg(status_filter)::text = 'running' AND completed_at IS NULL AND failed_at IS NULL AND locked_at IS NOT NULL) |
| 105 | OR (sqlc.narg(status_filter)::text = 'failed' AND failed_at IS NOT NULL) |
| 106 | OR (sqlc.narg(status_filter)::text = 'completed' AND completed_at IS NOT NULL) |
| 107 | ) |
| 108 | ORDER BY id DESC |
| 109 | LIMIT $1 OFFSET $2; |
| 110 | |
| 111 | -- name: GetJobForAdmin :one |
| 112 | SELECT id, kind, payload, run_at, attempts, max_attempts, |
| 113 | last_error, locked_by, locked_at, completed_at, failed_at, created_at |
| 114 | FROM jobs |
| 115 | WHERE id = $1; |
| 116 | |
| 117 | -- name: AdminRetryJob :exec |
| 118 | -- Re-arm a failed/dead job for immediate retry: clear failure state, |
| 119 | -- reset attempts, and set run_at to now() so the pool picks it up |
| 120 | -- on the next tick. |
| 121 | UPDATE jobs |
| 122 | SET run_at = now(), |
| 123 | attempts = 0, |
| 124 | last_error = NULL, |
| 125 | failed_at = NULL, |
| 126 | completed_at = NULL, |
| 127 | locked_by = NULL, |
| 128 | locked_at = NULL |
| 129 | WHERE id = $1; |
| 130 | |
| 131 | -- name: AdminDiscardJob :exec |
| 132 | -- Mark a job dead without running it. Caller has already confirmed. |
| 133 | UPDATE jobs |
| 134 | SET failed_at = now(), |
| 135 | last_error = 'discarded by site admin' |
| 136 | WHERE id = $1; |
| 137 | |
| 138 | -- ─── audit log viewer ──────────────────────────────────────────── |
| 139 | |
| 140 | -- name: ListAuditForAdmin :many |
| 141 | -- Filters: actor, action prefix, target type+id, time range. Keyset |
| 142 | -- pagination on (created_at, id) keeps deep pages cheap. |
| 143 | SELECT id, actor_id, action, target_type, target_id, meta, created_at |
| 144 | FROM auth_audit_log |
| 145 | WHERE |
| 146 | (sqlc.narg(actor_id)::bigint IS NULL OR actor_id = sqlc.narg(actor_id)::bigint) |
| 147 | AND (sqlc.narg(action_prefix)::text IS NULL OR action ILIKE sqlc.narg(action_prefix)::text || '%') |
| 148 | AND (sqlc.narg(target_type)::text IS NULL OR target_type = sqlc.narg(target_type)::text) |
| 149 | AND (sqlc.narg(target_id)::bigint IS NULL OR target_id = sqlc.narg(target_id)::bigint) |
| 150 | AND (sqlc.narg(since)::timestamptz IS NULL OR created_at >= sqlc.narg(since)::timestamptz) |
| 151 | AND (sqlc.narg(until)::timestamptz IS NULL OR created_at < sqlc.narg(until)::timestamptz) |
| 152 | ORDER BY created_at DESC, id DESC |
| 153 | LIMIT $1 OFFSET $2; |
| 154 | |
| 155 | -- ─── transactional email log ───────────────────────────────────── |
| 156 | |
| 157 | -- name: InsertTransactionalEmail :one |
| 158 | INSERT INTO transactional_email_log ( |
| 159 | recipient_user_id, recipient_email, kind, subject, provider_id, |
| 160 | status, error_summary |
| 161 | ) VALUES ( |
| 162 | sqlc.narg(recipient_user_id)::bigint, |
| 163 | sqlc.arg(recipient_email), |
| 164 | sqlc.arg(kind), |
| 165 | sqlc.arg(subject), |
| 166 | sqlc.arg(provider_id), |
| 167 | sqlc.arg(status), |
| 168 | sqlc.narg(error_summary)::text |
| 169 | ) |
| 170 | RETURNING id; |
| 171 | |
| 172 | -- name: ListRecentTransactionalEmails :many |
| 173 | -- Admin email-queue surface. Most recent first; status filter optional. |
| 174 | SELECT id, recipient_user_id, recipient_email, kind, subject, |
| 175 | provider_id, status, error_summary, sent_at, delivered_at |
| 176 | FROM transactional_email_log |
| 177 | WHERE (sqlc.narg(status)::transactional_email_status IS NULL |
| 178 | OR status = sqlc.narg(status)::transactional_email_status) |
| 179 | ORDER BY sent_at DESC |
| 180 | LIMIT $1; |
| 181 | |
| 182 | -- name: MarkTransactionalEmailDelivered :exec |
| 183 | UPDATE transactional_email_log |
| 184 | SET status = 'sent', |
| 185 | delivered_at = now(), |
| 186 | error_summary = NULL |
| 187 | WHERE id = $1; |
| 188 | |
| 189 | -- name: MarkTransactionalEmailFailed :exec |
| 190 | UPDATE transactional_email_log |
| 191 | SET status = $2, |
| 192 | error_summary = $3 |
| 193 | WHERE id = $1; |
| 194 |