605 lines
24 KiB
TypeScript
605 lines
24 KiB
TypeScript
'use client';
|
|
|
|
import { FC, useCallback, useState } from 'react';
|
|
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
|
import useSWR from 'swr';
|
|
import { useToaster } from '@gitroom/react/toaster/toaster';
|
|
import { useDecisionModal, useModals } from '@gitroom/frontend/components/layout/new-modal';
|
|
import { MediaBox } from '@gitroom/frontend/components/media/media.component';
|
|
import copy from 'copy-to-clipboard';
|
|
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
|
|
|
const useOAuthApp = () => {
|
|
const fetch = useFetch();
|
|
const load = useCallback(async () => {
|
|
const res = await fetch('/user/oauth-app');
|
|
const text = await res.text();
|
|
if (!text || text === 'null' || text === 'false') {
|
|
return null;
|
|
}
|
|
return JSON.parse(text);
|
|
}, []);
|
|
return useSWR('oauth-app', load, {
|
|
revalidateOnFocus: false,
|
|
revalidateOnReconnect: false,
|
|
revalidateIfStale: false,
|
|
});
|
|
};
|
|
|
|
const CopyButton = ({
|
|
text,
|
|
label,
|
|
}: {
|
|
text: string;
|
|
label: string;
|
|
}) => {
|
|
const toaster = useToaster();
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
copy(text);
|
|
toaster.show(`${label} copied to clipboard`, 'success');
|
|
}}
|
|
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
|
>
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
|
</svg>
|
|
{label}
|
|
</button>
|
|
);
|
|
};
|
|
|
|
export const DeveloperComponent: FC = () => {
|
|
const fetch = useFetch();
|
|
const toaster = useToaster();
|
|
const decision = useDecisionModal();
|
|
const modals = useModals();
|
|
const t = useT();
|
|
const { data: app, mutate } = useOAuthApp();
|
|
const [plaintextSecret, setPlaintextSecret] = useState<string | null>(null);
|
|
const [creating, setCreating] = useState(false);
|
|
const [editing, setEditing] = useState(false);
|
|
|
|
const [name, setName] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [redirectUrl, setRedirectUrl] = useState('');
|
|
const [pictureId, setPictureId] = useState<string | undefined>(undefined);
|
|
const [picturePath, setPicturePath] = useState<string | undefined>(undefined);
|
|
|
|
const startEditing = useCallback(() => {
|
|
if (!app) return;
|
|
setName(app.name || '');
|
|
setDescription(app.description || '');
|
|
setRedirectUrl(app.redirectUrl || '');
|
|
setPictureId(app.pictureId || undefined);
|
|
setPicturePath(app.picture?.path || undefined);
|
|
setEditing(true);
|
|
}, [app]);
|
|
|
|
const changeMedia = useCallback((selected: { id: string; path: string }[]) => {
|
|
const media = Array.isArray(selected) ? selected[0] : selected;
|
|
if (media) {
|
|
setPictureId(media.id);
|
|
setPicturePath(media.path);
|
|
}
|
|
}, []);
|
|
|
|
const openMedia = useCallback(() => {
|
|
modals.openModal({
|
|
title: t('media_library', 'Media Library'),
|
|
askClose: false,
|
|
closeOnEscape: true,
|
|
fullScreen: true,
|
|
size: 'calc(100% - 80px)',
|
|
height: 'calc(100% - 80px)',
|
|
children: (close: () => void) => (
|
|
<MediaBox
|
|
setMedia={changeMedia}
|
|
closeModal={close}
|
|
/>
|
|
),
|
|
});
|
|
}, [modals, t, changeMedia]);
|
|
|
|
const createApp = useCallback(async () => {
|
|
if (!name || !redirectUrl) {
|
|
toaster.show('Name and Redirect URL are required', 'warning');
|
|
return;
|
|
}
|
|
try {
|
|
const result = await (
|
|
await fetch('/user/oauth-app', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
name,
|
|
description,
|
|
redirectUrl,
|
|
pictureId,
|
|
}),
|
|
})
|
|
).json();
|
|
|
|
if (result.clientSecret) {
|
|
setPlaintextSecret(result.clientSecret);
|
|
toaster.show(
|
|
'App created! Copy your client secret now - it will only be shown once.',
|
|
'success'
|
|
);
|
|
}
|
|
setCreating(false);
|
|
mutate();
|
|
} catch {
|
|
toaster.show('Failed to create app', 'warning');
|
|
}
|
|
}, [name, description, redirectUrl, pictureId]);
|
|
|
|
const updateApp = useCallback(async () => {
|
|
try {
|
|
await fetch('/user/oauth-app', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
name,
|
|
description,
|
|
redirectUrl,
|
|
pictureId,
|
|
}),
|
|
});
|
|
toaster.show('App updated', 'success');
|
|
setEditing(false);
|
|
mutate();
|
|
} catch {
|
|
toaster.show('Failed to update app', 'warning');
|
|
}
|
|
}, [name, description, redirectUrl, pictureId]);
|
|
|
|
const rotateSecret = useCallback(async () => {
|
|
const approved = await decision.open({
|
|
title: 'Rotate Client Secret?',
|
|
description:
|
|
'This will generate a new client secret and invalidate the current one. Any integrations using the old secret will stop working.',
|
|
approveLabel: 'Rotate',
|
|
cancelLabel: 'Cancel',
|
|
});
|
|
if (!approved) return;
|
|
try {
|
|
const result = await (
|
|
await fetch('/user/oauth-app/rotate-secret', { method: 'POST' })
|
|
).json();
|
|
if (result.clientSecret) {
|
|
setPlaintextSecret(result.clientSecret);
|
|
toaster.show(
|
|
'Secret rotated! Copy your new client secret now.',
|
|
'success'
|
|
);
|
|
mutate();
|
|
}
|
|
} catch {
|
|
toaster.show('Failed to rotate secret', 'warning');
|
|
}
|
|
}, [decision]);
|
|
|
|
const deleteApp = useCallback(async () => {
|
|
const approved = await decision.open({
|
|
title: 'Delete OAuth App?',
|
|
description:
|
|
'This will delete the OAuth application and revoke all user authorizations. This action cannot be undone.',
|
|
approveLabel: 'Delete',
|
|
cancelLabel: 'Cancel',
|
|
});
|
|
if (!approved) return;
|
|
try {
|
|
await fetch('/user/oauth-app', { method: 'DELETE' });
|
|
toaster.show('OAuth app deleted', 'success');
|
|
setPlaintextSecret(null);
|
|
mutate();
|
|
} catch {
|
|
toaster.show('Failed to delete app', 'warning');
|
|
}
|
|
}, [decision]);
|
|
|
|
if (app === undefined) {
|
|
return null;
|
|
}
|
|
|
|
// No app yet — show create prompt
|
|
if (!app && !creating) {
|
|
return (
|
|
<div className="flex flex-col gap-[40px]">
|
|
<div className="text-[14px] text-textColor leading-[1.7]">
|
|
{t(
|
|
'oauth_app_note_line1',
|
|
'Create an OAuth App to let other Postiz users authorize your product to post on their behalf.'
|
|
)}
|
|
<br />
|
|
{t(
|
|
'oauth_app_note_line2',
|
|
'After a user completes the OAuth2 flow, you receive a pos_ prefixed token that works everywhere an API Key does — API, MCP, and CLI.'
|
|
)}
|
|
</div>
|
|
<div className="bg-newBgColorInner rounded-[12px] border border-newBorder overflow-hidden">
|
|
<div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px]">
|
|
<div>
|
|
<div className="text-[15px] font-[600]">
|
|
{t('oauth_application', 'OAuth Application')}
|
|
</div>
|
|
<div className="text-[13px] text-customColor18 mt-[2px]">
|
|
{t(
|
|
'create_an_oauth_application',
|
|
'Create an OAuth application to allow third-party integrations with Postiz on behalf of your users.'
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-[6px] shrink-0 pt-[2px]">
|
|
<a
|
|
className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
|
href="https://docs.postiz.com/public-api/oauth"
|
|
target="_blank"
|
|
>
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /></svg>
|
|
{t('read_the_docs', 'Docs')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div className="p-[20px]">
|
|
<button
|
|
type="button"
|
|
onClick={() => setCreating(true)}
|
|
className="cursor-pointer px-[20px] h-[44px] bg-[#612BD3] hover:bg-[#5520CB] transition-colors text-white rounded-[8px] text-[15px] font-[600]"
|
|
>
|
|
{t('create_oauth_app', 'Create OAuth App')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Create form
|
|
if (creating && !app) {
|
|
return (
|
|
<div className="flex flex-col gap-[40px]">
|
|
<div className="text-[14px] text-textColor leading-[1.7]">
|
|
{t(
|
|
'oauth_app_note_line1',
|
|
'Create an OAuth App to let other Postiz users authorize your product to post on their behalf.'
|
|
)}
|
|
<br />
|
|
{t(
|
|
'oauth_app_note_line2',
|
|
'After a user completes the OAuth2 flow, you receive a pos_ prefixed token that works everywhere an API Key does — API, MCP, and CLI.'
|
|
)}
|
|
</div>
|
|
<div className="bg-newBgColorInner rounded-[12px] border border-newBorder overflow-hidden">
|
|
<div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder">
|
|
<div className="text-[15px] font-[600]">
|
|
{t('create_oauth_app', 'Create OAuth App')}
|
|
</div>
|
|
<div className="text-[13px] text-customColor18 mt-[2px]">
|
|
{t(
|
|
'fill_in_the_details_for_your_oauth_application',
|
|
'Fill in the details for your OAuth application.'
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="p-[20px] flex flex-col gap-[16px]">
|
|
<div className="flex flex-col gap-[6px]">
|
|
<label className="text-[13px] font-[600] text-customColor18">
|
|
{t('app_name', 'App Name')} *
|
|
</label>
|
|
<input
|
|
className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] text-textColor outline-none"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="My Application"
|
|
maxLength={100}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-[6px]">
|
|
<label className="text-[13px] font-[600] text-customColor18">
|
|
{t('description', 'Description')}
|
|
</label>
|
|
<textarea
|
|
className="bg-newBgColorInner border border-newBorder rounded-[8px] p-[16px] text-textColor outline-none min-h-[80px]"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="Describe what your app does"
|
|
maxLength={500}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-[6px]">
|
|
<label className="text-[13px] font-[600] text-customColor18">
|
|
{t('profile_picture', 'Profile Picture')}
|
|
</label>
|
|
<div className="flex items-center gap-[12px]">
|
|
{picturePath ? (
|
|
<img
|
|
src={picturePath}
|
|
alt="App picture"
|
|
className="w-[48px] h-[48px] rounded-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-[48px] h-[48px] rounded-full bg-btnSimple flex items-center justify-center text-customColor18">
|
|
?
|
|
</div>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={openMedia}
|
|
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600]"
|
|
>
|
|
{t('choose_image', 'Choose Image')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-[6px]">
|
|
<label className="text-[13px] font-[600] text-customColor18">
|
|
{t('redirect_url', 'Redirect URL')} *
|
|
</label>
|
|
<input
|
|
className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] text-textColor outline-none"
|
|
value={redirectUrl}
|
|
onChange={(e) => setRedirectUrl(e.target.value)}
|
|
placeholder="https://yourapp.com/callback"
|
|
/>
|
|
</div>
|
|
<div className="flex gap-[8px]">
|
|
<button
|
|
type="button"
|
|
onClick={createApp}
|
|
className="cursor-pointer px-[20px] h-[44px] bg-[#612BD3] hover:bg-[#5520CB] transition-colors text-white rounded-[8px] text-[15px] font-[600]"
|
|
>
|
|
{t('create', 'Create')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setCreating(false)}
|
|
className="cursor-pointer px-[20px] h-[44px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[15px] font-[600]"
|
|
>
|
|
{t('cancel', 'Cancel')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// App exists — show details
|
|
return (
|
|
<div className="flex flex-col gap-[40px]">
|
|
<div className="text-[14px] text-textColor leading-[1.7]">
|
|
{t(
|
|
'oauth_app_note_line1',
|
|
'Create an OAuth App to let other Postiz users authorize your product to post on their behalf.'
|
|
)}
|
|
<br />
|
|
{t(
|
|
'oauth_app_note_line2',
|
|
'After a user completes the OAuth2 flow, you receive a pos_ prefixed token that works everywhere an API Key does — API, MCP, and CLI.'
|
|
)}
|
|
</div>
|
|
{/* App details / edit */}
|
|
<div className="bg-newBgColorInner rounded-[12px] border border-newBorder overflow-hidden">
|
|
<div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px]">
|
|
<div>
|
|
<div className="text-[15px] font-[600]">
|
|
{t('oauth_application', 'OAuth Application')}
|
|
</div>
|
|
<div className="text-[13px] text-customColor18 mt-[2px]">
|
|
{t(
|
|
'manage_your_oauth_application',
|
|
'Manage your OAuth application for third-party integrations.'
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-[6px] shrink-0 pt-[2px]">
|
|
<a
|
|
className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
|
href="https://docs.postiz.com/public-api/oauth"
|
|
target="_blank"
|
|
>
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /></svg>
|
|
{t('read_the_docs', 'Docs')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{editing ? (
|
|
<div className="p-[20px] flex flex-col gap-[16px]">
|
|
<div className="flex flex-col gap-[6px]">
|
|
<label className="text-[13px] font-[600] text-customColor18">
|
|
{t('app_name', 'App Name')} *
|
|
</label>
|
|
<input
|
|
className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] text-textColor outline-none"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="My Application"
|
|
maxLength={100}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-[6px]">
|
|
<label className="text-[13px] font-[600] text-customColor18">
|
|
{t('description', 'Description')}
|
|
</label>
|
|
<textarea
|
|
className="bg-newBgColorInner border border-newBorder rounded-[8px] p-[16px] text-textColor outline-none min-h-[80px]"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="Describe what your app does"
|
|
maxLength={500}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-[6px]">
|
|
<label className="text-[13px] font-[600] text-customColor18">
|
|
{t('profile_picture', 'Profile Picture')}
|
|
</label>
|
|
<div className="flex items-center gap-[12px]">
|
|
{picturePath ? (
|
|
<img
|
|
src={picturePath}
|
|
alt="App picture"
|
|
className="w-[48px] h-[48px] rounded-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-[48px] h-[48px] rounded-full bg-btnSimple flex items-center justify-center text-customColor18">
|
|
?
|
|
</div>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={openMedia}
|
|
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600]"
|
|
>
|
|
{t('choose_image', 'Choose Image')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-[6px]">
|
|
<label className="text-[13px] font-[600] text-customColor18">
|
|
{t('redirect_url', 'Redirect URL')} *
|
|
</label>
|
|
<input
|
|
className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] text-textColor outline-none"
|
|
value={redirectUrl}
|
|
onChange={(e) => setRedirectUrl(e.target.value)}
|
|
placeholder="https://yourapp.com/callback"
|
|
/>
|
|
</div>
|
|
<div className="flex gap-[8px]">
|
|
<button
|
|
type="button"
|
|
onClick={updateApp}
|
|
className="cursor-pointer px-[20px] h-[44px] bg-[#612BD3] hover:bg-[#5520CB] transition-colors text-white rounded-[8px] text-[15px] font-[600]"
|
|
>
|
|
{t('save', 'Save')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setEditing(false)}
|
|
className="cursor-pointer px-[20px] h-[44px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[15px] font-[600]"
|
|
>
|
|
{t('cancel', 'Cancel')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="p-[20px] flex flex-col gap-[16px]">
|
|
<div className="flex items-center gap-[12px]">
|
|
{app.picture?.path ? (
|
|
<img
|
|
src={app.picture.path}
|
|
alt={app.name}
|
|
className="w-[48px] h-[48px] rounded-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-[48px] h-[48px] rounded-full bg-btnSimple flex items-center justify-center text-customColor18 text-[18px] font-[600]">
|
|
{app.name?.[0]?.toUpperCase() || '?'}
|
|
</div>
|
|
)}
|
|
<div>
|
|
<div className="text-[15px] font-[600]">{app.name}</div>
|
|
{app.description && (
|
|
<div className="text-customColor18 text-[13px]">
|
|
{app.description}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-[4px]">
|
|
<div className="text-[13px] font-[600] text-customColor18">
|
|
{t('redirect_url', 'Redirect URL')}
|
|
</div>
|
|
<div className="text-[14px]">{app.redirectUrl}</div>
|
|
</div>
|
|
<div className="flex gap-[8px]">
|
|
<button
|
|
type="button"
|
|
onClick={startEditing}
|
|
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" /><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" /></svg>
|
|
{t('edit_app', 'Edit App')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Credentials */}
|
|
<div className="bg-newBgColorInner rounded-[12px] border border-newBorder overflow-hidden">
|
|
<div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder">
|
|
<div className="text-[15px] font-[600]">
|
|
{t('credentials', 'Credentials')}
|
|
</div>
|
|
</div>
|
|
<div className="p-[20px] flex flex-col gap-[16px]">
|
|
<div className="flex flex-col gap-[6px]">
|
|
<div className="text-[13px] font-[600] text-customColor18">
|
|
{t('client_id', 'Client ID')}
|
|
</div>
|
|
<div className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] flex items-center overflow-hidden">
|
|
<code className="text-[14px] flex-1 truncate">{app.clientId}</code>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-[6px]">
|
|
<div className="text-[13px] font-[600] text-customColor18">
|
|
{t('client_secret', 'Client Secret')}
|
|
</div>
|
|
<div className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] flex items-center overflow-hidden">
|
|
{plaintextSecret ? (
|
|
<code className="text-[14px] flex-1 truncate">
|
|
{plaintextSecret}
|
|
</code>
|
|
) : (
|
|
<span className="text-customColor18 text-[13px]">
|
|
{t(
|
|
'secret_only_shown_on_creation',
|
|
'Secret is only shown on creation or rotation'
|
|
)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-[8px]">
|
|
<CopyButton text={app.clientId} label={t('copy_id', 'Copy ID')} />
|
|
{plaintextSecret && (
|
|
<CopyButton
|
|
text={plaintextSecret}
|
|
label={t('copy_secret', 'Copy Secret')}
|
|
/>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={rotateSecret}
|
|
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21.5 2v6h-6" /><path d="M21.34 15.57a10 10 0 11-.57-8.38L21.5 8" /></svg>
|
|
{t('rotate_secret', 'Rotate Secret')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={deleteApp}
|
|
className="cursor-pointer px-[16px] h-[36px] bg-red-600 hover:bg-red-700 text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6" /><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" /></svg>
|
|
{t('delete_app', 'Delete App')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|