mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
frontend components for API keys
This commit is contained in:
parent
aece3367ce
commit
6cf808a4df
2 changed files with 170 additions and 22 deletions
|
|
@ -13,6 +13,7 @@ import useChannelsTableStore from './store/channelsTable';
|
|||
import useStreamsTableStore from './store/streamsTable';
|
||||
import useUsersStore from './store/users';
|
||||
import useConnectStore from './store/connect';
|
||||
import Limiter from './utils';
|
||||
|
||||
// If needed, you can set a base host or keep it empty if relative requests
|
||||
const host = import.meta.env.DEV
|
||||
|
|
@ -105,6 +106,41 @@ export default class API {
|
|||
return await useAuthStore.getState().getToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all pages for a paginated endpoint when you already know totalCount.
|
||||
* Builds page calls from totalCount and pageSize and aggregates all results.
|
||||
* - endpoint: path like "/api/channels/channels/"
|
||||
* - params: URLSearchParams for filters (will not be mutated)
|
||||
* - totalCount: total number of matching items
|
||||
* - pageSize: items per page
|
||||
* Returns a flat array of results. Supports both array and {results, next} responses.
|
||||
*/
|
||||
static async fetchAllByCount(endpoint, params, totalCount, pageSize = 200) {
|
||||
const total = Number(totalCount) || 0;
|
||||
const size = Number(pageSize) || 200;
|
||||
const totalPages = Math.max(1, Math.ceil(total / size));
|
||||
|
||||
const requests = [];
|
||||
for (let page = 1; page <= totalPages; page++) {
|
||||
const q = new URLSearchParams(params || new URLSearchParams());
|
||||
q.set('page', String(page));
|
||||
q.set('page_size', String(size));
|
||||
const url = `${host}${endpoint}?${q.toString()}`;
|
||||
requests.push(request(url));
|
||||
}
|
||||
|
||||
const responses = await Promise.all(requests);
|
||||
const all = [];
|
||||
for (const data of responses) {
|
||||
if (Array.isArray(data)) {
|
||||
all.push(...data);
|
||||
} else if (Array.isArray(data?.results)) {
|
||||
all.push(...data.results);
|
||||
}
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
static async fetchSuperUser() {
|
||||
try {
|
||||
return await request(`${host}/api/accounts/initialize-superuser/`, {
|
||||
|
|
@ -181,32 +217,39 @@ export default class API {
|
|||
try {
|
||||
// Paginate through channels to avoid heavy single response
|
||||
const pageSize = 200;
|
||||
let page = 1;
|
||||
let allChannels = [];
|
||||
const allChannels = [];
|
||||
|
||||
while (true) {
|
||||
const data = await request(
|
||||
`${host}/api/channels/channels/?page=${page}&page_size=${pageSize}`
|
||||
);
|
||||
// Get first page to get total results count
|
||||
const data = await request(
|
||||
`${host}/api/channels/channels/?page=1&page_size=${pageSize}`
|
||||
);
|
||||
|
||||
// Backward compatibility: if endpoint returns an array (legacy), just return it
|
||||
if (Array.isArray(data)) {
|
||||
allChannels = data;
|
||||
break;
|
||||
}
|
||||
|
||||
const results = Array.isArray(data?.results) ? data.results : [];
|
||||
allChannels = allChannels.concat(results);
|
||||
|
||||
const hasMore = Boolean(data?.next);
|
||||
if (!hasMore || results.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
page += 1;
|
||||
// Backward compatibility: if endpoint returns an array (legacy), just return it
|
||||
if (Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return allChannels;
|
||||
allChannels.concat(Array.isArray(data?.results) ? data.results : []);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(data.count / pageSize)) - 1;
|
||||
const apiCalls = [];
|
||||
for (let page = 2; page <= totalPages; page++) {
|
||||
apiCalls.push(
|
||||
new Promise(async (resolve) => {
|
||||
const response = await request(
|
||||
`${host}/api/channels/channels/?page=${page}&page_size=${pageSize}`
|
||||
);
|
||||
|
||||
return resolve(
|
||||
Array.isArray(response?.results) ? response.results : []
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const allResults = await Limiter.all(5, apiCalls);
|
||||
|
||||
return allResults;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to retrieve channels', e);
|
||||
}
|
||||
|
|
@ -2832,6 +2875,35 @@ export default class API {
|
|||
}
|
||||
}
|
||||
|
||||
static async generateApiKey({ user_id = null, name = '' } = {}) {
|
||||
try {
|
||||
const body = {};
|
||||
if (user_id) body.user_id = user_id;
|
||||
if (name) body.name = name;
|
||||
|
||||
const response = await request(
|
||||
`${host}/api/accounts/api-keys/generate/`,
|
||||
{
|
||||
method: 'POST',
|
||||
body,
|
||||
}
|
||||
);
|
||||
|
||||
// If the backend returned an updated user, refresh the users store
|
||||
try {
|
||||
if (response && response.user) {
|
||||
useUsersStore.getState().updateUser(response.user);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore store update errors
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to generate API key', e);
|
||||
}
|
||||
}
|
||||
|
||||
static async updateUser(id, body) {
|
||||
try {
|
||||
const response = await request(`${host}/api/accounts/users/${id}/`, {
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ import {
|
|||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { RotateCcwKey, X } from 'lucide-react';
|
||||
import { Copy, Key } from 'lucide-react';
|
||||
import { useForm } from '@mantine/form';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import { USER_LEVELS, USER_LEVEL_LABELS } from '../../constants';
|
||||
import useAuthStore from '../../store/auth';
|
||||
import { copyToClipboard } from '../../utils';
|
||||
|
||||
const User = ({ user = null, isOpen, onClose }) => {
|
||||
const profiles = useChannelsStore((s) => s.profiles);
|
||||
|
|
@ -29,6 +31,9 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
|
||||
const [, setEnableXC] = useState(false);
|
||||
const [selectedProfiles, setSelectedProfiles] = useState(new Set());
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [generatedKey, setGeneratedKey] = useState(null);
|
||||
const [userAPIKey, setUserAPIKey] = useState(user?.api_key || null);
|
||||
|
||||
const form = useForm({
|
||||
mode: 'uncontrolled',
|
||||
|
|
@ -139,6 +144,10 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
if (customProps.xc_password) {
|
||||
setEnableXC(true);
|
||||
}
|
||||
|
||||
if (user?.api_key) {
|
||||
setUserAPIKey(user.api_key);
|
||||
}
|
||||
} else {
|
||||
form.reset();
|
||||
}
|
||||
|
|
@ -157,6 +166,34 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
const showPermissions =
|
||||
authUser.user_level == USER_LEVELS.ADMIN && authUser.id !== user?.id;
|
||||
|
||||
const canGenerateKey =
|
||||
authUser.user_level == USER_LEVELS.ADMIN || authUser.id === user?.id;
|
||||
|
||||
const onGenerateKey = async () => {
|
||||
if (!canGenerateKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
setGenerating(true);
|
||||
try {
|
||||
const payload = {};
|
||||
if (authUser.user_level == USER_LEVELS.ADMIN && user?.id) {
|
||||
payload.user_id = user.id;
|
||||
}
|
||||
|
||||
const resp = await API.generateApiKey(payload);
|
||||
const newKey = resp && (resp.key || resp.raw_key);
|
||||
if (newKey) {
|
||||
setGeneratedKey(newKey);
|
||||
setUserAPIKey(newKey);
|
||||
}
|
||||
} catch (e) {
|
||||
// API shows notifications
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal opened={isOpen} onClose={onClose} title="User" size="xl">
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
|
|
@ -269,6 +306,45 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{canGenerateKey && (
|
||||
<Stack>
|
||||
{userAPIKey && (
|
||||
<TextInput
|
||||
label="API Key"
|
||||
disabled={true}
|
||||
value={userAPIKey}
|
||||
rightSection={
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={() =>
|
||||
copyToClipboard(userAPIKey, {
|
||||
successTitle: 'API Key Copied!',
|
||||
successMessage:
|
||||
'The API Key has been copied to your clipboard.',
|
||||
})
|
||||
}
|
||||
>
|
||||
<Copy />
|
||||
</ActionIcon>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
leftSection={<Key size={14} />}
|
||||
size="xs"
|
||||
onClick={onGenerateKey}
|
||||
loading={generating}
|
||||
color="blue"
|
||||
variant="light"
|
||||
>
|
||||
{userAPIKey ? 'Regenerate API Key' : 'Generate API Key'}
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue