mirror of
https://github.com/owncast/owncast.git
synced 2024-11-29 03:29:03 +03:00
Add the Client-side Input Validators for Stream Keys and the Admin Password (#2619)
* add the minimum stream key complexity rules on the client side * add an admin password validator * merge TextField and TextFieldAdmin components * update Input Validators for Streak Keys and Admin Password * fix a small regex typo * code cleanup * update Textfield and TextFieldWithSubmit * Prettified Code! * update the TextFieldWithSubmit component * correct the admin password endpoind API * refactor the Admin Password Input field and add a new boolean field for it * refactor the Form Input field name from adminPassword to InputFieldPassword * put password regex rules into config-constants.tsx * regex constant typo fix * change the boolean variable isAdminPwdField to hasComplexityRequirements * fix a merge conflict * Prettified Code! --------- Co-authored-by: dorj222 <dorj222@users.noreply.github.com>
This commit is contained in:
parent
e3a63606eb
commit
3653db3a6a
4 changed files with 181 additions and 40 deletions
|
@ -1,10 +1,11 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Input, InputNumber } from 'antd';
|
import { Input, Form, InputNumber, Button } from 'antd';
|
||||||
import { FieldUpdaterFunc } from '../../types/config-section';
|
import { FieldUpdaterFunc } from '../../types/config-section';
|
||||||
// import InfoTip from '../info-tip';
|
// import InfoTip from '../info-tip';
|
||||||
import { StatusState } from '../../utils/input-statuses';
|
import { StatusState } from '../../utils/input-statuses';
|
||||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||||
|
import { PASSWORD_COMPLEXITY_RULES, REGEX_PASSWORD } from '../../utils/config-constants';
|
||||||
|
|
||||||
export const TEXTFIELD_TYPE_TEXT = 'default';
|
export const TEXTFIELD_TYPE_TEXT = 'default';
|
||||||
export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password
|
export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password
|
||||||
|
@ -17,7 +18,7 @@ export type TextFieldProps = {
|
||||||
|
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
onPressEnter?: () => void;
|
onPressEnter?: () => void;
|
||||||
|
onHandleSubmit?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
@ -31,6 +32,7 @@ export type TextFieldProps = {
|
||||||
useTrim?: boolean;
|
useTrim?: boolean;
|
||||||
useTrimLead?: boolean;
|
useTrimLead?: boolean;
|
||||||
value?: string | number;
|
value?: string | number;
|
||||||
|
hasComplexityRequirements?: boolean;
|
||||||
onBlur?: FieldUpdaterFunc;
|
onBlur?: FieldUpdaterFunc;
|
||||||
onChange?: FieldUpdaterFunc;
|
onChange?: FieldUpdaterFunc;
|
||||||
};
|
};
|
||||||
|
@ -44,6 +46,7 @@ export const TextField: FC<TextFieldProps> = ({
|
||||||
onBlur,
|
onBlur,
|
||||||
onChange,
|
onChange,
|
||||||
onPressEnter,
|
onPressEnter,
|
||||||
|
onHandleSubmit,
|
||||||
pattern,
|
pattern,
|
||||||
placeholder,
|
placeholder,
|
||||||
required,
|
required,
|
||||||
|
@ -52,15 +55,30 @@ export const TextField: FC<TextFieldProps> = ({
|
||||||
type,
|
type,
|
||||||
useTrim,
|
useTrim,
|
||||||
value,
|
value,
|
||||||
|
hasComplexityRequirements,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [hasPwdChanged, setHasPwdChanged] = useState(false);
|
||||||
|
const [showPwdButton, setShowPwdButton] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
const handleChange = (e: any) => {
|
const handleChange = (e: any) => {
|
||||||
// if an extra onChange handler was sent in as a prop, let's run that too.
|
// if an extra onChange handler was sent in as a prop, let's run that too.
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
const val = type === TEXTFIELD_TYPE_NUMBER ? e : e.target.value;
|
const val = type === TEXTFIELD_TYPE_NUMBER ? e : e.target.value;
|
||||||
|
setShowPwdButton(true);
|
||||||
|
if (hasComplexityRequirements && REGEX_PASSWORD.test(val)) {
|
||||||
|
setHasPwdChanged(true);
|
||||||
|
} else {
|
||||||
|
setHasPwdChanged(false);
|
||||||
|
}
|
||||||
|
|
||||||
onChange({ fieldName, value: useTrim ? val.trim() : val });
|
onChange({ fieldName, value: useTrim ? val.trim() : val });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setFieldsValue({ inputFieldPassword: value });
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
// if you blur a required field with an empty value, restore its original value in state (parent's state), if an onChange from parent is available.
|
// if you blur a required field with an empty value, restore its original value in state (parent's state), if an onChange from parent is available.
|
||||||
const handleBlur = (e: any) => {
|
const handleBlur = (e: any) => {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
|
@ -74,7 +92,8 @@ export const TextField: FC<TextFieldProps> = ({
|
||||||
onPressEnter();
|
onPressEnter();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Password Complexity rules
|
||||||
|
const passwordComplexityRules = [];
|
||||||
// display the appropriate Ant text field
|
// display the appropriate Ant text field
|
||||||
let Field = Input as
|
let Field = Input as
|
||||||
| typeof Input
|
| typeof Input
|
||||||
|
@ -88,6 +107,9 @@ export const TextField: FC<TextFieldProps> = ({
|
||||||
autoSize: true,
|
autoSize: true,
|
||||||
};
|
};
|
||||||
} else if (type === TEXTFIELD_TYPE_PASSWORD) {
|
} else if (type === TEXTFIELD_TYPE_PASSWORD) {
|
||||||
|
PASSWORD_COMPLEXITY_RULES.forEach(element => {
|
||||||
|
passwordComplexityRules.push(element);
|
||||||
|
});
|
||||||
Field = Input.Password;
|
Field = Input.Password;
|
||||||
fieldProps = {
|
fieldProps = {
|
||||||
visibilityToggle: true,
|
visibilityToggle: true,
|
||||||
|
@ -128,25 +150,66 @@ export const TextField: FC<TextFieldProps> = ({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="input-side">
|
{!hasComplexityRequirements ? (
|
||||||
<div className="input-group">
|
<div className="input-side">
|
||||||
<Field
|
<div className="input-group">
|
||||||
id={fieldId}
|
<Field
|
||||||
className={`field ${className} ${fieldId}`}
|
id={fieldId}
|
||||||
{...fieldProps}
|
className={`field ${className} ${fieldId}`}
|
||||||
{...(type !== TEXTFIELD_TYPE_NUMBER && { allowClear: true })}
|
{...fieldProps}
|
||||||
placeholder={placeholder}
|
{...(type !== TEXTFIELD_TYPE_NUMBER && { allowClear: true })}
|
||||||
maxLength={maxLength}
|
placeholder={placeholder}
|
||||||
onChange={handleChange}
|
maxLength={maxLength}
|
||||||
onBlur={handleBlur}
|
onChange={handleChange}
|
||||||
onPressEnter={handlePressEnter}
|
onBlur={handleBlur}
|
||||||
disabled={disabled}
|
onPressEnter={handlePressEnter}
|
||||||
value={value as number | (readonly string[] & number)}
|
disabled={disabled}
|
||||||
/>
|
value={value as number | (readonly string[] & number)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormStatusIndicator status={status} />
|
||||||
|
<p className="field-tip">{tip}</p>
|
||||||
</div>
|
</div>
|
||||||
<FormStatusIndicator status={status} />
|
) : (
|
||||||
<p className="field-tip">{tip}</p>
|
<div className="input-side">
|
||||||
</div>
|
<div className="input-group">
|
||||||
|
<Form
|
||||||
|
name="basic"
|
||||||
|
form={form}
|
||||||
|
initialValues={{ inputFieldPassword: value }}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Form.Item name="inputFieldPassword" rules={passwordComplexityRules}>
|
||||||
|
<Input.Password
|
||||||
|
id={fieldId}
|
||||||
|
className={`field ${className} ${fieldId}`}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onPressEnter={handlePressEnter}
|
||||||
|
disabled={disabled}
|
||||||
|
value={value as number | (readonly string[] & number)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
{showPwdButton && (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'row-reverse' }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
className="submit-button"
|
||||||
|
onClick={onHandleSubmit}
|
||||||
|
disabled={!hasPwdChanged}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<FormStatusIndicator status={status} />
|
||||||
|
<p className="field-tip">{tip}</p>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -168,9 +231,11 @@ TextField.defaultProps = {
|
||||||
pattern: '',
|
pattern: '',
|
||||||
useTrim: false,
|
useTrim: false,
|
||||||
useTrimLead: false,
|
useTrimLead: false,
|
||||||
|
hasComplexityRequirements: false,
|
||||||
|
|
||||||
onSubmit: () => {},
|
onSubmit: () => {},
|
||||||
onBlur: () => {},
|
onBlur: () => {},
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
onPressEnter: () => {},
|
onPressEnter: () => {},
|
||||||
|
onHandleSubmit: () => {},
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,6 +24,7 @@ export type TextFieldWithSubmitProps = TextFieldProps & {
|
||||||
apiPath: string;
|
apiPath: string;
|
||||||
configPath?: string;
|
configPath?: string;
|
||||||
initialValue?: string;
|
initialValue?: string;
|
||||||
|
hasComplexityRequirements?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextFieldWithSubmit: FC<TextFieldWithSubmitProps> = ({
|
export const TextFieldWithSubmit: FC<TextFieldWithSubmitProps> = ({
|
||||||
|
@ -43,7 +44,8 @@ export const TextFieldWithSubmit: FC<TextFieldWithSubmitProps> = ({
|
||||||
|
|
||||||
let resetTimer = null;
|
let resetTimer = null;
|
||||||
|
|
||||||
const { fieldName, required, tip, status, value, onChange, onSubmit } = textFieldProps;
|
const { fieldName, required, tip, status, value, hasComplexityRequirements, onChange, onSubmit } =
|
||||||
|
textFieldProps;
|
||||||
|
|
||||||
// Clear out any validation states and messaging
|
// Clear out any validation states and messaging
|
||||||
const resetStates = () => {
|
const resetStates = () => {
|
||||||
|
@ -118,6 +120,7 @@ export const TextFieldWithSubmit: FC<TextFieldWithSubmitProps> = ({
|
||||||
'textfield-with-submit-container': true,
|
'textfield-with-submit-container': true,
|
||||||
submittable: hasChanged,
|
submittable: hasChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={textfieldContainerClass}>
|
<div className={textfieldContainerClass}>
|
||||||
<div className="textfield-component">
|
<div className="textfield-component">
|
||||||
|
@ -126,6 +129,7 @@ export const TextFieldWithSubmit: FC<TextFieldWithSubmitProps> = ({
|
||||||
onSubmit={null}
|
onSubmit={null}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
onHandleSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="formfield-container lower-container">
|
<div className="formfield-container lower-container">
|
||||||
|
@ -134,15 +138,17 @@ export const TextFieldWithSubmit: FC<TextFieldWithSubmitProps> = ({
|
||||||
<div className="field-tip">{tip}</div>
|
<div className="field-tip">{tip}</div>
|
||||||
<FormStatusIndicator status={status || submitStatus} />
|
<FormStatusIndicator status={status || submitStatus} />
|
||||||
<div className="update-button-container">
|
<div className="update-button-container">
|
||||||
<Button
|
{!hasComplexityRequirements && (
|
||||||
type="primary"
|
<Button
|
||||||
size="small"
|
type="primary"
|
||||||
className="submit-button"
|
size="small"
|
||||||
onClick={handleSubmit}
|
className="submit-button"
|
||||||
disabled={!hasChanged}
|
onClick={handleSubmit}
|
||||||
>
|
disabled={!hasChanged}
|
||||||
Update
|
>
|
||||||
</Button>
|
Update
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { Table, Space, Button, Typography, Alert, Input, Form } from 'antd';
|
import { Table, Space, Button, Typography, Alert, Input, Form } from 'antd';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { ServerStatusContext } from '../../../../utils/server-status-context';
|
import { ServerStatusContext } from '../../../../utils/server-status-context';
|
||||||
|
|
||||||
import { fetchData, UPDATE_STREAM_KEYS } from '../../../../utils/apis';
|
import { fetchData, UPDATE_STREAM_KEYS } from '../../../../utils/apis';
|
||||||
|
import { PASSWORD_COMPLEXITY_RULES, REGEX_PASSWORD } from '../../../../utils/config-constants';
|
||||||
|
|
||||||
const { Paragraph } = Typography;
|
const { Paragraph } = Typography;
|
||||||
const { Item } = Form;
|
|
||||||
|
|
||||||
// Lazy loaded components
|
// Lazy loaded components
|
||||||
|
|
||||||
|
@ -54,6 +54,18 @@ const generateRndKey = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddKeyForm = ({ setShowAddKeyForm, setFieldInConfigState, streamKeys, setError }) => {
|
const AddKeyForm = ({ setShowAddKeyForm, setFieldInConfigState, streamKeys, setError }) => {
|
||||||
|
const [hasChanged, setHasChanged] = useState(true);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const { Item } = Form;
|
||||||
|
// Password Complexity rules
|
||||||
|
const passwordComplexityRules = [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
PASSWORD_COMPLEXITY_RULES.forEach(element => {
|
||||||
|
passwordComplexityRules.push(element);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleAddKey = (newkey: any) => {
|
const handleAddKey = (newkey: any) => {
|
||||||
const updatedKeys = [...streamKeys, newkey];
|
const updatedKeys = [...streamKeys, newkey];
|
||||||
|
|
||||||
|
@ -67,19 +79,50 @@ const AddKeyForm = ({ setShowAddKeyForm, setFieldInConfigState, streamKeys, setE
|
||||||
setShowAddKeyForm(false);
|
setShowAddKeyForm(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (event: any) => {
|
||||||
|
const val = event.target.value;
|
||||||
|
if (REGEX_PASSWORD.test(val)) {
|
||||||
|
setHasChanged(true);
|
||||||
|
} else {
|
||||||
|
setHasChanged(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Default auto-generated key
|
// Default auto-generated key
|
||||||
const defaultKey = generateRndKey();
|
const defaultKey = generateRndKey();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form layout="inline" autoComplete="off" onFinish={handleAddKey}>
|
<Form
|
||||||
<Item label="Key" name="key" tooltip="The key you provide your broadcasting software">
|
layout="horizontal"
|
||||||
<Input placeholder="def456" defaultValue={defaultKey} />
|
autoComplete="off"
|
||||||
|
onFinish={handleAddKey}
|
||||||
|
form={form}
|
||||||
|
style={{ display: 'flex', flexDirection: 'row' }}
|
||||||
|
>
|
||||||
|
<Item
|
||||||
|
style={{ width: '60%', marginRight: '5px' }}
|
||||||
|
label="Key"
|
||||||
|
name="key"
|
||||||
|
tooltip={
|
||||||
|
<p>
|
||||||
|
The key you provide your broadcasting software. Please note that the key must be a
|
||||||
|
minimum of eight characters and must include at least one uppercase letter, at least one
|
||||||
|
lowercase letter, at least one special character, and at least one number.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
rules={PASSWORD_COMPLEXITY_RULES}
|
||||||
|
>
|
||||||
|
<Input placeholder="def456" defaultValue={defaultKey} onChange={handleInputChange} />
|
||||||
</Item>
|
</Item>
|
||||||
<Item label="Comment" name="comment" tooltip="For remembering why you added this key">
|
<Item
|
||||||
|
style={{ width: '60%', marginRight: '5px' }}
|
||||||
|
label="Comment"
|
||||||
|
name="comment"
|
||||||
|
tooltip="For remembering why you added this key"
|
||||||
|
>
|
||||||
<Input placeholder="My OBS Key" />
|
<Input placeholder="My OBS Key" />
|
||||||
</Item>
|
</Item>
|
||||||
|
<Button type="primary" htmlType="submit" disabled={!hasChanged}>
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -122,6 +122,7 @@ export const TEXTFIELD_PROPS_ADMIN_PASSWORD = {
|
||||||
label: 'Admin Password',
|
label: 'Admin Password',
|
||||||
tip: 'Save this password somewhere safe, you will need it to login to the admin dashboard!',
|
tip: 'Save this password somewhere safe, you will need it to login to the admin dashboard!',
|
||||||
required: true,
|
required: true,
|
||||||
|
hasComplexityRequirements: true,
|
||||||
};
|
};
|
||||||
export const TEXTFIELD_PROPS_FFMPEG = {
|
export const TEXTFIELD_PROPS_FFMPEG = {
|
||||||
apiPath: API_FFMPEG,
|
apiPath: API_FFMPEG,
|
||||||
|
@ -131,6 +132,7 @@ export const TEXTFIELD_PROPS_FFMPEG = {
|
||||||
label: 'FFmpeg Path',
|
label: 'FFmpeg Path',
|
||||||
tip: 'Absolute file path of the FFMPEG application on your server',
|
tip: 'Absolute file path of the FFMPEG application on your server',
|
||||||
required: true,
|
required: true,
|
||||||
|
hasComplexityRequirements: false,
|
||||||
};
|
};
|
||||||
export const TEXTFIELD_PROPS_WEB_PORT = {
|
export const TEXTFIELD_PROPS_WEB_PORT = {
|
||||||
apiPath: API_WEB_PORT,
|
apiPath: API_WEB_PORT,
|
||||||
|
@ -140,6 +142,7 @@ export const TEXTFIELD_PROPS_WEB_PORT = {
|
||||||
label: 'Owncast port',
|
label: 'Owncast port',
|
||||||
tip: 'What port is your Owncast web server listening? Default is 8080',
|
tip: 'What port is your Owncast web server listening? Default is 8080',
|
||||||
required: true,
|
required: true,
|
||||||
|
hasComplexityRequirements: false,
|
||||||
};
|
};
|
||||||
export const TEXTFIELD_PROPS_RTMP_PORT = {
|
export const TEXTFIELD_PROPS_RTMP_PORT = {
|
||||||
apiPath: API_RTMP_PORT,
|
apiPath: API_RTMP_PORT,
|
||||||
|
@ -149,6 +152,7 @@ export const TEXTFIELD_PROPS_RTMP_PORT = {
|
||||||
label: 'RTMP port',
|
label: 'RTMP port',
|
||||||
tip: 'What port should accept inbound broadcasts? Default is 1935',
|
tip: 'What port should accept inbound broadcasts? Default is 1935',
|
||||||
required: true,
|
required: true,
|
||||||
|
hasComplexityRequirements: false,
|
||||||
};
|
};
|
||||||
export const TEXTFIELD_PROPS_INSTANCE_URL = {
|
export const TEXTFIELD_PROPS_INSTANCE_URL = {
|
||||||
apiPath: API_INSTANCE_URL,
|
apiPath: API_INSTANCE_URL,
|
||||||
|
@ -558,3 +562,26 @@ export const BROWSER_PUSH_CONFIG_FIELDS = {
|
||||||
placeholder: `I've gone live! Come watch!`,
|
placeholder: `I've gone live! Come watch!`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PASSWORD_COMPLEXITY_RULES = [
|
||||||
|
{ min: 8, message: '- minimum 8 characters' },
|
||||||
|
{ max: 192, message: '- maximum 192 characters' },
|
||||||
|
{
|
||||||
|
pattern: /^(?=.*[a-z])/,
|
||||||
|
message: '- at least one lowercase letter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^(?=.*[A-Z])/,
|
||||||
|
message: '- at least one uppercase letter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /\d/,
|
||||||
|
message: '- at least one digit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^(?=.*?[#?!@$%^&*-])/,
|
||||||
|
message: '- at least one special character: !@#$%^&*',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const REGEX_PASSWORD = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!@#$%^&*]).{8,192}$/;
|
||||||
|
|
Loading…
Reference in a new issue