diff --git a/frontend/src/settings/useApiTokens.js b/frontend/src/settings/useApiTokens.js new file mode 100644 index 0000000..0b95506 --- /dev/null +++ b/frontend/src/settings/useApiTokens.js @@ -0,0 +1,196 @@ +import { useCallback, useState } from 'react'; + +const useApiTokens = ({ api, notifyApiError, setStatusMessage, token }) => { + const [tokens, setTokens] = useState([]); + const [loading, setLoading] = useState(false); + const [creating, setCreating] = useState(false); + const [deletingId, setDeletingId] = useState(null); + const [regeneratingId, setRegeneratingId] = useState(null); + const [updatingId, setUpdatingId] = useState(null); + const [createdSecret, setCreatedSecret] = useState(null); + + const refresh = useCallback(async () => { + if (!token) { + return; + } + setLoading(true); + try { + const { data } = await api.get('/profile/api-tokens'); + setTokens(Array.isArray(data) ? data : []); + } catch (error) { + notifyApiError?.(error, 'Failed to load API tokens.'); + } finally { + setLoading(false); + } + }, [api, notifyApiError, token]); + + const create = useCallback( + async ({ label, expires_at, capabilities } = {}) => { + if (creating) { + return false; + } + setCreating(true); + try { + const payload = {}; + if (label) { + payload.label = label; + } + if (expires_at) { + payload.expires_at = expires_at; + } + if (Array.isArray(capabilities) && capabilities.length > 0) { + payload.capabilities = capabilities; + } + + const { data } = await api.post('/profile/api-tokens', payload); + if (data?.token_info) { + setTokens((previous) => { + const filtered = previous.filter((entry) => entry.id !== data.token_info.id); + return [data.token_info, ...filtered]; + }); + } else { + await refresh(); + } + + if (data?.token) { + setCreatedSecret(data.token); + } + + setStatusMessage?.('API token created.', 'success'); + return data; + } catch (error) { + notifyApiError?.(error, 'Failed to create API token.'); + return false; + } finally { + setCreating(false); + } + }, + [api, creating, notifyApiError, refresh, setStatusMessage], + ); + + const revoke = useCallback( + async (tokenId) => { + if (!tokenId) { + return false; + } + setDeletingId(tokenId); + try { + await api.delete(`/profile/api-tokens/${tokenId}`); + await refresh(); + setStatusMessage?.('API token revoked.', 'success'); + return true; + } catch (error) { + notifyApiError?.(error, 'Failed to revoke API token.'); + return false; + } finally { + setDeletingId(null); + } + }, + [api, notifyApiError, refresh, setStatusMessage], + ); + + const regenerate = useCallback( + async (tokenId) => { + if (!tokenId) { + return false; + } + setRegeneratingId(tokenId); + try { + const { data } = await api.post(`/profile/api-tokens/${tokenId}/regenerate`); + if (data?.token_info) { + setTokens((previous) => { + let found = false; + const next = previous.map((entry) => { + if (entry.id === data.token_info.id) { + found = true; + return data.token_info; + } + return entry; + }); + if (!found) { + return [data.token_info, ...previous]; + } + return next; + }); + } else { + await refresh(); + } + + if (data?.token) { + setCreatedSecret(data.token); + } + + setStatusMessage?.('API token regenerated.', 'success'); + return true; + } catch (error) { + notifyApiError?.(error, 'Failed to regenerate API token.'); + return false; + } finally { + setRegeneratingId(null); + } + }, + [api, notifyApiError, refresh, setStatusMessage], + ); + + const updateCapabilities = useCallback( + async (tokenId, capabilities) => { + if (!tokenId) { + return false; + } + setUpdatingId(tokenId); + try { + const { data } = await api.patch(`/profile/api-tokens/${tokenId}`, { + capabilities, + }); + if (data) { + setTokens((previous) => { + let found = false; + const next = previous.map((entry) => { + if (entry.id === data.id) { + found = true; + return data; + } + return entry; + }); + if (!found) { + return [data, ...previous]; + } + return next; + }); + } else { + await refresh(); + } + setStatusMessage?.('API token updated.', 'success'); + return true; + } catch (error) { + notifyApiError?.(error, 'Failed to update API token.'); + return false; + } finally { + setUpdatingId(null); + } + }, + [api, notifyApiError, refresh, setStatusMessage], + ); + + const dismissSecret = useCallback(() => { + setCreatedSecret(null); + }, []); + + return { + tokens, + loading, + creating, + deletingId, + regeneratingId, + updatingId, + createdSecret, + refresh, + create, + revoke, + regenerate, + updateCapabilities, + dismissSecret, + }; +}; + +export default useApiTokens;