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

877: Link checker #878

Merged
merged 16 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
12 changes: 10 additions & 2 deletions administration/src/ErrorHandler.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import { Button, Card, Typography } from '@mui/material'
import React, { ReactElement } from 'react'
import React, { ReactElement, ReactNode } from 'react'
import { styled } from '@mui/system'

type ErrorHandlerProps = {
title?: string
description?: string | ReactNode
refetch: () => void
}

const ErrorContainer = styled(Card)`
padding: 20px;
`

const ErrorHandler = ({ refetch, title = 'Ein Fehler ist aufgetreten.' }: ErrorHandlerProps): ReactElement => {
const ErrorHandler = ({
refetch,
title = 'Ein Fehler ist aufgetreten.',
description,
}: ErrorHandlerProps): ReactElement => {
return (
<ErrorContainer>
<Typography variant='h6'>{title}</Typography>
<Typography my='8px' variant='body1' component='div'>
{description}
</Typography>
<Button color='primary' variant='contained' onClick={() => refetch()}>
Erneut versuchen
</Button>
Expand Down
2 changes: 1 addition & 1 deletion administration/src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const Router = () => {
const isLoggedIn = authData !== null && authData.expiry > new Date()
const routes: (RouteObject | null)[] = [
{ path: '/forgot-password', element: <ForgotPasswordController /> },
{ path: '/reset-password/:passwordResetKey', element: <ResetPasswordController /> },
{ path: '/reset-password/', element: <ResetPasswordController /> },
{ path: '/data-privacy-policy', element: <DataPrivacyPolicy /> },
...(projectConfig.applicationFeatureEnabled
? [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { format } from 'date-fns'
import { SnackbarProvider, useSnackbar } from 'notistack'
import { Alert, AlertTitle, Button, Card, CircularProgress, Divider, styled, Typography } from '@mui/material'
import { Close, Check } from '@mui/icons-material'
import getMessageFromApolloError from '../components/errors/getMessageFromApolloError'

const ApplicationViewCard = styled(Card)`
max-width: 800px;
Expand Down Expand Up @@ -71,7 +72,10 @@ const ApplicationVerification = ({ applicationVerificationAccessKey }: Applicati
})

if (loading) return <CircularProgress style={{ margin: 'auto' }} />
else if (error || !data) return <ErrorHandler refetch={refetch} />
else if (error) {
const { title, description } = getMessageFromApolloError(error)
return <ErrorHandler title={title} description={description} refetch={refetch} />
} else if (!data) return <ErrorHandler refetch={refetch} />
if (data.verification.rejectedDate || data.verification.verifiedDate)
return <CenteredMessage>Sie haben diesen Antrag bereits bearbeitet.</CenteredMessage>
if (data.application.withdrawalDate)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useGetApplicationByApplicantQuery } from '../../generated/graphql'
import ApplicationApplicantView from './ApplicationApplicantView'
import { Alert, CircularProgress } from '@mui/material'
import { SnackbarProvider, useSnackbar } from 'notistack'
import getMessageFromApolloError from '../errors/getMessageFromApolloError'

const CenteredMessage = styled(Alert)`
margin: auto;
Expand All @@ -24,7 +25,10 @@ const ApplicationApplicantController = (props: { providedKey: string }) => {
})

if (loading) return <CircularProgress style={{ margin: 'auto' }} />
else if (error || !data) return <ErrorHandler refetch={refetch} />
else if (error) {
const { title, description } = getMessageFromApolloError(error)
return <ErrorHandler title={title} description={description} refetch={refetch} />
} else if (!data) return <ErrorHandler refetch={refetch} />
if (data.application.withdrawalDate) return <CenteredMessage>Ihr Antrag wurde bereits zurückgezogen.</CenteredMessage>
if (withdrawed) return <CenteredMessage>Ihr Antrag wurde zurückgezogen.</CenteredMessage>
else {
Expand Down
33 changes: 21 additions & 12 deletions administration/src/components/auth/ResetPasswordController.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
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 { Link, useNavigate, useSearchParams } from 'react-router-dom'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import StandaloneCenter from '../StandaloneCenter'
import { useAppToaster } from '../AppToaster'
import { useResetPasswordMutation } from '../../generated/graphql'
import { useCheckPasswordResetLinkQuery, useResetPasswordMutation } from '../../generated/graphql'
import PasswordInput from '../PasswordInput'
import validateNewPasswordInput from './validateNewPasswordInput'
import ErrorHandler from '../../ErrorHandler'
import getMessageFromApolloError from '../errors/getMessageFromApolloError'

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

const { error, refetch } = useCheckPasswordResetLinkQuery({
variables: {
project: config.projectId,
resetKey: queryParams.get('token')!,
},
})

const [resetPassword, { loading }] = useResetPasswordMutation({
onCompleted: () => {
appToaster?.show({ intent: 'success', message: 'Ihr Passwort wurde erfolgreich zurückgesetzt.' })
Expand All @@ -33,15 +42,20 @@ const ResetPasswordController = () => {
resetPassword({
variables: {
project: config.projectId,
email,
email: adminEmail,
newPassword,
passwordResetKey: passwordResetKey!,
passwordResetKey: queryParams.get('token')!,
},
})

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

if (error) {
const { title, description } = getMessageFromApolloError(error)
return <ErrorHandler title={title} refetch={refetch} description={description} />
}

return (
<StandaloneCenter>
<Card style={{ width: '100%', maxWidth: '500px' }}>
Expand All @@ -58,12 +72,7 @@ const ResetPasswordController = () => {
submit()
}}>
<FormGroup label='Email-Adresse'>
<InputGroup
value={email}
onChange={e => setEmail(e.target.value)}
type='email'
placeholder='erika.musterfrau@example.org'
/>
<InputGroup value={adminEmail} disabled type='email' />
</FormGroup>
<PasswordInput label='Neues Passwort' setValue={setNewPassword} value={newPassword} />
<PasswordInput label='Neues Passwort bestätigen' setValue={setRepeatNewPassword} value={repeatNewPassword} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ApolloError } from '@apollo/client'
import { ReactNode } from 'react'
import InvalidPasswordResetLink from './templates/InvalidPasswordResetLink'
import PasswordResetKeyExpired from './templates/PasswordResetKeyExpired'
import InvalidLink from './templates/InvalidLink'

type GraphQLErrorMessage = {
title: string
description?: string | ReactNode
}
const getMessageFromApolloError = (error: ApolloError): GraphQLErrorMessage => {
const defaultMessage = 'Etwas ist schief gelaufen.'
const codesEqual = error.graphQLErrors.every(
(value, index, array) => value.extensions.code === array[0].extensions.code
)
if (error.graphQLErrors.length !== 1 && !codesEqual) {
return { title: defaultMessage }
}

const graphQLError = error.graphQLErrors[0]
if ('code' in graphQLError.extensions) {
switch (graphQLError.extensions['code']) {
case 'EMAIL_ALREADY_EXISTS':
return { title: 'Die Email-Adresse wird bereits verwendet.' }
case 'PASSWORD_RESET_KEY_EXPIRED':
return {
title: 'Die Gültigkeit ihres Links ist abgelaufen',
description: <PasswordResetKeyExpired />,
}
case 'INVALID_LINK':
return {
title: 'Ihr Link ist ungültig',
description: <InvalidLink />,
}
case 'INVALID_PASSWORD_RESET_LINK':
return {
title: 'Ihr Link ist ungültig',
description: <InvalidPasswordResetLink />,
}
}
}
return { title: defaultMessage }
}

export default getMessageFromApolloError
12 changes: 12 additions & 0 deletions administration/src/components/errors/templates/InvalidLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const InvalidLink = () => (
<>
<span>Der von Ihnen geöffnete Link ist ungültig. Möglicher Grund:</span>
<ul>
<li>
Der Link wurde fehlerhaft in den Browser übertragen. Versuchen Sie, den Link manuell aus der Email in die
Adresszeile Ihres Browsers zu kopieren.
</li>
</ul>
</>
)
export default InvalidLink
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const InvalidPasswordResetLink = () => (
<>
<span>Der von Ihnen geöffnete Link ist ungültig. Mögliche Gründe:</span>
<ul>
<li>
Der Link wurde fehlerhaft in den Browser übertragen. Versuchen Sie, den Link manuell aus der Email in die
Adresszeile Ihres Browsers zu kopieren.
</li>
<li>Sie haben Ihr Passwort mithilfe des Links bereits zurückgesetzt.</li>
<li>Sie haben einen weiteren Link angefordert. Es ist immer nur der aktuellste Link gültig.</li>
</ul>
</>
)
export default InvalidPasswordResetLink
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const PasswordResetKeyExpired = () => (
<>
Unter folgendem Link können Sie Ihr Passwort erneut zurücksetzen und erhalten einen neuen Link.
<a href={window.location.origin + '/forgot-password'} target='_blank' rel='noreferrer'>
{window.location.origin + '/forgot-password'}
</a>
</>
)
export default PasswordResetKeyExpired
18 changes: 0 additions & 18 deletions administration/src/components/getMessageFromApolloError.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions administration/src/components/users/CreateUserDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import RoleHelpButton from './RoleHelpButton'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import RegionSelector from '../RegionSelector'
import RoleSelector from './RoleSelector'
import getMessageFromApolloError from '../getMessageFromApolloError'
import getMessageFromApolloError from '../errors/getMessageFromApolloError'

const RoleFormGroupLabel = styled.span`
& span {
Expand Down Expand Up @@ -39,7 +39,7 @@ const CreateUserDialog = ({
const [createAdministrator, { loading }] = useCreateAdministratorMutation({
onError: error => {
console.error(error)
appToaster?.show({ intent: 'danger', message: 'Fehler: ' + getMessageFromApolloError(error) })
appToaster?.show({ intent: 'danger', message: 'Fehler: ' + getMessageFromApolloError(error).title })
},
onCompleted: () => {
appToaster?.show({ intent: 'success', message: 'Benutzer erfolgreich erstellt.' })
Expand Down
4 changes: 2 additions & 2 deletions administration/src/components/users/DeleteUserDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { Administrator, useDeleteAdministratorMutation } from '../../generated/g
import { useAppToaster } from '../AppToaster'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import { AuthContext } from '../../AuthProvider'
import getMessageFromApolloError from '../getMessageFromApolloError'
import { WhoAmIContext } from '../../WhoAmIProvider'
import getMessageFromApolloError from '../errors/getMessageFromApolloError'

const DeleteUserDialog = ({
selectedUser,
Expand All @@ -24,7 +24,7 @@ const DeleteUserDialog = ({
const [deleteAdministrator, { loading }] = useDeleteAdministratorMutation({
onError: error => {
console.error(error)
appToaster?.show({ intent: 'danger', message: 'Fehler: ' + getMessageFromApolloError(error) })
appToaster?.show({ intent: 'danger', message: 'Fehler: ' + getMessageFromApolloError(error).title })
},
onCompleted: () => {
appToaster?.show({ intent: 'success', message: 'Benutzer erfolgreich gelöscht.' })
Expand Down
4 changes: 2 additions & 2 deletions administration/src/components/users/EditUserDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import RoleHelpButton from './RoleHelpButton'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import RegionSelector from '../RegionSelector'
import RoleSelector from './RoleSelector'
import getMessageFromApolloError from '../getMessageFromApolloError'
import { WhoAmIContext } from '../../WhoAmIProvider'
import getMessageFromApolloError from '../errors/getMessageFromApolloError'

const RoleFormGroupLabel = styled.span`
& span {
Expand Down Expand Up @@ -48,7 +48,7 @@ const EditUserDialog = ({
const [editAdministrator, { loading }] = useEditAdministratorMutation({
onError: error => {
console.error(error)
appToaster?.show({ intent: 'danger', message: 'Fehler: ' + getMessageFromApolloError(error) })
appToaster?.show({ intent: 'danger', message: 'Fehler: ' + getMessageFromApolloError(error).title })
},
onCompleted: () => {
appToaster?.show({ intent: 'success', message: 'Benutzer erfolgreich bearbeitet.' })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
query checkPasswordResetLink($project: String!, $resetKey: String!) {
valid: checkPasswordResetLink(project: $project, resetKey: $resetKey)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import app.ehrenamtskarte.backend.application.webservice.schema.view.JsonField
import app.ehrenamtskarte.backend.application.webservice.utils.ExtractedApplicationVerification
import app.ehrenamtskarte.backend.common.database.sortByKeys
import app.ehrenamtskarte.backend.common.webservice.GraphQLContext
import app.ehrenamtskarte.backend.common.webservice.InvalidLinkException
import app.ehrenamtskarte.backend.projects.database.ProjectEntity
import app.ehrenamtskarte.backend.projects.database.Projects
import app.ehrenamtskarte.backend.regions.database.Regions
Expand Down Expand Up @@ -98,28 +99,28 @@ object ApplicationRepository {

fun getApplicationByApplicant(accessKey: String): ApplicationView {
return transaction {
ApplicationEntity.find { Applications.accessKey eq accessKey }
.single()
.let { ApplicationView.fromDbEntity(it) }
val application = ApplicationEntity.find { Applications.accessKey eq accessKey }
.singleOrNull()
application?.let { ApplicationView.fromDbEntity(it) } ?: throw InvalidLinkException()
}
}

fun getApplicationByApplicationVerificationAccessKey(applicationVerificationAccessKey: String): ApplicationView {
return transaction {
(Applications innerJoin ApplicationVerifications)
val application = (Applications innerJoin ApplicationVerifications)
.slice(Applications.columns)
.select { ApplicationVerifications.accessKey eq applicationVerificationAccessKey }
.single()
.let {
ApplicationView.fromDbEntity(ApplicationEntity.wrapRow(it))
}
.singleOrNull()
application?.let {
ApplicationView.fromDbEntity(ApplicationEntity.wrapRow(it))
} ?: throw InvalidLinkException()
}
}

fun getApplicationVerification(accessKey: String): ApplicationVerificationEntity {
return transaction {
ApplicationVerificationEntity.find { ApplicationVerifications.accessKey eq accessKey }
.single()
.singleOrNull() ?: throw InvalidLinkException()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import app.ehrenamtskarte.backend.auth.webservice.dataloader.administratorLoader
import app.ehrenamtskarte.backend.auth.webservice.schema.ChangePasswordMutationService
import app.ehrenamtskarte.backend.auth.webservice.schema.ManageUsersMutationService
import app.ehrenamtskarte.backend.auth.webservice.schema.ResetPasswordMutationService
import app.ehrenamtskarte.backend.auth.webservice.schema.ResetPasswordQueryService
import app.ehrenamtskarte.backend.auth.webservice.schema.SignInMutationService
import app.ehrenamtskarte.backend.auth.webservice.schema.ViewAdministratorsQueryService
import app.ehrenamtskarte.backend.common.webservice.GraphQLParams
Expand All @@ -22,5 +23,6 @@ val authGraphQlParams = GraphQLParams(
),
queries = listOf(
TopLevelObject(ViewAdministratorsQueryService()),
TopLevelObject(ResetPasswordQueryService())
),
)
Loading