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
263 changes: 261 additions & 2 deletions apps/dashboard/app/api-keys/server-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -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")
}
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