diff --git a/src/dto/deleted.model.ts b/src/dto/deleted.model.ts new file mode 100644 index 0000000..7ff97e7 --- /dev/null +++ b/src/dto/deleted.model.ts @@ -0,0 +1,11 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType('Deleted') +export class DeletedModel { + @Field() + id: string + + constructor(id: string) { + this.id = id + } +} diff --git a/src/dto/form/form.statistic.model.ts b/src/dto/form/form.statistic.model.ts new file mode 100644 index 0000000..6bb5967 --- /dev/null +++ b/src/dto/form/form.statistic.model.ts @@ -0,0 +1,6 @@ +import { ObjectType } from '@nestjs/graphql'; + +@ObjectType('FormStatistic') +export class FormStatisticModel { + +} diff --git a/src/dto/user/own.user.model.ts b/src/dto/profile/profile.model.ts similarity index 68% rename from src/dto/user/own.user.model.ts rename to src/dto/profile/profile.model.ts index 74c459d..4f0d4fb 100644 --- a/src/dto/user/own.user.model.ts +++ b/src/dto/profile/profile.model.ts @@ -1,9 +1,9 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { UserDocument } from '../../schema/user.schema'; -import { UserModel } from './user.model'; +import { UserModel } from '../user/user.model'; -@ObjectType('OwnUser') -export class OwnUserModel extends UserModel { +@ObjectType('Profile') +export class ProfileModel extends UserModel { @Field(() => [String]) readonly roles: string[] diff --git a/src/dto/profile/profile.update.input.ts b/src/dto/profile/profile.update.input.ts new file mode 100644 index 0000000..fa72507 --- /dev/null +++ b/src/dto/profile/profile.update.input.ts @@ -0,0 +1,25 @@ +import { Field, ID, InputType } from '@nestjs/graphql'; + +@InputType() +export class ProfileUpdateInput { + @Field(() => ID) + readonly id: string + + @Field({ nullable: true }) + readonly username: string + + @Field({ nullable: true }) + readonly email: string + + @Field({ nullable: true }) + readonly firstName: string + + @Field({ nullable: true }) + readonly lastName: string + + @Field({ nullable: true }) + readonly password: string + + @Field({ nullable: true }) + readonly language: string +} diff --git a/src/dto/submission/submission.statistic.model.ts b/src/dto/submission/submission.statistic.model.ts new file mode 100644 index 0000000..8ec9208 --- /dev/null +++ b/src/dto/submission/submission.statistic.model.ts @@ -0,0 +1,6 @@ +import { ObjectType } from '@nestjs/graphql'; + +@ObjectType('SubmissionStatistic') +export class SubmissionStatisticModel { + +} diff --git a/src/dto/user/pager.user.model.ts b/src/dto/user/pager.user.model.ts new file mode 100644 index 0000000..3654546 --- /dev/null +++ b/src/dto/user/pager.user.model.ts @@ -0,0 +1,25 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { GraphQLInt } from 'graphql'; +import { UserModel } from './user.model'; + +@ObjectType('PagerUser') +export class PagerUserModel { + @Field(() => [UserModel]) + entries: UserModel[] + + @Field(() => GraphQLInt) + total: number + + @Field(() => GraphQLInt) + limit: number + + @Field(() => GraphQLInt) + start: number + + constructor(entries: UserModel[], total: number, limit: number, start: number) { + this.entries = entries + this.total = total + this.limit = limit + this.start = start + } +} diff --git a/src/dto/user/user.model.ts b/src/dto/user/user.model.ts index 7e16fb8..987f9ab 100644 --- a/src/dto/user/user.model.ts +++ b/src/dto/user/user.model.ts @@ -6,6 +6,9 @@ export class UserModel { @Field(() => ID) readonly id: string + @Field() + readonly verifiedEmail: boolean + @Field() readonly username: string @@ -15,12 +18,18 @@ export class UserModel { @Field() readonly language: string - @Field() + @Field({ nullable: true }) readonly firstName?: string - @Field() + @Field({ nullable: true }) readonly lastName?: string + @Field() + readonly created: Date + + @Field({ nullable: true }) + readonly lastModified: Date + constructor(user: UserDocument) { this.id = user.id this.username = user.username @@ -29,5 +38,10 @@ export class UserModel { this.language = user.language this.firstName = user.firstName this.lastName = user.lastName + + this.verifiedEmail = !user.token + + this.created = user.created + this.lastModified = user.lastModified } } diff --git a/src/dto/user/user.statistic.model.ts b/src/dto/user/user.statistic.model.ts new file mode 100644 index 0000000..cdfa572 --- /dev/null +++ b/src/dto/user/user.statistic.model.ts @@ -0,0 +1,6 @@ +import { ObjectType } from '@nestjs/graphql'; + +@ObjectType('UserStatistic') +export class UserStatisticModel { + +} diff --git a/src/dto/user/user.update.input.ts b/src/dto/user/user.update.input.ts new file mode 100644 index 0000000..94d6c5d --- /dev/null +++ b/src/dto/user/user.update.input.ts @@ -0,0 +1,29 @@ +import { Field, ID, InputType } from '@nestjs/graphql'; +import { GraphQLString } from 'graphql'; + +@InputType() +export class UserUpdateInput { + @Field(() => ID) + readonly id: string + + @Field({ nullable: true }) + readonly username: string + + @Field({ nullable: true }) + readonly email: string + + @Field({ nullable: true }) + readonly firstName: string + + @Field({ nullable: true }) + readonly lastName: string + + @Field({ nullable: true }) + readonly password: string + + @Field(() => [GraphQLString], { nullable: true }) + readonly roles: string[] + + @Field({ nullable: true }) + readonly language: string +} diff --git a/src/resolver/context.cache.ts b/src/resolver/context.cache.ts index 10257f5..7bc36bb 100644 --- a/src/resolver/context.cache.ts +++ b/src/resolver/context.cache.ts @@ -1,5 +1,6 @@ import { FormFieldDocument } from '../schema/form.field.schema'; import { FormDocument } from '../schema/form.schema'; +import { SubmissionFieldDocument } from '../schema/submission.field.schema'; import { SubmissionDocument } from '../schema/submission.schema'; import { UserDocument } from '../schema/user.schema'; @@ -16,7 +17,11 @@ export class ContextCache { [id: string]: SubmissionDocument, } = {} - private formField: { + private submissionFields: { + [id: string]: SubmissionFieldDocument, + } = {} + + private formFields: { [id: string]: FormFieldDocument, } = {} @@ -44,11 +49,19 @@ export class ContextCache { return this.submissions[id] } - public addFormField(formField: FormFieldDocument) { - this.formField[formField.id] = formField + public addFormField(field: FormFieldDocument) { + this.formFields[field.id] = field } public async getFormField(id: any): Promise { - return this.formField[id] + return this.formFields[id] + } + + public addSubmissionField(field: SubmissionFieldDocument) { + this.submissionFields[field.id] = field + } + + public async getSubmissionField(id: any): Promise { + return this.submissionFields[id] } } diff --git a/src/resolver/form/form.delete.mutation.ts b/src/resolver/form/form.delete.mutation.ts index 49f6610..3fc0968 100644 --- a/src/resolver/form/form.delete.mutation.ts +++ b/src/resolver/form/form.delete.mutation.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { Args, ID, Mutation } from '@nestjs/graphql'; import { Roles } from '../../decorator/roles.decorator'; import { User } from '../../decorator/user.decorator'; -import { FormModel } from '../../dto/form/form.model'; +import { DeletedModel } from '../../dto/deleted.model'; import { UserDocument } from '../../schema/user.schema'; import { FormDeleteService } from '../../service/form/form.delete.service'; import { FormService } from '../../service/form/form.service'; @@ -15,7 +15,7 @@ export class FormDeleteMutation { ) { } - @Mutation(() => FormModel) + @Mutation(() => DeletedModel) @Roles('admin') async deleteForm( @User() user: UserDocument, @@ -29,6 +29,6 @@ export class FormDeleteMutation { await this.deleteService.delete(id) - return new FormModel(form) + return new DeletedModel(id) } } diff --git a/src/resolver/form/form.statistic.resolver.ts b/src/resolver/form/form.statistic.resolver.ts new file mode 100644 index 0000000..5857059 --- /dev/null +++ b/src/resolver/form/form.statistic.resolver.ts @@ -0,0 +1,24 @@ +import { Query, ResolveField, Resolver } from '@nestjs/graphql'; +import { GraphQLInt } from 'graphql'; +import { Roles } from '../../decorator/roles.decorator'; +import { FormStatisticModel } from '../../dto/form/form.statistic.model'; +import { FormStatisticService } from '../../service/form/form.statistic.service'; + +@Resolver(() => FormStatisticModel) +export class FormStatisticResolver { + constructor( + private readonly statisticService: FormStatisticService, + ) { + } + + @Query(() => FormStatisticModel) + async getFormStatistic(): Promise { + return new FormStatisticModel() + } + + @ResolveField('total', () => GraphQLInt) + @Roles('admin') + getTotal(): Promise { + return this.statisticService.getTotal() + } +} diff --git a/src/resolver/form/form.update.mutation.ts b/src/resolver/form/form.update.mutation.ts index cd18798..732dcfc 100644 --- a/src/resolver/form/form.update.mutation.ts +++ b/src/resolver/form/form.update.mutation.ts @@ -24,13 +24,13 @@ export class FormUpdateMutation { @Args({ name: 'form', type: () => FormUpdateInput }) input: FormUpdateInput, @Context('cache') cache: ContextCache, ): Promise { - let form = await this.formService.findById(input.id) + const form = await this.formService.findById(input.id) if (!form.isLive && !await this.formService.isAdmin(form, user)) { throw new Error('invalid form') } - form = await this.updateService.update(form, input) + await this.updateService.update(form, input) cache.addForm(form) diff --git a/src/resolver/form/index.ts b/src/resolver/form/index.ts index 4942134..94701fa 100644 --- a/src/resolver/form/index.ts +++ b/src/resolver/form/index.ts @@ -2,12 +2,14 @@ import { FormCreateMutation } from './form.create.mutation'; import { FormDeleteMutation } from './form.delete.mutation'; import { FormResolver } from './form.resolver'; import { FormSearchResolver } from './form.search.resolver'; +import { FormStatisticResolver } from './form.statistic.resolver'; import { FormUpdateMutation } from './form.update.mutation'; export const formResolvers = [ - FormResolver, - FormSearchResolver, FormCreateMutation, FormDeleteMutation, + FormResolver, + FormSearchResolver, + FormStatisticResolver, FormUpdateMutation, ] diff --git a/src/resolver/index.ts b/src/resolver/index.ts index 8cbac75..d77c4db 100644 --- a/src/resolver/index.ts +++ b/src/resolver/index.ts @@ -1,6 +1,6 @@ import { authServices } from './auth'; import { formResolvers } from './form'; -import { myResolvers } from './me'; +import { profileResolvers } from './profile'; import { StatusResolver } from './status.resolver'; import { submissionResolvers } from './submission'; import { userResolvers } from './user'; @@ -9,7 +9,7 @@ export const resolvers = [ StatusResolver, ...userResolvers, ...authServices, - ...myResolvers, + ...profileResolvers, ...formResolvers, ...submissionResolvers, ] diff --git a/src/resolver/me/index.ts b/src/resolver/me/index.ts deleted file mode 100644 index df9bced..0000000 --- a/src/resolver/me/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ProfileResolver } from './profile.resolver'; - -export const myResolvers = [ - ProfileResolver, -] diff --git a/src/resolver/profile/index.ts b/src/resolver/profile/index.ts new file mode 100644 index 0000000..c504594 --- /dev/null +++ b/src/resolver/profile/index.ts @@ -0,0 +1,7 @@ +import { ProfileResolver } from './profile.resolver'; +import { ProfileUpdateMutation } from './profile.update.mutation'; + +export const profileResolvers = [ + ProfileResolver, + ProfileUpdateMutation, +] diff --git a/src/resolver/me/profile.resolver.ts b/src/resolver/profile/profile.resolver.ts similarity index 73% rename from src/resolver/me/profile.resolver.ts rename to src/resolver/profile/profile.resolver.ts index d4683ca..2ad5796 100644 --- a/src/resolver/me/profile.resolver.ts +++ b/src/resolver/profile/profile.resolver.ts @@ -1,19 +1,19 @@ import { Context, Query } from '@nestjs/graphql'; import { Roles } from '../../decorator/roles.decorator'; import { User } from '../../decorator/user.decorator'; -import { OwnUserModel } from '../../dto/user/own.user.model'; +import { ProfileModel } from '../../dto/profile/profile.model'; import { UserDocument } from '../../schema/user.schema'; import { ContextCache } from '../context.cache'; export class ProfileResolver { - @Query(() => OwnUserModel) + @Query(() => ProfileModel) @Roles('user') async me( @User() user: UserDocument, @Context('cache') cache: ContextCache, - ): Promise { + ): Promise { cache.addUser(user) - return new OwnUserModel(user) + return new ProfileModel(user) } } diff --git a/src/resolver/profile/profile.update.mutation.ts b/src/resolver/profile/profile.update.mutation.ts new file mode 100644 index 0000000..80f49a9 --- /dev/null +++ b/src/resolver/profile/profile.update.mutation.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { Args, Context, Mutation } from '@nestjs/graphql'; +import { User } from '../../decorator/user.decorator'; +import { ProfileModel } from '../../dto/profile/profile.model'; +import { ProfileUpdateInput } from '../../dto/profile/profile.update.input'; +import { UserDocument } from '../../schema/user.schema'; +import { ProfileUpdateService } from '../../service/profile/profile.update.service'; +import { ContextCache } from '../context.cache'; + +@Injectable() +export class ProfileUpdateMutation { + constructor( + private readonly updateService: ProfileUpdateService, + ) { + } + + @Mutation(() => ProfileModel) + async updateProfile( + @User() user: UserDocument, + @Args({ name: 'user', type: () => ProfileUpdateInput }) input: ProfileUpdateInput, + @Context('cache') cache: ContextCache, + ): Promise { + await this.updateService.update(user, input) + + cache.addUser(user) + + return new ProfileModel(user) + } +} diff --git a/src/resolver/submission/index.ts b/src/resolver/submission/index.ts index 015b7f0..741fa26 100644 --- a/src/resolver/submission/index.ts +++ b/src/resolver/submission/index.ts @@ -1,13 +1,17 @@ +import { SubmissionFieldResolver } from './submission.field.resolver'; import { SubmissionProgressResolver } from './submission.progress.resolver'; import { SubmissionResolver } from './submission.resolver'; import { SubmissionSearchResolver } from './submission.search.resolver'; import { SubmissionSetFieldMutation } from './submission.set.field.mutation'; import { SubmissionStartMutation } from './submission.start.mutation'; +import { SubmissionStatisticResolver } from './submission.statistic.resolver'; export const submissionResolvers = [ + SubmissionFieldResolver, SubmissionProgressResolver, SubmissionResolver, + SubmissionSearchResolver, SubmissionSetFieldMutation, SubmissionStartMutation, - SubmissionSearchResolver, + SubmissionStatisticResolver, ] diff --git a/src/resolver/submission/submission.field.resolver.ts b/src/resolver/submission/submission.field.resolver.ts new file mode 100644 index 0000000..421872d --- /dev/null +++ b/src/resolver/submission/submission.field.resolver.ts @@ -0,0 +1,25 @@ +import { Context, Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { FormFieldModel } from '../../dto/form/form.field.model'; +import { SubmissionFieldModel } from '../../dto/submission/submission.field.model'; +import { ContextCache } from '../context.cache'; + +@Resolver(() => SubmissionFieldModel) +export class SubmissionFieldResolver { + @ResolveField('field', () => FormFieldModel, { nullable: true }) + async getFields( + @Parent() parent: SubmissionFieldModel, + @Context('cache') cache: ContextCache, + ): Promise { + const submissionField = await cache.getSubmissionField(parent.id) + + console.log(submissionField.field) + + const field = await cache.getFormField(submissionField.field) + + if (!field) { + return null + } + + return new FormFieldModel(field) + } +} diff --git a/src/resolver/submission/submission.resolver.ts b/src/resolver/submission/submission.resolver.ts index 179082a..cbde1f1 100644 --- a/src/resolver/submission/submission.resolver.ts +++ b/src/resolver/submission/submission.resolver.ts @@ -25,6 +25,9 @@ export class SubmissionResolver { cache.addFormField(field) }) - return submission.fields.map(field => new SubmissionFieldModel(field)) + return submission.fields.map(field => { + cache.addSubmissionField(field) + return new SubmissionFieldModel(field) + }) } } diff --git a/src/resolver/submission/submission.statistic.resolver.ts b/src/resolver/submission/submission.statistic.resolver.ts new file mode 100644 index 0000000..72a9ca0 --- /dev/null +++ b/src/resolver/submission/submission.statistic.resolver.ts @@ -0,0 +1,25 @@ +import { Query, ResolveField, Resolver } from '@nestjs/graphql'; +import { GraphQLInt } from 'graphql'; +import { Roles } from '../../decorator/roles.decorator'; +import { SubmissionStatisticModel } from '../../dto/submission/submission.statistic.model'; +import { SubmissionStatisticService } from '../../service/submission/submission.statistic.service'; + +@Resolver(() => SubmissionStatisticModel) +export class SubmissionStatisticResolver { + constructor( + private readonly statisticService: SubmissionStatisticService, + ) { + } + + + @Query(() => SubmissionStatisticModel) + async getSubmissionStatistic(): Promise { + return new SubmissionStatisticModel() + } + + @ResolveField('total', () => GraphQLInt) + @Roles('admin') + getTotal(): Promise { + return this.statisticService.getTotal() + } +} diff --git a/src/resolver/user/index.ts b/src/resolver/user/index.ts index 2a4f1ed..0d6ef2a 100644 --- a/src/resolver/user/index.ts +++ b/src/resolver/user/index.ts @@ -1,5 +1,13 @@ +import { UserDeleteMutation } from './user.delete.mutation'; import { UserResolver } from './user.resolver'; +import { UserSearchResolver } from './user.search.resolver'; +import { UserStatisticResolver } from './user.statistic.resolver'; +import { UserUpdateMutation } from './user.update.mutation'; export const userResolvers = [ + UserDeleteMutation, UserResolver, + UserSearchResolver, + UserStatisticResolver, + UserUpdateMutation, ] diff --git a/src/resolver/user/user.delete.mutation.ts b/src/resolver/user/user.delete.mutation.ts new file mode 100644 index 0000000..16d4b93 --- /dev/null +++ b/src/resolver/user/user.delete.mutation.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { Args, ID, Mutation } from '@nestjs/graphql'; +import { Roles } from '../../decorator/roles.decorator'; +import { User } from '../../decorator/user.decorator'; +import { DeletedModel } from '../../dto/deleted.model'; +import { UserDocument } from '../../schema/user.schema'; +import { UserDeleteService } from '../../service/user/user.delete.service'; + +@Injectable() +export class UserDeleteMutation { + constructor( + private readonly deleteService: UserDeleteService, + ) { + } + + @Mutation(() => DeletedModel) + @Roles('admin') + async deleteUser( + @User() auth: UserDocument, + @Args({ name: 'id', type: () => ID}) id: string, + ): Promise { + if (auth.id === id) { + throw new Error('cannot delete your own user') + } + + await this.deleteService.delete(id) + + return new DeletedModel(id) + } +} diff --git a/src/resolver/user/user.resolver.ts b/src/resolver/user/user.resolver.ts index 3d02641..1672f66 100644 --- a/src/resolver/user/user.resolver.ts +++ b/src/resolver/user/user.resolver.ts @@ -1,7 +1,8 @@ -import { Args, Context, GraphQLExecutionContext, ID, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; -import { rolesType } from '../../config/roles'; +import { Args, Context, ID, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; import { Roles } from '../../decorator/roles.decorator'; +import { User } from '../../decorator/user.decorator'; import { UserModel } from '../../dto/user/user.model'; +import { UserDocument } from '../../schema/user.schema'; import { UserService } from '../../service/user/user.service'; import { ContextCache } from '../context.cache'; @@ -26,11 +27,24 @@ export class UserResolver { } @ResolveField('roles', () => [String]) - @Roles('superuser') + @Roles('user') async getRoles( - @Parent() user: UserModel, + @User() user: UserDocument, + @Parent() parent: UserModel, @Context('cache') cache: ContextCache, ): Promise { - return (await cache.getUser(user.id)).roles + return await this.returnFieldForSuperuser( + await cache.getUser(parent.id), + user, + c => c.roles + ) + } + + async returnFieldForSuperuser(parent: UserDocument, user: UserDocument, callback: (user: UserDocument) => T): Promise { + if (user.id !== parent.id && !await this.userService.isSuperuser(user)) { + throw new Error('No access to roles') + } + + return callback(parent) } } diff --git a/src/resolver/user/user.search.resolver.ts b/src/resolver/user/user.search.resolver.ts new file mode 100644 index 0000000..3012d98 --- /dev/null +++ b/src/resolver/user/user.search.resolver.ts @@ -0,0 +1,35 @@ +import { Args, Context, Query, Resolver } from '@nestjs/graphql'; +import { GraphQLInt } from 'graphql'; +import { Roles } from '../../decorator/roles.decorator'; +import { PagerUserModel } from '../../dto/user/pager.user.model'; +import { UserModel } from '../../dto/user/user.model'; +import { UserService } from '../../service/user/user.service'; +import { ContextCache } from '../context.cache'; + +@Resolver(() => PagerUserModel) +export class UserSearchResolver { + constructor( + private readonly userService: UserService, + ) { + } + + @Query(() => PagerUserModel) + @Roles('superuser') + async listUsers( + @Args('start', {type: () => GraphQLInt, defaultValue: 0, nullable: true}) start, + @Args('limit', {type: () => GraphQLInt, defaultValue: 50, nullable: true}) limit, + @Context('cache') cache: ContextCache, + ): Promise { + const [entities, total] = await this.userService.find(start, limit) + + return new PagerUserModel( + entities.map(entity => { + cache.addUser(entity) + return new UserModel(entity) + }), + total, + limit, + start, + ) + } +} diff --git a/src/resolver/user/user.statistic.resolver.ts b/src/resolver/user/user.statistic.resolver.ts new file mode 100644 index 0000000..1bd19b0 --- /dev/null +++ b/src/resolver/user/user.statistic.resolver.ts @@ -0,0 +1,24 @@ +import { Query, ResolveField, Resolver } from '@nestjs/graphql'; +import { GraphQLInt } from 'graphql'; +import { Roles } from '../../decorator/roles.decorator'; +import { UserStatisticModel } from '../../dto/user/user.statistic.model'; +import { UserStatisticService } from '../../service/user/user.statistic.service'; + +@Resolver(() => UserStatisticModel) +export class UserStatisticResolver { + constructor( + private readonly statisticService: UserStatisticService, + ) { + } + + @Query(() => UserStatisticModel) + async getUserStatistic(): Promise { + return new UserStatisticModel() + } + + @ResolveField('total', () => GraphQLInt) + @Roles('admin') + getTotal(): Promise { + return this.statisticService.getTotal() + } +} diff --git a/src/resolver/user/user.update.mutation.ts b/src/resolver/user/user.update.mutation.ts new file mode 100644 index 0000000..0304e80 --- /dev/null +++ b/src/resolver/user/user.update.mutation.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { Args, Context, Mutation } from '@nestjs/graphql'; +import { Roles } from '../../decorator/roles.decorator'; +import { User } from '../../decorator/user.decorator'; +import { UserModel } from '../../dto/user/user.model'; +import { UserUpdateInput } from '../../dto/user/user.update.input'; +import { UserDocument } from '../../schema/user.schema'; +import { UserService } from '../../service/user/user.service'; +import { UserUpdateService } from '../../service/user/user.update.service'; +import { ContextCache } from '../context.cache'; + +@Injectable() +export class UserUpdateMutation { + constructor( + private readonly updateService: UserUpdateService, + private readonly userService: UserService, + ) { + } + + @Mutation(() => UserModel) + @Roles('superuser') + async updateUser( + @User() auth: UserDocument, + @Args({ name: 'user', type: () => UserUpdateInput }) input: UserUpdateInput, + @Context('cache') cache: ContextCache, + ): Promise { + if (auth.id === input.id) { + throw new Error('cannot update your own user') + } + + const user = await this.userService.findById(input.id) + + await this.updateService.update(user, input) + + cache.addUser(user) + + return new UserModel(user) + } +} diff --git a/src/service/form/form.delete.service.ts b/src/service/form/form.delete.service.ts index 6d8a84b..83b2dd4 100644 --- a/src/service/form/form.delete.service.ts +++ b/src/service/form/form.delete.service.ts @@ -2,16 +2,20 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { FormDocument, FormSchemaName } from '../../schema/form.schema'; +import { SubmissionDocument, SubmissionSchemaName } from '../../schema/submission.schema'; @Injectable() export class FormDeleteService { constructor( @InjectModel(FormSchemaName) private formModel: Model, + @InjectModel(SubmissionSchemaName) private readonly submissionModel: Model, ) { } async delete(id: string): Promise { - // TODO - throw new Error('form.delete not yet implemented') + const form = await this.formModel.findByIdAndDelete(id).exec() + await this.submissionModel.deleteMany({ + form + }).exec() } } diff --git a/src/service/form/form.service.ts b/src/service/form/form.service.ts index dd27c6a..b1e4f1e 100644 --- a/src/service/form/form.service.ts +++ b/src/service/form/form.service.ts @@ -28,15 +28,14 @@ export class FormService { } } - const qb = this.formModel.find(conditions) - - // TODO apply restrictions based on user! - return [ - await qb.sort(sort) + await this.formModel + .find(conditions) + .sort(sort) .skip(start) .limit(limit), - await qb.count() + await this.formModel + .countDocuments(conditions) ] } diff --git a/src/service/form/form.statistic.service.ts b/src/service/form/form.statistic.service.ts new file mode 100644 index 0000000..f7020fe --- /dev/null +++ b/src/service/form/form.statistic.service.ts @@ -0,0 +1,14 @@ +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { FormDocument, FormSchemaName } from '../../schema/form.schema'; + +export class FormStatisticService { + constructor( + @InjectModel(FormSchemaName) private formModel: Model, + ) { + } + + async getTotal(): Promise { + return await this.formModel.estimatedDocumentCount(); + } +} diff --git a/src/service/form/index.ts b/src/service/form/index.ts index 265120d..6e30391 100644 --- a/src/service/form/index.ts +++ b/src/service/form/index.ts @@ -1,11 +1,13 @@ import { FormCreateService } from './form.create.service'; import { FormDeleteService } from './form.delete.service'; import { FormService } from './form.service'; +import { FormStatisticService } from './form.statistic.service'; import { FormUpdateService } from './form.update.service'; export const formServices = [ - FormService, FormCreateService, - FormUpdateService, FormDeleteService, + FormService, + FormStatisticService, + FormUpdateService, ] diff --git a/src/service/index.ts b/src/service/index.ts index 8125edc..98e4816 100644 --- a/src/service/index.ts +++ b/src/service/index.ts @@ -6,11 +6,13 @@ import { PinoLogger } from 'nestjs-pino/dist'; import { authServices } from './auth'; import { formServices } from './form'; import { MailService } from './mail.service'; +import { profileServices } from './profile'; import { submissionServices } from './submission'; import { userServices } from './user'; export const services = [ ...userServices, + ...profileServices, ...formServices, ...authServices, ...submissionServices, diff --git a/src/service/profile/index.ts b/src/service/profile/index.ts new file mode 100644 index 0000000..fb9089a --- /dev/null +++ b/src/service/profile/index.ts @@ -0,0 +1,5 @@ +import { ProfileUpdateService } from './profile.update.service'; + +export const profileServices = [ + ProfileUpdateService, +] diff --git a/src/service/profile/profile.update.service.ts b/src/service/profile/profile.update.service.ts new file mode 100644 index 0000000..60673e1 --- /dev/null +++ b/src/service/profile/profile.update.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { ProfileUpdateInput } from '../../dto/profile/profile.update.input'; +import { UserDocument } from '../../schema/user.schema'; + +@Injectable() +export class ProfileUpdateService { + async update(user: UserDocument, input: ProfileUpdateInput): Promise { + if (input.firstName !== undefined) { + user.set('firstName', input.firstName) + } + + if (input.lastName !== undefined) { + user.set('lastName', input.lastName) + } + + if (input.email !== undefined) { + user.set('email', input.email) + // TODO request email verification + } + + if (input.username !== undefined) { + user.set('username', input.username) + } + + if (input.language !== undefined) { + user.set('language', input.language) + } + + if (input.password !== undefined) { + // user.set('language', input.language) + // TODO password handling + } + + await user.save() + + return user + } +} diff --git a/src/service/submission/index.ts b/src/service/submission/index.ts index 4266a5a..54e57eb 100644 --- a/src/service/submission/index.ts +++ b/src/service/submission/index.ts @@ -1,11 +1,13 @@ import { SubmissionService } from './submission.service'; import { SubmissionSetFieldService } from './submission.set.field.service'; import { SubmissionStartService } from './submission.start.service'; +import { SubmissionStatisticService } from './submission.statistic.service'; import { SubmissionTokenService } from './submission.token.service'; export const submissionServices = [ + SubmissionService, SubmissionSetFieldService, SubmissionStartService, - SubmissionService, + SubmissionStatisticService, SubmissionTokenService, ] diff --git a/src/service/submission/submission.service.ts b/src/service/submission/submission.service.ts index 18e4c81..307186d 100644 --- a/src/service/submission/submission.service.ts +++ b/src/service/submission/submission.service.ts @@ -1,5 +1,5 @@ import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; +import { FilterQuery, Model } from 'mongoose'; import { FormDocument } from '../../schema/form.schema'; import { SubmissionDocument, SubmissionSchemaName } from '../../schema/submission.schema'; import { SubmissionTokenService } from './submission.token.service'; @@ -16,15 +16,18 @@ export class SubmissionService { } async find(form: FormDocument, start: number, limit: number, sort: any = {}): Promise<[SubmissionDocument[], number]> { - const qb = this.submissionModel.find({ + const conditions: FilterQuery = { form - }) + } return [ - await qb.sort(sort) + await this.submissionModel + .find(conditions) + .sort(sort) .skip(start) .limit(limit), - await qb.count() + await this.submissionModel + .countDocuments(conditions) ] } diff --git a/src/service/submission/submission.statistic.service.ts b/src/service/submission/submission.statistic.service.ts new file mode 100644 index 0000000..e3f70af --- /dev/null +++ b/src/service/submission/submission.statistic.service.ts @@ -0,0 +1,14 @@ +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { SubmissionDocument, SubmissionSchemaName } from '../../schema/submission.schema'; + +export class SubmissionStatisticService { + constructor( + @InjectModel(SubmissionSchemaName) private readonly submissionModel: Model, + ) { + } + + async getTotal(): Promise { + return await this.submissionModel.estimatedDocumentCount(); + } +} diff --git a/src/service/user/index.ts b/src/service/user/index.ts index dea6548..e56e51d 100644 --- a/src/service/user/index.ts +++ b/src/service/user/index.ts @@ -1,7 +1,13 @@ import { UserCreateService } from './user.create.service'; +import { UserDeleteService } from './user.delete.service'; import { UserService } from './user.service'; +import { UserStatisticService } from './user.statistic.service'; +import { UserUpdateService } from './user.update.service'; export const userServices = [ UserCreateService, + UserDeleteService, UserService, + UserStatisticService, + UserUpdateService, ] diff --git a/src/service/user/user.delete.service.ts b/src/service/user/user.delete.service.ts new file mode 100644 index 0000000..e501cf0 --- /dev/null +++ b/src/service/user/user.delete.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { UserDocument, UserSchemaName } from '../../schema/user.schema'; + +@Injectable() +export class UserDeleteService { + constructor( + @InjectModel(UserSchemaName) private userModel: Model, + ) { + } + + async delete(id: string): Promise { + await this.userModel.findByIdAndDelete(id).exec() + } +} diff --git a/src/service/user/user.service.ts b/src/service/user/user.service.ts index 27e8b5f..15530bf 100644 --- a/src/service/user/user.service.ts +++ b/src/service/user/user.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { GraphQLError } from 'graphql'; import { Model } from 'mongoose'; import { UserDocument, UserSchemaName } from '../../schema/user.schema'; @@ -11,6 +10,22 @@ export class UserService { ) { } + async isSuperuser(user: UserDocument): Promise { + return user.roles.includes('superuser') + } + + async find(start: number, limit: number, sort: any = {}): Promise<[UserDocument[], number]> { + return [ + await this.userModel + .find() + .sort(sort) + .skip(start) + .limit(limit), + await this.userModel + .countDocuments() + ] + } + async findById(id: string): Promise { const user = await this.userModel.findById(id); diff --git a/src/service/user/user.statistic.service.ts b/src/service/user/user.statistic.service.ts new file mode 100644 index 0000000..507c203 --- /dev/null +++ b/src/service/user/user.statistic.service.ts @@ -0,0 +1,14 @@ +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { UserDocument, UserSchemaName } from '../../schema/user.schema'; + +export class UserStatisticService { + constructor( + @InjectModel(UserSchemaName) private userModel: Model, + ) { + } + + async getTotal(): Promise { + return await this.userModel.estimatedDocumentCount(); + } +} diff --git a/src/service/user/user.update.service.ts b/src/service/user/user.update.service.ts new file mode 100644 index 0000000..487fe3a --- /dev/null +++ b/src/service/user/user.update.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { UserUpdateInput } from '../../dto/user/user.update.input'; +import { UserDocument } from '../../schema/user.schema'; + +@Injectable() +export class UserUpdateService { + async update(user: UserDocument, input: UserUpdateInput): Promise { + if (input.firstName !== undefined) { + user.set('firstName', input.firstName) + } + + if (input.lastName !== undefined) { + user.set('lastName', input.lastName) + } + + if (input.email !== undefined) { + user.set('email', input.email) + } + + if (input.username !== undefined) { + user.set('username', input.username) + } + + if (input.roles !== undefined) { + user.set('roles', input.roles) + } + + if (input.language !== undefined) { + user.set('language', input.language) + } + + if (input.password !== undefined) { + // user.set('language', input.language) + // TODO password handling + } + + await user.save() + + return user + } +}