diff --git a/administration/src/App.tsx b/administration/src/App.tsx
index cc3a3f829..bc5a84a80 100644
--- a/administration/src/App.tsx
+++ b/administration/src/App.tsx
@@ -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'
@@ -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!')
@@ -53,25 +55,37 @@ const App = () => (
{({ data: authData, signIn, signOut }) => (
- {authData !== null && authData.expiry > new Date() ? (
-
-
-
-
-
-
- } />
- } />
- } />
- } />
-
-
-
-
-
- ) : (
-
- )}
+
+
+ } />
+ } />
+
+ ) : (
+
+
+
+
+
+ }
+ />
+ } />
+ } />
+ } />
+
+
+
+
+ )
+ }
+ />
+
+
)}
diff --git a/administration/src/components/StandaloneCenter.tsx b/administration/src/components/StandaloneCenter.tsx
new file mode 100644
index 000000000..57836b046
--- /dev/null
+++ b/administration/src/components/StandaloneCenter.tsx
@@ -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
diff --git a/administration/src/components/auth/ForgotPasswordController.tsx b/administration/src/components/auth/ForgotPasswordController.tsx
new file mode 100644
index 000000000..fba5d62ca
--- /dev/null
+++ b/administration/src/components/auth/ForgotPasswordController.tsx
@@ -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 (
+
+
+ {config.name}
+ Verwaltung
+ Passwort vergessen
+ {finished ? (
+ <>
+
+ Wir haben eine E-Mail an {email} gesendet. Darin finden Sie einen Link, mit dem Sie Ihr Passwort
+ zurücksetzen können.
+
+ Bitte prüfen Sie Ihren Spam-Ordner.
+
+ Zum Login
+
+ >
+ ) : (
+ <>
+ Falls Sie Ihr Passwort vergessen haben, können Sie es hier zurücksetzen.
+
+ >
+ )}
+
+
+ )
+}
+
+export default ForgotPasswordController
diff --git a/administration/src/components/auth/Login.tsx b/administration/src/components/auth/Login.tsx
index f08887409..a04808fdc 100644
--- a/administration/src/components/auth/Login.tsx
+++ b/administration/src/components/auth/Login.tsx
@@ -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
@@ -36,8 +28,8 @@ const Login = (props: { onSignIn: (payload: SignInPayload) => void }) => {
})
return (
-
-
+
+
{config.name}
Verwaltung
Login
@@ -50,7 +42,7 @@ const Login = (props: { onSignIn: (payload: SignInPayload) => void }) => {
loading={mutationState.loading}
/>
-
+
)
}
diff --git a/administration/src/components/auth/LoginForm.tsx b/administration/src/components/auth/LoginForm.tsx
index 370cf680e..0f3de8f99 100644
--- a/administration/src/components/auth/LoginForm.tsx
+++ b/administration/src/components/auth/LoginForm.tsx
@@ -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
@@ -37,7 +38,10 @@ const LoginForm = (props: Props) => {
label=''
/>
-
+
+ Passwort vergessen
diff --git a/administration/src/components/auth/ResetPasswordController.tsx b/administration/src/components/auth/ResetPasswordController.tsx
new file mode 100644
index 000000000..9d1f55a36
--- /dev/null
+++ b/administration/src/components/auth/ResetPasswordController.tsx
@@ -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 (
+
+
+ {config.name}
+ Verwaltung
+ Passwort zurücksetzen.
+
+ 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.
+
+
+
+
+ )
+}
+
+export default ResetPasswordController
diff --git a/administration/src/components/auth/validateNewPasswordInput.ts b/administration/src/components/auth/validateNewPasswordInput.ts
new file mode 100644
index 000000000..757cd248d
--- /dev/null
+++ b/administration/src/components/auth/validateNewPasswordInput.ts
@@ -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
diff --git a/administration/src/components/user-settings/ChangePasswordForm.tsx b/administration/src/components/user-settings/ChangePasswordForm.tsx
index cdbf4901e..31fcac4ca 100644
--- a/administration/src/components/user-settings/ChangePasswordForm.tsx
+++ b/administration/src/components/user-settings/ChangePasswordForm.tsx
@@ -18,8 +18,8 @@ const ChangePasswordForm = (props: {
Passwort ändern
- 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.