Commit 7c8c713e authored by Marek Veselý's avatar Marek Veselý
Browse files

Resolve "Chore: New version of Backend - v3.30.3"

parent f936d4a3
Loading
Loading
Loading
Loading
Compare ba2a4a50 to e3879174
Original line number Diff line number Diff line
Subproject commit ba2a4a5086a7c021c8181d3f1e43e0d70a16bdbd
Subproject commit e3879174c07ab4a24cd3b09fdd7260a85e90d668
+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",
+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",
+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 (
@@ -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>
  )
}
+1 −0
Original line number Diff line number Diff line
@@ -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