Appearance
Historical snapshot archived 2026-09-25. This records an earlier review or plan, not current implementation or live ticket state. For current work, follow root AGENTS.md, the relevant BloxClips skill, and owning repository source/tests. Preserve approved decisions as evidence; verify their present authority before acting.
Permissions & Actors
Who can do what with submissions.
Actors
| Actor | Description | Auth Method |
|---|---|---|
| Clipper | Authenticated user submitting videos | Cookie/JWT (requireAuth) |
| Admin | Staff with admin flag | requireAdmin middleware |
| System Worker | Background scheduler | Internal (no auth) |
| Discord Bot | Legacy (removed for submissions) | — |
Note: No separate "moderator" or "staff" role for submissions — only admin/non-admin.
Permission Matrix
| Action | Clipper | Admin | System | Notes |
|---|---|---|---|---|
| Create submission | ✅ | ✅ (via addedByAdmin) | ❌ | Clipper: own campaigns only. Admin: manual add (no UI) |
| View own submissions | ✅ | ✅ | ❌ | Backend: userOwnedWhere (webUserId OR discordId) |
| View all submissions | ❌ | ✅ | ❌ | Admin queue (/admin/submissions/review) |
| View submission detail | ✅ (own) | ✅ | ❌ | Clipper: history page. Admin: user-detail, campaign-detail |
| Accept submission | ❌ | ✅ | ❌ | PUT /admin/submissions/:id/status {ACCEPTED} |
| Deny submission | ❌ | ✅ | ❌ | PUT /admin/submissions/:id/status {DENIED} |
| Flag submission | ❌ | ✅ | ✅ (auto) | Admin: manual. System: 3 scrape failures |
| Re-review (re-open) | ❌ | ✅ | ❌ | Set status back to PENDING |
| Delete submission | ❌ | ✅ | ❌ | DELETE /api/admin/submissions/:id (exists, no ownership check) |
| Update manual views | ❌ | ✅ | ❌ | PUT /admin/submissions/:id/views |
| Update custom rate | ❌ | ✅ | ❌ | PUT /admin/submissions/:id/rate |
| Update custom cap | ❌ | ✅ | ❌ | PUT /admin/submissions/:id/cap |
| Request payout | ✅ | ❌ | ❌ | POST /api/payouts/request (own submissions only) |
| View payout history | ✅ | ✅ | ❌ | Own / all |
| Review payout items | ❌ | ✅ | ❌ | Admin payout review tab |
| Approve payout | ❌ | ✅ | ❌ | POST /admin/payouts/review/:id/approve |
| Send payout | ❌ | ✅ (TOTP) | ❌ | POST /admin/payouts/review/:id/send |
| Trigger tracking poll | ❌ | ❌ | ✅ | Scheduler (30 min) |
| Budget clamp/freeze | ❌ | ❌ | ✅ | Automatic in tick/rescrape |
| Campaign auto-close | ❌ | ❌ | ✅ | checkAndCloseCampaign (95%) |
Authorization Implementation
Clipper Access Control
Backend: src/api/routes/submissions.ts:11-15
typescript
function userOwnedWhere(req) {
const clauses = [{ webUserId: req.user!.userId }];
if (req.user!.discordId) clauses.push({ userId: req.user!.discordId });
return { OR: clauses };
}Used in: GET /api/submissions, POST /api/submissions (implicit via req.user)
Admin Access Control
Middleware: src/api/middleware/adminAuth.ts
typescript
export function requireAdmin(req, res, next) {
if (!isAdminRequestUser(req.user)) return res.status(403).json({error: 'Admin required'});
next();
}Check: isAdminRequestUser → checks req.user.isAdmin or Discord role
Applied to: All /api/admin/* routes
System Worker (No Auth)
Scheduler: src/scheduler.ts — runs on server startup, no auth context
Functions: runTrackingTick, checkAndCloseCampaign, rescrapeForPayout — called internally
Frontend vs. Backend Enforcement Gaps
| Action | Frontend Gate | Backend Enforcement | Gap |
|---|---|---|---|
| Submit video | Auth + campaign select | requireAuth + campaign checks | None |
| View own submissions | Auth | userOwnedWhere | None |
| Admin queue | Admin nav link | requireAdmin | None |
| Accept/Deny/Flag | Admin UI buttons | requireAdmin + audit | None |
| Manual view override | Admin user detail | requireAdmin | None |
| Custom rate/cap | Admin user detail | requireAdmin | None |
| Delete submission | Admin user detail button | requireAdmin + auditLog | Exists but no ownership check |
| View all submissions | Admin only | requireAdmin | None |
| Request payout | Auth + balance check | requireAuth + ownership | None |
Ownership Model
Submission Ownership
- Primary:
webUserId(WebUser CUID) — preferred, set on create - Legacy:
userId(Discord ID) — fallback for pre-webUser submissions - Query:
userOwnedWherechecks BOTH
Campaign Ownership
- No per-user ownership — campaigns are global
acceptingSubmissions+active+isDeletedgates submission creation
Payout Ownership
Payout.webUserId(preferred) oruserId(legacy)- User scope:
userScopeOr(req)checks both
Audit Trail
Every admin action on submissions creates AdminAuditLog:
- Status change:
SUBMISSION_ACCEPTED/DENIED/FLAGGED/PENDING - View update:
UPDATE_SUBMISSION_VIEWS - Rate update:
UPDATE_SUBMISSION_RATE - Cap update:
UPDATE_SUBMISSION_CAP
Fields: adminId, action, resourceType=SUBMISSION, resourceId, details, ipAddress, userAgent
Special Cases
Cross-User Submission (Admin Manual Add)
- Field:
Submission.addedByAdmin: Boolean - No frontend UI — would require direct DB insert or API call
- Still subject to campaign rules (platform, budget)
Orphan Submissions (Pre-WebUser)
webUserId = null, onlyuserId(Discord ID)userOwnedWherehandles via discordId fallback- Admin user list shows "orphan" users (submissions without WebUser)
Banned Users
Banmodel +IdentityBlacklistblock submission creation?- Not enforced in
POST /api/submissions— banned users can still submit - Ban only blocks Discord DM reports and login
Missing Permissions
| Missing Check | Risk |
|---|---|
| Banned user can submit | Fraud/abuse |
| No rate limit on submission create | Spam |
No ownership check on DELETE (if implemented) | Data loss |
| Admin can accept own submission? | Conflict of interest (no self-check) |
| No "moderator" role — only admin | Over-privileged staff |