typescript
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
interface ApiTokenRecord {
|
||||
id?: string | number;
|
||||
label?: string;
|
||||
expires_at?: string;
|
||||
created_at?: string;
|
||||
last_used_at?: string;
|
||||
capability_set_id?: string | number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ApiTokensResponse {
|
||||
token_info?: ApiTokenRecord;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
interface CreateTokenArgs {
|
||||
label?: string;
|
||||
expires_at?: string;
|
||||
capability_set_id?: string | number;
|
||||
}
|
||||
|
||||
interface UseApiTokensArgs {
|
||||
api: {
|
||||
get: <T = unknown>(url: string) => Promise<{ data: T }>;
|
||||
post: <T = unknown>(url: string, payload?: unknown) => Promise<{ data: T }>;
|
||||
delete: (url: string) => Promise<unknown>;
|
||||
};
|
||||
notifyApiError?: (error: unknown, message: string) => void;
|
||||
setStatusMessage?: (message: string, variant?: string) => void;
|
||||
token?: string | null;
|
||||
}
|
||||
|
||||
interface UseApiTokensResult {
|
||||
tokens: ApiTokenRecord[];
|
||||
loading: boolean;
|
||||
creating: boolean;
|
||||
deletingId: string | number | null;
|
||||
regeneratingId: string | number | null;
|
||||
createdSecret: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
create: (args?: CreateTokenArgs) => Promise<ApiTokensResponse | false>;
|
||||
revoke: (tokenId?: string | number | null) => Promise<boolean>;
|
||||
regenerate: (tokenId?: string | number | null) => Promise<boolean>;
|
||||
dismissSecret: () => void;
|
||||
}
|
||||
|
||||
const useApiTokens = ({ api, notifyApiError, setStatusMessage, token }: UseApiTokensArgs): UseApiTokensResult => {
|
||||
const [tokens, setTokens] = useState<ApiTokenRecord[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState<string | number | null>(null);
|
||||
const [regeneratingId, setRegeneratingId] = useState<string | number | null>(null);
|
||||
const [createdSecret, setCreatedSecret] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await api.get<ApiTokenRecord[]>('/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, capability_set_id }: CreateTokenArgs = {}) => {
|
||||
if (creating || !capability_set_id) {
|
||||
return false;
|
||||
}
|
||||
setCreating(true);
|
||||
try {
|
||||
const payload: Record<string, unknown> = { capability_set_id };
|
||||
if (label) {
|
||||
payload.label = label;
|
||||
}
|
||||
if (expires_at) {
|
||||
payload.expires_at = expires_at;
|
||||
}
|
||||
|
||||
const { data } = await api.post<ApiTokensResponse>('/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 ? [data.token_info, ...filtered] : 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?: string | number | null) => {
|
||||
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?: string | number | null) => {
|
||||
if (!tokenId) {
|
||||
return false;
|
||||
}
|
||||
setRegeneratingId(tokenId);
|
||||
try {
|
||||
const { data } = await api.post<ApiTokensResponse>(`/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 && data.token_info) {
|
||||
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 dismissSecret = useCallback(() => {
|
||||
setCreatedSecret(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
tokens,
|
||||
loading,
|
||||
creating,
|
||||
deletingId,
|
||||
regeneratingId,
|
||||
createdSecret,
|
||||
refresh,
|
||||
create,
|
||||
revoke,
|
||||
regenerate,
|
||||
dismissSecret,
|
||||
};
|
||||
};
|
||||
|
||||
export default useApiTokens;
|
||||
Reference in New Issue
Block a user