Skip to content

Admin credit grant UI and backend#1481

Merged
riderx merged 5 commits into
mainfrom
riderx/admin-credit-grant
Jan 22, 2026
Merged

Admin credit grant UI and backend#1481
riderx merged 5 commits into
mainfrom
riderx/admin-credit-grant

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Jan 21, 2026

Summary

Add secure admin interface to grant credits to customer organizations with full audit trail and multi-language support. Admins can search organizations, view credit balances, grant credits with optional expiration, and view recent grant history. All operations are verified with admin authentication and logged with the admin user ID.

Test plan

  1. Login as admin user (admin@capgo.app)
  2. Navigate to Admin > Credits tab
  3. Search for an organization by name, email, or ID
  4. View current credit balance and next expiration
  5. Grant 100 credits with notes and optional 12-month expiration
  6. Verify success message and grants appear in recent history
  7. Refresh page and confirm grants persist

Checklist

  • Code passes bun run lint and bun typecheck
  • Backend uses existing top_up_usage_credits RPC with admin verification
  • Frontend protected by admin route guard and mainStore.isAdmin check
  • All 15 language files have admin-credits translations
  • UI follows existing admin page patterns (users.vue, admin_stats.ts)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added comprehensive admin credits management system enabling administrators to grant credits to customer organizations with configurable expiration periods and notes for audit logging. Includes real-time organization search, current balance display, and detailed grant history tracking.
  • UI Updates

    • New Credits tab in admin dashboard navigation for access to credit management tools. Full localization support in 17 languages.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings January 21, 2026 18:15
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 21, 2026

Warning

Rate limit exceeded

@riderx has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 14 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

This PR introduces a complete admin credits management system. It adds translation keys across 17 languages, a new admin dashboard page for granting credits to organizations, backend APIs for credit operations (grant, search, balance, history), and comprehensive tests for the new endpoints.

Changes

Cohort / File(s) Summary
API Routing
cloudflare_workers/api/index.ts, supabase/functions/private/index.ts
Register new /admin_credits private API routes linking to backend handlers
Backend Implementation
supabase/functions/_backend/private/admin_credits.ts
New Hono-based module with admin-only endpoints: POST /grant (allocate credits with validation), GET /search-orgs (org lookup with sanitization), GET /org-balance/:orgId (fetch balance), GET /grants-history (audit trail); includes admin verification, error logging, and RLS-aware database queries
Frontend UI
src/pages/admin/dashboard/credits.vue
New Vue component with org search (debounced), balance display, grant form (amount/expiry/notes), grant history table, and toast notifications for success/error feedback
Navigation & Routing
src/constants/adminTabs.ts, src/typed-router.d.ts
Add credits tab to admin navigation menu and register new /admin/dashboard/credits route
Translations
messages/{de,en,es,fr,hi,id,it,ja,ko,pl,pt-br,ru,tr,vi,zh-cn}.json
Add 27 new translation keys per language (titles, labels, placeholders, error/success messages, table columns) for admin credits feature
Tests
tests/admin-credits.test.ts
Integration test suite covering access control (401/400 responses), input validation (amount/org checks), SQL injection protection, and consistent error messaging across all four endpoints

Sequence Diagram(s)

sequenceDiagram
    participant Client as Admin Client
    participant Frontend as Vue Component
    participant Cloudflare as Cloudflare Worker
    participant Supabase as Supabase Function
    participant DB as Database

    Client->>Frontend: Search for organization
    Frontend->>Cloudflare: GET /private/admin_credits/search-orgs?q=...
    Cloudflare->>Supabase: Forward request + validate token
    Supabase->>Supabase: verifyAdmin() - check is_admin RLC
    alt Admin verified
        Supabase->>DB: Query orgs (name/email/id match)
        DB-->>Supabase: Return matching orgs
        Supabase-->>Cloudflare: Return org list
        Cloudflare-->>Frontend: org search results
        Frontend-->>Client: Display dropdown
        
        Client->>Frontend: Select organization
        Frontend->>Cloudflare: GET /private/admin_credits/org-balance/:orgId
        Cloudflare->>Supabase: Forward request
        Supabase->>DB: Query usage_credit_balances
        DB-->>Supabase: Return balance
        Supabase-->>Cloudflare: balance response
        Cloudflare-->>Frontend: Current balance
        Frontend-->>Client: Display balance
        
        Client->>Frontend: Submit grant form (amount, expiry, notes)
        Frontend->>Cloudflare: POST /private/admin_credits/grant
        Cloudflare->>Supabase: Forward request + body
        Supabase->>Supabase: Validate request (amount, org_id)
        alt Validation passed
            Supabase->>DB: Call top_up_usage_credits RPC
            DB-->>Supabase: Grant created
            Supabase-->>Cloudflare: Success response
            Cloudflare-->>Frontend: Grant confirmation
            Frontend-->>Client: Success toast
        else Validation failed
            Supabase-->>Cloudflare: Error (400)
            Cloudflare-->>Frontend: Error message
            Frontend-->>Client: Error toast
        end
    else Not admin
        Supabase-->>Cloudflare: 400 not_admin
        Cloudflare-->>Frontend: Unauthorized
        Frontend-->>Client: Error feedback
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

