From 3653db3a6a113e73075687ad35b5d15e8dd74132 Mon Sep 17 00:00:00 2001 From: Jambaldorj Ochirpurev Date: Fri, 3 Mar 2023 06:20:53 +0100 Subject: [PATCH] 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 --- web/components/admin/TextField.tsx | 109 ++++++++++++++---- web/components/admin/TextFieldWithSubmit.tsx | 26 +++-- .../admin/config/server/StreamKeys.tsx | 59 ++++++++-- web/utils/config-constants.tsx | 27 +++++ 4 files changed, 181 insertions(+), 40 deletions(-) diff --git a/web/components/admin/TextField.tsx b/web/components/admin/TextField.tsx index cc9b37721..db772f318 100644 --- a/web/components/admin/TextField.tsx +++ b/web/components/admin/TextField.tsx @@ -1,10 +1,11 @@ -import React, { FC } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import classNames from 'classnames'; -import { Input, InputNumber } from 'antd'; +import { Input, Form, InputNumber, Button } from 'antd'; import { FieldUpdaterFunc } from '../../types/config-section'; // import InfoTip from '../info-tip'; import { StatusState } from '../../utils/input-statuses'; import { FormStatusIndicator } from './FormStatusIndicator'; +import { PASSWORD_COMPLEXITY_RULES, REGEX_PASSWORD } from '../../utils/config-constants'; export const TEXTFIELD_TYPE_TEXT = 'default'; export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password @@ -17,7 +18,7 @@ export type TextFieldProps = { onSubmit?: () => void; onPressEnter?: () => void; - + onHandleSubmit?: () => void; className?: string; disabled?: boolean; label?: string; @@ -31,6 +32,7 @@ export type TextFieldProps = { useTrim?: boolean; useTrimLead?: boolean; value?: string | number; + hasComplexityRequirements?: boolean; onBlur?: FieldUpdaterFunc; onChange?: FieldUpdaterFunc; }; @@ -44,6 +46,7 @@ export const TextField: FC = ({ onBlur, onChange, onPressEnter, + onHandleSubmit, pattern, placeholder, required, @@ -52,15 +55,30 @@ export const TextField: FC = ({ type, useTrim, value, + hasComplexityRequirements, }) => { + const [hasPwdChanged, setHasPwdChanged] = useState(false); + const [showPwdButton, setShowPwdButton] = useState(false); + const [form] = Form.useForm(); const handleChange = (e: any) => { // if an extra onChange handler was sent in as a prop, let's run that too. if (onChange) { 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 }); } }; + 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. const handleBlur = (e: any) => { const val = e.target.value; @@ -74,7 +92,8 @@ export const TextField: FC = ({ onPressEnter(); } }; - + // Password Complexity rules + const passwordComplexityRules = []; // display the appropriate Ant text field let Field = Input as | typeof Input @@ -88,6 +107,9 @@ export const TextField: FC = ({ autoSize: true, }; } else if (type === TEXTFIELD_TYPE_PASSWORD) { + PASSWORD_COMPLEXITY_RULES.forEach(element => { + passwordComplexityRules.push(element); + }); Field = Input.Password; fieldProps = { visibilityToggle: true, @@ -128,25 +150,66 @@ export const TextField: FC = ({ ) : null} -
-
- + {!hasComplexityRequirements ? ( +
+
+ +
+ +

{tip}

- -

{tip}

-
+ ) : ( +
+
+
+ + + + {showPwdButton && ( +
+ +
+ )} + +

{tip}

+ +
+
+ )}
); }; @@ -168,9 +231,11 @@ TextField.defaultProps = { pattern: '', useTrim: false, useTrimLead: false, + hasComplexityRequirements: false, onSubmit: () => {}, onBlur: () => {}, onChange: () => {}, onPressEnter: () => {}, + onHandleSubmit: () => {}, }; diff --git a/web/components/admin/TextFieldWithSubmit.tsx b/web/components/admin/TextFieldWithSubmit.tsx index ce89c7030..facc2ebae 100644 --- a/web/components/admin/TextFieldWithSubmit.tsx +++ b/web/components/admin/TextFieldWithSubmit.tsx @@ -24,6 +24,7 @@ export type TextFieldWithSubmitProps = TextFieldProps & { apiPath: string; configPath?: string; initialValue?: string; + hasComplexityRequirements?: boolean; }; export const TextFieldWithSubmit: FC = ({ @@ -43,7 +44,8 @@ export const TextFieldWithSubmit: FC = ({ 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 const resetStates = () => { @@ -118,6 +120,7 @@ export const TextFieldWithSubmit: FC = ({ 'textfield-with-submit-container': true, submittable: hasChanged, }); + return (
@@ -126,6 +129,7 @@ export const TextFieldWithSubmit: FC = ({ onSubmit={null} onBlur={handleBlur} onChange={handleChange} + onHandleSubmit={handleSubmit} />
@@ -134,15 +138,17 @@ export const TextFieldWithSubmit: FC = ({
{tip}
- + {!hasComplexityRequirements && ( + + )}
diff --git a/web/components/admin/config/server/StreamKeys.tsx b/web/components/admin/config/server/StreamKeys.tsx index 24685c777..2ca850215 100644 --- a/web/components/admin/config/server/StreamKeys.tsx +++ b/web/components/admin/config/server/StreamKeys.tsx @@ -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 dynamic from 'next/dynamic'; import { ServerStatusContext } from '../../../../utils/server-status-context'; import { fetchData, UPDATE_STREAM_KEYS } from '../../../../utils/apis'; +import { PASSWORD_COMPLEXITY_RULES, REGEX_PASSWORD } from '../../../../utils/config-constants'; const { Paragraph } = Typography; -const { Item } = Form; // Lazy loaded components @@ -54,6 +54,18 @@ const generateRndKey = () => { }; 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 updatedKeys = [...streamKeys, newkey]; @@ -67,19 +79,50 @@ const AddKeyForm = ({ setShowAddKeyForm, setFieldInConfigState, streamKeys, setE 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 const defaultKey = generateRndKey(); return ( -
- - + + + 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. +

+ } + rules={PASSWORD_COMPLEXITY_RULES} + > +
- + - - diff --git a/web/utils/config-constants.tsx b/web/utils/config-constants.tsx index 2bcac6799..6835326d5 100644 --- a/web/utils/config-constants.tsx +++ b/web/utils/config-constants.tsx @@ -122,6 +122,7 @@ export const TEXTFIELD_PROPS_ADMIN_PASSWORD = { label: 'Admin Password', tip: 'Save this password somewhere safe, you will need it to login to the admin dashboard!', required: true, + hasComplexityRequirements: true, }; export const TEXTFIELD_PROPS_FFMPEG = { apiPath: API_FFMPEG, @@ -131,6 +132,7 @@ export const TEXTFIELD_PROPS_FFMPEG = { label: 'FFmpeg Path', tip: 'Absolute file path of the FFMPEG application on your server', required: true, + hasComplexityRequirements: false, }; export const TEXTFIELD_PROPS_WEB_PORT = { apiPath: API_WEB_PORT, @@ -140,6 +142,7 @@ export const TEXTFIELD_PROPS_WEB_PORT = { label: 'Owncast port', tip: 'What port is your Owncast web server listening? Default is 8080', required: true, + hasComplexityRequirements: false, }; export const TEXTFIELD_PROPS_RTMP_PORT = { apiPath: API_RTMP_PORT, @@ -149,6 +152,7 @@ export const TEXTFIELD_PROPS_RTMP_PORT = { label: 'RTMP port', tip: 'What port should accept inbound broadcasts? Default is 1935', required: true, + hasComplexityRequirements: false, }; export const TEXTFIELD_PROPS_INSTANCE_URL = { apiPath: API_INSTANCE_URL, @@ -558,3 +562,26 @@ export const BROWSER_PUSH_CONFIG_FIELDS = { 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}$/;