mirror of
				https://github.com/owncast/owncast.git
				synced 2025-11-01 02:44:31 +08:00 
			
		
		
		
	 b10ba1dcc2
			
		
	
	b10ba1dcc2
	
	
	
		
			
			* First pass at displaying user data in admin * Hide chat blurb on home page if chat is disabled * Hide sidebar chat section if chat is disabled * Block/unblock user interface for https://github.com/owncast/owncast/issues/1096 * Simplify past display name handling * Updates to reflect the api access token change * Update paths * Clean up the new access token page * Fix linter * Update linter workflow action * Cleanup * Fix exception rendering table row * Commit next-env file that seems to be required with next 11 * chat refactor - admin adjustments (#250) * add useragent parser; clean up some html; * some ui changes - use modal instead of popover to confirm block/unblock user - update styles, table styles for consistency - rename some user/chat labels in nav and content * format user info modal a bit * add some sort of mild treatment and delay while processing ban of users * rename button to 'ban' * add some notes * Prettified Code! * fix disableChat toggle for nav bar * Support sorting the disabled user list * Fix linter error around table sorting * No longer restoring messages on unban so change message prompt * Standardize on forbiddenUsername terminology * The linter broke the webhooks page. Fixed it. Linter is probably pissed. * Move chat welcome message to chat config * Other submenus don't have icons so remove these ones Co-authored-by: gingervitis <omqmail@gmail.com> Co-authored-by: gabek <gabek@users.noreply.github.com>
		
			
				
	
	
		
			263 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			263 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import React, { useState, useEffect } from 'react';
 | |
| import {
 | |
|   Table,
 | |
|   Tag,
 | |
|   Space,
 | |
|   Button,
 | |
|   Modal,
 | |
|   Checkbox,
 | |
|   Input,
 | |
|   Typography,
 | |
|   Tooltip,
 | |
|   Row,
 | |
|   Col,
 | |
| } from 'antd';
 | |
| import { DeleteOutlined } from '@ant-design/icons';
 | |
| 
 | |
| import format from 'date-fns/format';
 | |
| 
 | |
| import { fetchData, ACCESS_TOKENS, DELETE_ACCESS_TOKEN, CREATE_ACCESS_TOKEN } from '../utils/apis';
 | |
| 
 | |
| const { Title, Paragraph } = Typography;
 | |
| 
 | |
| 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
 | |
|   visible: boolean;
 | |
| }
 | |
| function NewTokenModal(props: Props) {
 | |
|   const { onOk, onCancel, visible } = 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"
 | |
|       visible={visible}
 | |
|       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>
 | |
|   );
 | |
| }
 | |
| 
 | |
| export default function AccessTokens() {
 | |
|   const [tokens, setTokens] = useState([]);
 | |
|   const [isTokenModalVisible, setIsTokenModalVisible] = 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: (text, 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 = () => {
 | |
|     setIsTokenModalVisible(true);
 | |
|   };
 | |
| 
 | |
|   const handleTokenModalSaveButton = (name, scopes) => {
 | |
|     setIsTokenModalVisible(false);
 | |
|     handleSaveToken(name, scopes);
 | |
|   };
 | |
| 
 | |
|   const handleTokenModalCancel = () => {
 | |
|     setIsTokenModalVisible(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
 | |
|         visible={isTokenModalVisible}
 | |
|         onOk={handleTokenModalSaveButton}
 | |
|         onCancel={handleTokenModalCancel}
 | |
|       />
 | |
|     </div>
 | |
|   );
 | |
| }
 |