diff --git a/apps/dashboard/app/api-keys/server-actions.ts b/apps/dashboard/app/api-keys/server-actions.ts index f27072ceea..fb502d30b6 100644 --- a/apps/dashboard/app/api-keys/server-actions.ts +++ b/apps/dashboard/app/api-keys/server-actions.ts @@ -5,8 +5,13 @@ import { revalidatePath } from "next/cache" import { ApiKeyResponse } from "./api-key.types" import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { createApiKey, revokeApiKey } from "@/services/graphql/mutations/api-keys" -import { Scope } from "@/services/graphql/generated" +import { + createApiKey, + revokeApiKey, + setApiKeyLimit, + removeApiKeyLimit, +} from "@/services/graphql/mutations/api-keys" +import { LimitTimeWindow, Scope } from "@/services/graphql/generated" export const revokeApiKeyServerAction = async (id: string) => { if (!id || typeof id !== "string") { @@ -113,9 +118,263 @@ export const createApiKeyServerAction = async ( } } + if (data?.apiKeyCreate.apiKey.id) { + const apiKeyId = data.apiKeyCreate.apiKey.id + try { + const limitFields: Array<{ formField: string; timeWindow: LimitTimeWindow }> = [ + { formField: "dailyLimitSats", timeWindow: LimitTimeWindow.Daily }, + { formField: "weeklyLimitSats", timeWindow: LimitTimeWindow.Weekly }, + { formField: "monthlyLimitSats", timeWindow: LimitTimeWindow.Monthly }, + { formField: "annualLimitSats", timeWindow: LimitTimeWindow.Annual }, + ] + + for (const { formField, timeWindow } of limitFields) { + const value = form.get(formField) + if (value && value !== "") { + const limit = parseInt(value as string, 10) + if (limit > 0) { + await setApiKeyLimit({ + id: apiKeyId, + limitTimeWindow: timeWindow, + limitSats: limit, + }) + } + } + } + } catch (err) { + console.log("error in setting API key limits ", err) + // Don't fail the entire operation if limits fail to set + // The API key was created successfully + } + } + return { error: false, message: "API Key created successfully", responsePayload: { apiKeySecret: data?.apiKeyCreate.apiKeySecret }, } } + +export const setDailyLimit = async ({ + id, + dailyLimitSats, +}: { + id: string + dailyLimitSats: number +}) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + if (!dailyLimitSats || dailyLimitSats <= 0) { + throw new Error("Daily limit must be greater than 0") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await setApiKeyLimit({ + id, + limitTimeWindow: LimitTimeWindow.Daily, + limitSats: dailyLimitSats, + }) + } catch (err) { + console.log("error in setApiKeyLimit (daily) ", err) + throw new Error("Failed to set API key daily limit") + } + + revalidatePath("/api-keys") +} + +export const setWeeklyLimit = async ({ + id, + weeklyLimitSats, +}: { + id: string + weeklyLimitSats: number +}) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + if (!weeklyLimitSats || weeklyLimitSats <= 0) { + throw new Error("Weekly limit must be greater than 0") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await setApiKeyLimit({ + id, + limitTimeWindow: LimitTimeWindow.Weekly, + limitSats: weeklyLimitSats, + }) + } catch (err) { + console.log("error in setApiKeyLimit (weekly) ", err) + throw new Error("Failed to set API key weekly limit") + } + + revalidatePath("/api-keys") +} + +export const setMonthlyLimit = async ({ + id, + monthlyLimitSats, +}: { + id: string + monthlyLimitSats: number +}) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + if (!monthlyLimitSats || monthlyLimitSats <= 0) { + throw new Error("Monthly limit must be greater than 0") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await setApiKeyLimit({ + id, + limitTimeWindow: LimitTimeWindow.Monthly, + limitSats: monthlyLimitSats, + }) + } catch (err) { + console.log("error in setApiKeyLimit (monthly) ", err) + throw new Error("Failed to set API key monthly limit") + } + + revalidatePath("/api-keys") +} + +export const setAnnualLimit = async ({ + id, + annualLimitSats, +}: { + id: string + annualLimitSats: number +}) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + if (!annualLimitSats || annualLimitSats <= 0) { + throw new Error("Annual limit must be greater than 0") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await setApiKeyLimit({ + id, + limitTimeWindow: LimitTimeWindow.Annual, + limitSats: annualLimitSats, + }) + } catch (err) { + console.log("error in setApiKeyLimit (annual) ", err) + throw new Error("Failed to set API key annual limit") + } + + revalidatePath("/api-keys") +} + +export const removeLimit = async ({ id }: { id: string }) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await removeApiKeyLimit({ id, limitTimeWindow: LimitTimeWindow.Daily }) + } catch (err) { + console.log("error in removeApiKeyLimit (daily) ", err) + throw new Error("Failed to remove API key limit") + } + + revalidatePath("/api-keys") +} + +export const removeWeeklyLimit = async ({ id }: { id: string }) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await removeApiKeyLimit({ id, limitTimeWindow: LimitTimeWindow.Weekly }) + } catch (err) { + console.log("error in removeApiKeyLimit (weekly) ", err) + throw new Error("Failed to remove API key weekly limit") + } + + revalidatePath("/api-keys") +} + +export const removeMonthlyLimit = async ({ id }: { id: string }) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await removeApiKeyLimit({ id, limitTimeWindow: LimitTimeWindow.Monthly }) + } catch (err) { + console.log("error in removeApiKeyLimit (monthly) ", err) + throw new Error("Failed to remove API key monthly limit") + } + + revalidatePath("/api-keys") +} + +export const removeAnnualLimit = async ({ id }: { id: string }) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await removeApiKeyLimit({ id, limitTimeWindow: LimitTimeWindow.Annual }) + } catch (err) { + console.log("error in removeApiKeyLimit (annual) ", err) + throw new Error("Failed to remove API key annual limit") + } + + revalidatePath("/api-keys") +} diff --git a/apps/dashboard/components/api-keys/form.tsx b/apps/dashboard/components/api-keys/form.tsx index 9d71c925f4..ecdec3f881 100644 --- a/apps/dashboard/components/api-keys/form.tsx +++ b/apps/dashboard/components/api-keys/form.tsx @@ -23,6 +23,7 @@ type ApiKeyFormProps = { const ApiKeyForm = ({ state, formAction }: ApiKeyFormProps) => { const [enableCustomExpiresInDays, setEnableCustomExpiresInDays] = useState(false) const [expiresInDays, setExpiresInDays] = useState(null) + const [showSpendingLimits, setShowSpendingLimits] = useState(false) const handleSubmit = async (event: React.FormEvent) => { event.preventDefault() @@ -53,6 +54,11 @@ const ApiKeyForm = ({ state, formAction }: ApiKeyFormProps) => { )} {state.error && } + + {showSpendingLimits && } @@ -179,6 +185,84 @@ const ScopeCheckboxes = () => ( ) +const SpendingLimitsToggle = ({ + showSpendingLimits, + setShowSpendingLimits, +}: { + showSpendingLimits: boolean + setShowSpendingLimits: (value: boolean) => void +}) => ( + + setShowSpendingLimits(e.target.checked)} + label="Set budget limits" + /> + +) + +const SpendingLimitsInputs = () => ( + + + Limits (in satoshis) + + + Daily Limit + + Rolling 24-hour window + + + Weekly Limit + + Rolling 7-day window + + + Monthly Limit + + Rolling 30-day window + + + Annual Limit + + Rolling 365-day window + + +) + const SubmitButton = () => ( = ({ id, limits, spent }) => { + const [open, setOpen] = useState(false) + const [selectedPeriod, setSelectedPeriod] = useState("daily") + const [limitValues, setLimitValues] = useState({ + daily: limits.daily?.toString() || "", + weekly: limits.weekly?.toString() || "", + monthly: limits.monthly?.toString() || "", + annual: limits.annual?.toString() || "", + }) + const [loading, setLoading] = useState(false) + + const periodConfig = { + daily: { + label: "Daily (24h)", + description: "Set a rolling 24-hour spending limit", + currentLimit: limits.daily, + spent: spent.last24h, + setValue: (val: string) => setLimitValues({ ...limitValues, daily: val }), + getValue: () => limitValues.daily, + }, + weekly: { + label: "Weekly (7 days)", + description: "Set a rolling 7-day spending limit", + currentLimit: limits.weekly, + spent: spent.last7d, + setValue: (val: string) => setLimitValues({ ...limitValues, weekly: val }), + getValue: () => limitValues.weekly, + }, + monthly: { + label: "Monthly (30 days)", + description: "Set a rolling 30-day spending limit", + currentLimit: limits.monthly, + spent: spent.last30d, + setValue: (val: string) => setLimitValues({ ...limitValues, monthly: val }), + getValue: () => limitValues.monthly, + }, + annual: { + label: "Annual (365 days)", + description: "Set a rolling 365-day spending limit", + currentLimit: limits.annual, + spent: spent.last365d, + setValue: (val: string) => setLimitValues({ ...limitValues, annual: val }), + getValue: () => limitValues.annual, + }, + } + + const handleSetLimit = async (period: LimitPeriod) => { + const config = periodConfig[period] + const limitValue = config.getValue() + + if (!limitValue || parseInt(limitValue) <= 0) { + alert("Please enter a valid limit in satoshis") + return + } + + setLoading(true) + try { + const satsValue = parseInt(limitValue) + switch (period) { + case "daily": + await setDailyLimit({ id, dailyLimitSats: satsValue }) + break + case "weekly": + await setWeeklyLimit({ id, weeklyLimitSats: satsValue }) + break + case "monthly": + await setMonthlyLimit({ id, monthlyLimitSats: satsValue }) + break + case "annual": + await setAnnualLimit({ id, annualLimitSats: satsValue }) + break + } + setOpen(false) + window.location.reload() + } catch (error) { + console.error("Error setting limit:", error) + alert("Failed to set limit. Please try again.") + } finally { + setLoading(false) + } + } + + const handleRemoveLimit = async (period: LimitPeriod) => { + const periodLabels = { + daily: "daily", + weekly: "weekly", + monthly: "monthly", + annual: "annual", + } + + if ( + !confirm( + `Are you sure you want to remove the ${periodLabels[period]} spending limit?`, + ) + ) { + return + } + + setLoading(true) + try { + switch (period) { + case "daily": + await removeLimit({ id }) + break + case "weekly": + await removeWeeklyLimit({ id }) + break + case "monthly": + await removeMonthlyLimit({ id }) + break + case "annual": + await removeAnnualLimit({ id }) + break + } + setOpen(false) + window.location.reload() + } catch (error) { + console.error("Error removing limit:", error) + alert("Failed to remove limit. Please try again.") + } finally { + setLoading(false) + } + } + + const formatSats = (sats: number | null) => { + if (sats === null) return "Unlimited" + return `${sats.toLocaleString()} sats` + } + + const hasAnyLimit = limits.daily || limits.weekly || limits.monthly || limits.annual + + return ( + <> + + + setOpen(false)}> + + Budget Limits + + Configure rolling budget limits for different time periods + + + setSelectedPeriod(value as LimitPeriod)} + > + + Daily + Weekly + Monthly + Annual + + + {(Object.keys(periodConfig) as LimitPeriod[]).map((period) => { + const config = periodConfig[period] + const remaining = config.currentLimit + ? config.currentLimit - config.spent + : null + + return ( + + + {config.description} + + {config.currentLimit && ( + + + + Current Limit:{" "} + {formatSats(config.currentLimit)} + + + Spent: {formatSats(config.spent)} + + + Remaining: {formatSats(remaining)} + + + + )} + + + {config.label} Limit (satoshis) + config.setValue(e.target.value)} + placeholder="Enter limit in sats (e.g., 100000)" + disabled={loading} + /> + + + + + {config.currentLimit && ( + + )} + + + + ) + })} + + + + + + + + + ) +} + +export default Limit diff --git a/apps/dashboard/components/api-keys/list.tsx b/apps/dashboard/components/api-keys/list.tsx index 13d8a9d217..5e94cd7ff4 100644 --- a/apps/dashboard/components/api-keys/list.tsx +++ b/apps/dashboard/components/api-keys/list.tsx @@ -1,9 +1,13 @@ +"use client" + import React from "react" import Table from "@mui/joy/Table" import Typography from "@mui/joy/Typography" import Divider from "@mui/joy/Divider" +import { Stack } from "@mui/joy" import RevokeKey from "./revoke" +import Limit from "./limit" import { formatDate, getScopeText } from "./utils" import { ApiKey } from "@/services/graphql/generated" @@ -25,25 +29,117 @@ const ApiKeysList: React.FC = ({ - - - - - - + + + + + + + - {activeKeys.map(({ id, name, expiresAt, lastUsedAt, scopes }) => { + {activeKeys.map(({ id, name, expiresAt, lastUsedAt, scopes, limits }) => { + const dailyLimitSats = limits?.dailyLimitSats + const weeklyLimitSats = limits?.weeklyLimitSats + const monthlyLimitSats = limits?.monthlyLimitSats + const annualLimitSats = limits?.annualLimitSats + const dailySpentSats = limits?.dailySpentSats ?? 0 + const weeklySpentSats = limits?.weeklySpentSats ?? 0 + const monthlySpentSats = limits?.monthlySpentSats ?? 0 + const annualSpentSats = limits?.annualSpentSats ?? 0 + + const remainingDailyLimitSats = + dailyLimitSats !== null && dailyLimitSats !== undefined + ? dailyLimitSats - dailySpentSats + : null + + const hasAnyLimit = + dailyLimitSats || weeklyLimitSats || monthlyLimitSats || annualLimitSats + return ( + ) diff --git a/apps/dashboard/services/graphql/generated.ts b/apps/dashboard/services/graphql/generated.ts index b4535f35c9..86451398d9 100644 --- a/apps/dashboard/services/graphql/generated.ts +++ b/apps/dashboard/services/graphql/generated.ts @@ -2536,7 +2536,7 @@ export type ApiKeyCreateMutationVariables = Exact<{ }>; -export type ApiKeyCreateMutation = { readonly __typename: 'Mutation', readonly apiKeyCreate: { readonly __typename: 'ApiKeyCreatePayload', readonly apiKeySecret: string, readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt?: number | null, readonly scopes: ReadonlyArray } } }; +export type ApiKeyCreateMutation = { readonly __typename: 'Mutation', readonly apiKeyCreate: { readonly __typename: 'ApiKeyCreatePayload', readonly apiKeySecret: string, readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt?: number | null, readonly scopes: ReadonlyArray, readonly limits: { readonly __typename: 'ApiKeyLimits', readonly dailyLimitSats?: number | null, readonly weeklyLimitSats?: number | null, readonly monthlyLimitSats?: number | null, readonly annualLimitSats?: number | null, readonly dailySpentSats: number, readonly weeklySpentSats: number, readonly monthlySpentSats: number, readonly annualSpentSats: number } } } }; export type ApiKeyRevokeMutationVariables = Exact<{ input: ApiKeyRevokeInput; @@ -2545,6 +2545,20 @@ export type ApiKeyRevokeMutationVariables = Exact<{ export type ApiKeyRevokeMutation = { readonly __typename: 'Mutation', readonly apiKeyRevoke: { readonly __typename: 'ApiKeyRevokePayload', readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt?: number | null, readonly scopes: ReadonlyArray } } }; +export type ApiKeySetLimitMutationVariables = Exact<{ + input: ApiKeySetLimitInput; +}>; + + +export type ApiKeySetLimitMutation = { readonly __typename: 'Mutation', readonly apiKeySetLimit: { readonly __typename: 'ApiKeySetLimitPayload', readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly limits: { readonly __typename: 'ApiKeyLimits', readonly dailyLimitSats?: number | null, readonly weeklyLimitSats?: number | null, readonly monthlyLimitSats?: number | null, readonly annualLimitSats?: number | null, readonly dailySpentSats: number, readonly weeklySpentSats: number, readonly monthlySpentSats: number, readonly annualSpentSats: number } } } }; + +export type ApiKeyRemoveLimitMutationVariables = Exact<{ + input: ApiKeyRemoveLimitInput; +}>; + + +export type ApiKeyRemoveLimitMutation = { readonly __typename: 'Mutation', readonly apiKeyRemoveLimit: { readonly __typename: 'ApiKeySetLimitPayload', readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly limits: { readonly __typename: 'ApiKeyLimits', readonly dailyLimitSats?: number | null, readonly weeklyLimitSats?: number | null, readonly monthlyLimitSats?: number | null, readonly annualLimitSats?: number | null, readonly dailySpentSats: number, readonly weeklySpentSats: number, readonly monthlySpentSats: number, readonly annualSpentSats: number } } } }; + export type CallbackEndpointAddMutationVariables = Exact<{ input: CallbackEndpointAddInput; }>; @@ -2667,6 +2681,16 @@ export const ApiKeyCreateDocument = gql` lastUsedAt expiresAt scopes + limits { + dailyLimitSats + weeklyLimitSats + monthlyLimitSats + annualLimitSats + dailySpentSats + weeklySpentSats + monthlySpentSats + annualSpentSats + } } apiKeySecret } @@ -2740,6 +2764,98 @@ export function useApiKeyRevokeMutation(baseOptions?: Apollo.MutationHookOptions export type ApiKeyRevokeMutationHookResult = ReturnType; export type ApiKeyRevokeMutationResult = Apollo.MutationResult; export type ApiKeyRevokeMutationOptions = Apollo.BaseMutationOptions; +export const ApiKeySetLimitDocument = gql` + mutation ApiKeySetLimit($input: ApiKeySetLimitInput!) { + apiKeySetLimit(input: $input) { + apiKey { + id + name + limits { + dailyLimitSats + weeklyLimitSats + monthlyLimitSats + annualLimitSats + dailySpentSats + weeklySpentSats + monthlySpentSats + annualSpentSats + } + } + } +} + `; +export type ApiKeySetLimitMutationFn = Apollo.MutationFunction; + +/** + * __useApiKeySetLimitMutation__ + * + * To run a mutation, you first call `useApiKeySetLimitMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useApiKeySetLimitMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [apiKeySetLimitMutation, { data, loading, error }] = useApiKeySetLimitMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useApiKeySetLimitMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(ApiKeySetLimitDocument, options); + } +export type ApiKeySetLimitMutationHookResult = ReturnType; +export type ApiKeySetLimitMutationResult = Apollo.MutationResult; +export type ApiKeySetLimitMutationOptions = Apollo.BaseMutationOptions; +export const ApiKeyRemoveLimitDocument = gql` + mutation ApiKeyRemoveLimit($input: ApiKeyRemoveLimitInput!) { + apiKeyRemoveLimit(input: $input) { + apiKey { + id + name + limits { + dailyLimitSats + weeklyLimitSats + monthlyLimitSats + annualLimitSats + dailySpentSats + weeklySpentSats + monthlySpentSats + annualSpentSats + } + } + } +} + `; +export type ApiKeyRemoveLimitMutationFn = Apollo.MutationFunction; + +/** + * __useApiKeyRemoveLimitMutation__ + * + * To run a mutation, you first call `useApiKeyRemoveLimitMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useApiKeyRemoveLimitMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [apiKeyRemoveLimitMutation, { data, loading, error }] = useApiKeyRemoveLimitMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useApiKeyRemoveLimitMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(ApiKeyRemoveLimitDocument, options); + } +export type ApiKeyRemoveLimitMutationHookResult = ReturnType; +export type ApiKeyRemoveLimitMutationResult = Apollo.MutationResult; +export type ApiKeyRemoveLimitMutationOptions = Apollo.BaseMutationOptions; export const CallbackEndpointAddDocument = gql` mutation CallbackEndpointAdd($input: CallbackEndpointAddInput!) { callbackEndpointAdd(input: $input) { diff --git a/apps/dashboard/services/graphql/mutations/api-keys.ts b/apps/dashboard/services/graphql/mutations/api-keys.ts index a0a0a414d8..6326c85b6e 100644 --- a/apps/dashboard/services/graphql/mutations/api-keys.ts +++ b/apps/dashboard/services/graphql/mutations/api-keys.ts @@ -6,6 +6,11 @@ import { ApiKeyCreateMutation, ApiKeyRevokeDocument, ApiKeyRevokeMutation, + ApiKeySetLimitDocument, + ApiKeySetLimitMutation, + ApiKeyRemoveLimitDocument, + ApiKeyRemoveLimitMutation, + LimitTimeWindow, Scope, } from "../generated" @@ -21,6 +26,16 @@ gql` lastUsedAt expiresAt scopes + limits { + dailyLimitSats + weeklyLimitSats + monthlyLimitSats + annualLimitSats + dailySpentSats + weeklySpentSats + monthlySpentSats + annualSpentSats + } } apiKeySecret } @@ -40,6 +55,44 @@ gql` } } } + + mutation ApiKeySetLimit($input: ApiKeySetLimitInput!) { + apiKeySetLimit(input: $input) { + apiKey { + id + name + limits { + dailyLimitSats + weeklyLimitSats + monthlyLimitSats + annualLimitSats + dailySpentSats + weeklySpentSats + monthlySpentSats + annualSpentSats + } + } + } + } + + mutation ApiKeyRemoveLimit($input: ApiKeyRemoveLimitInput!) { + apiKeyRemoveLimit(input: $input) { + apiKey { + id + name + limits { + dailyLimitSats + weeklyLimitSats + monthlyLimitSats + annualLimitSats + dailySpentSats + weeklySpentSats + monthlySpentSats + annualSpentSats + } + } + } + } ` export async function createApiKey({ @@ -77,3 +130,45 @@ export async function revokeApiKey({ id }: { id: string }) { throw new Error("Error in apiKeyRevoke") } } + +export async function setApiKeyLimit({ + id, + limitTimeWindow, + limitSats, +}: { + id: string + limitTimeWindow: LimitTimeWindow + limitSats: number +}) { + const client = await apolloClient.authenticated() + try { + const { data } = await client.mutate({ + mutation: ApiKeySetLimitDocument, + variables: { input: { id, limitTimeWindow, limitSats } }, + }) + return data + } catch (error) { + console.error("Error executing mutation: apiKeySetLimit ==> ", error) + throw new Error("Error in apiKeySetLimit") + } +} + +export async function removeApiKeyLimit({ + id, + limitTimeWindow, +}: { + id: string + limitTimeWindow: LimitTimeWindow +}) { + const client = await apolloClient.authenticated() + try { + const { data } = await client.mutate({ + mutation: ApiKeyRemoveLimitDocument, + variables: { input: { id, limitTimeWindow } }, + }) + return data + } catch (error) { + console.error("Error executing mutation: apiKeyRemoveLimit ==> ", error) + throw new Error("Error in apiKeyRemoveLimit") + } +}
NameAPI Key IDScopeExpires AtLast UsedActionNameAPI Key IDScopeBudget LimitsExpires AtLast UsedActions
{name} {id} {getScopeText(scopes)} + {hasAnyLimit ? ( + + {dailyLimitSats && ( +
+ + Daily: {dailyLimitSats.toLocaleString()} sats + + + Spent: {dailySpentSats.toLocaleString()} / Remaining:{" "} + {remainingDailyLimitSats?.toLocaleString() || 0} + +
+ )} + {weeklyLimitSats && ( +
+ + Weekly: {weeklyLimitSats.toLocaleString()}{" "} + sats + + + Spent: {weeklySpentSats.toLocaleString()} / Remaining:{" "} + {(weeklyLimitSats - weeklySpentSats).toLocaleString()} + +
+ )} + {monthlyLimitSats && ( +
+ + Monthly: {monthlyLimitSats.toLocaleString()}{" "} + sats + + + Spent: {monthlySpentSats.toLocaleString()} / Remaining:{" "} + {(monthlyLimitSats - monthlySpentSats).toLocaleString()} + +
+ )} + {annualLimitSats && ( +
+ + Annual: {annualLimitSats.toLocaleString()}{" "} + sats + + + Spent: {annualSpentSats.toLocaleString()} / Remaining:{" "} + {(annualLimitSats - annualSpentSats).toLocaleString()} + +
+ )} +
+ ) : ( + + Unlimited + + )} +
{expiresAt ? formatDate(expiresAt) : "Never"} {lastUsedAt ? formatDate(lastUsedAt) : "Never"} - + + + +