Skip to content
Open
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
6 changes: 4 additions & 2 deletions packages/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,14 @@ type Mutation {
"""
Marks User to be deleted
"""
markUserForDeletion(id: ID!): UserInterface
adminMarkUserForDeletion(id: ID!): UserInterface
trainerMarkUserForDeletion(id: ID!): UserInterface

"""
Unmarks User from deletion
"""
unmarkUserForDeletion(id: ID!): UserInterface
adminUnMarkUserForDeletion(id: ID!): UserInterface
trainerUnMarkUserForDeletion(id: ID!): UserInterface

"""
Updates Trainee.
Expand Down
36 changes: 25 additions & 11 deletions packages/api/src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ export type GqlMutation = {
_devloginuser?: Maybe<GqlOAuthPayload>;
/** [DEV] Sets the users type. */
_devsetusertype: GqlDevSetUserPayload;
/** Marks User to be deleted */
adminMarkUserForDeletion?: Maybe<GqlUserInterface>;
/** Unmarks User from deletion */
adminUnMarkUserForDeletion?: Maybe<GqlUserInterface>;
/** Claims a Trainee by the current Trainer */
claimTrainee?: Maybe<GqlTrainerTraineePayload>;
/** Creates Admin. */
Expand Down Expand Up @@ -196,16 +200,14 @@ export type GqlMutation = {
linkAlexa?: Maybe<GqlUserInterface>;
/** Login via microsoft */
login?: Maybe<GqlOAuthPayload>;
/** Marks User to be deleted */
markUserForDeletion?: Maybe<GqlUserInterface>;
/** Publishes all comments on a report which is identified by the id argument. */
publishAllComments: GqlPublishCommentsPayload;
trainerMarkUserForDeletion?: Maybe<GqlUserInterface>;
trainerUnMarkUserForDeletion?: Maybe<GqlUserInterface>;
/** Unclaims a Trainee by the current Trainer */
unclaimTrainee?: Maybe<GqlTrainerTraineePayload>;
/** Unlink Alexa account */
unlinkAlexa?: Maybe<GqlUserInterface>;
/** Unmarks User from deletion */
unmarkUserForDeletion?: Maybe<GqlUserInterface>;
/** Updates Admin. */
updateAdmin?: Maybe<GqlAdmin>;
/** Updates a comment on a Day which is identified by the id argument. */
Expand Down Expand Up @@ -241,6 +243,16 @@ export type GqlMutation_DevsetusertypeArgs = {
};


export type GqlMutationAdminMarkUserForDeletionArgs = {
id: Scalars['ID']['input'];
};


export type GqlMutationAdminUnMarkUserForDeletionArgs = {
id: Scalars['ID']['input'];
};


export type GqlMutationClaimTraineeArgs = {
id: Scalars['ID']['input'];
};
Expand Down Expand Up @@ -330,23 +342,23 @@ export type GqlMutationLoginArgs = {
};


export type GqlMutationMarkUserForDeletionArgs = {
export type GqlMutationPublishAllCommentsArgs = {
id: Scalars['ID']['input'];
traineeId: Scalars['ID']['input'];
};


export type GqlMutationPublishAllCommentsArgs = {
export type GqlMutationTrainerMarkUserForDeletionArgs = {
id: Scalars['ID']['input'];
traineeId: Scalars['ID']['input'];
};


export type GqlMutationUnclaimTraineeArgs = {
export type GqlMutationTrainerUnMarkUserForDeletionArgs = {
id: Scalars['ID']['input'];
};


export type GqlMutationUnmarkUserForDeletionArgs = {
export type GqlMutationUnclaimTraineeArgs = {
id: Scalars['ID']['input'];
};

Expand Down Expand Up @@ -909,6 +921,8 @@ export type GqlMutateEntryPayloadResolvers<ContextType = Context, ParentType ext
export type GqlMutationResolvers<ContextType = Context, ParentType extends GqlResolversParentTypes['Mutation'] = GqlResolversParentTypes['Mutation']> = ResolversObject<{
_devloginuser?: Resolver<Maybe<GqlResolversTypes['OAuthPayload']>, ParentType, ContextType, RequireFields<GqlMutation_DevloginuserArgs, 'id'>>;
_devsetusertype?: Resolver<GqlResolversTypes['DevSetUserPayload'], ParentType, ContextType, RequireFields<GqlMutation_DevsetusertypeArgs, 'type'>>;
adminMarkUserForDeletion?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationAdminMarkUserForDeletionArgs, 'id'>>;
adminUnMarkUserForDeletion?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationAdminUnMarkUserForDeletionArgs, 'id'>>;
claimTrainee?: Resolver<Maybe<GqlResolversTypes['TrainerTraineePayload']>, ParentType, ContextType, RequireFields<GqlMutationClaimTraineeArgs, 'id'>>;
createAdmin?: Resolver<Maybe<GqlResolversTypes['Admin']>, ParentType, ContextType, RequireFields<GqlMutationCreateAdminArgs, 'input'>>;
createCommentOnDay?: Resolver<GqlResolversTypes['CreateCommentPayload'], ParentType, ContextType, RequireFields<GqlMutationCreateCommentOnDayArgs, 'id' | 'text' | 'traineeId'>>;
Expand All @@ -925,11 +939,11 @@ export type GqlMutationResolvers<ContextType = Context, ParentType extends GqlRe
getAvatarSignedUrl?: Resolver<Maybe<GqlResolversTypes['String']>, ParentType, ContextType, RequireFields<GqlMutationGetAvatarSignedUrlArgs, 'id'>>;
linkAlexa?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationLinkAlexaArgs, 'code' | 'state'>>;
login?: Resolver<Maybe<GqlResolversTypes['OAuthPayload']>, ParentType, ContextType, RequireFields<GqlMutationLoginArgs, 'email'>>;
markUserForDeletion?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationMarkUserForDeletionArgs, 'id'>>;
publishAllComments?: Resolver<GqlResolversTypes['PublishCommentsPayload'], ParentType, ContextType, RequireFields<GqlMutationPublishAllCommentsArgs, 'id' | 'traineeId'>>;
trainerMarkUserForDeletion?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationTrainerMarkUserForDeletionArgs, 'id'>>;
trainerUnMarkUserForDeletion?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationTrainerUnMarkUserForDeletionArgs, 'id'>>;
unclaimTrainee?: Resolver<Maybe<GqlResolversTypes['TrainerTraineePayload']>, ParentType, ContextType, RequireFields<GqlMutationUnclaimTraineeArgs, 'id'>>;
unlinkAlexa?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType>;
unmarkUserForDeletion?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationUnmarkUserForDeletionArgs, 'id'>>;
updateAdmin?: Resolver<Maybe<GqlResolversTypes['Admin']>, ParentType, ContextType, RequireFields<GqlMutationUpdateAdminArgs, 'id' | 'input'>>;
updateCommentOnDay?: Resolver<GqlResolversTypes['UpdateCommentPayload'], ParentType, ContextType, RequireFields<GqlMutationUpdateCommentOnDayArgs, 'commentId' | 'id' | 'text' | 'traineeId'>>;
updateCommentOnEntry?: Resolver<GqlResolversTypes['UpdateCommentPayload'], ParentType, ContextType, RequireFields<GqlMutationUpdateCommentOnEntryArgs, 'commentId' | 'id' | 'text' | 'traineeId'>>;
Expand Down
6 changes: 4 additions & 2 deletions packages/backend/src/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export const permissions = shield<unknown, Context>(
// Trainer mutations
claimTrainee: and(authenticated, trainer),
unclaimTrainee: and(authenticated, trainer),
trainerMarkUserForDeletion: and(authenticated, trainer),
trainerUnMarkUserForDeletion: and(authenticated, trainer),

// Trainer and Admin mutations
createTrainee: and(authenticated, or(admin, trainer)),
Expand All @@ -94,8 +96,8 @@ export const permissions = shield<unknown, Context>(
updateTrainee: and(authenticated, admin),
createTrainer: and(authenticated, admin),
updateTrainer: and(authenticated, admin),
markUserForDeletion: and(authenticated, admin),
unmarkUserForDeletion: and(authenticated, admin),
adminMarkUserForDeletion: and(authenticated, admin),
adminUnMarkUserForDeletion: and(authenticated, admin),
},
},
{ allowExternalErrors: true }
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/resolvers/admin.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const adminResolver: GqlResolvers<AdminContext> = {
},
},
Mutation: {
markUserForDeletion: async (_parent, { id }, { currentUser }) => {
adminMarkUserForDeletion: async (_parent, { id }, { currentUser }) => {
const user = await userById(id)

if (!user) {
Expand All @@ -85,7 +85,7 @@ export const adminResolver: GqlResolvers<AdminContext> = {

return user
},
unmarkUserForDeletion: async (_parent, { id }, { currentUser }) => {
adminUnMarkUserForDeletion: async (_parent, { id }, { currentUser }) => {
const user = await userById(id)

if (!user) {
Expand Down
57 changes: 55 additions & 2 deletions packages/backend/src/resolvers/trainer.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { GqlResolvers, TrainerContext } from '@lara/api'

import { reportByYearAndWeek } from '../repositories/report.repo'
import { allTrainees, traineeById, traineesByTrainerId } from '../repositories/trainee.repo'
import { updateUser } from '../repositories/user.repo'
import { updateUser, userById } from '../repositories/user.repo'
import { alexaSkillLinked } from '../services/alexa.service'
import { createT } from '../i18n'
import { addMonths } from 'date-fns'
import { isTrainee } from '../permissions'
import { sendDeletionMail } from '../services/email.service'

export const trainerResolver: GqlResolvers<TrainerContext> = {
Trainer: {
Expand Down Expand Up @@ -78,7 +81,7 @@ export const trainerResolver: GqlResolvers<TrainerContext> = {
throw new GraphQLError(t('errors.missingReport'))
}

return reportCleaned
return report
},
},
Mutation: {
Expand Down Expand Up @@ -120,5 +123,55 @@ export const trainerResolver: GqlResolvers<TrainerContext> = {
trainer: currentUser,
}
},

trainerMarkUserForDeletion: async (_parent, { id }, { currentUser }) => {
const user = await userById(id)
const t = createT(currentUser.language)

if (!user) {
throw new GraphQLError(t('errors.missingUser'))
}

if (user.id === currentUser.id) {
throw new GraphQLError(t('errors.cantDeleteYourself'))
}

if (!isTrainee(user)) {
throw new GraphQLError(t('errors.insufficientPermissions'))
}

// // Prüfe ob der Trainer der Trainer des Trainees ist
// if (user.trainerId !== currentUser.id) {
// throw new GraphQLError(t('errors.cantDeleteOtherTrainersTrainee'))
// }

user.deleteAt = addMonths(new Date(), 3).toISOString()

await updateUser(user, { updateKeys: ['deleteAt'] })

await sendDeletionMail(user)

return user
},

trainerUnMarkUserForDeletion: async (_parent, { id }, { currentUser }) => {
const user = await userById(id)
const t = createT(currentUser.language)

if (!user) {
throw new GraphQLError(t('errors.missingUser'))
}

if (!isTrainee(user)) {
throw new GraphQLError(t('errors.insufficientPermissions'))
}

// // Prüfe ob der Trainer der Trainer des Trainees ist
// if (user.trainerId !== currentUser.id) {
// throw new GraphQLError(t('errors.cantUnmarkOtherTrainersTrainee'))
// }

return updateUser(user, { removeKeys: ['deleteAt'] })
},
},
}
4 changes: 2 additions & 2 deletions packages/components/src/edit-user.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { JSX } from 'react'
import React, { JSX, ReactNode } from 'react'
import styled from 'styled-components'

import { Container } from './container'
Expand All @@ -7,7 +7,7 @@ import { UnstyledLink } from './unstyled-link'

type EditUserLayoutProps = {
backButton: JSX.Element
content: JSX.Element
content?: ReactNode
actions: JSX.Element
}

Expand Down
66 changes: 66 additions & 0 deletions packages/frontend/src/components/DeletionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react'
import { H1, Paragraph, Spacings, Flex, Box } from '@lara/components'
import { PrimaryButton, SecondaryButton } from './button'
import Modal from './modal'
import { useToastContext } from '../hooks/use-toast-context'
import { GraphQLError } from 'graphql'
import strings from '../locales/localization'

interface DeletionModalProps {
show: boolean
onClose: () => void
onConfirm: () => Promise<unknown>
userName: string
loading?: boolean
}

export const DeletionModal: React.FC<DeletionModalProps> = ({
show,
onClose,
onConfirm,
userName,
loading = false,
}) => {
const { addToast } = useToastContext()

const handleConfirm = () => {
onConfirm()
.then(() => {
onClose()
addToast({
icon: 'PersonAttention',
title: strings.userDelete.title,
text: strings.userDelete.description,
type: 'error',
})
})
.catch((exception: GraphQLError) => {
addToast({
title: strings.errors.error,
text: exception.message,
type: 'error',
})
})
}

return (
<Modal show={show} customClose handleClose={onClose}>
<H1 noMargin>{strings.formatString(strings.deleteTrainer.title, userName)}</H1>
<Paragraph margin={`${Spacings.l}`} color="darkFont">
{strings.deleteTrainer.description}
</Paragraph>
<Flex justifyContent="flex-end">
<Box pr={'1'}>
<SecondaryButton ghost onClick={onClose} disabled={loading}>
{strings.cancel}
</SecondaryButton>
</Box>
<Box pl={'1'}>
<PrimaryButton danger onClick={handleConfirm}>
{strings.deactivate}
</PrimaryButton>
</Box>
</Flex>
</Modal>
)
}
75 changes: 75 additions & 0 deletions packages/frontend/src/components/renderDeleteAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { SecondaryButton } from './button'
import strings from '../locales/localization'
import React from 'react'
import {
useAdminMarkUserForDeleteMutation,
useAdminUnmarkUserForDeleteMutation,
useTrainerMarkUserForDeleteMutation,
useTrainerUnmarkUserForDeleteMutation,
} from '../graphql'
interface UseDeleteActionsProps {
//id = routesId
id: string | undefined
currentUserId: string | undefined
}

interface MutationVariables {
variables: {
id: string
}
}

export const useDeleteActions = ({ currentUserId, id }: UseDeleteActionsProps) => {
const vars = { variables: { id: id ?? '' } }

const [markForDeleteAdmin, { loading: deleteLoadingAdmin }] = useTrainerMarkUserForDeleteMutation()
const [unmarkDeleteAdmin, { loading: undeleteLoadingAdmin }] = useTrainerUnmarkUserForDeleteMutation()
const [markForDeleteTrainer, { loading: deleteLoadingTrainer }] = useAdminMarkUserForDeleteMutation()
const [unmarkDeleteTrainer, { loading: undeleteLoadingTrainer }] = useAdminUnmarkUserForDeleteMutation()

const isTrainer = currentUserId === '456'
const isAdmin = currentUserId === '789'

const deleteActionLoading = isTrainer
? deleteLoadingTrainer || undeleteLoadingTrainer
: deleteLoadingAdmin || undeleteLoadingAdmin

const [showDeletionModal, setShowDeletionModal] = React.useState(false)

const toggleDeletionModal = () => {
setShowDeletionModal(!showDeletionModal)
}

const selectQueryForType = (vars: MutationVariables) => {
if (isTrainer) {
unmarkDeleteTrainer(vars)
} else if (isAdmin) {
unmarkDeleteAdmin(vars)
}
}

const renderDeleteAction = (deleteAt?: string) => {
if (currentUserId === id) return <></>
if (deleteAt) {
return (
<SecondaryButton disabled={deleteActionLoading} onClick={() => selectQueryForType(vars)}>
{strings.unmarkDelete}
</SecondaryButton>
)
}

return (
<SecondaryButton danger disabled={deleteActionLoading} onClick={() => toggleDeletionModal()}>
{strings.markDelete}
</SecondaryButton>
)
}

return {
renderDeleteAction,
toggleDeletionModal,
showDeletionModal,
markForDeleteTrainer,
markForDeleteAdmin,
}
}
Loading
Loading