-
Notifications
You must be signed in to change notification settings - Fork 495
Justin/credit admin page #4729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Justin/credit admin page #4729
Changes from all commits
a6562cb
3ec9d16
b2e5234
8facb01
573eb9f
a25286f
e53b1a5
8b4b6b4
480b288
4753909
fe4ea3c
94c8558
4d178c1
924ee68
d6e363e
5375540
4add2ca
fff6632
564701f
d58996c
b8559cd
047af78
4fff619
2626ba6
0b02c97
3c7b408
35ded80
2fe57c4
605545d
e9ddb1d
1ebeea8
913faee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,7 @@ import { clickhouseDb } from "../../lib/db/ClickhouseWrapper"; | |
| import { prepareRequestAzure } from "../../lib/experiment/requestPrep/azure"; | ||
| import { dbExecute } from "../../lib/shared/db/dbExecute"; | ||
| import type { JawnAuthenticatedRequest } from "../../types/request"; | ||
| import { Setting } from "../../utils/settings"; | ||
| import { Setting, SettingsManager } from "../../utils/settings"; | ||
| import type { SettingName } from "../../utils/settings"; | ||
| import Stripe from "stripe"; | ||
| import { AdminManager } from "../../managers/admin/AdminManager"; | ||
|
|
@@ -28,6 +28,8 @@ import { | |
| } from "@helicone-package/cost"; | ||
|
|
||
| import { err, ok, Result } from "../../packages/common/result"; | ||
| import { COST_PRECISION_MULTIPLIER } from "@helicone-package/cost/costCalc"; | ||
| import { ENVIRONMENT } from "../../lib/clients/constant"; | ||
| import { InAppThread } from "../../managers/InAppThreadsManager"; | ||
|
|
||
| export const authCheckThrow = async (userId: string | undefined) => { | ||
|
|
@@ -57,6 +59,134 @@ export const authCheckThrow = async (userId: string | undefined) => { | |
| @Tags("Admin") | ||
| @Security("api_key") | ||
| export class AdminController extends Controller { | ||
| @Post("/gateway/dashboard_data") | ||
| public async getGatewayDashboardData( | ||
| @Request() request: JawnAuthenticatedRequest | ||
| ) { | ||
| await authCheckThrow(request.authParams.userId); | ||
| const settingsManager = new SettingsManager(); | ||
| const stripeProductSettings = | ||
| await settingsManager.getSetting("stripe:products"); | ||
| if ( | ||
| !stripeProductSettings || | ||
| !stripeProductSettings.cloudGatewayTokenUsageProduct | ||
| ) { | ||
| return err("stripe:products setting is not configured"); | ||
| } | ||
| const tokenUsageProductId = | ||
| stripeProductSettings.cloudGatewayTokenUsageProduct; | ||
|
|
||
| // Get organizations with payments | ||
| const paymentsResult = await dbExecute<{ | ||
| org_id: string; | ||
| org_name: string; | ||
| stripe_customer_id: string; | ||
| tier: string; | ||
| total_amount_received: number; | ||
| last_payment_date: string; | ||
| owner_email: string; | ||
| }>( | ||
| ` | ||
| SELECT | ||
| organization.id as org_id, | ||
| organization.name as org_name, | ||
| organization.stripe_customer_id, | ||
| organization.tier, | ||
| SUM(stripe.payment_intents.amount_received) as total_amount_received, | ||
| MAX(stripe.payment_intents.created) as last_payment_date, | ||
| auth.users.email as owner_email | ||
| FROM stripe.payment_intents | ||
| LEFT JOIN organization ON organization.stripe_customer_id = stripe.payment_intents.customer | ||
| LEFT JOIN auth.users ON organization.owner = auth.users.id | ||
| WHERE stripe.payment_intents.metadata->>'productId' = $1 | ||
| AND stripe.payment_intents.status = 'succeeded' | ||
| AND organization.id IS NOT NULL | ||
| GROUP BY organization.id, organization.name, organization.stripe_customer_id, organization.tier, auth.users.email | ||
| ORDER BY total_amount_received DESC | ||
| LIMIT 1000 | ||
| `, | ||
| [tokenUsageProductId] | ||
| ); | ||
|
|
||
| if (paymentsResult.error) { | ||
| return err(paymentsResult.error); | ||
| } | ||
|
|
||
| if (!paymentsResult.data || paymentsResult.data.length === 0) { | ||
| return ok({ | ||
| organizations: [], | ||
| summary: { | ||
| totalOrgsWithCredits: 0, | ||
| totalCreditsIssued: 0, | ||
| totalCreditsSpent: 0, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| // Get ClickHouse spending for these organizations | ||
| const orgIds = paymentsResult.data.map((org) => org.org_id); | ||
|
|
||
| const clickhouseSpendResult = await clickhouseDb.dbQuery<{ | ||
| organization_id: string; | ||
| total_cost: number; | ||
| }>( | ||
| ` | ||
| SELECT | ||
| organization_id, | ||
| SUM(cost) as total_cost | ||
| FROM request_response_rmt | ||
| WHERE organization_id IN (${orgIds.map(() => "?").join(",")}) | ||
| GROUP BY organization_id | ||
| `, | ||
| orgIds | ||
chitalian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ); | ||
|
|
||
| const clickhouseSpendMap = new Map<string, number>(); | ||
| if (!clickhouseSpendResult.error && clickhouseSpendResult.data) { | ||
| clickhouseSpendResult.data.forEach((row) => { | ||
| // Divide by precision multiplier to get dollars | ||
| clickhouseSpendMap.set( | ||
| row.organization_id, | ||
| Number(row.total_cost) / COST_PRECISION_MULTIPLIER | ||
| ); | ||
| }); | ||
| } | ||
|
|
||
| // Combine the data | ||
| const organizations = paymentsResult.data.map((org) => ({ | ||
| orgId: org.org_id, | ||
| orgName: org.org_name || "Unknown", | ||
| stripeCustomerId: org.stripe_customer_id, | ||
| totalPayments: org.total_amount_received / 100, // Convert cents to dollars | ||
| clickhouseTotalSpend: clickhouseSpendMap.get(org.org_id) || 0, | ||
| lastPaymentDate: org.last_payment_date | ||
| ? Number(org.last_payment_date) * 1000 | ||
| : null, // Convert seconds to milliseconds | ||
| tier: org.tier || "free", | ||
| ownerEmail: org.owner_email || "Unknown", | ||
| })); | ||
|
|
||
| // Calculate summary | ||
| const totalCreditsIssued = organizations.reduce( | ||
| (sum, org) => sum + org.totalPayments, | ||
| 0 | ||
| ); | ||
| const totalCreditsSpent = organizations.reduce( | ||
| (sum, org) => sum + org.clickhouseTotalSpend, | ||
| 0 | ||
| ); | ||
|
|
||
| return ok({ | ||
| organizations, | ||
| summary: { | ||
| totalOrgsWithCredits: organizations.length, | ||
| totalCreditsIssued, | ||
| totalCreditsSpent, | ||
| }, | ||
| isProduction: ENVIRONMENT === "production", | ||
| }); | ||
| } | ||
|
|
||
| @Post("/has-feature-flag") | ||
| public async hasFeatureFlag( | ||
| @Request() request: JawnAuthenticatedRequest, | ||
|
|
@@ -1496,6 +1626,121 @@ export class AdminController extends Controller { | |
| return { query }; | ||
| } | ||
|
|
||
| @Post("/wallet/{orgId}") | ||
| public async getWalletDetails( | ||
| @Request() request: JawnAuthenticatedRequest, | ||
| @Path() orgId: string | ||
| ) { | ||
| await authCheckThrow(request.authParams.userId); | ||
|
|
||
| // Get the wallet state from the worker API using admin credentials | ||
| const workerApiUrl = | ||
| process.env.HELICONE_WORKER_API || | ||
| process.env.WORKER_API_URL || | ||
| "https://api.helicone.ai"; | ||
| const adminAccessKey = process.env.HELICONE_MANUAL_ACCESS_KEY; | ||
|
|
||
| if (!adminAccessKey) { | ||
| return err("Admin access key not configured"); | ||
| } | ||
|
|
||
| try { | ||
| // Use the admin endpoint that can query any org's wallet | ||
| const response = await fetch( | ||
| `${workerApiUrl}/admin/wallet/${orgId}/state`, | ||
| { | ||
| method: "GET", | ||
| headers: { | ||
| Authorization: `Bearer ${adminAccessKey}`, | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| return err(`Failed to fetch wallet state: ${errorText}`); | ||
| } | ||
|
|
||
| const walletState = await response.json(); | ||
| return ok(walletState); | ||
| } catch (error) { | ||
| console.error("Error fetching wallet state:", error); | ||
| return err(`Error fetching wallet state: ${error}`); | ||
| } | ||
| } | ||
|
|
||
| @Post("/wallet/{orgId}/tables/{tableName}") | ||
| public async getWalletTableData( | ||
| @Request() request: JawnAuthenticatedRequest, | ||
| @Path() orgId: string, | ||
| @Path() tableName: string, | ||
| @Query() page?: number, | ||
| @Query() pageSize?: number | ||
| ) { | ||
| // Validate pagination parameters | ||
| const validatedPage = Math.max(0, page ?? 0); | ||
| const validatedPageSize = Math.min(Math.max(1, pageSize ?? 50), 100); | ||
|
Comment on lines
+1680
to
+1682
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Incorrect validation order creates duplicate URLSearchParams instance and doesn't use validated parameters Prompt To Fix With AIThis is a comment left during a code review.
Path: valhalla/jawn/src/controllers/private/adminController.ts
Line: 1680:1682
Comment:
logic: Incorrect validation order creates duplicate URLSearchParams instance and doesn't use validated parameters
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| const params = new URLSearchParams(); | ||
| if (validatedPage > 0) params.set("page", validatedPage.toString()); | ||
| params.set("pageSize", validatedPageSize.toString()); | ||
|
|
||
| await authCheckThrow(request.authParams.userId); | ||
|
|
||
| // Validate table name to prevent injection | ||
| const allowedTables = [ | ||
| "credit_purchases", | ||
| "aggregated_debits", | ||
| "escrows", | ||
| "disallow_list", | ||
| "processed_webhook_events", | ||
| ]; | ||
|
|
||
| if (!allowedTables.includes(tableName)) { | ||
| return err(`Invalid table name: ${tableName}`); | ||
| } | ||
|
|
||
| // Get table data from the worker API using admin credentials | ||
| const workerApiUrl = | ||
| process.env.HELICONE_WORKER_API || | ||
| process.env.WORKER_API_URL || | ||
| "https://api.helicone.ai"; | ||
| const adminAccessKey = process.env.HELICONE_MANUAL_ACCESS_KEY; | ||
|
|
||
| if (!adminAccessKey) { | ||
| return err("Admin access key not configured"); | ||
| } | ||
|
|
||
| try { | ||
| // Build query params for pagination | ||
| const params = new URLSearchParams(); | ||
chitalian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (page !== undefined) params.set("page", page.toString()); | ||
| if (pageSize !== undefined) params.set("pageSize", pageSize.toString()); | ||
chitalian marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+1715
to
+1718
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: URLSearchParams created twice - first validated params are unused in favor of potentially invalid raw params Prompt To Fix With AIThis is a comment left during a code review.
Path: valhalla/jawn/src/controllers/private/adminController.ts
Line: 1715:1718
Comment:
logic: URLSearchParams created twice - first validated params are unused in favor of potentially invalid raw params
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| // Use the admin endpoint that can query any org's table data | ||
| const response = await fetch( | ||
| `${workerApiUrl}/admin/wallet/${orgId}/tables/${tableName}?${params.toString()}`, | ||
chitalian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| method: "GET", | ||
| headers: { | ||
| Authorization: `Bearer ${adminAccessKey}`, | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| return err(`Failed to fetch table data: ${errorText}`); | ||
| } | ||
|
|
||
| const tableData = await response.json(); | ||
| return ok(tableData); | ||
| } catch (error) { | ||
| console.error("Error fetching table data:", error); | ||
| return err(`Error fetching table data: ${error}`); | ||
| } | ||
| } | ||
|
|
||
| @Get("/helix-thread/{sessionId}") | ||
| public async getHelixThread( | ||
| @Request() request: JawnAuthenticatedRequest, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.