diff --git a/frontend/src/app/SettingsRoute.jsx b/frontend/src/app/SettingsRoute.jsx index ad6b8ac..c6f6665 100644 --- a/frontend/src/app/SettingsRoute.jsx +++ b/frontend/src/app/SettingsRoute.jsx @@ -27,20 +27,27 @@ const SettingsRoute = () => { creating, deletingId, regeneratingId, - updatingId, createdSecret, refresh, + refreshCapabilitySets, create, revoke, regenerate, - updateCapabilities, dismissSecret, + capabilitySets, + capabilitySetsLoading, } = useApiTokens({ api, token, notifyApiError, setStatusMessage }); useEffect(() => { refresh(); + refreshCapabilitySets(); refreshPasskeys(); - }, [refresh, refreshPasskeys]); + }, [refresh, refreshCapabilitySets, refreshPasskeys]); + + const handleRefresh = useCallback(() => { + refresh(); + refreshCapabilitySets(); + }, [refresh, refreshCapabilitySets]); const handleClose = useCallback(() => { dismissSecret(); @@ -71,14 +78,14 @@ const SettingsRoute = () => { creating={creating} deletingId={deletingId} regeneratingId={regeneratingId} - updatingId={updatingId} - onRefresh={refresh} + onRefresh={handleRefresh} onCreate={create} onDelete={revoke} onRegenerate={regenerate} - onUpdateCapabilities={updateCapabilities} createdToken={createdSecret} onDismissCreatedToken={dismissSecret} + capabilitySets={capabilitySets} + capabilitySetsLoading={capabilitySetsLoading} passkeys={passkeys} passkeysSupported={passkeysSupported} passkeysLoading={passkeysLoading} diff --git a/frontend/src/settings/SettingsModal.jsx b/frontend/src/settings/SettingsModal.jsx index 3860394..4755734 100644 --- a/frontend/src/settings/SettingsModal.jsx +++ b/frontend/src/settings/SettingsModal.jsx @@ -20,12 +20,12 @@ const SettingsModal = ({ creating = false, deletingId = null, regeneratingId = null, - updatingId = null, + capabilitySets = [], + capabilitySetsLoading = false, onRefresh, onCreate, onDelete, onRegenerate, - onUpdateCapabilities, createdToken = null, onDismissCreatedToken, passkeys = [], @@ -41,7 +41,7 @@ const SettingsModal = ({ const [activeSection, setActiveSection] = useState(defaultSection); const [newTokenLabel, setNewTokenLabel] = useState(''); const [newTokenExpires, setNewTokenExpires] = useState(''); - const [newTokenCapabilities, setNewTokenCapabilities] = useState([]); + const [newTokenCapabilitySetId, setNewTokenCapabilitySetId] = useState(''); const [formError, setFormError] = useState(null); const [newPasskeyNickname, setNewPasskeyNickname] = useState(''); @@ -58,7 +58,7 @@ const SettingsModal = ({ setActiveSection(defaultSection); setNewTokenLabel(''); setNewTokenExpires(''); - setNewTokenCapabilities([]); + setNewTokenCapabilitySetId(''); setFormError(null); setNewPasskeyNickname(''); } @@ -120,57 +120,36 @@ const SettingsModal = ({ [onRevokePasskey], ); - const capabilityOptions = useMemo( - () => [ - { value: 'webdav', label: 'WebDAV access' }, - { value: 'api', label: 'REST API access' }, - ], - [], + const capabilitySetOptions = useMemo( + () => capabilitySets.map((set) => ({ + value: set.id, + label: set.label || set.slug || set.id, + capabilities: Array.isArray(set.capabilities) ? set.capabilities : [], + })), + [capabilitySets], ); - const handleNewCapabilityChange = useCallback((capability, enabled) => { + const capabilitySetMap = useMemo( + () => Object.fromEntries(capabilitySetOptions.map((option) => [option.value, option])), + [capabilitySetOptions], + ); + + useEffect(() => { + if (!capabilitySetOptions.length) { + setNewTokenCapabilitySetId(''); + return; + } + if (!newTokenCapabilitySetId + || !capabilitySetOptions.some((option) => option.value === newTokenCapabilitySetId)) { + setNewTokenCapabilitySetId(capabilitySetOptions[0].value); + } + }, [capabilitySetOptions, newTokenCapabilitySetId]); + + const handleNewCapabilitySetChange = useCallback((event) => { setFormError(null); - setNewTokenCapabilities((previous) => { - if (enabled) { - if (previous.includes(capability)) { - return previous; - } - return [...previous, capability]; - } - return previous.filter((value) => value !== capability); - }); + setNewTokenCapabilitySetId(event.target.value); }, []); - const handleToggleTokenCapability = useCallback( - async (token, capability, enabled) => { - if (!token?.id || !onUpdateCapabilities) { - return; - } - - const existing = Array.isArray(token.capabilities) ? [...token.capabilities] : []; - let next; - if (enabled) { - if (existing.includes(capability)) { - return; - } - next = [...existing, capability]; - } else { - next = existing.filter((value) => value !== capability); - if (next.length === 0) { - setFormError('Tokens must have at least one capability.'); - return; - } - } - - setFormError(null); - const result = await onUpdateCapabilities(token.id, next); - if (result === false) { - setFormError('Failed to update token capabilities.'); - } - }, - [onUpdateCapabilities], - ); - const handleCreateToken = useCallback( async (event) => { event.preventDefault(); @@ -190,25 +169,38 @@ const SettingsModal = ({ normalizedExpires = parsed.toISOString(); } - if (!newTokenCapabilities.length) { - setFormError('Select at least one capability.'); + if (!capabilitySetOptions.length) { + setFormError('Capability sets are still loading.'); + return; + } + + const selectedCapabilitySetId = newTokenCapabilitySetId + || capabilitySetOptions[0]?.value; + if (!selectedCapabilitySetId) { + setFormError('Select a capability set.'); return; } const result = await onCreate?.({ label: normalizedLabel, expires_at: normalizedExpires, - capabilities: newTokenCapabilities, + capability_set_id: selectedCapabilitySetId, }); if (result !== false) { setNewTokenLabel(''); setNewTokenExpires(''); - setNewTokenCapabilities([]); + setNewTokenCapabilitySetId(capabilitySetOptions[0]?.value || ''); setFormError(null); } }, - [newTokenExpires, newTokenLabel, newTokenCapabilities, onCreate], + [ + capabilitySetOptions, + newTokenCapabilitySetId, + newTokenExpires, + newTokenLabel, + onCreate, + ], ); const handleRegenerateToken = useCallback( @@ -231,15 +223,15 @@ const SettingsModal = ({ type="button" className="secondary" onClick={handleRefresh} - disabled={loading} + disabled={loading || capabilitySetsLoading} > - {loading ? 'Refreshing…' : 'Refresh'} + {loading || capabilitySetsLoading ? 'Refreshing…' : 'Refresh'}
- API tokens can grant access to the REST API, WebDAV, or both. Select at least one - capability for each token. You can adjust capabilities for existing tokens at any time. + API tokens use predefined capability sets. Choose the set that matches the access you + need when creating or updating a token.
{createdToken ? ( @@ -279,28 +271,49 @@ const SettingsModal = ({ onChange={(event) => setNewTokenExpires(event.target.value)} /> - +