Sync layouts, typography, and watermark studio for self-hosted OSS.
Unlock features without credits/Stripe, point docs to docs.songs2vid.com, and drop leftover billing admin surfaces. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+24
-130
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { API_DOCS_URL } from "@/lib/plans";
|
||||
|
||||
type ApiKeyStatus = {
|
||||
configured: boolean;
|
||||
@@ -16,39 +17,16 @@ type RateLimitStatus = {
|
||||
bonus?: number;
|
||||
};
|
||||
|
||||
type ExtensionRequest = {
|
||||
id: string;
|
||||
status: string;
|
||||
message: string;
|
||||
requestedAt: string;
|
||||
processedAt: string | null;
|
||||
adminNote: string | null;
|
||||
};
|
||||
|
||||
type ExtensionUsage = {
|
||||
used: number;
|
||||
limit: number;
|
||||
remaining: number;
|
||||
requests: ExtensionRequest[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
initialStatus: ApiKeyStatus;
|
||||
initialRateLimit: RateLimitStatus;
|
||||
initialExtensionUsage: ExtensionUsage;
|
||||
};
|
||||
|
||||
export function ApiKeySettings({
|
||||
initialStatus,
|
||||
initialRateLimit,
|
||||
initialExtensionUsage,
|
||||
}: Props) {
|
||||
export function ApiKeySettings({ initialStatus, initialRateLimit }: Props) {
|
||||
const [status, setStatus] = useState(initialStatus);
|
||||
const [rateLimit, setRateLimit] = useState(initialRateLimit);
|
||||
const [extensionUsage, setExtensionUsage] = useState(initialExtensionUsage);
|
||||
const [newKey, setNewKey] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
@@ -81,146 +59,58 @@ export function ApiKeySettings({
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setNewKey(null);
|
||||
setSuccess(null);
|
||||
|
||||
setNewKey(null);
|
||||
try {
|
||||
const res = await fetch("/api/account/api-key", { method: "POST" });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Failed to generate API key");
|
||||
|
||||
if (!res.ok) throw new Error(data.error || "Failed to generate key");
|
||||
setNewKey(data.apiKey);
|
||||
setStatus({ configured: true, prefix: data.prefix });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to generate API key");
|
||||
setSuccess("API key generated. Copy it now — it will not be shown again.");
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to generate key");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeKey() {
|
||||
if (!confirm("Revoke your API key? External integrations will stop working.")) return;
|
||||
|
||||
if (!confirm("Revoke the current API key?")) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
setNewKey(null);
|
||||
try {
|
||||
const res = await fetch("/api/account/api-key", { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Failed to revoke API key");
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "Failed to revoke key");
|
||||
setStatus({ configured: false, prefix: null });
|
||||
setNewKey(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to revoke API key");
|
||||
setSuccess("API key revoked.");
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to revoke key");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function requestRateLimitExtension() {
|
||||
if (extensionUsage.remaining <= 0) return;
|
||||
|
||||
const reason = prompt(
|
||||
"Optional: tell us why you need a higher API rate limit (leave blank to skip).",
|
||||
);
|
||||
if (reason === null) return;
|
||||
|
||||
setRequesting(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/account/api-rate-limit-extension-request", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: reason }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Failed to submit request");
|
||||
|
||||
setExtensionUsage({
|
||||
used: data.used,
|
||||
limit: data.limit,
|
||||
remaining: data.remaining,
|
||||
requests: data.requests ?? extensionUsage.requests,
|
||||
});
|
||||
setSuccess(
|
||||
`Rate limit extension request submitted. ${data.used} of ${data.limit} used this year.`,
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to submit request");
|
||||
} finally {
|
||||
setRequesting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const hasPending = extensionUsage.requests.some((r) => r.status === "PENDING");
|
||||
const usedPct = Math.min(100, Math.round((rateLimit.used / Math.max(1, rateLimit.limit)) * 100));
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-400">
|
||||
Use the REST API to upload files and create batch video jobs programmatically. Pro plan
|
||||
only.
|
||||
Use the REST API to upload files and create batch video jobs programmatically.
|
||||
</p>
|
||||
|
||||
<div className="rounded border border-gray-800 bg-surface p-4 space-y-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="text-sm font-medium text-white">API rate limit</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
Resets in {rateLimit.resetsInSeconds}s
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">Resets in {rateLimit.resetsInSeconds}s</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-300">
|
||||
{rateLimit.used} / {rateLimit.limit} requests used this minute
|
||||
{rateLimit.bonus ? (
|
||||
<span className="text-gray-500"> (includes +{rateLimit.bonus} bonus)</span>
|
||||
) : null}
|
||||
</p>
|
||||
<div className="h-2 overflow-hidden rounded bg-gray-800">
|
||||
<div
|
||||
className={`h-full rounded transition-all ${
|
||||
usedPct >= 90 ? "bg-red-500" : usedPct >= 70 ? "bg-amber-500" : "bg-accent"
|
||||
}`}
|
||||
style={{ width: `${usedPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
{rateLimit.remaining} requests remaining · window {rateLimit.windowSeconds}s
|
||||
Self-hosted: API rate limits are effectively unlimited.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-xs text-gray-400">
|
||||
Extension requests this year: {extensionUsage.used} / {extensionUsage.limit}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={requestRateLimitExtension}
|
||||
disabled={requesting || extensionUsage.remaining <= 0 || hasPending}
|
||||
className="inline-flex items-center justify-center rounded border border-accent/40 px-3 py-1.5 text-xs font-medium text-accent transition-colors hover:bg-accent/10 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{requesting
|
||||
? "Submitting…"
|
||||
: hasPending
|
||||
? "Pending request"
|
||||
: extensionUsage.remaining <= 0
|
||||
? "No extensions left"
|
||||
: "Request rate limit extension"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{extensionUsage.requests.length > 0 && (
|
||||
<ul className="space-y-1 border-t border-gray-800 pt-3 text-xs text-gray-500">
|
||||
{extensionUsage.requests.slice(0, 5).map((req) => (
|
||||
<li key={req.id}>
|
||||
{new Date(req.requestedAt).toLocaleDateString()} · {req.status}
|
||||
{req.adminNote ? ` · ${req.adminNote}` : ""}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{status.configured && status.prefix && (
|
||||
@@ -284,9 +174,13 @@ export function ApiKeySettings({
|
||||
</ul>
|
||||
<p className="mt-3">
|
||||
Send <span className="text-gray-300">Authorization: Bearer YOUR_API_KEY</span> on every
|
||||
request. For many audio files, upload each file then call{" "}
|
||||
<span className="text-gray-300">/api/v1/jobs</span> (avoid large one-shot batches).{" "}
|
||||
<a href="/dashboard/api-docs" className="text-accent hover:underline">
|
||||
request.{" "}
|
||||
<a
|
||||
href={API_DOCS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent hover:underline"
|
||||
>
|
||||
Full API docs
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user