Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

569 Reset Password #582

Merged
merged 14 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 34 additions & 20 deletions administration/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import Navigation from './components/Navigation'
import { ApolloClient, ApolloProvider, createHttpLink, InMemoryCache } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { HashRouter, Route, Routes } from 'react-router-dom'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import GenerationController from './components/generation/GenerationController'
import styled from 'styled-components'
import RegionProvider from './RegionProvider'
Expand All @@ -15,6 +15,8 @@ import HomeController from './components/home/HomeController'
import MetaTagsManager from './components/MetaTagsManager'
import { AppToasterProvider } from './components/AppToaster'
import UserSettingsController from './components/user-settings/UserSettingsController'
import ResetPasswordController from './components/auth/ResetPasswordController'
import ForgotPasswordController from './components/auth/ForgotPasswordController'

if (!process.env.REACT_APP_API_BASE_URL) {
throw new Error('REACT_APP_API_BASE_URL is not set!')
Expand Down Expand Up @@ -53,25 +55,37 @@ const App = () => (
<AuthContext.Consumer>
{({ data: authData, signIn, signOut }) => (
<ApolloProvider client={createClient(authData?.token)}>
{authData !== null && authData.expiry > new Date() ? (
<KeepAliveToken authData={authData} onSignIn={signIn} onSignOut={signOut}>
<RegionProvider>
<HashRouter>
<Navigation onSignOut={signOut} />
<Main>
<Routes>
<Route path={'/'} element={<HomeController />} />
<Route path={'/applications'} element={<ApplicationsController token={authData.token} />} />
<Route path={'/eak-generation'} element={<GenerationController />} />
<Route path={'/user-settings'} element={<UserSettingsController />} />
</Routes>
</Main>
</HashRouter>
</RegionProvider>
</KeepAliveToken>
) : (
<Login onSignIn={signIn} />
)}
<BrowserRouter>
<Routes>
<Route path={'/forgot-password'} element={<ForgotPasswordController />} />
<Route path={'/reset-password/:passwordResetKey'} element={<ResetPasswordController />} />
<Route
path={'*'}
element={
authData === null || authData.expiry <= new Date() ? (
<Login onSignIn={signIn} />
) : (
<KeepAliveToken authData={authData} onSignIn={signIn} onSignOut={signOut}>
<RegionProvider>
<Navigation onSignOut={signOut} />
<Main>
<Routes>
<Route
path={'/applications'}
element={<ApplicationsController token={authData.token} />}
/>
<Route path={'/eak-generation'} element={<GenerationController />} />
<Route path={'/user-settings'} element={<UserSettingsController />} />
<Route path={'*'} element={<HomeController />} />
</Routes>
</Main>
</RegionProvider>
</KeepAliveToken>
)
}
/>
</Routes>
</BrowserRouter>
</ApolloProvider>
)}
</AuthContext.Consumer>
Expand Down
11 changes: 11 additions & 0 deletions administration/src/components/StandaloneCenter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import styled from 'styled-components'

const StandaloneCenter = styled('div')`
display: flex;
flex-grow: 1;
flex-direction: column;
justify-content: center;
align-items: center;
`

export default StandaloneCenter
85 changes: 85 additions & 0 deletions administration/src/components/auth/ForgotPasswordController.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Button, Card, Classes, FormGroup, H2, H3, H4, InputGroup } from '@blueprintjs/core'
import { useContext, useState } from 'react'
import { Link } from 'react-router-dom'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import StandaloneCenter from '../StandaloneCenter'
import { useAppToaster } from '../AppToaster'
import { useSendResetMailMutation } from '../../generated/graphql'

const ForgotPasswordController = () => {
const config = useContext(ProjectConfigContext)
const appToaster = useAppToaster()
const [finished, setFinished] = useState(false)
const [email, setEmail] = useState('')

const [sendResetMail, { loading }] = useSendResetMailMutation({
onCompleted: () => setFinished(true),
onError: () =>
appToaster?.show({
intent: 'danger',
message: 'Etwas ist schief gelaufen. Prüfen Sie Ihre Eingaben.',
}),
})

const submit = () =>
sendResetMail({
variables: {
project: config.projectId,
email,
},
})

return (
<StandaloneCenter>
<Card style={{ width: '100%', maxWidth: '500px' }}>
<H2>{config.name}</H2>
<H3>Verwaltung</H3>
<H4>Passwort vergessen</H4>
{finished ? (
<>
<p>
Wir haben eine E-Mail an {email} gesendet. Darin finden Sie einen Link, mit dem Sie Ihr Passwort
zurücksetzen können.
</p>
<p>Bitte prüfen Sie Ihren Spam-Ordner.</p>
<p>
<Link to={'/'}>Zum Login</Link>
</p>
</>
) : (
<>
<p>Falls Sie Ihr Passwort vergessen haben, können Sie es hier zurücksetzen.</p>
<form
onSubmit={e => {
e.preventDefault()
submit()
}}>
<FormGroup label='E-Mail Adresse'>
<InputGroup
value={email}
onChange={e => setEmail(e.target.value)}
type='email'
placeholder='erika.musterfrau@example.org'
/>
</FormGroup>
<div
className={Classes.DIALOG_FOOTER_ACTIONS}
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Link to='/'>Zurück zum Login</Link>
<Button
type='submit'
intent='primary'
text='Passwort zurücksetzen'
loading={loading}
disabled={email === ''}
/>
</div>
</form>
</>
)}
</Card>
</StandaloneCenter>
)
}

export default ForgotPasswordController
16 changes: 4 additions & 12 deletions administration/src/components/auth/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import React, { useContext } from 'react'
import styled from 'styled-components'
import LoginForm from './LoginForm'
import { useAppToaster } from '../AppToaster'
import { Card, H2, H3, H4 } from '@blueprintjs/core'
import { SignInMutation, SignInPayload, useSignInMutation } from '../../generated/graphql'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'

const Center = styled('div')`
display: flex;
flex-grow: 1;
flex-direction: column;
justify-content: center;
align-items: center;
`
import StandaloneCenter from '../StandaloneCenter'

interface State {
email: string
Expand All @@ -36,8 +28,8 @@ const Login = (props: { onSignIn: (payload: SignInPayload) => void }) => {
})

return (
<Center>
<Card>
<StandaloneCenter>
<Card style={{ width: '100%', maxWidth: '500px' }}>
<H2>{config.name}</H2>
<H3>Verwaltung</H3>
<H4>Login</H4>
Expand All @@ -50,7 +42,7 @@ const Login = (props: { onSignIn: (payload: SignInPayload) => void }) => {
loading={mutationState.loading}
/>
</Card>
</Center>
</StandaloneCenter>
)
}

Expand Down
6 changes: 5 additions & 1 deletion administration/src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { ChangeEvent } from 'react'
import { Button, Classes, FormGroup, InputGroup } from '@blueprintjs/core'
import PasswordInput from '../PasswordInput'
import { Link } from 'react-router-dom'

interface Props {
loading?: boolean
Expand Down Expand Up @@ -37,7 +38,10 @@ const LoginForm = (props: Props) => {
label=''
/>
</FormGroup>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<div
className={Classes.DIALOG_FOOTER_ACTIONS}
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Link to='/forgot-password'>Passwort vergessen</Link>
<Button text='Anmelden' type='submit' intent='primary' loading={!!props.loading} />
</div>
</form>
Expand Down
89 changes: 89 additions & 0 deletions administration/src/components/auth/ResetPasswordController.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Button, Callout, Card, Classes, FormGroup, H2, H3, H4, InputGroup } from '@blueprintjs/core'
import { useContext, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import StandaloneCenter from '../StandaloneCenter'
import { useAppToaster } from '../AppToaster'
import { useResetPasswordMutation } from '../../generated/graphql'
import PasswordInput from '../PasswordInput'
import validateNewPasswordInput from './validateNewPasswordInput'

const ResetPasswordController = () => {
const config = useContext(ProjectConfigContext)
const appToaster = useAppToaster()
const [email, setEmail] = useState('')
const [newPassword, setNewPassword] = useState('')
const [repeatNewPassword, setRepeatNewPassword] = useState('')
const { passwordResetKey } = useParams()
const navigate = useNavigate()

const [resetPassword, { loading }] = useResetPasswordMutation({
onCompleted: () => {
appToaster?.show({ intent: 'success', message: 'Ihr Passwort wurde erfolgreich zurückgesetzt.' })
navigate('/')
},
onError: () =>
appToaster?.show({
intent: 'danger',
message: 'Etwas ist schief gelaufen. Prüfen Sie Ihre Eingaben.',
}),
})

const submit = () =>
resetPassword({
variables: {
project: config.projectId,
email,
newPassword,
passwordResetKey: passwordResetKey!,
},
})

const warnMessage = validateNewPasswordInput(newPassword, repeatNewPassword)
const isDirty = newPassword !== '' || repeatNewPassword !== ''

return (
<StandaloneCenter>
<Card style={{ width: '100%', maxWidth: '500px' }}>
<H2>{config.name}</H2>
<H3>Verwaltung</H3>
<H4>Passwort zurücksetzen.</H4>
<p>
Hier können Sie ein neues Passwort wählen. Ein gültiges Passwort ist mindestens zwölf Zeichen lang, enthält
mindestens einen Klein- und einen Großbuchstaben sowie mindestens ein Sonderzeichen.
</p>
<form
onSubmit={e => {
e.preventDefault()
submit()
}}>
<FormGroup label='E-Mail Adresse'>
<InputGroup
value={email}
onChange={e => setEmail(e.target.value)}
type='email'
placeholder='erika.musterfrau@example.org'
/>
</FormGroup>
<PasswordInput label='Neues Passwort' setValue={setNewPassword} value={newPassword} />
<PasswordInput label='Neues Passwort bestätigen' setValue={setRepeatNewPassword} value={repeatNewPassword} />
{warnMessage === null || !isDirty ? null : <Callout intent='danger'>{warnMessage}</Callout>}
<div
className={Classes.DIALOG_FOOTER_ACTIONS}
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingTop: '10px' }}>
<Link to='/'>Zurück zum Login</Link>
<Button
type='submit'
intent='primary'
text='Passwort zurücksetzen'
loading={loading}
disabled={warnMessage !== null}
/>
</div>
</form>
</Card>
</StandaloneCenter>
)
}

export default ResetPasswordController
37 changes: 37 additions & 0 deletions administration/src/components/auth/validateNewPasswordInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const isUpperCase = (value: string) => {
return value === value.toUpperCase() && value !== value.toLowerCase()
}

const isLowerCase = (value: string) => {
return value === value.toLowerCase() && value !== value.toUpperCase()
}

const isNumeric = (value: string) => {
return !isNaN(parseInt(value))
}

const isSpecialChar = (value: string) => {
return !isUpperCase(value) && !isLowerCase(value) && !isNumeric(value)
}

const getNumChars = (value: string, predicate: (char: string) => boolean): number => {
return value.split('').filter(predicate).length
}
const minPasswordLength = 12

const validateNewPasswordInput = (newPassword: string, repeatNewPassword: string) => {
if (newPassword.length < minPasswordLength) {
return `Ihr Passwort muss mindestens ${minPasswordLength} Zeichen lang sein (aktuell ${newPassword.length}).`
} else if (getNumChars(newPassword, isLowerCase) < 1) {
return 'Ihr Passwort muss mindestens einen Kleinbuchstaben enthalten.'
} else if (getNumChars(newPassword, isUpperCase) < 1) {
return 'Ihr Passwort muss mindestens einen Großbuchstaben enthalten.'
} else if (getNumChars(newPassword, isSpecialChar) < 1) {
return 'Ihr Passwort muss mindestens ein Sonderzeichen enthalten.'
} else if (newPassword !== repeatNewPassword) {
return 'Die Passwörter stimmen nicht überein.'
}
return null
}

export default validateNewPasswordInput
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const ChangePasswordForm = (props: {
<Card style={{ maxWidth: '500px' }}>
<H2>Passwort ändern</H2>
<p>
Ein gültiges Passwort besteht aus mindestens zwölf Zeichen, einem Klein- und einem Großbuchstaben sowie einem
Sonderzeichen.
Ein gültiges Passwort ist mindestens zwölf Zeichen lang, enthält mindestens einen Klein- und einen
Großbuchstaben sowie mindestens ein Sonderzeichen.
</p>
<form
onSubmit={event => {
Expand Down
Loading