44eafe69ac
* fix(deps): update dependency date-fns to v3 * fix(deps): update usage of date-fns format --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Gabe Kangas <gabek@real-ity.com>
282 lines
6.7 KiB
TypeScript
282 lines
6.7 KiB
TypeScript
import React, { useState, useEffect, ReactElement } from 'react';
|
|
import {
|
|
Table,
|
|
Tag,
|
|
Space,
|
|
Button,
|
|
Modal,
|
|
Checkbox,
|
|
Input,
|
|
Typography,
|
|
Row,
|
|
Col,
|
|
Tooltip,
|
|
} from 'antd';
|
|
|
|
import { format } from 'date-fns';
|
|
|
|
import dynamic from 'next/dynamic';
|
|
import {
|
|
fetchData,
|
|
ACCESS_TOKENS,
|
|
DELETE_ACCESS_TOKEN,
|
|
CREATE_ACCESS_TOKEN,
|
|
} from '../../utils/apis';
|
|
|
|
import { AdminLayout } from '../../components/layouts/AdminLayout';
|
|
|
|
const { Title, Paragraph } = Typography;
|
|
|
|
// Lazy loaded components
|
|
|
|
const DeleteOutlined = dynamic(() => import('@ant-design/icons/DeleteOutlined'), {
|
|
ssr: false,
|
|
});
|
|
|
|
const availableScopes = {
|
|
CAN_SEND_SYSTEM_MESSAGES: {
|
|
name: 'System messages',
|
|
description: 'Can send official messages on behalf of the system.',
|
|
color: 'purple',
|
|
},
|
|
CAN_SEND_MESSAGES: {
|
|
name: 'User chat messages',
|
|
description: 'Can send chat messages on behalf of the owner of this token.',
|
|
color: 'green',
|
|
},
|
|
HAS_ADMIN_ACCESS: {
|
|
name: 'Has admin access',
|
|
description: 'Can perform administrative actions such as moderation, get server statuses, etc.',
|
|
color: 'red',
|
|
},
|
|
};
|
|
|
|
function convertScopeStringToTag(scopeString: string) {
|
|
if (!scopeString || !availableScopes[scopeString]) {
|
|
return null;
|
|
}
|
|
|
|
const scope = availableScopes[scopeString];
|
|
|
|
return (
|
|
<Tooltip key={scopeString} title={scope.description}>
|
|
<Tag color={scope.color}>{scope.name}</Tag>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
interface Props {
|
|
onCancel: () => void;
|
|
onOk: any; // todo: make better type
|
|
open: boolean;
|
|
}
|
|
const NewTokenModal = (props: Props) => {
|
|
const { onOk, onCancel, open } = props;
|
|
const [selectedScopes, setSelectedScopes] = useState([]);
|
|
const [name, setName] = useState('');
|
|
|
|
const scopes = Object.keys(availableScopes).map(key => ({
|
|
value: key,
|
|
label: availableScopes[key].description,
|
|
}));
|
|
|
|
function onChange(checkedValues) {
|
|
setSelectedScopes(checkedValues);
|
|
}
|
|
|
|
function saveToken() {
|
|
onOk(name, selectedScopes);
|
|
|
|
// Clear the modal
|
|
setSelectedScopes([]);
|
|
setName('');
|
|
}
|
|
|
|
const okButtonProps = {
|
|
disabled: selectedScopes.length === 0 || name === '',
|
|
};
|
|
|
|
function selectAll() {
|
|
setSelectedScopes(Object.keys(availableScopes));
|
|
}
|
|
const checkboxes = scopes.map(singleEvent => (
|
|
<Col span={8} key={singleEvent.value}>
|
|
<Checkbox value={singleEvent.value}>{singleEvent.label}</Checkbox>
|
|
</Col>
|
|
));
|
|
|
|
return (
|
|
<Modal
|
|
title="Create New Access token"
|
|
open={open}
|
|
onOk={saveToken}
|
|
onCancel={onCancel}
|
|
okButtonProps={okButtonProps}
|
|
>
|
|
<p>
|
|
<p>
|
|
The name will be displayed as the chat user when sending messages with this access token.
|
|
</p>
|
|
<Input
|
|
value={name}
|
|
placeholder="Name of bot, service, or integration"
|
|
onChange={input => setName(input.currentTarget.value)}
|
|
/>
|
|
</p>
|
|
|
|
<p>
|
|
Select the permissions this access token will have. It cannot be edited after it's
|
|
created.
|
|
</p>
|
|
<Checkbox.Group style={{ width: '100%' }} value={selectedScopes} onChange={onChange}>
|
|
<Row>{checkboxes}</Row>
|
|
</Checkbox.Group>
|
|
|
|
<p>
|
|
<Button type="primary" onClick={selectAll}>
|
|
Select all
|
|
</Button>
|
|
</p>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
const AccessTokens = () => {
|
|
const [tokens, setTokens] = useState([]);
|
|
const [isTokenModalOpen, setIsTokenModalOpen] = useState(false);
|
|
|
|
function handleError(error) {
|
|
console.error('error', error);
|
|
}
|
|
|
|
async function getAccessTokens() {
|
|
try {
|
|
const result = await fetchData(ACCESS_TOKENS);
|
|
setTokens(result);
|
|
} catch (error) {
|
|
handleError(error);
|
|
}
|
|
}
|
|
useEffect(() => {
|
|
getAccessTokens();
|
|
}, []);
|
|
|
|
async function handleDeleteToken(token) {
|
|
try {
|
|
await fetchData(DELETE_ACCESS_TOKEN, {
|
|
method: 'POST',
|
|
data: { token },
|
|
});
|
|
getAccessTokens();
|
|
} catch (error) {
|
|
handleError(error);
|
|
}
|
|
}
|
|
|
|
async function handleSaveToken(name: string, scopes: string[]) {
|
|
try {
|
|
const newToken = await fetchData(CREATE_ACCESS_TOKEN, {
|
|
method: 'POST',
|
|
data: { name, scopes },
|
|
});
|
|
setTokens(tokens.concat(newToken));
|
|
} catch (error) {
|
|
handleError(error);
|
|
}
|
|
}
|
|
|
|
const columns = [
|
|
{
|
|
title: '',
|
|
key: 'delete',
|
|
render: (_, record) => (
|
|
<Space size="middle">
|
|
<Button onClick={() => handleDeleteToken(record.accessToken)} icon={<DeleteOutlined />} />
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
title: 'Name',
|
|
dataIndex: 'displayName',
|
|
key: 'displayName',
|
|
},
|
|
{
|
|
title: 'Token',
|
|
dataIndex: 'accessToken',
|
|
key: 'accessToken',
|
|
render: text => <Input.Password size="small" bordered={false} value={text} />,
|
|
},
|
|
{
|
|
title: 'Scopes',
|
|
dataIndex: 'scopes',
|
|
key: 'scopes',
|
|
// eslint-disable-next-line react/destructuring-assignment
|
|
render: scopes => <>{scopes.map(scope => convertScopeStringToTag(scope))}</>,
|
|
},
|
|
{
|
|
title: 'Last Used',
|
|
dataIndex: 'lastUsed',
|
|
key: 'lastUsed',
|
|
render: lastUsed => {
|
|
if (!lastUsed) {
|
|
return 'Never';
|
|
}
|
|
const dateObject = new Date(lastUsed);
|
|
return format(dateObject, 'P p');
|
|
},
|
|
},
|
|
];
|
|
|
|
const showCreateTokenModal = () => {
|
|
setIsTokenModalOpen(true);
|
|
};
|
|
|
|
const handleTokenModalSaveButton = (name, scopes) => {
|
|
setIsTokenModalOpen(false);
|
|
handleSaveToken(name, scopes);
|
|
};
|
|
|
|
const handleTokenModalCancel = () => {
|
|
setIsTokenModalOpen(false);
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<Title>Access Tokens</Title>
|
|
<Paragraph>
|
|
Access tokens are used to allow external, 3rd party tools to perform specific actions on
|
|
your Owncast server. They should be kept secure and never included in client code, instead
|
|
they should be kept on a server that you control.
|
|
</Paragraph>
|
|
<Paragraph>
|
|
Read more about how to use these tokens, with examples, at{' '}
|
|
<a
|
|
href="https://owncast.online/docs/integrations/?source=admin"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
our documentation
|
|
</a>
|
|
.
|
|
</Paragraph>
|
|
|
|
<Table rowKey="token" columns={columns} dataSource={tokens} pagination={false} />
|
|
<br />
|
|
<Button type="primary" onClick={showCreateTokenModal}>
|
|
Create Access Token
|
|
</Button>
|
|
<NewTokenModal
|
|
open={isTokenModalOpen}
|
|
onOk={handleTokenModalSaveButton}
|
|
onCancel={handleTokenModalCancel}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
AccessTokens.getLayout = function getLayout(page: ReactElement) {
|
|
return <AdminLayout page={page} />;
|
|
};
|
|
|
|
export default AccessTokens;
|