Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 265 additions & 1 deletion apps/dashboard/app/api-keys/server-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ 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 {
createApiKey,
revokeApiKey,
setApiKeyDailyLimit,
setApiKeyWeeklyLimit,
setApiKeyMonthlyLimit,
setApiKeyAnnualLimit,
removeApiKeyLimit,
removeApiKeyWeeklyLimit,
removeApiKeyMonthlyLimit,
removeApiKeyAnnualLimit,
} from "@/services/graphql/mutations/api-keys"
import { Scope } from "@/services/graphql/generated"

export const revokeApiKeyServerAction = async (id: string) => {
Expand Down Expand Up @@ -113,9 +124,262 @@ export const createApiKeyServerAction = async (
}
}

// Set budget limits if provided
if (data?.apiKeyCreate.apiKey.id) {
const apiKeyId = data.apiKeyCreate.apiKey.id
try {
const dailyLimitSats = form.get("dailyLimitSats")
if (dailyLimitSats && dailyLimitSats !== "") {
const limit = parseInt(dailyLimitSats as string, 10)
if (limit > 0) {
await setApiKeyDailyLimit({ id: apiKeyId, dailyLimitSats: limit })
}
}

const weeklyLimitSats = form.get("weeklyLimitSats")
if (weeklyLimitSats && weeklyLimitSats !== "") {
const limit = parseInt(weeklyLimitSats as string, 10)
if (limit > 0) {
await setApiKeyWeeklyLimit({ id: apiKeyId, weeklyLimitSats: limit })
}
}

const monthlyLimitSats = form.get("monthlyLimitSats")
if (monthlyLimitSats && monthlyLimitSats !== "") {
const limit = parseInt(monthlyLimitSats as string, 10)
if (limit > 0) {
await setApiKeyMonthlyLimit({ id: apiKeyId, monthlyLimitSats: limit })
}
}

const annualLimitSats = form.get("annualLimitSats")
if (annualLimitSats && annualLimitSats !== "") {
const limit = parseInt(annualLimitSats as string, 10)
if (limit > 0) {
await setApiKeyAnnualLimit({ id: apiKeyId, annualLimitSats: 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 setApiKeyDailyLimit({ id, dailyLimitSats })
} catch (err) {
console.log("error in setApiKeyDailyLimit ", err)
throw new Error("Failed to set API key daily limit")
}

revalidatePath("/api-keys")
}

// Keep old name for backward compatibility
export const setLimit = setDailyLimit

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 setApiKeyWeeklyLimit({ id, weeklyLimitSats })
} catch (err) {
console.log("error in setApiKeyWeeklyLimit ", 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 setApiKeyMonthlyLimit({ id, monthlyLimitSats })
} catch (err) {
console.log("error in setApiKeyMonthlyLimit ", 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 setApiKeyAnnualLimit({ id, annualLimitSats })
} catch (err) {
console.log("error in setApiKeyAnnualLimit ", 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 })
} catch (err) {
console.log("error in removeApiKeyLimit ", 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 removeApiKeyWeeklyLimit({ id })
} catch (err) {
console.log("error in removeApiKeyWeeklyLimit ", 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 removeApiKeyMonthlyLimit({ id })
} catch (err) {
console.log("error in removeApiKeyMonthlyLimit ", 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 removeApiKeyAnnualLimit({ id })
} catch (err) {
console.log("error in removeApiKeyAnnualLimit ", err)
throw new Error("Failed to remove API key annual limit")
}

revalidatePath("/api-keys")
}
84 changes: 84 additions & 0 deletions apps/dashboard/components/api-keys/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type ApiKeyFormProps = {
const ApiKeyForm = ({ state, formAction }: ApiKeyFormProps) => {
const [enableCustomExpiresInDays, setEnableCustomExpiresInDays] = useState(false)
const [expiresInDays, setExpiresInDays] = useState<number | null>(null)
const [showSpendingLimits, setShowSpendingLimits] = useState(false)

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
Expand Down Expand Up @@ -53,6 +54,11 @@ const ApiKeyForm = ({ state, formAction }: ApiKeyFormProps) => {
)}
{state.error && <ErrorMessage message={state.message} />}
<ScopeCheckboxes />
<SpendingLimitsToggle
showSpendingLimits={showSpendingLimits}
setShowSpendingLimits={setShowSpendingLimits}
/>
{showSpendingLimits && <SpendingLimitsInputs />}
<SubmitButton />
</form>
</FormControl>
Expand Down Expand Up @@ -179,6 +185,84 @@ const ScopeCheckboxes = () => (
</Box>
)

const SpendingLimitsToggle = ({
showSpendingLimits,
setShowSpendingLimits,
}: {
showSpendingLimits: boolean
setShowSpendingLimits: (value: boolean) => void
}) => (
<Box sx={{ display: "flex", flexDirection: "row", alignItems: "center", gap: "0.5em" }}>
<Checkbox
checked={showSpendingLimits}
onChange={(e) => setShowSpendingLimits(e.target.checked)}
label="Set budget limits"
/>
</Box>
)

const SpendingLimitsInputs = () => (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "1em",
padding: "1em",
border: "1px solid",
borderColor: "divider",
borderRadius: "8px",
}}
>
<Typography level="body-sm" sx={{ fontWeight: "bold" }}>
Limits (in satoshis)
</Typography>
<Box sx={{ display: "flex", flexDirection: "column", gap: "0.2em" }}>
<Typography level="body-sm">Daily Limit</Typography>
<Input
name="dailyLimitSats"
id="dailyLimitSats"
type="number"
placeholder="e.g., 100000"
sx={{ padding: "0.6em", width: "100%" }}
/>
<FormHelperText>Rolling 24-hour window</FormHelperText>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", gap: "0.2em" }}>
<Typography level="body-sm">Weekly Limit</Typography>
<Input
name="weeklyLimitSats"
id="weeklyLimitSats"
type="number"
placeholder="e.g., 500000"
sx={{ padding: "0.6em", width: "100%" }}
/>
<FormHelperText>Rolling 7-day window</FormHelperText>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", gap: "0.2em" }}>
<Typography level="body-sm">Monthly Limit</Typography>
<Input
name="monthlyLimitSats"
id="monthlyLimitSats"
type="number"
placeholder="e.g., 2000000"
sx={{ padding: "0.6em", width: "100%" }}
/>
<FormHelperText>Rolling 30-day window</FormHelperText>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", gap: "0.2em" }}>
<Typography level="body-sm">Annual Limit</Typography>
<Input
name="annualLimitSats"
id="annualLimitSats"
type="number"
placeholder="e.g., 20000000"
sx={{ padding: "0.6em", width: "100%" }}
/>
<FormHelperText>Rolling 365-day window</FormHelperText>
</Box>
</Box>
)

const SubmitButton = () => (
<Box
sx={{
Expand Down
Loading
Loading