postio / apps /frontend /src /components /public-api /public.component.tsx
Leon4gr45's picture
Upload folder using huggingface_hub (part 7)
ec4551b verified
Raw
History Blame Contribute Delete
27 kB
'use client';
import { useState, useCallback } from 'react';
import { useSWRConfig } from 'swr';
import { useUser } from '../layout/user.context';
import copy from 'copy-to-clipboard';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { useVariables } from '@gitroom/react/helpers/variable.context';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useDecisionModal } from '@gitroom/frontend/components/layout/new-modal';
import { DeveloperComponent } from '@gitroom/frontend/components/developer/developer.component';
import clsx from 'clsx';
const mcpClients = [
'Claude Code',
'Cursor',
'VS Code / Copilot',
'Windsurf',
'Amp',
'Codex',
'Gemini CLI',
'Warp',
] as const;
type McpClient = (typeof mcpClients)[number];
const getMcpConfig = (
client: McpClient,
method: 'header' | 'path',
mcpBase: string,
apiKey: string
): { config: string; hint: string } => {
const urlWithKey = `${mcpBase}/mcp/${apiKey}`;
const urlBase = `${mcpBase}/mcp`;
const bearer = `Bearer ${apiKey}`;
const json = (obj: object) => JSON.stringify(obj, null, 2);
if (method === 'path') {
switch (client) {
case 'Claude Code':
return {
config: `claude mcp add postiz --transport http "${urlWithKey}"`,
hint: 'Run this command in your terminal.',
};
case 'Cursor':
return {
config: json({ mcpServers: { postiz: { url: urlWithKey } } }),
hint: 'Add to .cursor/mcp.json in your project root.',
};
case 'VS Code / Copilot':
return {
config: json({
servers: { postiz: { type: 'http', url: urlWithKey } },
}),
hint: 'Add to .vscode/mcp.json in your project root.',
};
case 'Windsurf':
return {
config: json({
mcpServers: { postiz: { serverUrl: urlWithKey } },
}),
hint: 'Add to ~/.codeium/windsurf/mcp_config.json',
};
case 'Amp':
return {
config: `amp mcp add postiz ${urlWithKey}`,
hint: 'Run this command in your terminal.',
};
case 'Codex':
return {
config: `# ~/.codex/config.toml\n\n[mcp_servers.postiz]\nurl = "${urlWithKey}"`,
hint: 'Add to ~/.codex/config.toml',
};
case 'Gemini CLI':
return {
config: json({ mcpServers: { postiz: { url: urlWithKey } } }),
hint: 'Add to ~/.gemini/settings.json',
};
case 'Warp':
return {
config: json({ postiz: { url: urlWithKey } }),
hint: 'Settings > MCP Servers > + Add, then paste this config.',
};
}
}
switch (client) {
case 'Claude Code':
return {
config: `claude mcp add --transport http postiz ${urlBase} --header "Authorization: ${bearer}"`,
hint: 'Run this command in your terminal.',
};
case 'Cursor':
return {
config: json({
mcpServers: {
postiz: { url: urlBase, headers: { Authorization: bearer } },
},
}),
hint: 'Add to .cursor/mcp.json in your project root.',
};
case 'VS Code / Copilot':
return {
config: json({
servers: {
postiz: {
type: 'http',
url: urlBase,
headers: { Authorization: bearer },
},
},
}),
hint: 'Add to .vscode/mcp.json in your project root.',
};
case 'Windsurf':
return {
config: json({
mcpServers: {
postiz: {
serverUrl: urlBase,
headers: { Authorization: bearer },
},
},
}),
hint: 'Add to ~/.codeium/windsurf/mcp_config.json',
};
case 'Amp':
return {
config: json({
'amp.mcpServers': {
postiz: { url: urlBase, headers: { Authorization: bearer } },
},
}),
hint: 'Add to your Amp settings.json',
};
case 'Codex':
return {
config: `# ~/.codex/config.toml\n\n[mcp_servers.postiz]\nurl = "${urlBase}"\nhttp_headers = { "Authorization" = "${bearer}" }`,
hint: 'Add to ~/.codex/config.toml',
};
case 'Gemini CLI':
return {
config: json({
mcpServers: {
postiz: { url: urlBase, headers: { Authorization: bearer } },
},
}),
hint: 'Add to ~/.gemini/settings.json',
};
case 'Warp':
return {
config: json({
postiz: { url: urlBase, headers: { Authorization: bearer } },
}),
hint: 'Settings > MCP Servers > + Add, then paste this config.',
};
}
};
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>
);
};
const McpSection = ({
user,
mcpBase,
}: {
user: { publicApi: string };
mcpBase: string;
}) => {
const t = useT();
const [activeClient, setActiveClient] = useState<McpClient>('Claude Code');
const [method, setMethod] = useState<'header' | 'path'>('header');
const [revealed, setRevealed] = useState(false);
const { config, hint } = getMcpConfig(
activeClient,
method,
mcpBase,
user.publicApi
);
const remoteUrl = `${mcpBase}/mcp/${user.publicApi}`;
const cliUrl = `${mcpBase}/mcp`;
const maskedConfig = revealed
? config
: config.replace(new RegExp(user.publicApi.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '*'.repeat(user.publicApi.length));
const maskedRemoteUrl = revealed
? remoteUrl
: remoteUrl.replace(user.publicApi, '*'.repeat(user.publicApi.length));
return (
<div className="bg-newBgColorInnerInner 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('mcp_client_configuration', 'MCP Client Configuration')}
</div>
<div className="text-[13px] text-customColor18 mt-[2px]">
{t(
'connect_your_mcp_client_to_postiz_to_schedule_your_posts_faster',
'Connect Postiz MCP server to your client (Http streaming) to schedule your posts faster.'
)}
</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/mcp/introduction"
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] flex flex-col gap-[16px]">
<div className="flex flex-col gap-[6px]">
<div className="text-[13px] font-[600] text-customColor18">
{t('auth_method', 'Authentication')}
</div>
<div className="flex gap-[6px]">
{(['header', 'path'] as const).map((m) => (
<button
key={m}
type="button"
className={clsx(
'cursor-pointer px-[14px] h-[36px] text-[13px] font-[500] rounded-[8px] transition-colors',
method === m
? 'bg-[#612BD3] text-white'
: 'bg-btnSimple text-customColor18 hover:bg-boxHover hover:text-textColor'
)}
onClick={() => setMethod(m)}
>
{m === 'header'
? t('cli_claude_code_codex', 'CLI (Claude Code / Codex)')
: t('remote_servers', 'Remote servers (ChatGPT, Claude)')}
</button>
))}
</div>
</div>
{method === 'header' && (
<div className="flex flex-col gap-[6px]">
<div className="text-[13px] font-[600] text-customColor18">
{t('mcp_client', 'Client')}
</div>
<div className="flex flex-wrap gap-[6px]">
{mcpClients.map((client) => (
<button
key={client}
type="button"
className={clsx(
'cursor-pointer px-[14px] h-[36px] text-[13px] font-[500] rounded-[8px] transition-colors',
activeClient === client
? 'bg-[#612BD3] text-white'
: 'bg-btnSimple text-customColor18 hover:bg-boxHover hover:text-textColor'
)}
onClick={() => setActiveClient(client)}
>
{client}
</button>
))}
</div>
</div>
)}
<div className="flex flex-col gap-[8px]">
<div className="text-[12px] text-customColor18 font-[500]">
{method === 'header'
? hint
: t(
'remote_server_url_hint',
'Paste this URL into your remote MCP client (ChatGPT, Claude, etc.).'
)}
</div>
<pre className="bg-newBgColorInner border border-newBorder rounded-[8px] p-[16px] text-[13px] whitespace-pre-wrap break-all overflow-x-auto leading-[1.6]">
{method === 'header' ? maskedConfig : maskedRemoteUrl}
</pre>
<div className="flex gap-[8px]">
<button
type="button"
onClick={() => setRevealed(!revealed)}
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"
>
{revealed ? (
<>
<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94" />
<path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19" />
<line x1="1" y1="1" x2="23" y2="23" />
</>
) : (
<>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</>
)}
</svg>
{revealed ? t('hide', 'Hide') : t('reveal', 'Reveal')}
</button>
<CopyButton
text={method === 'header' ? config : remoteUrl}
label={t('copy', 'Copy')}
/>
{method === 'header' && (
<CopyButton
text={cliUrl}
label={t('copy_url', 'Copy URL')}
/>
)}
</div>
</div>
</div>
</div>
);
};
const localCliSteps = [
{
label: 'Install the CLI',
code: 'npm install -g postiz',
},
{
label: 'Run: postiz auth:login',
code: 'postiz auth:login',
},
{
label: 'Install the Postiz skill for your AI agent',
code: 'npx skills add gitroomhq/postiz-agent',
},
] as const;
const ciCliSteps = [
{
label: 'Install the CLI',
code: 'npm install -g postiz',
},
{
label: 'Set your API key as an environment variable',
code: 'export POSTIZ_API_KEY="{API_KEY}"',
},
{
label: 'Install the Postiz skill for your AI agent',
code: 'npx skills add gitroomhq/postiz-agent',
},
] as const;
const CliSection = ({ apiKey }: { apiKey: string }) => {
const t = useT();
const [mode, setMode] = useState<'local' | 'ci'>('local');
const [revealed, setRevealed] = useState(false);
const steps =
mode === 'local'
? localCliSteps.map((step) => ({ ...step }))
: ciCliSteps.map((step) => ({
...step,
code: step.code.replace('{API_KEY}', apiKey),
}));
const displaySteps =
mode === 'ci' && !revealed
? steps.map((step) => ({
...step,
code: step.code.replace(
new RegExp(apiKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
'*'.repeat(apiKey.length)
),
}))
: steps;
return (
<div className="bg-newBgColorInnerInner 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('cli_and_skills', 'CLI & AI Skills')}
</div>
<div className="text-[13px] text-customColor18 mt-[2px]">
{t(
'cli_description',
'Use the Postiz CLI to automate posting from your terminal, or install the skill to let your AI agent schedule posts for you.'
)}
</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/cli/introduction"
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] flex flex-col gap-[16px]">
<div className="flex gap-[6px]">
{(['local', 'ci'] as const).map((m) => (
<button
key={m}
type="button"
className={clsx(
'cursor-pointer px-[14px] h-[36px] text-[13px] font-[500] rounded-[8px] transition-colors',
mode === m
? 'bg-[#612BD3] text-white'
: 'bg-btnSimple text-customColor18 hover:bg-boxHover hover:text-textColor'
)}
onClick={() => setMode(m)}
>
{m === 'local'
? t('locally', 'Locally')
: t('ci_remote_servers', 'CI / Remote servers')}
</button>
))}
</div>
{displaySteps.map((step, i) => (
<div key={i} className="flex flex-col gap-[6px]">
<div className="text-[13px] font-[600] text-customColor18">
{i + 1}. {step.label}
</div>
<pre className="bg-newBgColorInner border border-newBorder rounded-[8px] p-[16px] text-[13px] whitespace-pre-wrap break-all overflow-x-auto leading-[1.6]">
{step.code}
</pre>
</div>
))}
<div className="flex gap-[8px]">
{mode === 'ci' && (
<button
type="button"
onClick={() => setRevealed(!revealed)}
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"
>
{revealed ? (
<>
<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94" />
<path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19" />
<line x1="1" y1="1" x2="23" y2="23" />
</>
) : (
<>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</>
)}
</svg>
{revealed ? t('hide', 'Hide') : t('reveal', 'Reveal')}
</button>
)}
<CopyButton
text={steps.map((s) => s.code).join(' && ')}
label={t('copy_all', 'Copy All')}
/>
</div>
</div>
</div>
);
};
const PublicApiContent = () => {
const user = useUser();
const { backendUrl, frontEndUrl, mcpUrl } = useVariables();
const toaster = useToaster();
const fetch = useFetch();
const decision = useDecisionModal();
const { mutate } = useSWRConfig();
const [reveal, setReveal] = useState(false);
const t = useT();
const rotateKey = useCallback(async () => {
const approved = await decision.open({
title: t('rotate_api_key', 'Rotate API Key?'),
description: t(
'rotate_api_key_description',
'This will generate a new API key and invalidate the current one. Any integrations using the old key will stop working.'
),
approveLabel: t('rotate', 'Rotate'),
cancelLabel: t('cancel', 'Cancel'),
});
if (!approved) return;
await fetch('/user/api-key/rotate', { method: 'POST' });
await mutate('/user/self');
setReveal(false);
toaster.show(
t('api_key_rotated', 'API Key rotated successfully'),
'success'
);
}, [decision, fetch, mutate, toaster]);
if (!user || !user.publicApi) {
return null;
}
const mcpBase = mcpUrl || backendUrl;
return (
<div className="flex flex-col gap-[40px]">
<div className="text-[14px] text-textColor leading-[1.7]">
{t(
'api_auth_note_line1',
'Use your API Key to automate your own account.'
)}
<br />
{t(
'api_auth_note_line2',
'If you are building a product that schedules posts on behalf of other Postiz users,'
)}
<br />
{t(
'api_auth_note_line3',
'create an OAuth App under the "Apps" tab. Your users will authorize your app via OAuth2,'
)}
<br />
{t(
'api_auth_note_line4',
'and you will receive a pos_ prefixed token that works with the API, MCP, and CLI — just like an API Key.'
)}
</div>
<div className="bg-newBgColorInnerInner 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('api_key', 'API Key')}
</div>
<div className="text-[13px] text-customColor18 mt-[2px]">
{t(
'use_postiz_api_to_integrate_with_your_tools',
'Use Postiz API to integrate with your tools.'
)}
</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"
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>
<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://www.npmjs.com/package/n8n-nodes-postiz"
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('n8n_node', 'N8N Node')}
</a>
</div>
</div>
<div className="p-[20px] flex flex-col gap-[16px]">
<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">
{reveal ? (
user.publicApi
) : (
<span className="flex items-center">
<span className="blur-sm select-none">
{user.publicApi.slice(0, -5)}
</span>
<span>{user.publicApi.slice(-5)}</span>
</span>
)}
</code>
</div>
<div className="flex gap-[8px]">
<button
type="button"
onClick={() => setReveal(!reveal)}
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"
>
{reveal ? (
<>
<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94" />
<path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19" />
<line x1="1" y1="1" x2="23" y2="23" />
</>
) : (
<>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</>
)}
</svg>
{reveal ? t('hide', 'Hide') : t('reveal', 'Reveal')}
</button>
<CopyButton text={user.publicApi} label={t('copy', 'Copy')} />
<button
type="button"
onClick={rotateKey}
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_key', 'Rotate Key')}
</button>
<button
type="button"
data-tooltip-id="tooltip"
data-tooltip-content={t(
'payload_wizard_description',
'Building a POST request to /posts can be complex. Use the wizard to schedule a post with the UI, then copy the generated payload.'
)}
onClick={() =>
window.open(`${frontEndUrl}/modal/dark/all`, '_blank')
}
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="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('open_wizard', 'Open Wizard')}
</button>
</div>
</div>
</div>
<CliSection apiKey={user.publicApi} />
<McpSection user={user} mcpBase={mcpBase} />
</div>
);
};
export const PublicComponent = () => {
const t = useT();
const [subTab, setSubTab] = useState<'api' | 'developer'>('api');
return (
<div className="flex flex-col gap-[20px]">
<div className="flex gap-[6px]">
{(['api', 'developer'] as const).map((tab) => (
<button
key={tab}
type="button"
className={clsx(
'cursor-pointer px-[20px] h-[44px] text-[15px] font-[600] rounded-[8px] transition-colors',
subTab === tab
? 'bg-[#612BD3] text-white'
: 'bg-btnSimple text-customColor18 hover:bg-boxHover hover:text-textColor'
)}
onClick={() => setSubTab(tab)}
>
{tab === 'api'
? t('access', 'Access')
: t('apps', 'Apps')}
</button>
))}
</div>
{subTab === 'api' && <PublicApiContent />}
{subTab === 'developer' && <DeveloperComponent />}
</div>
);
};