'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 ( ); }; 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(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(undefined); const [picturePath, setPicturePath] = useState(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) => ( ), }); }, [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 (
{t( 'oauth_app_note_line1', 'Create an OAuth App to let other Postiz users authorize your product to post on their behalf.' )}
{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.' )}
{t('oauth_application', 'OAuth Application')}
{t( 'create_an_oauth_application', 'Create an OAuth application to allow third-party integrations with Postiz on behalf of your users.' )}
); } // Create form if (creating && !app) { return (
{t( 'oauth_app_note_line1', 'Create an OAuth App to let other Postiz users authorize your product to post on their behalf.' )}
{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.' )}
{t('create_oauth_app', 'Create OAuth App')}
{t( 'fill_in_the_details_for_your_oauth_application', 'Fill in the details for your OAuth application.' )}
setName(e.target.value)} placeholder="My Application" maxLength={100} />