mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
api (#4892)
# Description of Changes <img width="1253" height="816" alt="image" src="https://github.com/user-attachments/assets/51154a0e-4353-4e62-83c5-0896bc4e98df" /> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
parent
350fdcf29a
commit
052a3ae653
@ -4650,6 +4650,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apiKeys": {
|
"apiKeys": {
|
||||||
|
"intro": "Use your API key to programmatically access Stirling PDF's processing capabilities.",
|
||||||
|
"docsTitle": "API Documentation",
|
||||||
|
"docsDescription": "Learn more about integrating with Stirling PDF:",
|
||||||
|
"docsLink": "API Documentation",
|
||||||
|
"schemaLink": "API Schema Reference",
|
||||||
|
"usage": "Include this key in the X-API-KEY header with all API requests.",
|
||||||
"description": "Your API key for accessing Stirling's suite of PDF tools. Copy it to your project or refresh to generate a new one.",
|
"description": "Your API key for accessing Stirling's suite of PDF tools. Copy it to your project or refresh to generate a new one.",
|
||||||
"publicKeyAriaLabel": "Public API key",
|
"publicKeyAriaLabel": "Public API key",
|
||||||
"copyKeyAriaLabel": "Copy API key",
|
"copyKeyAriaLabel": "Copy API key",
|
||||||
|
|||||||
@ -64,17 +64,19 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
|||||||
headerBorder: 'var(--modal-header-border)',
|
headerBorder: 'var(--modal-header-border)',
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
// Get isAdmin and runningEE from app config
|
// Get isAdmin, runningEE, and loginEnabled from app config
|
||||||
const isAdmin = config?.isAdmin ?? false;
|
const isAdmin = config?.isAdmin ?? false;
|
||||||
const runningEE = config?.runningEE ?? false;
|
const runningEE = config?.runningEE ?? false;
|
||||||
|
const loginEnabled = config?.enableLogin ?? false;
|
||||||
|
|
||||||
// Left navigation structure and icons
|
// Left navigation structure and icons
|
||||||
const configNavSections = useMemo(() =>
|
const configNavSections = useMemo(() =>
|
||||||
createConfigNavSections(
|
createConfigNavSections(
|
||||||
isAdmin,
|
isAdmin,
|
||||||
runningEE
|
runningEE,
|
||||||
|
loginEnabled
|
||||||
),
|
),
|
||||||
[isAdmin, runningEE]
|
[isAdmin, runningEE, loginEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeLabel = useMemo(() => {
|
const activeLabel = useMemo(() => {
|
||||||
|
|||||||
@ -41,7 +41,8 @@ export interface ConfigColors {
|
|||||||
|
|
||||||
export const createConfigNavSections = (
|
export const createConfigNavSections = (
|
||||||
isAdmin: boolean = false,
|
isAdmin: boolean = false,
|
||||||
runningEE: boolean = false
|
runningEE: boolean = false,
|
||||||
|
_loginEnabled: boolean = false
|
||||||
): ConfigNavSection[] => {
|
): ConfigNavSection[] => {
|
||||||
const sections: ConfigNavSection[] = [
|
const sections: ConfigNavSection[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -279,6 +279,15 @@
|
|||||||
--modal-content-bg: #ffffff;
|
--modal-content-bg: #ffffff;
|
||||||
--modal-header-border: rgba(0, 0, 0, 0.06);
|
--modal-header-border: rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
/* API Keys section colors (light mode) */
|
||||||
|
--api-keys-card-bg: #ffffff;
|
||||||
|
--api-keys-card-border: #e0e0e0;
|
||||||
|
--api-keys-card-shadow: rgba(0, 0, 0, 0.06);
|
||||||
|
--api-keys-input-bg: #f8f8f8;
|
||||||
|
--api-keys-input-border: #e0e0e0;
|
||||||
|
--api-keys-button-bg: #f5f5f5;
|
||||||
|
--api-keys-button-color: #333333;
|
||||||
|
|
||||||
/* PDF Report Colors (always light) */
|
/* PDF Report Colors (always light) */
|
||||||
--pdf-light-header-bg: 239 246 255;
|
--pdf-light-header-bg: 239 246 255;
|
||||||
--pdf-light-accent: 59 130 246;
|
--pdf-light-accent: 59 130 246;
|
||||||
@ -539,6 +548,15 @@
|
|||||||
--modal-content-bg: #2A2F36;
|
--modal-content-bg: #2A2F36;
|
||||||
--modal-header-border: rgba(255, 255, 255, 0.08);
|
--modal-header-border: rgba(255, 255, 255, 0.08);
|
||||||
|
|
||||||
|
/* API Keys section colors (dark mode) */
|
||||||
|
--api-keys-card-bg: #2A2F36;
|
||||||
|
--api-keys-card-border: #3A4047;
|
||||||
|
--api-keys-card-shadow: none;
|
||||||
|
--api-keys-input-bg: #1F2329;
|
||||||
|
--api-keys-input-border: #3A4047;
|
||||||
|
--api-keys-button-bg: #3A4047;
|
||||||
|
--api-keys-button-color: #D0D6DC;
|
||||||
|
|
||||||
/* Code token colors (dark mode - Cursor-like) */
|
/* Code token colors (dark mode - Cursor-like) */
|
||||||
--code-kw-color: #C792EA; /* purple */
|
--code-kw-color: #C792EA; /* purple */
|
||||||
--code-str-color: #C3E88D; /* green */
|
--code-str-color: #C3E88D; /* green */
|
||||||
|
|||||||
@ -2,13 +2,15 @@ import React from 'react';
|
|||||||
import { createConfigNavSections as createCoreConfigNavSections, ConfigNavSection } from '@core/components/shared/config/configNavSections';
|
import { createConfigNavSections as createCoreConfigNavSections, ConfigNavSection } from '@core/components/shared/config/configNavSections';
|
||||||
import PeopleSection from '@app/components/shared/config/configSections/PeopleSection';
|
import PeopleSection from '@app/components/shared/config/configSections/PeopleSection';
|
||||||
import TeamsSection from '@app/components/shared/config/configSections/TeamsSection';
|
import TeamsSection from '@app/components/shared/config/configSections/TeamsSection';
|
||||||
|
import ApiKeys from '@app/components/shared/config/configSections/ApiKeys';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Proprietary extension of createConfigNavSections that adds workspace sections
|
* Proprietary extension of createConfigNavSections that adds workspace sections
|
||||||
*/
|
*/
|
||||||
export const createConfigNavSections = (
|
export const createConfigNavSections = (
|
||||||
isAdmin: boolean = false,
|
isAdmin: boolean = false,
|
||||||
runningEE: boolean = false
|
runningEE: boolean = false,
|
||||||
|
loginEnabled: boolean = false
|
||||||
): ConfigNavSection[] => {
|
): ConfigNavSection[] => {
|
||||||
// Get the core sections
|
// Get the core sections
|
||||||
const sections = createCoreConfigNavSections(isAdmin, runningEE);
|
const sections = createCoreConfigNavSections(isAdmin, runningEE);
|
||||||
@ -37,6 +39,25 @@ export const createConfigNavSections = (
|
|||||||
sections.splice(1, 0, workspaceSection);
|
sections.splice(1, 0, workspaceSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Developer section if login is enabled
|
||||||
|
if (loginEnabled) {
|
||||||
|
const developerSection: ConfigNavSection = {
|
||||||
|
title: 'Developer',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'api-keys',
|
||||||
|
label: 'API Keys',
|
||||||
|
icon: 'key-rounded',
|
||||||
|
component: <ApiKeys />
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add Developer section after Preferences (or Workspace if it exists)
|
||||||
|
const insertIndex = isAdmin ? 2 : 1;
|
||||||
|
sections.splice(insertIndex, 0, developerSection);
|
||||||
|
}
|
||||||
|
|
||||||
return sections;
|
return sections;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,126 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Anchor, Group, Stack, Text, Paper, Skeleton } from "@mantine/core";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import ApiKeySection from "./apiKeys/ApiKeySection";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import RefreshModal from "./apiKeys/RefreshModal";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import useApiKey from "./apiKeys/hooks/useApiKey";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||||
|
|
||||||
|
export default function ApiKeys() {
|
||||||
|
const [copied, setCopied] = useState<string | null>(null);
|
||||||
|
const [showRefreshModal, setShowRefreshModal] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { apiKey, isLoading: apiKeyLoading, refresh, isRefreshing, error: apiKeyError, refetch } = useApiKey();
|
||||||
|
|
||||||
|
const copy = async (text: string, tag: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
setCopied(tag);
|
||||||
|
setTimeout(() => setCopied(null), 1600);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshKeys = async () => {
|
||||||
|
try {
|
||||||
|
await refresh();
|
||||||
|
} finally {
|
||||||
|
setShowRefreshModal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap={20} p={0}>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('config.apiKeys.intro', 'Use your API key to programmatically access Stirling PDF\'s processing capabilities.')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
background: "var(--bg-muted)",
|
||||||
|
border: "1px solid var(--border-subtle)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap="xs" wrap="nowrap" align="flex-start">
|
||||||
|
<LocalIcon icon="info-rounded" width={18} height={18} style={{ marginTop: 2, flexShrink: 0, opacity: 0.7 }} />
|
||||||
|
<Stack gap={8} style={{ flex: 1 }}>
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t('config.apiKeys.docsTitle', 'API Documentation')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('config.apiKeys.docsDescription', 'Learn more about integrating with Stirling PDF:')}
|
||||||
|
</Text>
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text size="sm">
|
||||||
|
<Anchor
|
||||||
|
href="https://docs.stirlingpdf.com/API"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
|
||||||
|
>
|
||||||
|
{t('config.apiKeys.docsLink', 'API Documentation')}
|
||||||
|
<LocalIcon icon="open-in-new-rounded" width={14} height={14} />
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
<Text size="sm">
|
||||||
|
<Anchor
|
||||||
|
href="https://registry.scalar.com/@stirlingpdf/apis/stirling-pdf-processing-api/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
|
||||||
|
>
|
||||||
|
{t('config.apiKeys.schemaLink', 'API Schema Reference')}
|
||||||
|
<LocalIcon icon="open-in-new-rounded" width={14} height={14} />
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{apiKeyError && (
|
||||||
|
<Text size="sm" c="red.5">
|
||||||
|
{t('config.apiKeys.generateError', "We couldn't generate your API key.")} {" "}
|
||||||
|
<Anchor component="button" underline="always" onClick={refetch} c="red.4">
|
||||||
|
{t('common.retry', 'Retry')}
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{apiKeyLoading ? (
|
||||||
|
<div style={{ padding: 18, borderRadius: 12, background: "var(--api-keys-card-bg)", border: "1px solid var(--api-keys-card-border)", boxShadow: "0 2px 8px var(--api-keys-card-shadow)" }}>
|
||||||
|
<Group align="center" gap={12} wrap="nowrap">
|
||||||
|
<Skeleton height={36} style={{ flex: 1 }} />
|
||||||
|
<Skeleton height={32} width={76} />
|
||||||
|
<Skeleton height={32} width={92} />
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ApiKeySection
|
||||||
|
publicKey={apiKey ?? ""}
|
||||||
|
copied={copied}
|
||||||
|
onCopy={copy}
|
||||||
|
onRefresh={() => setShowRefreshModal(true)}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Text size="sm" c="dimmed" style={{ marginTop: -8 }}>
|
||||||
|
{t('config.apiKeys.usage', 'Include this key in the X-API-KEY header with all API requests.')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<RefreshModal
|
||||||
|
opened={showRefreshModal}
|
||||||
|
onClose={() => setShowRefreshModal(false)}
|
||||||
|
onConfirm={refreshKeys}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||||
|
import FitText from "@app/components/shared/FitText";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface ApiKeySectionProps {
|
||||||
|
publicKey: string;
|
||||||
|
copied: string | null;
|
||||||
|
onCopy: (text: string, tag: string) => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ApiKeySection({
|
||||||
|
publicKey,
|
||||||
|
copied,
|
||||||
|
onCopy,
|
||||||
|
onRefresh,
|
||||||
|
disabled,
|
||||||
|
}: ApiKeySectionProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Paper radius="md" p={18} style={{ background: "var(--api-keys-card-bg)", border: "1px solid var(--api-keys-card-border)", boxShadow: "0 2px 8px var(--api-keys-card-shadow)" }}>
|
||||||
|
<Group align="flex-end" wrap="nowrap">
|
||||||
|
<Box style={{ flex: 1 }}>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
background: "var(--api-keys-input-bg)",
|
||||||
|
border: "1px solid var(--api-keys-input-border)",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: "8px 12px",
|
||||||
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||||
|
fontSize: 14,
|
||||||
|
minHeight: 36,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
aria-label={t('config.apiKeys.publicKeyAriaLabel', 'Public API key')}
|
||||||
|
>
|
||||||
|
<FitText text={publicKey} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => onCopy(publicKey, "public")}
|
||||||
|
leftSection={<LocalIcon icon="content-copy-rounded" width={14} height={14} />}
|
||||||
|
styles={{ root: { background: "var(--api-keys-button-bg)", color: "var(--api-keys-button-color)", border: "none", marginLeft: 12 } }}
|
||||||
|
aria-label={t('config.apiKeys.copyKeyAriaLabel', 'Copy API key')}
|
||||||
|
>
|
||||||
|
{copied === "public" ? t('common.copied', 'Copied!') : t('common.copy', 'Copy')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
onClick={onRefresh}
|
||||||
|
leftSection={<LocalIcon icon="refresh-rounded" width={14} height={14} />}
|
||||||
|
styles={{ root: { background: "var(--api-keys-button-bg)", color: "var(--api-keys-button-color)", border: "none", marginLeft: 8 } }}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-label={t('config.apiKeys.refreshAriaLabel', 'Refresh API key')}
|
||||||
|
>
|
||||||
|
{t('common.refresh', 'Refresh')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
Button,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex";
|
||||||
|
|
||||||
|
interface RefreshModalProps {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RefreshModal({ opened, onClose, onConfirm }: RefreshModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={onClose}
|
||||||
|
title={t('config.apiKeys.refreshModal.title', 'Refresh API Keys')}
|
||||||
|
centered
|
||||||
|
size="sm"
|
||||||
|
zIndex={Z_INDEX_OVER_CONFIG_MODAL}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text size="sm" c="red">
|
||||||
|
{t('config.apiKeys.refreshModal.warning', '⚠️ Warning: This action will generate new API keys and make your previous keys invalid.')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm">
|
||||||
|
{t('config.apiKeys.refreshModal.impact', 'Any applications or services currently using these keys will stop working until you update them with the new keys.')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t('config.apiKeys.refreshModal.confirmPrompt', 'Are you sure you want to continue?')}
|
||||||
|
</Text>
|
||||||
|
<Group justify="flex-end" gap="sm">
|
||||||
|
<Button variant="default" onClick={onClose}>
|
||||||
|
{t('common.cancel', 'Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button color="red" onClick={onConfirm}>
|
||||||
|
{t('config.apiKeys.refreshModal.confirmCta', 'Refresh Keys')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import apiClient from "@app/services/apiClient";
|
||||||
|
|
||||||
|
export function useApiKey() {
|
||||||
|
const [apiKey, setApiKey] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [hasAttempted, setHasAttempted] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const fetchKey = useCallback(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
// Backend is POST for get and update
|
||||||
|
const res = await apiClient.post("/api/v1/user/get-api-key");
|
||||||
|
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
|
||||||
|
if (typeof value === "string") setApiKey(value);
|
||||||
|
} catch (e: any) {
|
||||||
|
// If not found, try to create one by calling update endpoint
|
||||||
|
if (e?.response?.status === 404) {
|
||||||
|
try {
|
||||||
|
const createRes = await apiClient.post("/api/v1/user/update-api-key");
|
||||||
|
const created =
|
||||||
|
typeof createRes.data === "string"
|
||||||
|
? createRes.data
|
||||||
|
: createRes.data?.apiKey;
|
||||||
|
if (typeof created === "string") setApiKey(created);
|
||||||
|
} catch (createErr: any) {
|
||||||
|
setError(createErr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError(e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
setHasAttempted(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const res = await apiClient.post("/api/v1/user/update-api-key");
|
||||||
|
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
|
||||||
|
if (typeof value === "string") setApiKey(value);
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e);
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasAttempted) {
|
||||||
|
fetchKey();
|
||||||
|
}
|
||||||
|
}, [hasAttempted, fetchKey]);
|
||||||
|
|
||||||
|
return { apiKey, isLoading, isRefreshing, error, refetch: fetchKey, refresh, hasAttempted } as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useApiKey;
|
||||||
Loading…
Reference in New Issue
Block a user