Skip to content

Commit 6e32da8

Browse files
feat(all): deletion button for trainers can delete admins
1 parent 4605263 commit 6e32da8

File tree

16 files changed

+476
-180
lines changed

16 files changed

+476
-180
lines changed

packages/api/schema.gql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,12 +244,14 @@ type Mutation {
244244
"""
245245
Marks User to be deleted
246246
"""
247-
markUserForDeletion(id: ID!): UserInterface
247+
adminMarkUserForDeletion(id: ID!): UserInterface
248+
trainerMarkUserForDeletion(id: ID!): UserInterface
248249

249250
"""
250251
Unmarks User from deletion
251252
"""
252-
unmarkUserForDeletion(id: ID!): UserInterface
253+
adminUnMarkUserForDeletion(id: ID!): UserInterface
254+
trainerUnMarkUserForDeletion(id: ID!): UserInterface
253255

254256
"""
255257
Updates Trainee.

packages/api/src/graphql.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ export type GqlMutation = {
164164
_devloginuser?: Maybe<GqlOAuthPayload>;
165165
/** [DEV] Sets the users type. */
166166
_devsetusertype: GqlDevSetUserPayload;
167+
/** Marks User to be deleted */
168+
adminMarkUserForDeletion?: Maybe<GqlUserInterface>;
169+
/** Unmarks User from deletion */
170+
adminUnMarkUserForDeletion?: Maybe<GqlUserInterface>;
167171
/** Claims a Trainee by the current Trainer */
168172
claimTrainee?: Maybe<GqlTrainerTraineePayload>;
169173
/** Creates Admin. */
@@ -196,16 +200,14 @@ export type GqlMutation = {
196200
linkAlexa?: Maybe<GqlUserInterface>;
197201
/** Login via microsoft */
198202
login?: Maybe<GqlOAuthPayload>;
199-
/** Marks User to be deleted */
200-
markUserForDeletion?: Maybe<GqlUserInterface>;
201203
/** Publishes all comments on a report which is identified by the id argument. */
202204
publishAllComments: GqlPublishCommentsPayload;
205+
trainerMarkUserForDeletion?: Maybe<GqlUserInterface>;
206+
trainerUnMarkUserForDeletion?: Maybe<GqlUserInterface>;
203207
/** Unclaims a Trainee by the current Trainer */
204208
unclaimTrainee?: Maybe<GqlTrainerTraineePayload>;
205209
/** Unlink Alexa account */
206210
unlinkAlexa?: Maybe<GqlUserInterface>;
207-
/** Unmarks User from deletion */
208-
unmarkUserForDeletion?: Maybe<GqlUserInterface>;
209211
/** Updates Admin. */
210212
updateAdmin?: Maybe<GqlAdmin>;
211213
/** Updates a comment on a Day which is identified by the id argument. */
@@ -241,6 +243,16 @@ export type GqlMutation_DevsetusertypeArgs = {
241243
};
242244

243245

246+
export type GqlMutationAdminMarkUserForDeletionArgs = {
247+
id: Scalars['ID']['input'];
248+
};
249+
250+
251+
export type GqlMutationAdminUnMarkUserForDeletionArgs = {
252+
id: Scalars['ID']['input'];
253+
};
254+
255+
244256
export type GqlMutationClaimTraineeArgs = {
245257
id: Scalars['ID']['input'];
246258
};
@@ -330,23 +342,23 @@ export type GqlMutationLoginArgs = {
330342
};
331343

332344

333-
export type GqlMutationMarkUserForDeletionArgs = {
345+
export type GqlMutationPublishAllCommentsArgs = {
334346
id: Scalars['ID']['input'];
347+
traineeId: Scalars['ID']['input'];
335348
};
336349

337350

338-
export type GqlMutationPublishAllCommentsArgs = {
351+
export type GqlMutationTrainerMarkUserForDeletionArgs = {
339352
id: Scalars['ID']['input'];
340-
traineeId: Scalars['ID']['input'];
341353
};
342354

343355

344-
export type GqlMutationUnclaimTraineeArgs = {
356+
export type GqlMutationTrainerUnMarkUserForDeletionArgs = {
345357
id: Scalars['ID']['input'];
346358
};
347359

348360

349-
export type GqlMutationUnmarkUserForDeletionArgs = {
361+
export type GqlMutationUnclaimTraineeArgs = {
350362
id: Scalars['ID']['input'];
351363
};
352364

@@ -909,6 +921,8 @@ export type GqlMutateEntryPayloadResolvers<ContextType = Context, ParentType ext
909921
export type GqlMutationResolvers<ContextType = Context, ParentType extends GqlResolversParentTypes['Mutation'] = GqlResolversParentTypes['Mutation']> = ResolversObject<{
910922
_devloginuser?: Resolver<Maybe<GqlResolversTypes['OAuthPayload']>, ParentType, ContextType, RequireFields<GqlMutation_DevloginuserArgs, 'id'>>;
911923
_devsetusertype?: Resolver<GqlResolversTypes['DevSetUserPayload'], ParentType, ContextType, RequireFields<GqlMutation_DevsetusertypeArgs, 'type'>>;
924+
adminMarkUserForDeletion?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationAdminMarkUserForDeletionArgs, 'id'>>;
925+
adminUnMarkUserForDeletion?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationAdminUnMarkUserForDeletionArgs, 'id'>>;
912926
claimTrainee?: Resolver<Maybe<GqlResolversTypes['TrainerTraineePayload']>, ParentType, ContextType, RequireFields<GqlMutationClaimTraineeArgs, 'id'>>;
913927
createAdmin?: Resolver<Maybe<GqlResolversTypes['Admin']>, ParentType, ContextType, RequireFields<GqlMutationCreateAdminArgs, 'input'>>;
914928
createCommentOnDay?: Resolver<GqlResolversTypes['CreateCommentPayload'], ParentType, ContextType, RequireFields<GqlMutationCreateCommentOnDayArgs, 'id' | 'text' | 'traineeId'>>;
@@ -925,11 +939,11 @@ export type GqlMutationResolvers<ContextType = Context, ParentType extends GqlRe
925939
getAvatarSignedUrl?: Resolver<Maybe<GqlResolversTypes['String']>, ParentType, ContextType, RequireFields<GqlMutationGetAvatarSignedUrlArgs, 'id'>>;
926940
linkAlexa?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationLinkAlexaArgs, 'code' | 'state'>>;
927941
login?: Resolver<Maybe<GqlResolversTypes['OAuthPayload']>, ParentType, ContextType, RequireFields<GqlMutationLoginArgs, 'email'>>;
928-
markUserForDeletion?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationMarkUserForDeletionArgs, 'id'>>;
929942
publishAllComments?: Resolver<GqlResolversTypes['PublishCommentsPayload'], ParentType, ContextType, RequireFields<GqlMutationPublishAllCommentsArgs, 'id' | 'traineeId'>>;
943+
trainerMarkUserForDeletion?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationTrainerMarkUserForDeletionArgs, 'id'>>;
944+
trainerUnMarkUserForDeletion?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationTrainerUnMarkUserForDeletionArgs, 'id'>>;
930945
unclaimTrainee?: Resolver<Maybe<GqlResolversTypes['TrainerTraineePayload']>, ParentType, ContextType, RequireFields<GqlMutationUnclaimTraineeArgs, 'id'>>;
931946
unlinkAlexa?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType>;
932-
unmarkUserForDeletion?: Resolver<Maybe<GqlResolversTypes['UserInterface']>, ParentType, ContextType, RequireFields<GqlMutationUnmarkUserForDeletionArgs, 'id'>>;
933947
updateAdmin?: Resolver<Maybe<GqlResolversTypes['Admin']>, ParentType, ContextType, RequireFields<GqlMutationUpdateAdminArgs, 'id' | 'input'>>;
934948
updateCommentOnDay?: Resolver<GqlResolversTypes['UpdateCommentPayload'], ParentType, ContextType, RequireFields<GqlMutationUpdateCommentOnDayArgs, 'commentId' | 'id' | 'text' | 'traineeId'>>;
935949
updateCommentOnEntry?: Resolver<GqlResolversTypes['UpdateCommentPayload'], ParentType, ContextType, RequireFields<GqlMutationUpdateCommentOnEntryArgs, 'commentId' | 'id' | 'text' | 'traineeId'>>;

packages/backend/src/permissions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export const permissions = shield<unknown, Context>(
8686
// Trainer mutations
8787
claimTrainee: and(authenticated, trainer),
8888
unclaimTrainee: and(authenticated, trainer),
89+
trainerMarkUserForDeletion: and(authenticated, trainer),
90+
trainerUnMarkUserForDeletion: and(authenticated, trainer),
8991

9092
// Trainer and Admin mutations
9193
createTrainee: and(authenticated, or(admin, trainer)),
@@ -94,8 +96,8 @@ export const permissions = shield<unknown, Context>(
9496
updateTrainee: and(authenticated, admin),
9597
createTrainer: and(authenticated, admin),
9698
updateTrainer: and(authenticated, admin),
97-
markUserForDeletion: and(authenticated, admin),
98-
unmarkUserForDeletion: and(authenticated, admin),
99+
adminMarkUserForDeletion: and(authenticated, admin),
100+
adminUnMarkUserForDeletion: and(authenticated, admin),
99101
},
100102
},
101103
{ allowExternalErrors: true }

packages/backend/src/resolvers/admin.resolver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const adminResolver: GqlResolvers<AdminContext> = {
6666
},
6767
},
6868
Mutation: {
69-
markUserForDeletion: async (_parent, { id }, { currentUser }) => {
69+
adminMarkUserForDeletion: async (_parent, { id }, { currentUser }) => {
7070
const user = await userById(id)
7171

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

8686
return user
8787
},
88-
unmarkUserForDeletion: async (_parent, { id }, { currentUser }) => {
88+
adminUnMarkUserForDeletion: async (_parent, { id }, { currentUser }) => {
8989
const user = await userById(id)
9090

9191
if (!user) {

packages/backend/src/resolvers/trainer.resolver.ts

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import { GqlResolvers, TrainerContext } from '@lara/api'
44

55
import { reportByYearAndWeek } from '../repositories/report.repo'
66
import { allTrainees, traineeById, traineesByTrainerId } from '../repositories/trainee.repo'
7-
import { updateUser } from '../repositories/user.repo'
7+
import { updateUser, userById } from '../repositories/user.repo'
88
import { alexaSkillLinked } from '../services/alexa.service'
99
import { createT } from '../i18n'
10+
import { addMonths } from 'date-fns'
11+
import { isTrainee } from '../permissions'
12+
import { sendDeletionMail } from '../services/email.service'
1013

1114
export const trainerResolver: GqlResolvers<TrainerContext> = {
1215
Trainer: {
@@ -31,7 +34,50 @@ export const trainerResolver: GqlResolvers<TrainerContext> = {
3134

3235
const report = await reportByYearAndWeek(year, week, trainee.id)
3336

34-
if (!report || report.traineeId !== trainee.id) {
37+
const reportCleaned = report
38+
? {
39+
...report,
40+
comments: report?.comments.map((com) => {
41+
if (com.published == null) com.published = true
42+
if (com.published === false) {
43+
return com
44+
} else {
45+
return {
46+
...com,
47+
published: true,
48+
}
49+
}
50+
}),
51+
days: report?.days.map((day) => ({
52+
...day,
53+
entries: day.entries.map((entry) => ({
54+
...entry,
55+
comments: entry.comments.map((com) => {
56+
if (com.published === false) {
57+
return com
58+
} else {
59+
return {
60+
...com,
61+
published: true,
62+
}
63+
}
64+
}),
65+
})),
66+
comments: day.comments.map((com) => {
67+
if (com.published === false) {
68+
return com
69+
} else {
70+
return {
71+
...com,
72+
published: true,
73+
}
74+
}
75+
}),
76+
})),
77+
}
78+
: undefined
79+
80+
if (!reportCleaned || reportCleaned.traineeId !== trainee.id) {
3581
throw new GraphQLError(t('errors.missingReport'))
3682
}
3783

@@ -77,5 +123,55 @@ export const trainerResolver: GqlResolvers<TrainerContext> = {
77123
trainer: currentUser,
78124
}
79125
},
126+
127+
trainerMarkUserForDeletion: async (_parent, { id }, { currentUser }) => {
128+
const user = await userById(id)
129+
const t = createT(currentUser.language)
130+
131+
if (!user) {
132+
throw new GraphQLError(t('errors.missingUser'))
133+
}
134+
135+
if (user.id === currentUser.id) {
136+
throw new GraphQLError(t('errors.cantDeleteYourself'))
137+
}
138+
139+
if (!isTrainee(user)) {
140+
throw new GraphQLError(t('errors.insufficientPermissions'))
141+
}
142+
143+
// // Prüfe ob der Trainer der Trainer des Trainees ist
144+
// if (user.trainerId !== currentUser.id) {
145+
// throw new GraphQLError(t('errors.cantDeleteOtherTrainersTrainee'))
146+
// }
147+
148+
user.deleteAt = addMonths(new Date(), 3).toISOString()
149+
150+
await updateUser(user, { updateKeys: ['deleteAt'] })
151+
152+
await sendDeletionMail(user)
153+
154+
return user
155+
},
156+
157+
trainerUnMarkUserForDeletion: async (_parent, { id }, { currentUser }) => {
158+
const user = await userById(id)
159+
const t = createT(currentUser.language)
160+
161+
if (!user) {
162+
throw new GraphQLError(t('errors.missingUser'))
163+
}
164+
165+
if (!isTrainee(user)) {
166+
throw new GraphQLError(t('errors.insufficientPermissions'))
167+
}
168+
169+
// // Prüfe ob der Trainer der Trainer des Trainees ist
170+
// if (user.trainerId !== currentUser.id) {
171+
// throw new GraphQLError(t('errors.cantUnmarkOtherTrainersTrainee'))
172+
// }
173+
174+
return updateUser(user, { removeKeys: ['deleteAt'] })
175+
},
80176
},
81177
}

packages/components/src/edit-user.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { JSX } from 'react'
1+
import React, { JSX, ReactNode } from 'react'
22
import styled from 'styled-components'
33

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

88
type EditUserLayoutProps = {
99
backButton: JSX.Element
10-
content: JSX.Element
10+
content?: ReactNode
1111
actions: JSX.Element
1212
}
1313

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from 'react'
2+
import { H1, Paragraph, Spacings, Flex, Box } from '@lara/components'
3+
import { PrimaryButton, SecondaryButton } from './button'
4+
import Modal from './modal'
5+
import { useToastContext } from '../hooks/use-toast-context'
6+
import { GraphQLError } from 'graphql'
7+
import strings from '../locales/localization'
8+
9+
interface DeletionModalProps {
10+
show: boolean
11+
onClose: () => void
12+
onConfirm: () => Promise<unknown>
13+
userName: string
14+
loading?: boolean
15+
}
16+
17+
export const DeletionModal: React.FC<DeletionModalProps> = ({
18+
show,
19+
onClose,
20+
onConfirm,
21+
userName,
22+
loading = false,
23+
}) => {
24+
const { addToast } = useToastContext()
25+
26+
const handleConfirm = () => {
27+
onConfirm()
28+
.then(() => {
29+
onClose()
30+
addToast({
31+
icon: 'PersonAttention',
32+
title: strings.userDelete.title,
33+
text: strings.userDelete.description,
34+
type: 'error',
35+
})
36+
})
37+
.catch((exception: GraphQLError) => {
38+
addToast({
39+
title: strings.errors.error,
40+
text: exception.message,
41+
type: 'error',
42+
})
43+
})
44+
}
45+
46+
return (
47+
<Modal show={show} customClose handleClose={onClose}>
48+
<H1 noMargin>{strings.formatString(strings.deleteTrainer.title, userName)}</H1>
49+
<Paragraph margin={`${Spacings.l}`} color="darkFont">
50+
{strings.deleteTrainer.description}
51+
</Paragraph>
52+
<Flex justifyContent="flex-end">
53+
<Box pr={'1'}>
54+
<SecondaryButton ghost onClick={onClose} disabled={loading}>
55+
{strings.cancel}
56+
</SecondaryButton>
57+
</Box>
58+
<Box pl={'1'}>
59+
<PrimaryButton danger onClick={handleConfirm}>
60+
{strings.deactivate}
61+
</PrimaryButton>
62+
</Box>
63+
</Flex>
64+
</Modal>
65+
)
66+
}

0 commit comments

Comments
 (0)