Loading backend @ e3879174 Compare ba2a4a50 to e3879174 Original line number Diff line number Diff line Subproject commit ba2a4a5086a7c021c8181d3f1e43e0d70a16bdbd Subproject commit e3879174c07ab4a24cd3b09fdd7260a85e90d668 codegen/package.json +1 −1 Original line number Diff line number Diff line { "name": "@inject/codegen", "version": "3.28.1", "version": "3.30.0", "description": "GraphQL API Codegen Setup for the Inject Backend", "main": "index.js", "license": "MIT", Loading frontend/package.json +1 −1 Original line number Diff line number Diff line { "name": "@inject/frontend", "version": "3.28.1", "version": "3.30.0", "description": "Main wrapper for rendering INJECT Frontend", "main": "index.js", "license": "MIT", Loading frontend/src/clientsettings/components/ChangePassword.tsx +117 −140 Original line number Diff line number Diff line import { Button, FormGroup, InputGroup, Label, Section, SectionCard, } from '@blueprintjs/core' import { css } from '@emotion/css' import type { PropsOf } from '@emotion/react' import { ChangePassword, useTypedMutation } from '@inject/graphql' import { CenteredSpinner } from '@inject/shared' import type { ChangeEventHandler, FC, FormEventHandler } from 'react' import { useRef } from 'react' import { css, cx } from '@emotion/css' import { useHost } from '@inject/graphql' import { authenticatedFetch, changePasswordUrl, ErrorMessage, notifyNoncommmit, setSessionId, useWiggle, wiggleClass, } from '@inject/shared' import type { FormEvent } from 'react' import { useState } from 'react' const passwordInputProps: PropsOf<typeof InputGroup> = { required: true, type: 'password', placeholder: 'New_password123', inputClassName: css` &:invalid:not(:placeholder-shown), &:user-invalid:not(:placeholder-shown) { border-color: red !important; border: 1px solid; } `, autoComplete: 'new-password', } const marginTop = css` margin-top: 0.25rem; ` const ChangePasswordSetting: FC<{ onSubmit: FormEventHandler<HTMLFormElement> }> = ({ onSubmit }) => { const newPasswordRef = useRef<HTMLInputElement>(null) const repeatPasswordRef = useRef<HTMLInputElement>(null) const oldPasswordRef = useRef<HTMLInputElement>(null) export const ChangePasswordForm = () => { const [open, setOpen] = useState(false) const handleNewPasswordChange: ChangeEventHandler<HTMLInputElement> = () => { const passwordInput = newPasswordRef.current if (!passwordInput) return const errors = [] passwordInput.reportValidity() if (passwordInput.validity.tooShort) { errors.push('have at least 8 characters') } if (!/[a-z]/.test(passwordInput.value)) { errors.push('contain at least one lowercase letter') } if (!/[A-Z]/.test(passwordInput.value)) { errors.push('contain at least one uppercase letter') } if (!/[0-9]/.test(passwordInput.value)) { errors.push('contain at least one number') } if (!/[!@#$%^&*_=+-]/.test(passwordInput.value)) { errors.push('at least one of these symbols !@#$%^&*_=+-') } if (errors.length === 0) { passwordInput.setCustomValidity('') } else { passwordInput.setCustomValidity(`Password must ${errors.join(', ')}`) } } const [oldPassword, setOldPassword] = useState('') const [newPassword, setNewPassword] = useState('') const handleRepeatPasswordChange: ChangeEventHandler< HTMLInputElement > = event => { const repeatPasswordInput = repeatPasswordRef.current const newPassword = newPasswordRef.current?.value const [loading, setLoading] = useState(false) const [errors, setErrors] = useState<{ oldPassword: string newPassword: string submit: string }>({ oldPassword: '', newPassword: '', submit: '', }) if (!repeatPasswordInput) return const { wiggling, beginWiggling } = useWiggle() if (event.target.value !== newPassword) { repeatPasswordInput.setCustomValidity('Passwords do not match.') } else { repeatPasswordInput.setCustomValidity('') } } const host = useHost() return ( <SectionCard padded> <form onSubmit={onSubmit}> <input id='username' autoComplete='username' hidden /> <Label htmlFor='oldpassword'> Current password: <InputGroup id='oldpassword' name='oldpassword' inputRef={oldPasswordRef} autoComplete='current-password' type='password' required /> </Label> <Label htmlFor='newpassword1'> New password: <InputGroup id='newpassword1' name='newpassword1' inputRef={newPasswordRef} onChange={handleNewPasswordChange} {...passwordInputProps} /> </Label> <Label htmlFor='newpassword2'> New password (again): <InputGroup id='newpassword2' name='newpassword2' inputRef={repeatPasswordRef} onChange={handleRepeatPasswordChange} {...passwordInputProps} /> </Label> <Button type='submit' style={{ float: 'right' }}> Change password </Button> </form> <p> New password must be at least 8 characters long and contain uppercase and lowercase letters, number and special symbols (!@#$%^&*_=+-) </p> </SectionCard> ) const handleSubmit = (event: FormEvent<HTMLFormElement>) => { event.preventDefault() setErrors({ oldPassword: '', newPassword: '', submit: '' }) if (!oldPassword || !newPassword) { setErrors({ oldPassword: !oldPassword ? 'Old password is required.' : '', newPassword: !newPassword ? 'New password is required.' : '', submit: '', }) beginWiggling() return } export const ChangePasswordForm = () => { const [{ data, fetching, error }, mutate] = useTypedMutation(ChangePassword) const handler: FormEventHandler<HTMLFormElement> = event => { event.preventDefault() const oldPassword = event.currentTarget.querySelector( '#oldpassword' ) as HTMLInputElement const newPassword = event.currentTarget.querySelector( '#newpassword1' ) as HTMLInputElement const oldPasswordValue = oldPassword.value const newPasswordValue = newPassword.value if (oldPasswordValue === newPasswordValue) { newPassword.setCustomValidity('The new password must be different') setLoading(true) authenticatedFetch(changePasswordUrl(host || ''), { method: 'POST', body: JSON.stringify({ old_password: oldPassword, new_password: newPassword, }), headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, }) .then(result => result.json()) .then((result: { status: string; detail: string; sessionid: string }) => { if (result.status === 'error') { setErrors(prev => ({ ...prev, submit: result.detail })) beginWiggling() return } mutate({ newPassword: newPasswordValue, newPasswordRepeat: newPasswordValue, oldPassword: oldPasswordValue, setOldPassword('') setNewPassword('') setSessionId(result.sessionid) setOpen(false) notifyNoncommmit('Password changed successfully.', { intent: 'success', }) }) .finally(() => setLoading(false)) } return ( Loading @@ -151,18 +92,54 @@ export const ChangePasswordForm = () => { title='Change Password' icon='key' collapsible collapseProps={{ defaultIsOpen: false }} collapseProps={{ isOpen: open, onToggle: () => setOpen(prev => !prev) }} > {error && ( <SectionCard padded> <p>Error: {error.message}</p> </SectionCard> )} {fetching && <CenteredSpinner />} {(!data || !data.passwordChange?.passwordChanged) && ( <ChangePasswordSetting onSubmit={handler} /> <form onSubmit={handleSubmit}> <FormGroup label='Old Password' intent={errors.oldPassword ? 'danger' : 'none'} helperText={errors.oldPassword ? errors.oldPassword : undefined} > <InputGroup intent={errors.oldPassword ? 'danger' : 'none'} placeholder='Old Password' value={oldPassword} onChange={e => setOldPassword(e.target.value)} type='password' /> </FormGroup> <FormGroup label='New Password' intent={errors.newPassword ? 'danger' : 'none'} helperText={errors.newPassword ? errors.newPassword : undefined} > <InputGroup intent={errors.oldPassword ? 'danger' : 'none'} placeholder='New Password' value={newPassword} onChange={e => setNewPassword(e.target.value)} type='password' /> </FormGroup> {errors.submit && ( <ErrorMessage minimal>{errors.submit}</ErrorMessage> )} {data?.passwordChange?.passwordChanged && <p>Password changed</p>} <Button disabled={wiggling} type='submit' intent='primary' className={cx({ [wiggleClass]: wiggling, [marginTop]: !!errors.submit, })} loading={loading} > Submit </Button> </form> </SectionCard> </Section> ) } frontend/src/components/OnDemandStartAlert/index.tsx +1 −0 Original line number Diff line number Diff line Loading @@ -84,6 +84,7 @@ export const OnDemandStartAlert: FC<OnDemandStartAlertProps> = ({ loading={fetching} cancelButtonText='Cancel' onCancel={() => // TODO: if only one team, navigate to exercise selection nav({ to: TraineeExerciseRoute.to, params: { Loading Loading
backend @ e3879174 Compare ba2a4a50 to e3879174 Original line number Diff line number Diff line Subproject commit ba2a4a5086a7c021c8181d3f1e43e0d70a16bdbd Subproject commit e3879174c07ab4a24cd3b09fdd7260a85e90d668
codegen/package.json +1 −1 Original line number Diff line number Diff line { "name": "@inject/codegen", "version": "3.28.1", "version": "3.30.0", "description": "GraphQL API Codegen Setup for the Inject Backend", "main": "index.js", "license": "MIT", Loading
frontend/package.json +1 −1 Original line number Diff line number Diff line { "name": "@inject/frontend", "version": "3.28.1", "version": "3.30.0", "description": "Main wrapper for rendering INJECT Frontend", "main": "index.js", "license": "MIT", Loading
frontend/src/clientsettings/components/ChangePassword.tsx +117 −140 Original line number Diff line number Diff line import { Button, FormGroup, InputGroup, Label, Section, SectionCard, } from '@blueprintjs/core' import { css } from '@emotion/css' import type { PropsOf } from '@emotion/react' import { ChangePassword, useTypedMutation } from '@inject/graphql' import { CenteredSpinner } from '@inject/shared' import type { ChangeEventHandler, FC, FormEventHandler } from 'react' import { useRef } from 'react' import { css, cx } from '@emotion/css' import { useHost } from '@inject/graphql' import { authenticatedFetch, changePasswordUrl, ErrorMessage, notifyNoncommmit, setSessionId, useWiggle, wiggleClass, } from '@inject/shared' import type { FormEvent } from 'react' import { useState } from 'react' const passwordInputProps: PropsOf<typeof InputGroup> = { required: true, type: 'password', placeholder: 'New_password123', inputClassName: css` &:invalid:not(:placeholder-shown), &:user-invalid:not(:placeholder-shown) { border-color: red !important; border: 1px solid; } `, autoComplete: 'new-password', } const marginTop = css` margin-top: 0.25rem; ` const ChangePasswordSetting: FC<{ onSubmit: FormEventHandler<HTMLFormElement> }> = ({ onSubmit }) => { const newPasswordRef = useRef<HTMLInputElement>(null) const repeatPasswordRef = useRef<HTMLInputElement>(null) const oldPasswordRef = useRef<HTMLInputElement>(null) export const ChangePasswordForm = () => { const [open, setOpen] = useState(false) const handleNewPasswordChange: ChangeEventHandler<HTMLInputElement> = () => { const passwordInput = newPasswordRef.current if (!passwordInput) return const errors = [] passwordInput.reportValidity() if (passwordInput.validity.tooShort) { errors.push('have at least 8 characters') } if (!/[a-z]/.test(passwordInput.value)) { errors.push('contain at least one lowercase letter') } if (!/[A-Z]/.test(passwordInput.value)) { errors.push('contain at least one uppercase letter') } if (!/[0-9]/.test(passwordInput.value)) { errors.push('contain at least one number') } if (!/[!@#$%^&*_=+-]/.test(passwordInput.value)) { errors.push('at least one of these symbols !@#$%^&*_=+-') } if (errors.length === 0) { passwordInput.setCustomValidity('') } else { passwordInput.setCustomValidity(`Password must ${errors.join(', ')}`) } } const [oldPassword, setOldPassword] = useState('') const [newPassword, setNewPassword] = useState('') const handleRepeatPasswordChange: ChangeEventHandler< HTMLInputElement > = event => { const repeatPasswordInput = repeatPasswordRef.current const newPassword = newPasswordRef.current?.value const [loading, setLoading] = useState(false) const [errors, setErrors] = useState<{ oldPassword: string newPassword: string submit: string }>({ oldPassword: '', newPassword: '', submit: '', }) if (!repeatPasswordInput) return const { wiggling, beginWiggling } = useWiggle() if (event.target.value !== newPassword) { repeatPasswordInput.setCustomValidity('Passwords do not match.') } else { repeatPasswordInput.setCustomValidity('') } } const host = useHost() return ( <SectionCard padded> <form onSubmit={onSubmit}> <input id='username' autoComplete='username' hidden /> <Label htmlFor='oldpassword'> Current password: <InputGroup id='oldpassword' name='oldpassword' inputRef={oldPasswordRef} autoComplete='current-password' type='password' required /> </Label> <Label htmlFor='newpassword1'> New password: <InputGroup id='newpassword1' name='newpassword1' inputRef={newPasswordRef} onChange={handleNewPasswordChange} {...passwordInputProps} /> </Label> <Label htmlFor='newpassword2'> New password (again): <InputGroup id='newpassword2' name='newpassword2' inputRef={repeatPasswordRef} onChange={handleRepeatPasswordChange} {...passwordInputProps} /> </Label> <Button type='submit' style={{ float: 'right' }}> Change password </Button> </form> <p> New password must be at least 8 characters long and contain uppercase and lowercase letters, number and special symbols (!@#$%^&*_=+-) </p> </SectionCard> ) const handleSubmit = (event: FormEvent<HTMLFormElement>) => { event.preventDefault() setErrors({ oldPassword: '', newPassword: '', submit: '' }) if (!oldPassword || !newPassword) { setErrors({ oldPassword: !oldPassword ? 'Old password is required.' : '', newPassword: !newPassword ? 'New password is required.' : '', submit: '', }) beginWiggling() return } export const ChangePasswordForm = () => { const [{ data, fetching, error }, mutate] = useTypedMutation(ChangePassword) const handler: FormEventHandler<HTMLFormElement> = event => { event.preventDefault() const oldPassword = event.currentTarget.querySelector( '#oldpassword' ) as HTMLInputElement const newPassword = event.currentTarget.querySelector( '#newpassword1' ) as HTMLInputElement const oldPasswordValue = oldPassword.value const newPasswordValue = newPassword.value if (oldPasswordValue === newPasswordValue) { newPassword.setCustomValidity('The new password must be different') setLoading(true) authenticatedFetch(changePasswordUrl(host || ''), { method: 'POST', body: JSON.stringify({ old_password: oldPassword, new_password: newPassword, }), headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, }) .then(result => result.json()) .then((result: { status: string; detail: string; sessionid: string }) => { if (result.status === 'error') { setErrors(prev => ({ ...prev, submit: result.detail })) beginWiggling() return } mutate({ newPassword: newPasswordValue, newPasswordRepeat: newPasswordValue, oldPassword: oldPasswordValue, setOldPassword('') setNewPassword('') setSessionId(result.sessionid) setOpen(false) notifyNoncommmit('Password changed successfully.', { intent: 'success', }) }) .finally(() => setLoading(false)) } return ( Loading @@ -151,18 +92,54 @@ export const ChangePasswordForm = () => { title='Change Password' icon='key' collapsible collapseProps={{ defaultIsOpen: false }} collapseProps={{ isOpen: open, onToggle: () => setOpen(prev => !prev) }} > {error && ( <SectionCard padded> <p>Error: {error.message}</p> </SectionCard> )} {fetching && <CenteredSpinner />} {(!data || !data.passwordChange?.passwordChanged) && ( <ChangePasswordSetting onSubmit={handler} /> <form onSubmit={handleSubmit}> <FormGroup label='Old Password' intent={errors.oldPassword ? 'danger' : 'none'} helperText={errors.oldPassword ? errors.oldPassword : undefined} > <InputGroup intent={errors.oldPassword ? 'danger' : 'none'} placeholder='Old Password' value={oldPassword} onChange={e => setOldPassword(e.target.value)} type='password' /> </FormGroup> <FormGroup label='New Password' intent={errors.newPassword ? 'danger' : 'none'} helperText={errors.newPassword ? errors.newPassword : undefined} > <InputGroup intent={errors.oldPassword ? 'danger' : 'none'} placeholder='New Password' value={newPassword} onChange={e => setNewPassword(e.target.value)} type='password' /> </FormGroup> {errors.submit && ( <ErrorMessage minimal>{errors.submit}</ErrorMessage> )} {data?.passwordChange?.passwordChanged && <p>Password changed</p>} <Button disabled={wiggling} type='submit' intent='primary' className={cx({ [wiggleClass]: wiggling, [marginTop]: !!errors.submit, })} loading={loading} > Submit </Button> </form> </SectionCard> </Section> ) }
frontend/src/components/OnDemandStartAlert/index.tsx +1 −0 Original line number Diff line number Diff line Loading @@ -84,6 +84,7 @@ export const OnDemandStartAlert: FC<OnDemandStartAlertProps> = ({ loading={fetching} cancelButtonText='Cancel' onCancel={() => // TODO: if only one team, navigate to exercise selection nav({ to: TraineeExerciseRoute.to, params: { Loading