StreamKeys.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import React, { useContext, useEffect, useState } from 'react';
  2. import { Table, Space, Button, Typography, Alert, Input, Form, message } from 'antd';
  3. import dynamic from 'next/dynamic';
  4. import { ServerStatusContext } from '../../../../utils/server-status-context';
  5. import { fetchData, UPDATE_STREAM_KEYS } from '../../../../utils/apis';
  6. import { PASSWORD_COMPLEXITY_RULES, REGEX_PASSWORD } from '../../../../utils/config-constants';
  7. const { Paragraph } = Typography;
  8. // Lazy loaded components
  9. const DeleteOutlined = dynamic(() => import('@ant-design/icons/DeleteOutlined'), {
  10. ssr: false,
  11. });
  12. const EyeOutlined = dynamic(() => import('@ant-design/icons/EyeOutlined'), {
  13. ssr: false,
  14. });
  15. const PlusOutlined = dynamic(() => import('@ant-design/icons/PlusOutlined'), {
  16. ssr: false,
  17. });
  18. const saveKeys = async (keys, setError) => {
  19. try {
  20. await fetchData(UPDATE_STREAM_KEYS, {
  21. method: 'POST',
  22. auth: true,
  23. data: { value: keys },
  24. });
  25. } catch (error) {
  26. console.error(error);
  27. setError(error.message);
  28. }
  29. };
  30. export const generateRndKey = () => {
  31. let defaultKey = '';
  32. let isValidStreamKey = false;
  33. const streamKeyRegex = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!@#$^&*]).{8,192}$/;
  34. const s = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$^&*';
  35. while (!isValidStreamKey) {
  36. const temp = Array.apply(20, Array(30))
  37. .map(() => s.charAt(Math.floor(Math.random() * s.length)))
  38. .join('');
  39. if (streamKeyRegex.test(temp)) {
  40. isValidStreamKey = true;
  41. defaultKey = temp;
  42. }
  43. }
  44. return defaultKey;
  45. };
  46. const AddKeyForm = ({ setShowAddKeyForm, setFieldInConfigState, streamKeys, setError }) => {
  47. const [hasChanged, setHasChanged] = useState(true);
  48. const [form] = Form.useForm();
  49. const { Item } = Form;
  50. // Password Complexity rules
  51. const passwordComplexityRules = [];
  52. useEffect(() => {
  53. PASSWORD_COMPLEXITY_RULES.forEach(element => {
  54. passwordComplexityRules.push(element);
  55. });
  56. }, []);
  57. const handleAddKey = (newkey: any) => {
  58. const updatedKeys = [...streamKeys, newkey];
  59. setFieldInConfigState({
  60. fieldName: 'streamKeys',
  61. value: updatedKeys,
  62. });
  63. saveKeys(updatedKeys, setError);
  64. setShowAddKeyForm(false);
  65. };
  66. const handleInputChange = (event: any) => {
  67. const val = event.target.value;
  68. if (REGEX_PASSWORD.test(val)) {
  69. setHasChanged(true);
  70. } else {
  71. setHasChanged(false);
  72. }
  73. };
  74. // Default auto-generated key
  75. const defaultKey = generateRndKey();
  76. return (
  77. <Form
  78. layout="horizontal"
  79. autoComplete="off"
  80. onFinish={handleAddKey}
  81. form={form}
  82. style={{ display: 'flex', flexDirection: 'row' }}
  83. initialValues={{ key: defaultKey, comment: 'My new key' }}
  84. >
  85. <Item
  86. style={{ width: '60%', marginRight: '5px' }}
  87. label="Key"
  88. name="key"
  89. tooltip={
  90. <p>
  91. The key you provide your broadcasting software. Please note that the key must be a
  92. minimum of eight characters and must include at least one uppercase letter, at least one
  93. lowercase letter, at least one special character, and at least one number.
  94. </p>
  95. }
  96. rules={PASSWORD_COMPLEXITY_RULES}
  97. >
  98. <Input placeholder="your key" onChange={handleInputChange} />
  99. </Item>
  100. <Item
  101. style={{ width: '60%', marginRight: '5px' }}
  102. label="Comment"
  103. name="comment"
  104. tooltip="For remembering why you added this key"
  105. >
  106. <Input placeholder="My OBS Key" />
  107. </Item>
  108. <Button type="primary" htmlType="submit" disabled={!hasChanged}>
  109. Add
  110. </Button>
  111. </Form>
  112. );
  113. };
  114. const AddKeyButton = ({ setShowAddKeyForm }) => (
  115. <Button type="default" onClick={() => setShowAddKeyForm(true)}>
  116. <PlusOutlined />
  117. </Button>
  118. );
  119. const copyText = (text: string) => {
  120. navigator.clipboard
  121. .writeText(text)
  122. .then(() => message.success('Copied to clipboard'))
  123. .catch(() => message.error('Failed to copy to clipboard'));
  124. };
  125. const StreamKeys = () => {
  126. const serverStatusData = useContext(ServerStatusContext);
  127. const { serverConfig, setFieldInConfigState } = serverStatusData || {};
  128. const { streamKeys } = serverConfig;
  129. const [showAddKeyForm, setShowAddKeyForm] = useState(false);
  130. const [showKeyMap, setShowKeyMap] = useState({});
  131. const [error, setError] = useState(null);
  132. const handleDeleteKey = keyToRemove => {
  133. const newKeys = streamKeys.filter(k => k !== keyToRemove);
  134. setFieldInConfigState({
  135. fieldName: 'streamKeys',
  136. value: newKeys,
  137. });
  138. saveKeys(newKeys, setError);
  139. };
  140. const handleToggleShowKey = key => {
  141. setShowKeyMap({
  142. ...showKeyMap,
  143. [key]: !showKeyMap[key],
  144. });
  145. };
  146. const columns = [
  147. {
  148. title: 'Key',
  149. dataIndex: 'key',
  150. key: 'key',
  151. render: text => (
  152. <Space direction="horizontal">
  153. <Paragraph
  154. copyable={{
  155. text: showKeyMap[text] ? text : '**********',
  156. onCopy: () => copyText(text),
  157. }}
  158. >
  159. {showKeyMap[text] ? text : '**********'}
  160. </Paragraph>
  161. <Button
  162. type="link"
  163. style={{ top: '-7px' }}
  164. icon={<EyeOutlined />}
  165. onClick={() => handleToggleShowKey(text)}
  166. />
  167. </Space>
  168. ),
  169. },
  170. {
  171. title: 'Comment',
  172. dataIndex: 'comment',
  173. key: 'comment',
  174. },
  175. {
  176. title: '',
  177. key: 'delete',
  178. render: text => (
  179. <Button
  180. disabled={streamKeys.length === 1}
  181. onClick={() => handleDeleteKey(text)}
  182. icon={<DeleteOutlined />}
  183. />
  184. ),
  185. },
  186. ];
  187. return (
  188. <div>
  189. <Paragraph>
  190. A streaming key is used with your broadcasting software to authenticate itself to Owncast.
  191. Most people will only need one. However, if you share a server with others or you want
  192. different keys for different broadcasting sources you can add more here.
  193. </Paragraph>
  194. <Paragraph>
  195. These keys are unrelated to the admin password and will not grant you access to make changes
  196. to Owncast&apos;s configuration.
  197. </Paragraph>
  198. <Paragraph>
  199. Read more about broadcasting at{' '}
  200. <a
  201. href="https://owncast.online/docs/broadcasting/?source=admin"
  202. target="_blank"
  203. rel="noopener noreferrer"
  204. >
  205. the documentation
  206. </a>
  207. .
  208. </Paragraph>
  209. <Space direction="vertical" style={{ width: '70%' }}>
  210. {error && <Alert type="error" message="Saving Keys Error" description={error} />}
  211. {streamKeys.length === 0 && (
  212. <Alert
  213. message="No stream keys!"
  214. description="You will not be able to stream until you create at least one stream key and add it to your broadcasting software."
  215. type="error"
  216. />
  217. )}
  218. <Table
  219. rowKey="key"
  220. columns={columns}
  221. dataSource={streamKeys}
  222. pagination={false}
  223. // eslint-disable-next-line react/no-unstable-nested-components
  224. footer={() =>
  225. showAddKeyForm ? (
  226. <AddKeyForm
  227. setShowAddKeyForm={setShowAddKeyForm}
  228. streamKeys={streamKeys}
  229. setFieldInConfigState={setFieldInConfigState}
  230. setError={setError}
  231. />
  232. ) : (
  233. <AddKeyButton setShowAddKeyForm={setShowAddKeyForm} />
  234. )
  235. }
  236. />
  237. <br />
  238. </Space>
  239. </div>
  240. );
  241. };
  242. export default StreamKeys;