The changes span multiple domains (backend, frontend, translations, routing) with new complex logic in the Vue component and Supabase handler, but homogeneous repetition across 17 translation files reduces overall cognitive load. The backend implementation is focused and test-covered, and the frontend follows established patterns.

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 A rabbit hops through admin lands so bright,
With credits to grant and balances in sight!
New routes are registered, translations take flight,
Through seventeen tongues—the grants shine with might! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Admin credit grant UI and backend' accurately and concisely summarizes the main changes in the pull request, highlighting the primary feature addition.
Description check ✅ Passed The PR description covers the required sections (Summary, Test plan, and Checklist) with sufficient detail about the feature, testing steps, and verification of code standards.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a secure admin interface for granting credits to customer organizations. The feature includes backend API endpoints for searching organizations, viewing balances, granting credits, and viewing grant history, along with a Vue frontend component that provides the admin UI. All operations verify admin status and log admin user IDs for audit trails.

Changes:

  • Added 4 new backend endpoints under /private/admin_credits/ with admin authentication
  • Created Vue component with organization search, credit granting form, and grant history table
  • Added translations for admin credits UI in all 15 supported languages
  • Integrated new credits tab into admin dashboard navigation

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
supabase/functions/_backend/private/admin_credits.ts New backend module with 4 admin-only endpoints: grant credits, search orgs, get balance, view grant history
supabase/functions/private/index.ts Routes new admin_credits backend module for Supabase functions
cloudflare_workers/api/index.ts Routes new admin_credits backend module for Cloudflare Workers
src/pages/admin/dashboard/credits.vue New admin UI page with org search, credit grant form, balance display, and grant history table
src/constants/adminTabs.ts Adds credits tab to admin dashboard navigation
messages/*.json (15 files) Translation strings for admin credits UI in all supported languages

orgBalance.value = result.balance ?? null
}
catch (error) {
console.error('Balance load error:', error)
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling only logs to console but doesn't provide user feedback via toast for failed balance loads. Consider adding a toast.error() call when balance fetch fails to inform users of the error.

Suggested change
console.error('Balance load error:', error)
console.error('Balance load error:', error)
toast.error(t('admin-credits-balance-error'))

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +137
function handleSearchInput() {
if (searchDebounce)
clearTimeout(searchDebounce)

searchDebounce = setTimeout(() => {
searchOrgs(searchQuery.value)
}, 300)
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debounce timeout is not cleared when the component unmounts. This can lead to memory leaks if the component is destroyed before the timeout executes. Add cleanup in onUnmounted to clear the timeout.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +128
async function searchOrgs(query: string) {
if (query.length < 2) {
searchResults.value = []
return
}

isSearching.value = true

try {
const { data } = await supabase.auth.getSession()
const response = await fetch(`${defaultApiHost}/private/admin_credits/search-orgs?q=${encodeURIComponent(query)}`, {
headers: {
authorization: `Bearer ${data.session?.access_token}`,
},
})

if (!response.ok) {
throw new Error('Search failed')
}

const result = await response.json() as { orgs?: OrgSearchResult[] }
searchResults.value = result.orgs || []
}
catch (error) {
console.error('Search error:', error)
searchResults.value = []
}
finally {
isSearching.value = false
}
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition where if a user types quickly and changes the search query, multiple concurrent search requests may be in flight. The last request to complete will set the results, which may not correspond to the current search query. Consider tracking a request ID or canceling previous requests.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +14
org_id: z.string().check(z.minLength(1)),
amount: z.number().check(z.minimum(1)),
notes: z.optional(z.string().check(z.minLength(1))),
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema validation uses check() which is not a standard Zod method. The correct Zod method is min(). This should be z.string().min(1) instead of z.string().check(z.minLength(1)).

Suggested change
org_id: z.string().check(z.minLength(1)),
amount: z.number().check(z.minimum(1)),
notes: z.optional(z.string().check(z.minLength(1))),
org_id: z.string().min(1),
amount: z.number().min(1),
notes: z.optional(z.string().min(1)),

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +14
org_id: z.string().check(z.minLength(1)),
amount: z.number().check(z.minimum(1)),
notes: z.optional(z.string().check(z.minLength(1))),
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema validation uses check() which is not a standard Zod method. The correct Zod method is min(). This should be z.number().min(1) instead of z.number().check(z.minimum(1)).

Suggested change
org_id: z.string().check(z.minLength(1)),
amount: z.number().check(z.minimum(1)),
notes: z.optional(z.string().check(z.minLength(1))),
org_id: z.string().min(1),
amount: z.number().min(1),
notes: z.optional(z.string().min(1)),

Copilot uses AI. Check for mistakes.
recentGrants.value = result.grants || []
}
catch (error) {
console.error('Grants load error:', error)
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling only logs to console but doesn't provide user feedback via toast for failed grant history loads. Consider adding a toast.error() call when grant history fetch fails to inform users of the error.

Suggested change
console.error('Grants load error:', error)
console.error('Grants load error:', error)
toast.error(t('admin-credits-grants-load-error'))

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +14
org_id: z.string().check(z.minLength(1)),
amount: z.number().check(z.minimum(1)),
notes: z.optional(z.string().check(z.minLength(1))),
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema validation uses check() which is not a standard Zod method. The correct Zod method is min(). This should be z.optional(z.string().min(1)) instead of z.optional(z.string().check(z.minLength(1))).

Suggested change
org_id: z.string().check(z.minLength(1)),
amount: z.number().check(z.minimum(1)),
notes: z.optional(z.string().check(z.minLength(1))),
org_id: z.string().min(1),
amount: z.number().min(1),
notes: z.optional(z.string().min(1)),

Copilot uses AI. Check for mistakes.
p_notes: notes || `Admin grant by ${userId}`,
p_source_ref: sourceRef,
})
.single()
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RPC call uses .single() but top_up_usage_credits is an RPC function that returns a single value by default. The .single() modifier is typically used with select queries, not RPC calls. This may cause an error. Remove .single() from this RPC call.

Suggested change
.single()

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +178
const adminSupabase = supabaseAdmin(c)

// Search by name, email, or exact ID match
const { data: orgs, error } = await adminSupabase
.from('orgs')
.select('id, name, management_email, created_at')
.or(`name.ilike.%${searchTerm}%,management_email.ilike.%${searchTerm}%,id.eq.${searchTerm}`)
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SQL query uses string interpolation for the search term which can cause SQL injection vulnerabilities. The .or() method in Supabase should use parameterized queries. Consider using .or() with proper escaping or use separate .or() clauses with .ilike() filters.

Suggested change
const adminSupabase = supabaseAdmin(c)
// Search by name, email, or exact ID match
const { data: orgs, error } = await adminSupabase
.from('orgs')
.select('id, name, management_email, created_at')
.or(`name.ilike.%${searchTerm}%,management_email.ilike.%${searchTerm}%,id.eq.${searchTerm}`)
// Escape special characters to avoid breaking the PostgREST filter grammar
const sanitizedSearchTerm = searchTerm.replace(/[%_,]/g, (c) => '\\' + c)
const ilikePattern = `%${sanitizedSearchTerm}%`
const adminSupabase = supabaseAdmin(c)
// Search by name, email, or exact ID match
const { data: orgs, error } = await adminSupabase
.from('orgs')
.select('id, name, management_email, created_at')
.or(`name.ilike.${ilikePattern},management_email.ilike.${ilikePattern},id.eq.${sanitizedSearchTerm}`)

Copilot uses AI. Check for mistakes.
searchResults.value = result.orgs || []
}
catch (error) {
console.error('Search error:', error)
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling only logs to console but doesn't provide user feedback via toast for failed searches. Consider adding a toast.error() call when search fails to inform users of the error.

Suggested change
console.error('Search error:', error)
console.error('Search error:', error)
toast.error('Failed to search organizations. Please try again.')

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09652f86aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const { data: orgs, error } = await adminSupabase
.from('orgs')
.select('id, name, management_email, created_at')
.or(`name.ilike.%${searchTerm}%,management_email.ilike.%${searchTerm}%,id.eq.${searchTerm}`)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard UUID search filter to avoid 400s

The OR filter always includes id.eq.${searchTerm} even though orgs.id is a UUID column. For typical name/email searches (e.g., “acme” or “billing@…”), PostgREST will reject the filter with an invalid UUID error, so the endpoint returns search_failed and the UI shows no results unless the query happens to be a valid UUID. Consider only adding the id.eq clause when searchTerm validates as a UUID (or cast the ID to text before comparing) so non-UUID searches still work.

Useful? React with 👍 / 👎.

Comment thread src/pages/admin/dashboard/credits.vue Outdated
Comment on lines +84 to +87
const expiresAt = computed(() => {
if (expiresInMonths.value <= 0)
return null
return dayjs().add(expiresInMonths.value, 'month').toISOString()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute expiration timestamp per grant

expiresAt is a cached computed value that only recalculates when expiresInMonths changes. If an admin grants credits multiple times with the same month value, the later grants reuse the original timestamp from the first grant, so their expiration is earlier by the elapsed time. Computing the timestamp inside grantCredits (or using a non-cached getter) would ensure each grant’s expiration is based on the actual grant time.

Useful? React with 👍 / 👎.

@riderx riderx force-pushed the riderx/admin-credit-grant branch 2 times, most recently from 23aae77 to f9fedda Compare January 21, 2026 22:21
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@messages/fr.json`:
- Around line 507-533: Convert the French admin-credits strings from Title Case
to sentence case and fix agreement for “notes”: update keys such as
"admin-credits", "admin-credits-title", "admin-credits-description",
"admin-credits-grant-title", "admin-credits-select-org",
"admin-credits-search-placeholder", "admin-credits-current-balance",
"admin-credits-no-balance", "admin-credits-expires",
"admin-credits-amount-label", "admin-credits-amount-required",
"admin-credits-expires-months", "admin-credits-notes-label",
"admin-credits-notes-placeholder", "admin-credits-grant-button",
"admin-credits-grant-success", "admin-credits-grant-error",
"admin-credits-search-error", "admin-credits-balance-error",
"admin-credits-grants-load-error", "admin-credits-recent-grants",
"admin-credits-no-grants", "admin-credits-col-org", "admin-credits-col-amount",
"admin-credits-col-notes", "admin-credits-col-date", and
"admin-credits-col-expires" to sentence case (e.g., "Accorder des crédits" ->
"Accorder des crédits" remains but others like "Solde Actuel" -> "Solde
actuel"), and change "Notes (optionnel)" to agree with plural feminine as "Notes
(optionnelles)". Ensure punctuation and placeholders ({amount}, {org}) are
preserved.

In `@messages/ru.json`:
- Around line 507-533: The RU translations for the admin credits UI use Title
Case but the project prefers sentence case; update the values for the keys
admin-credits, admin-credits-title, admin-credits-description,
admin-credits-grant-title, admin-credits-select-org,
admin-credits-search-placeholder, admin-credits-current-balance,
admin-credits-no-balance, admin-credits-expires, admin-credits-amount-label,
admin-credits-amount-required, admin-credits-expires-months,
admin-credits-notes-label, admin-credits-notes-placeholder,
admin-credits-grant-button, admin-credits-grant-success,
admin-credits-grant-error, admin-credits-search-error,
admin-credits-balance-error, admin-credits-grants-load-error,
admin-credits-recent-grants, admin-credits-no-grants, admin-credits-col-org,
admin-credits-col-amount, admin-credits-col-notes, admin-credits-col-date and
admin-credits-col-expires to use sentence case (lowercase internal nouns and
only capitalize where grammatically required) so they match the rest of the RU
file and read naturally in the UI.

In `@messages/tr.json`:
- Around line 507-533: Update the "admin-credits-expires" translation value to
include the noun "tarihi" so it reads consistently with other keys (e.g.,
"credit-transaction-expiry", "expiration-date"); locate the
"admin-credits-expires" entry in the messages/tr.json diff and change its value
from "Sonraki son kullanma" to a phrasing that includes "tarihi" (e.g., "Sonraki
son kullanma tarihi").

In `@supabase/functions/_backend/private/admin_credits.ts`:
- Around line 3-4: The file imports Hono directly; per guidelines replace direct
Hono usage with the app factory createHono from utils/hono.ts: remove the direct
import of Hono and import createHono instead, then initialize the app using
createHono() wherever new Hono() is currently used (refer to the Hono import and
any initialization around line 48), updating routes to use the returned app
instance; ensure the zod import remains unchanged.
🧹 Nitpick comments (3)
messages/pt-br.json (1)

507-533: Consider aligning “Admin” wording with the existing “administrador” terms.

Several new strings use “Admin” (e.g., “Créditos de Admin”, “Concessões Recentes de Admin”), while the file already uses “administrador” elsewhere. A consistent form (e.g., “Créditos do administrador”, “Concessões recentes do administrador”) would read more naturally and avoid mixed terminology.

tests/admin-credits.test.ts (1)

41-305: Consider using it.concurrent() for test parallelism.

Per coding guidelines, tests should use it.concurrent() to maximize parallelism. Since these tests only read from the test org (none modify it), they could run concurrently within each describe block.

♻️ Example refactor for one describe block
 describe('[POST] /private/admin_credits/grant - Admin Access Control', () => {
-  it('should return 401 when no authorization header is provided', async () => {
+  it.concurrent('should return 401 when no authorization header is provided', async () => {
     // ...
   })

-  it('should return 400 not_admin when non-admin user tries to grant credits', async () => {
+  it.concurrent('should return 400 not_admin when non-admin user tries to grant credits', async () => {
     // ...
   })
   // ... apply to other tests
 })
supabase/functions/_backend/private/admin_credits.ts (1)

211-251: Consider validating orgId as UUID before querying.

The endpoint accepts any string as orgId (line 224). If an invalid UUID is passed, the database query will fail. Consider adding UUID validation similar to the search-orgs endpoint.

♻️ Proposed validation
   const orgId = c.req.param('orgId')

   if (!orgId) {
     throw simpleError('missing_org_id', 'Organization ID is required')
   }

+  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
+  if (!uuidRegex.test(orgId)) {
+    throw simpleError('invalid_org_id', 'Invalid organization ID format')
+  }
+
   const adminSupabase = supabaseAdmin(c)

Comment thread messages/fr.json Outdated
Comment thread messages/ru.json Outdated
Comment thread messages/tr.json
Comment thread supabase/functions/_backend/private/admin_credits.ts Outdated
@riderx riderx force-pushed the riderx/admin-credit-grant branch from af77e3c to a28fb9e Compare January 21, 2026 23:03
riderx and others added 5 commits January 22, 2026 01:30
Add secure admin interface to grant credits to customer organizations with full audit trail and multi-language support. Includes organization search, balance display, grant form with optional expiration, and recent grants history. Backend verifies admin privileges and logs all actions with admin user ID.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add onUnmounted cleanup for debounce timeout (prevents memory leaks)
- Track search query to avoid race conditions with concurrent requests
- Add toast.error() for balance, grants, and search load failures
- Compute expiresAt freshly per grant (fixes stale timestamp bug)
- Use parsedBodyResult.data instead of raw body after validation
- Remove unnecessary .single() from RPC call
- Sanitize search term and only add UUID filter when valid UUID
- Add 3 new error translation keys to all 15 language files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Verify that only admin users can access admin_credits endpoints:
- POST /grant - Grant credits to an organization
- GET /search-orgs - Search organizations
- GET /org-balance/:orgId - Get organization credit balance
- GET /grants-history - View admin grant history

Tests verify:
- Requests without auth are rejected (401)
- Non-admin users get not_admin error (400)
- Invalid inputs are properly validated
- SQL injection patterns are blocked by admin check

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Auto-generated route types for the new admin credits page.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use sentence case in French translations
- Use sentence case in Russian translations
- Add "tarihi" to Turkish admin-credits-expires for consistency
- Use createHono from utils instead of importing Hono directly

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@riderx riderx force-pushed the riderx/admin-credit-grant branch from a28fb9e to e58b8e4 Compare January 22, 2026 01:30
@riderx riderx merged commit e0055de into main Jan 22, 2026
8 of 9 checks passed
@riderx riderx deleted the riderx/admin-credit-grant branch January 22, 2026 01:35
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants