ability to load submission by id if token is present and cleanup structure

This commit is contained in:
Michael Schramm 2022-01-02 22:31:03 +01:00
parent 3840bb585c
commit 8e87ca5eed
28 changed files with 195 additions and 102 deletions

View File

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- user confirmation tokens
- email verification
- idx for fields and logic to have stable order
- ability to load submission by id if token is present
### Changed

View File

@ -4,7 +4,7 @@ import { AuthJwtModel } from '../../dto/auth/auth.jwt.model'
import { AuthService } from '../../service/auth/auth.service'
@Injectable()
export class AuthLoginResolver {
export class AuthLoginMutation {
constructor(
private readonly auth: AuthService
) {

View File

@ -8,7 +8,7 @@ import { SettingService } from '../../service/setting.service'
import { UserCreateService } from '../../service/user/user.create.service'
@Injectable()
export class AuthRegisterResolver {
export class AuthRegisterMutation {
constructor(
private readonly createUser: UserCreateService,
private readonly settingService: SettingService,

View File

@ -1,7 +1,7 @@
import { AuthLoginResolver } from './auth.login.resolver'
import { AuthRegisterResolver } from './auth.register.resolver'
import { AuthLoginMutation } from './auth.login.mutation'
import { AuthRegisterMutation } from './auth.register.mutation'
export const authServices = [
AuthRegisterResolver,
AuthLoginResolver,
AuthRegisterMutation,
AuthLoginMutation,
]

View File

@ -1,5 +1,5 @@
import { Args, Context, Query, Resolver } from '@nestjs/graphql'
import { GraphQLInt } from 'graphql'
import { Injectable } from '@nestjs/common'
import { Args, Context, Int, Query } from '@nestjs/graphql'
import { Roles } from '../../decorator/roles.decorator'
import { User } from '../../decorator/user.decorator'
import { FormModel } from '../../dto/form/form.model'
@ -9,8 +9,8 @@ import { UserEntity } from '../../entity/user.entity'
import { FormService } from '../../service/form/form.service'
import { ContextCache } from '../context.cache'
@Resolver(() => FormPagerModel)
export class FormSearchResolver {
@Injectable()
export class FormListQuery {
constructor(
private readonly formService: FormService,
) {
@ -20,8 +20,8 @@ export class FormSearchResolver {
@Roles('user')
async listForms(
@User() user: UserEntity,
@Args('start', {type: () => GraphQLInt, defaultValue: 0, nullable: true}) start: number,
@Args('limit', {type: () => GraphQLInt, defaultValue: 50, nullable: true}) limit: number,
@Args('start', {type: () => Int, defaultValue: 0, nullable: true}) start: number,
@Args('limit', {type: () => Int, defaultValue: 50, nullable: true}) limit: number,
@Context('cache') cache: ContextCache,
): Promise<FormPagerModel> {
const [forms, total] = await this.formService.find(

View File

@ -1,4 +1,5 @@
import { Args, Context, ID, Query, Resolver } from '@nestjs/graphql'
import { Injectable } from '@nestjs/common'
import { Args, Context, ID, Query } from '@nestjs/graphql'
import { User } from '../../decorator/user.decorator'
import { FormModel } from '../../dto/form/form.model'
import { FormEntity } from '../../entity/form.entity'
@ -6,7 +7,7 @@ import { UserEntity } from '../../entity/user.entity'
import { FormService } from '../../service/form/form.service'
import { ContextCache } from '../context.cache'
@Resolver(() => FormModel)
@Injectable()
export class FormQuery {
constructor(
private readonly formService: FormService,

View File

@ -20,19 +20,19 @@ export class FormResolver {
) {
}
@ResolveField('fields', () => [FormFieldModel])
async getFields(
@ResolveField(() => [FormFieldModel])
async fields(
@User() user: UserEntity,
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<FormFieldModel[]> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
return form.fields?.map(field => new FormFieldModel(field)) || []
return form.fields?.map(field => new FormFieldModel(field)).sort((a,b) => a.idx - b.idx) || []
}
@ResolveField('hooks', () => [FormHookModel])
async getHooks(
@ResolveField(() => [FormHookModel])
async hooks(
@User() user: UserEntity,
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
@ -42,9 +42,9 @@ export class FormResolver {
return form.hooks?.map(hook => new FormHookModel(hook)) || []
}
@ResolveField('isLive', () => Boolean)
@ResolveField(() => Boolean)
@Roles('admin')
async getRoles(
async isLive(
@User() user: UserEntity,
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
@ -58,9 +58,9 @@ export class FormResolver {
return form.isLive
}
@ResolveField('notifications', () => [FormNotificationModel])
@ResolveField(() => [FormNotificationModel])
@Roles('admin')
async getNotifications(
async notifications(
@User() user: UserEntity,
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
@ -74,8 +74,8 @@ export class FormResolver {
return form.notifications?.map(notification => new FormNotificationModel(notification)) || []
}
@ResolveField('design', () => DesignModel)
async getDesign(
@ResolveField(() => DesignModel)
async design(
@User() user: UserEntity,
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
@ -85,8 +85,8 @@ export class FormResolver {
return new DesignModel(form.design)
}
@ResolveField('startPage', () => PageModel)
async getStartPage(
@ResolveField(() => PageModel)
async startPage(
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<PageModel> {
@ -95,8 +95,8 @@ export class FormResolver {
return new PageModel(form.startPage)
}
@ResolveField('endPage', () => PageModel)
async getEndPage(
@ResolveField(() => PageModel)
async endPage(
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<PageModel> {
@ -105,9 +105,9 @@ export class FormResolver {
return new PageModel(form.endPage)
}
@ResolveField('admin', () => UserModel, { nullable: true })
@ResolveField(() => UserModel, { nullable: true })
@Roles('admin')
async getAdmin(
async admin(
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<UserModel> {

View File

@ -0,0 +1,11 @@
import { Injectable } from '@nestjs/common'
import { Query } from '@nestjs/graphql'
import { FormStatisticModel } from '../../dto/form/form.statistic.model'
@Injectable()
export class FormStatisticQuery {
@Query(() => FormStatisticModel)
getFormStatistic(): FormStatisticModel {
return new FormStatisticModel()
}
}

View File

@ -1,5 +1,4 @@
import { Query, ResolveField, Resolver } from '@nestjs/graphql'
import { GraphQLInt } from 'graphql'
import { Int, ResolveField, Resolver } from '@nestjs/graphql'
import { Roles } from '../../decorator/roles.decorator'
import { FormStatisticModel } from '../../dto/form/form.statistic.model'
import { FormStatisticService } from '../../service/form/form.statistic.service'
@ -11,14 +10,9 @@ export class FormStatisticResolver {
) {
}
@Query(() => FormStatisticModel)
getFormStatistic(): FormStatisticModel {
return new FormStatisticModel()
}
@ResolveField('total', () => GraphQLInt)
@ResolveField(() => Int)
@Roles('admin')
getTotal(): Promise<number> {
total(): Promise<number> {
return this.statisticService.getTotal()
}
}

View File

@ -1,8 +1,9 @@
import { FormCreateMutation } from './form.create.mutation'
import { FormDeleteMutation } from './form.delete.mutation'
import { FormListQuery } from './form.list.query'
import { FormQuery } from './form.query'
import { FormResolver } from './form.resolver'
import { FormSearchResolver } from './form.search.resolver'
import { FormStatisticQuery } from './form.statistic.query'
import { FormStatisticResolver } from './form.statistic.resolver'
import { FormUpdateMutation } from './form.update.mutation'
@ -11,7 +12,8 @@ export const formResolvers = [
FormDeleteMutation,
FormQuery,
FormResolver,
FormSearchResolver,
FormListQuery,
FormStatisticQuery,
FormStatisticResolver,
FormUpdateMutation,
]

View File

@ -1,7 +1,7 @@
import { ProfileResolver } from './profile.resolver'
import { ProfileQuery } from './profile.query'
import { ProfileUpdateMutation } from './profile.update.mutation'
export const profileResolvers = [
ProfileResolver,
ProfileQuery,
ProfileUpdateMutation,
]

View File

@ -1,3 +1,4 @@
import { Injectable } from '@nestjs/common'
import { Context, Query } from '@nestjs/graphql'
import { Roles } from '../../decorator/roles.decorator'
import { User } from '../../decorator/user.decorator'
@ -5,7 +6,8 @@ import { ProfileModel } from '../../dto/profile/profile.model'
import { UserEntity } from '../../entity/user.entity'
import { ContextCache } from '../context.cache'
export class ProfileResolver {
@Injectable()
export class ProfileQuery {
@Query(() => ProfileModel)
@Roles('user')
public me(

View File

@ -1,7 +1,7 @@
import { SettingMutation } from './setting.mutation'
import { SettingResolver } from './setting.resolver'
import { SettingQuery } from './setting.query'
export const settingsResolvers = [
SettingResolver,
SettingQuery,
SettingMutation,
]

View File

@ -8,7 +8,7 @@ import { UserEntity } from '../../entity/user.entity'
import { SettingService } from '../../service/setting.service'
@Injectable()
export class SettingResolver {
export class SettingQuery {
constructor(
private readonly settingService: SettingService,
) {

View File

@ -1,17 +1,21 @@
import { SubmissionFieldResolver } from './submission.field.resolver'
import { SubmissionListQuery } from './submission.list.query'
import { SubmissionProgressResolver } from './submission.progress.resolver'
import { SubmissionQuery } from './submission.query'
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 { SubmissionStatisticQuery } from './submission.statistic.query'
import { SubmissionStatisticResolver } from './submission.statistic.resolver'
export const submissionResolvers = [
SubmissionFieldResolver,
SubmissionListQuery,
SubmissionProgressResolver,
SubmissionQuery,
SubmissionResolver,
SubmissionSearchResolver,
SubmissionSetFieldMutation,
SubmissionStartMutation,
SubmissionStatisticQuery,
SubmissionStatisticResolver,
]

View File

@ -13,8 +13,8 @@ export class SubmissionFieldResolver {
) {
}
@ResolveField('field', () => FormFieldModel, { nullable: true })
async getFields(
@ResolveField(() => FormFieldModel, { nullable: true })
async field(
@Parent() parent: SubmissionFieldModel,
@Context('cache') cache: ContextCache,
): Promise<FormFieldModel> {

View File

@ -1,5 +1,5 @@
import { Args, Context, ID, Query, Resolver } from '@nestjs/graphql'
import { GraphQLInt } from 'graphql'
import { Injectable } from '@nestjs/common'
import { Args, Context, ID, Int, Query } from '@nestjs/graphql'
import { User } from '../../decorator/user.decorator'
import { SubmissionModel } from '../../dto/submission/submission.model'
import { SubmissionPagerModel } from '../../dto/submission/submission.pager.model'
@ -9,8 +9,8 @@ import { FormService } from '../../service/form/form.service'
import { SubmissionService } from '../../service/submission/submission.service'
import { ContextCache } from '../context.cache'
@Resolver(() => SubmissionPagerModel)
export class SubmissionSearchResolver {
@Injectable()
export class SubmissionListQuery {
constructor(
private readonly formService: FormService,
private readonly submissionService: SubmissionService,
@ -21,8 +21,8 @@ export class SubmissionSearchResolver {
async listSubmissions(
@User() user: UserEntity,
@Args('form', {type: () => ID}) id: string,
@Args('start', {type: () => GraphQLInt, defaultValue: 0, nullable: true}) start: number,
@Args('limit', {type: () => GraphQLInt, defaultValue: 50, nullable: true}) limit: number,
@Args('start', {type: () => Int, defaultValue: 0, nullable: true}) start: number,
@Args('limit', {type: () => Int, defaultValue: 50, nullable: true}) limit: number,
@Context('cache') cache: ContextCache,
): Promise<SubmissionPagerModel> {
const form = await this.formService.findById(id)

View File

@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common'
import { Args, Context, ID, Query } from '@nestjs/graphql'
import { User } from '../../decorator/user.decorator'
import { SubmissionModel } from '../../dto/submission/submission.model'
import { SubmissionEntity } from '../../entity/submission.entity'
import { UserEntity } from '../../entity/user.entity'
import { FormService } from '../../service/form/form.service'
import { SubmissionService } from '../../service/submission/submission.service'
import { SubmissionTokenService } from '../../service/submission/submission.token.service'
import { ContextCache } from '../context.cache'
@Injectable()
export class SubmissionQuery {
constructor(
private readonly formService: FormService,
private readonly submissionService: SubmissionService,
private readonly tokenService: SubmissionTokenService,
) {
}
@Query(() => SubmissionModel)
async getSubmissionById(
@User() user: UserEntity,
@Args('id', {type: () => ID}) id: string,
@Args('token', {nullable: true}) token: string,
@Context('cache') cache: ContextCache,
): Promise<SubmissionModel> {
const submission = await this.submissionService.findById(id)
if (
!await this.tokenService.verify(token, submission.tokenHash)
&& !this.formService.isAdmin(submission.form, user)
) {
throw new Error('invalid form')
}
cache.add(cache.getCacheKey(SubmissionEntity.name, submission.id), submission)
return new SubmissionModel(submission)
}
}

View File

@ -9,8 +9,8 @@ import { ContextCache } from '../context.cache'
@Resolver(() => SubmissionModel)
export class SubmissionResolver {
@ResolveField('fields', () => [SubmissionFieldModel])
async getFields(
@ResolveField(() => [SubmissionFieldModel])
async fields(
@User() user: UserEntity,
@Parent() parent: SubmissionModel,
@Context('cache') cache: ContextCache,

View File

@ -0,0 +1,11 @@
import { Injectable } from '@nestjs/common'
import { Query } from '@nestjs/graphql'
import { SubmissionStatisticModel } from '../../dto/submission/submission.statistic.model'
@Injectable()
export class SubmissionStatisticQuery {
@Query(() => SubmissionStatisticModel)
getSubmissionStatistic(): SubmissionStatisticModel {
return new SubmissionStatisticModel()
}
}

View File

@ -1,4 +1,4 @@
import { Query, ResolveField, Resolver } from '@nestjs/graphql'
import { ResolveField, Resolver } from '@nestjs/graphql'
import { GraphQLInt } from 'graphql'
import { Roles } from '../../decorator/roles.decorator'
import { SubmissionStatisticModel } from '../../dto/submission/submission.statistic.model'
@ -11,15 +11,9 @@ export class SubmissionStatisticResolver {
) {
}
@Query(() => SubmissionStatisticModel)
getSubmissionStatistic(): SubmissionStatisticModel {
return new SubmissionStatisticModel()
}
@ResolveField('total', () => GraphQLInt)
@ResolveField(() => GraphQLInt)
@Roles('admin')
getTotal(): Promise<number> {
total(): Promise<number> {
return this.statisticService.getTotal()
}
}

View File

@ -1,13 +1,17 @@
import { UserDeleteMutation } from './user.delete.mutation'
import { UserListQuery } from './user.list.query'
import { UserQuery } from './user.query'
import { UserResolver } from './user.resolver'
import { UserSearchResolver } from './user.search.resolver'
import { UserStatisticQuery } from './user.statistic.query'
import { UserStatisticResolver } from './user.statistic.resolver'
import { UserUpdateMutation } from './user.update.mutation'
export const userResolvers = [
UserDeleteMutation,
UserListQuery,
UserQuery,
UserResolver,
UserSearchResolver,
UserStatisticQuery,
UserStatisticResolver,
UserUpdateMutation,
]

View File

@ -1,5 +1,4 @@
import { Args, Context, Query, Resolver } from '@nestjs/graphql'
import { GraphQLInt } from 'graphql'
import { Args, Context, Int, Query, Resolver } from '@nestjs/graphql'
import { Roles } from '../../decorator/roles.decorator'
import { UserModel } from '../../dto/user/user.model'
import { UserPagerModel } from '../../dto/user/user.pager.model'
@ -8,7 +7,7 @@ import { UserService } from '../../service/user/user.service'
import { ContextCache } from '../context.cache'
@Resolver(() => UserPagerModel)
export class UserSearchResolver {
export class UserListQuery {
constructor(
private readonly userService: UserService,
) {
@ -17,8 +16,8 @@ export class UserSearchResolver {
@Query(() => UserPagerModel)
@Roles('superuser')
async listUsers(
@Args('start', {type: () => GraphQLInt, defaultValue: 0, nullable: true}) start: number,
@Args('limit', {type: () => GraphQLInt, defaultValue: 50, nullable: true}) limit: number,
@Args('start', {type: () => Int, defaultValue: 0, nullable: true}) start: number,
@Args('limit', {type: () => Int, defaultValue: 50, nullable: true}) limit: number,
@Context('cache') cache: ContextCache,
): Promise<UserPagerModel> {
const [entities, total] = await this.userService.find(start, limit)

View File

@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common'
import { Args, Context, ID, Query } from '@nestjs/graphql'
import { Roles } from '../../decorator/roles.decorator'
import { UserModel } from '../../dto/user/user.model'
import { UserEntity } from '../../entity/user.entity'
import { UserService } from '../../service/user/user.service'
import { ContextCache } from '../context.cache'
@Injectable()
export class UserQuery {
constructor(
private readonly userService: UserService,
) {
}
@Query(() => UserModel)
@Roles('admin')
public async getUserById(
@Args('id', {type: () => ID}) id: string,
@Context('cache') cache: ContextCache,
): Promise<UserModel> {
const user = await this.userService.findById(id)
cache.add(cache.getCacheKey(UserEntity.name, user.id), user)
return new UserModel(user)
}
}

View File

@ -1,4 +1,4 @@
import { Args, Context, ID, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'
import { Context, Parent, ResolveField, Resolver } from '@nestjs/graphql'
import { Roles } from '../../decorator/roles.decorator'
import { User } from '../../decorator/user.decorator'
import { UserModel } from '../../dto/user/user.model'
@ -13,22 +13,9 @@ export class UserResolver {
) {
}
@Query(() => UserModel)
@Roles('admin')
public async getUserById(
@Args('id', {type: () => ID}) id: string,
@Context('cache') cache: ContextCache,
): Promise<UserModel> {
const user = await this.userService.findById(id)
cache.add(cache.getCacheKey(UserEntity.name, user.id), user)
return new UserModel(user)
}
@ResolveField('roles', () => [String])
@ResolveField(() => [String])
@Roles('user')
async getRoles(
async roles(
@User() user: UserEntity,
@Parent() parent: UserModel,
@Context('cache') cache: ContextCache,
@ -40,7 +27,7 @@ export class UserResolver {
)
}
returnFieldForSuperuser<T>(
private returnFieldForSuperuser<T>(
parent: UserEntity,
user: UserEntity,
callback: (user: UserEntity) => T

View File

@ -0,0 +1,11 @@
import { Injectable } from '@nestjs/common'
import { Query } from '@nestjs/graphql'
import { UserStatisticModel } from '../../dto/user/user.statistic.model'
@Injectable()
export class UserStatisticQuery {
@Query(() => UserStatisticModel)
getUserStatistic(): UserStatisticModel {
return new UserStatisticModel()
}
}

View File

@ -1,4 +1,4 @@
import { Int, Query, ResolveField, Resolver } from '@nestjs/graphql'
import { Int, ResolveField, Resolver } from '@nestjs/graphql'
import { Roles } from '../../decorator/roles.decorator'
import { UserStatisticModel } from '../../dto/user/user.statistic.model'
import { UserStatisticService } from '../../service/user/user.statistic.service'
@ -10,14 +10,9 @@ export class UserStatisticResolver {
) {
}
@Query(() => UserStatisticModel)
getUserStatistic(): UserStatisticModel {
return new UserStatisticModel()
}
@ResolveField('total', () => Int)
@ResolveField(() => Int)
@Roles('admin')
getTotal(): Promise<number> {
total(): Promise<number> {
return this.statisticService.getTotal()
}
}

View File

@ -43,7 +43,15 @@ export class SubmissionService {
}
async findById(id: string): Promise<SubmissionEntity> {
const submission = await this.submissionRepository.findOne(id);
const submission = await this.submissionRepository.findOne(
id,
{
relations: [
'form',
'form.admin',
],
}
);
if (!submission) {
throw new Error('no form found')