From fe3821ad420269b9fbb83d4984bda40392f16777 Mon Sep 17 00:00:00 2001 From: Michael Schramm Date: Mon, 28 Feb 2022 22:50:20 +0100 Subject: [PATCH] stop exposing internal ids and switch over to hashids --- CHANGELOG.md | 2 ++ package.json | 1 + src/app.providers.ts | 6 ++-- src/dto/deleted.model.ts | 4 +-- src/dto/form/form.model.ts | 7 ++-- src/dto/profile/profile.model.ts | 4 +-- src/dto/submission/submission.field.model.ts | 7 ++-- src/dto/submission/submission.model.ts | 9 ++++-- .../submission/submission.progress.model.ts | 7 ++-- src/dto/user/user.model.ts | 7 ++-- src/entity/form.entity.ts | 6 ++++ src/pipe/form/form.by.id.pipe.ts | 24 ++++++++++++++ src/pipe/form/index.ts | 3 ++ src/pipe/index.ts | 9 ++++++ src/pipe/submission/index.ts | 3 ++ src/pipe/submission/submission.by.id.pipe.ts | 19 +++++++++++ src/pipe/user/index.ts | 3 ++ src/pipe/user/user.by.id.pipe.ts | 19 +++++++++++ src/resolver/context.cache.ts | 4 +-- src/resolver/form/form.create.mutation.ts | 6 ++-- src/resolver/form/form.delete.mutation.ts | 12 ++++--- src/resolver/form/form.list.query.ts | 4 ++- src/resolver/form/form.query.ts | 13 ++++---- src/resolver/form/form.resolver.ts | 20 ++++++------ src/resolver/form/form.update.mutation.ts | 6 ++-- src/resolver/profile/profile.query.ts | 8 ++++- .../profile/profile.update.mutation.ts | 6 ++-- .../submission/submission.field.resolver.ts | 2 +- .../submission/submission.finish.mutation.ts | 12 +++---- .../submission/submission.list.query.ts | 15 +++++---- src/resolver/submission/submission.query.ts | 11 +++---- .../submission/submission.resolver.ts | 10 ++++-- .../submission.set.field.mutation.ts | 9 +++--- .../submission/submission.start.mutation.ts | 10 +++--- src/resolver/user/user.delete.mutation.ts | 11 ++++--- src/resolver/user/user.list.query.ts | 4 ++- src/resolver/user/user.query.ts | 15 ++++----- src/resolver/user/user.resolver.ts | 2 +- src/resolver/user/user.update.mutation.ts | 6 ++-- src/service/id.service.ts | 32 +++++++++++++++++++ src/service/index.ts | 2 ++ src/service/submission/submission.service.ts | 2 +- src/service/user/user.delete.service.ts | 2 +- src/service/user/user.service.ts | 2 +- yarn.lock | 5 +++ 45 files changed, 274 insertions(+), 97 deletions(-) create mode 100644 src/pipe/form/form.by.id.pipe.ts create mode 100644 src/pipe/form/index.ts create mode 100644 src/pipe/index.ts create mode 100644 src/pipe/submission/index.ts create mode 100644 src/pipe/submission/submission.by.id.pipe.ts create mode 100644 src/pipe/user/index.ts create mode 100644 src/pipe/user/user.by.id.pipe.ts create mode 100644 src/service/id.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3023479..83f0665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Security +- start using hashids to prevent insights into form ids + ## [1.0.0] - 2022-02-28 ### Added diff --git a/package.json b/package.json index fcaf699..ec10ce5 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "graphql-subscriptions": "^2.0.0", "graphql-tools": "^8.2.0", "handlebars": "^4.7.7", + "hashids": "^2.2.10", "html-to-text": "^8.1.0", "inquirer": "^8.2.0", "ioredis": "^4.28.5", diff --git a/src/app.providers.ts b/src/app.providers.ts index 9df9955..1e8efde 100644 --- a/src/app.providers.ts +++ b/src/app.providers.ts @@ -1,11 +1,13 @@ import { commands } from './command' import { guards } from './guard' +import { pipes } from './pipe' import { resolvers } from './resolver' import { services } from './service' export const providers = [ - ...resolvers, ...commands, - ...services, ...guards, + ...pipes, + ...resolvers, + ...services, ] diff --git a/src/dto/deleted.model.ts b/src/dto/deleted.model.ts index a096cd3..4e1a408 100644 --- a/src/dto/deleted.model.ts +++ b/src/dto/deleted.model.ts @@ -1,8 +1,8 @@ -import { Field, ObjectType } from '@nestjs/graphql' +import { Field, ID, ObjectType } from '@nestjs/graphql' @ObjectType('Deleted') export class DeletedModel { - @Field() + @Field(() => ID) id: string constructor(id: string) { diff --git a/src/dto/form/form.model.ts b/src/dto/form/form.model.ts index 59060d6..306f8fd 100644 --- a/src/dto/form/form.model.ts +++ b/src/dto/form/form.model.ts @@ -3,6 +3,8 @@ import { FormEntity } from '../../entity/form.entity' @ObjectType('Form') export class FormModel { + readonly _id: number + @Field(() => ID) readonly id: string @@ -24,8 +26,9 @@ export class FormModel { @Field() readonly anonymousSubmission: boolean - constructor(form: FormEntity) { - this.id = form.id.toString() + constructor(id: string, form: FormEntity) { + this._id = form.id + this.id = id this.title = form.title this.created = form.created this.lastModified = form.lastModified diff --git a/src/dto/profile/profile.model.ts b/src/dto/profile/profile.model.ts index d8d69f0..1b6ee0a 100644 --- a/src/dto/profile/profile.model.ts +++ b/src/dto/profile/profile.model.ts @@ -7,8 +7,8 @@ export class ProfileModel extends UserModel { @Field(() => [String]) readonly roles: string[] - constructor(user: UserEntity) { - super(user) + constructor(id: string, user: UserEntity) { + super(id, user) this.roles = user.roles } diff --git a/src/dto/submission/submission.field.model.ts b/src/dto/submission/submission.field.model.ts index e13a63d..d7e796d 100644 --- a/src/dto/submission/submission.field.model.ts +++ b/src/dto/submission/submission.field.model.ts @@ -3,6 +3,8 @@ import { SubmissionFieldEntity } from '../../entity/submission.field.entity' @ObjectType('SubmissionField') export class SubmissionFieldModel { + readonly _id: number + @Field(() => ID) readonly id: string @@ -12,8 +14,9 @@ export class SubmissionFieldModel { @Field() readonly type: string - constructor(field: SubmissionFieldEntity) { - this.id = field.id.toString() + constructor(id: string, field: SubmissionFieldEntity) { + this._id = field.id + this.id = id this.value = JSON.stringify(field.content) this.type = field.type } diff --git a/src/dto/submission/submission.model.ts b/src/dto/submission/submission.model.ts index 21b045e..8885df2 100644 --- a/src/dto/submission/submission.model.ts +++ b/src/dto/submission/submission.model.ts @@ -5,8 +5,10 @@ import { GeoLocationModel } from './geo.location.model' @ObjectType('Submission') export class SubmissionModel { + readonly _id: number + @Field(() => ID) - readonly id: number + readonly id: string @Field() readonly ipAddr: string @@ -29,8 +31,9 @@ export class SubmissionModel { @Field({ nullable: true }) readonly lastModified?: Date - constructor(submission: SubmissionEntity) { - this.id = submission.id + constructor(id: string, submission: SubmissionEntity) { + this._id = submission.id + this.id = id this.ipAddr = submission.ipAddr this.geoLocation = new GeoLocationModel(submission.geoLocation) diff --git a/src/dto/submission/submission.progress.model.ts b/src/dto/submission/submission.progress.model.ts index 8695340..8a6ebb7 100644 --- a/src/dto/submission/submission.progress.model.ts +++ b/src/dto/submission/submission.progress.model.ts @@ -3,6 +3,8 @@ import { SubmissionEntity } from '../../entity/submission.entity' @ObjectType('SubmissionProgress') export class SubmissionProgressModel { + readonly _id: number + @Field(() => ID) readonly id: string @@ -18,8 +20,9 @@ export class SubmissionProgressModel { @Field({ nullable: true }) readonly lastModified?: Date - constructor(submission: Partial) { - this.id = submission.id.toString() + constructor(id: string, submission: Partial) { + this._id = submission.id + this.id = id this.timeElapsed = submission.timeElapsed this.percentageComplete = submission.percentageComplete diff --git a/src/dto/user/user.model.ts b/src/dto/user/user.model.ts index e790292..bae5007 100644 --- a/src/dto/user/user.model.ts +++ b/src/dto/user/user.model.ts @@ -3,6 +3,8 @@ import { UserEntity } from '../../entity/user.entity' @ObjectType('User') export class UserModel { + readonly _id: number + @Field(() => ID) readonly id: string @@ -36,8 +38,9 @@ export class UserModel { @Field({ nullable: true }) readonly lastModified: Date - constructor(user: UserEntity) { - this.id = user.id.toString() + constructor(id: string, user: UserEntity) { + this._id = user.id + this.id = id this.username = user.username this.email = user.email diff --git a/src/entity/form.entity.ts b/src/entity/form.entity.ts index 2c6f713..74c1103 100644 --- a/src/entity/form.entity.ts +++ b/src/entity/form.entity.ts @@ -72,4 +72,10 @@ export class FormEntity { @UpdateDateColumn() public lastModified: Date + + constructor(partial?: Partial) { + if (partial) { + Object.assign(this, partial) + } + } } diff --git a/src/pipe/form/form.by.id.pipe.ts b/src/pipe/form/form.by.id.pipe.ts new file mode 100644 index 0000000..babae99 --- /dev/null +++ b/src/pipe/form/form.by.id.pipe.ts @@ -0,0 +1,24 @@ +import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common' +import { FormEntity } from '../../entity/form.entity' +import { FormService } from '../../service/form/form.service' +import { IdService } from '../../service/id.service' + +@Injectable() +export class FormByIdPipe implements PipeTransform> { + constructor( + private readonly formService: FormService, + private readonly idService: IdService, + ) { + } + + async transform(value: string, metadata: ArgumentMetadata): Promise { + const id = this.idService.decode(value) + + console.log({ + id, + value, + }) + + return await this.formService.findById(id) + } +} diff --git a/src/pipe/form/index.ts b/src/pipe/form/index.ts new file mode 100644 index 0000000..2d90edb --- /dev/null +++ b/src/pipe/form/index.ts @@ -0,0 +1,3 @@ +import { FormByIdPipe } from './form.by.id.pipe' + +export const formPipes = [FormByIdPipe] diff --git a/src/pipe/index.ts b/src/pipe/index.ts new file mode 100644 index 0000000..ceeb99a --- /dev/null +++ b/src/pipe/index.ts @@ -0,0 +1,9 @@ +import { formPipes } from './form' +import { submissionPipes } from './submission' +import { userPipes } from './user' + +export const pipes = [ + ...formPipes, + ...submissionPipes, + ...userPipes, +] diff --git a/src/pipe/submission/index.ts b/src/pipe/submission/index.ts new file mode 100644 index 0000000..71f9350 --- /dev/null +++ b/src/pipe/submission/index.ts @@ -0,0 +1,3 @@ +import { SubmissionByIdPipe } from './submission.by.id.pipe' + +export const submissionPipes = [SubmissionByIdPipe] diff --git a/src/pipe/submission/submission.by.id.pipe.ts b/src/pipe/submission/submission.by.id.pipe.ts new file mode 100644 index 0000000..db6a7df --- /dev/null +++ b/src/pipe/submission/submission.by.id.pipe.ts @@ -0,0 +1,19 @@ +import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common' +import { SubmissionEntity } from '../../entity/submission.entity' +import { IdService } from '../../service/id.service' +import { SubmissionService } from '../../service/submission/submission.service' + +@Injectable() +export class SubmissionByIdPipe implements PipeTransform> { + constructor( + private readonly submissionService: SubmissionService, + private readonly idService: IdService, + ) { + } + + async transform(value: string, metadata: ArgumentMetadata): Promise { + const id = this.idService.decode(value) + + return await this.submissionService.findById(id) + } +} diff --git a/src/pipe/user/index.ts b/src/pipe/user/index.ts new file mode 100644 index 0000000..425d7b2 --- /dev/null +++ b/src/pipe/user/index.ts @@ -0,0 +1,3 @@ +import { UserByIdPipe } from './user.by.id.pipe' + +export const userPipes = [UserByIdPipe] diff --git a/src/pipe/user/user.by.id.pipe.ts b/src/pipe/user/user.by.id.pipe.ts new file mode 100644 index 0000000..ef3d624 --- /dev/null +++ b/src/pipe/user/user.by.id.pipe.ts @@ -0,0 +1,19 @@ +import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common' +import { UserEntity } from '../../entity/user.entity' +import { IdService } from '../../service/id.service' +import { UserService } from '../../service/user/user.service' + +@Injectable() +export class UserByIdPipe implements PipeTransform> { + constructor( + private readonly userService: UserService, + private readonly idService: IdService, + ) { + } + + async transform(value: string, metadata: ArgumentMetadata): Promise { + const id = this.idService.decode(value) + + return await this.userService.findById(id) + } +} diff --git a/src/resolver/context.cache.ts b/src/resolver/context.cache.ts index eb07369..2a05b99 100644 --- a/src/resolver/context.cache.ts +++ b/src/resolver/context.cache.ts @@ -1,12 +1,10 @@ -type ID = string | number - export class ContextCache { private cache: { [key: string]: any } = {} - public getCacheKey(type: string, id: ID): string { + public getCacheKey(type: string, id: number): string { return `${type}:${id}` } diff --git a/src/resolver/form/form.create.mutation.ts b/src/resolver/form/form.create.mutation.ts index b50094b..791bd43 100644 --- a/src/resolver/form/form.create.mutation.ts +++ b/src/resolver/form/form.create.mutation.ts @@ -7,12 +7,14 @@ import { FormModel } from '../../dto/form/form.model' import { FormEntity } from '../../entity/form.entity' import { UserEntity } from '../../entity/user.entity' import { FormCreateService } from '../../service/form/form.create.service' +import { IdService } from '../../service/id.service' import { ContextCache } from '../context.cache' @Injectable() export class FormCreateMutation { constructor( - private readonly createService: FormCreateService + private readonly createService: FormCreateService, + private readonly idService: IdService, ) { } @@ -27,6 +29,6 @@ export class FormCreateMutation { cache.add(cache.getCacheKey(FormEntity.name, form.id), form) - return new FormModel(form) + return new FormModel(this.idService.encode(form.id), form) } } diff --git a/src/resolver/form/form.delete.mutation.ts b/src/resolver/form/form.delete.mutation.ts index bb95ae6..223436c 100644 --- a/src/resolver/form/form.delete.mutation.ts +++ b/src/resolver/form/form.delete.mutation.ts @@ -3,15 +3,19 @@ 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 { FormEntity } from '../../entity/form.entity' import { UserEntity } from '../../entity/user.entity' +import { FormByIdPipe } from '../../pipe/form/form.by.id.pipe' import { FormDeleteService } from '../../service/form/form.delete.service' import { FormService } from '../../service/form/form.service' +import { IdService } from '../../service/id.service' @Injectable() export class FormDeleteMutation { constructor( private readonly deleteService: FormDeleteService, private readonly formService: FormService, + private readonly idService: IdService, ) { } @@ -19,16 +23,14 @@ export class FormDeleteMutation { @Roles('admin') async deleteForm( @User() user: UserEntity, - @Args({ name: 'id', type: () => ID}) id: string, + @Args('id', {type: () => ID}, FormByIdPipe) form: FormEntity, ): Promise { - const form = await this.formService.findById(id) - if (!form.isLive && !this.formService.isAdmin(form, user)) { throw new Error('invalid form') } - await this.deleteService.delete(id) + await this.deleteService.delete(form.id) - return new DeletedModel(id) + return new DeletedModel(this.idService.encode(form.id)) } } diff --git a/src/resolver/form/form.list.query.ts b/src/resolver/form/form.list.query.ts index 14c154b..47ac096 100644 --- a/src/resolver/form/form.list.query.ts +++ b/src/resolver/form/form.list.query.ts @@ -7,12 +7,14 @@ import { FormPagerModel } from '../../dto/form/form.pager.model' import { FormEntity } from '../../entity/form.entity' import { UserEntity } from '../../entity/user.entity' import { FormService } from '../../service/form/form.service' +import { IdService } from '../../service/id.service' import { ContextCache } from '../context.cache' @Injectable() export class FormListQuery { constructor( private readonly formService: FormService, + private readonly idService: IdService, ) { } @@ -34,7 +36,7 @@ export class FormListQuery { forms.forEach(form => cache.add(cache.getCacheKey(FormEntity.name, form.id), form)) return new FormPagerModel( - forms.map(form => new FormModel(form)), + forms.map(form => new FormModel(this.idService.encode(form.id), form)), total, limit, start, diff --git a/src/resolver/form/form.query.ts b/src/resolver/form/form.query.ts index b571320..bb4a48d 100644 --- a/src/resolver/form/form.query.ts +++ b/src/resolver/form/form.query.ts @@ -4,30 +4,31 @@ import { User } from '../../decorator/user.decorator' import { FormModel } from '../../dto/form/form.model' import { FormEntity } from '../../entity/form.entity' import { UserEntity } from '../../entity/user.entity' +import { FormByIdPipe } from '../../pipe/form/form.by.id.pipe' import { FormService } from '../../service/form/form.service' +import { IdService } from '../../service/id.service' import { ContextCache } from '../context.cache' @Injectable() export class FormQuery { constructor( private readonly formService: FormService, + private readonly idService: IdService, ) { } @Query(() => FormModel) - async getFormById( + getFormById( @User() user: UserEntity, - @Args('id', {type: () => ID}) id, + @Args('id', {type: () => ID}, FormByIdPipe) form: FormEntity, @Context('cache') cache: ContextCache, - ): Promise { - const form = await this.formService.findById(id) - + ): FormModel { if (!form.isLive && !this.formService.isAdmin(form, user)) { throw new Error('invalid form') } cache.add(cache.getCacheKey(FormEntity.name, form.id), form) - return new FormModel(form) + return new FormModel(this.idService.encode(form.id), form) } } diff --git a/src/resolver/form/form.resolver.ts b/src/resolver/form/form.resolver.ts index 339268c..4dd9262 100644 --- a/src/resolver/form/form.resolver.ts +++ b/src/resolver/form/form.resolver.ts @@ -11,12 +11,14 @@ import { UserModel } from '../../dto/user/user.model' import { FormEntity } from '../../entity/form.entity' import { UserEntity } from '../../entity/user.entity' import { FormService } from '../../service/form/form.service' +import { IdService } from '../../service/id.service' import { ContextCache } from '../context.cache' @Resolver(() => FormModel) export class FormResolver { constructor( private readonly formService: FormService, + private readonly idService: IdService, ) { } @@ -26,7 +28,7 @@ export class FormResolver { @Parent() parent: FormModel, @Context('cache') cache: ContextCache, ): Promise { - const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id)) + const form = await cache.get(cache.getCacheKey(FormEntity.name, parent._id)) return form.fields?.map(field => new FormFieldModel(field)).sort((a,b) => a.idx - b.idx) || [] } @@ -37,7 +39,7 @@ export class FormResolver { @Parent() parent: FormModel, @Context('cache') cache: ContextCache, ): Promise { - const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id)) + const form = await cache.get(cache.getCacheKey(FormEntity.name, parent._id)) return form.hooks?.map(hook => new FormHookModel(hook)) || [] } @@ -49,7 +51,7 @@ export class FormResolver { @Parent() parent: FormModel, @Context('cache') cache: ContextCache, ): Promise { - const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id)) + const form = await cache.get(cache.getCacheKey(FormEntity.name, parent._id)) if (!this.formService.isAdmin(form, user)) { throw new Error('no access to field') @@ -65,7 +67,7 @@ export class FormResolver { @Parent() parent: FormModel, @Context('cache') cache: ContextCache, ): Promise { - const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id)) + const form = await cache.get(cache.getCacheKey(FormEntity.name, parent._id)) if (!this.formService.isAdmin(form, user)) { throw new Error('no access to field') @@ -80,7 +82,7 @@ export class FormResolver { @Parent() parent: FormModel, @Context('cache') cache: ContextCache, ): Promise { - const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id)) + const form = await cache.get(cache.getCacheKey(FormEntity.name, parent._id)) return new DesignModel(form.design) } @@ -90,7 +92,7 @@ export class FormResolver { @Parent() parent: FormModel, @Context('cache') cache: ContextCache, ): Promise { - const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id)) + const form = await cache.get(cache.getCacheKey(FormEntity.name, parent._id)) return new PageModel(form.startPage) } @@ -100,7 +102,7 @@ export class FormResolver { @Parent() parent: FormModel, @Context('cache') cache: ContextCache, ): Promise { - const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id)) + const form = await cache.get(cache.getCacheKey(FormEntity.name, parent._id)) return new PageModel(form.endPage) } @@ -111,12 +113,12 @@ export class FormResolver { @Parent() parent: FormModel, @Context('cache') cache: ContextCache, ): Promise { - const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id)) + const form = await cache.get(cache.getCacheKey(FormEntity.name, parent._id)) if (!form.admin) { return null } - return new UserModel(form.admin) + return new UserModel(this.idService.encode(form.admin.id), form.admin) } } diff --git a/src/resolver/form/form.update.mutation.ts b/src/resolver/form/form.update.mutation.ts index 1d4f158..62485ed 100644 --- a/src/resolver/form/form.update.mutation.ts +++ b/src/resolver/form/form.update.mutation.ts @@ -8,6 +8,7 @@ import { FormEntity } from '../../entity/form.entity' import { UserEntity } from '../../entity/user.entity' import { FormService } from '../../service/form/form.service' import { FormUpdateService } from '../../service/form/form.update.service' +import { IdService } from '../../service/id.service' import { ContextCache } from '../context.cache' @Injectable() @@ -15,6 +16,7 @@ export class FormUpdateMutation { constructor( private readonly updateService: FormUpdateService, private readonly formService: FormService, + private readonly idService: IdService, ) { } @@ -25,7 +27,7 @@ export class FormUpdateMutation { @Args({ name: 'form', type: () => FormUpdateInput }) input: FormUpdateInput, @Context('cache') cache: ContextCache, ): Promise { - const form = await this.formService.findById(input.id) + const form = await this.formService.findById(this.idService.decode(input.id)) if (!form.isLive && !this.formService.isAdmin(form, user)) { throw new Error('invalid form') @@ -35,6 +37,6 @@ export class FormUpdateMutation { cache.add(cache.getCacheKey(FormEntity.name, form.id), form) - return new FormModel(form) + return new FormModel(this.idService.encode(form.id), form) } } diff --git a/src/resolver/profile/profile.query.ts b/src/resolver/profile/profile.query.ts index 41d425b..ce82125 100644 --- a/src/resolver/profile/profile.query.ts +++ b/src/resolver/profile/profile.query.ts @@ -4,10 +4,16 @@ import { Roles } from '../../decorator/roles.decorator' import { User } from '../../decorator/user.decorator' import { ProfileModel } from '../../dto/profile/profile.model' import { UserEntity } from '../../entity/user.entity' +import { IdService } from '../../service/id.service' import { ContextCache } from '../context.cache' @Injectable() export class ProfileQuery { + constructor( + private readonly idService: IdService, + ) { + } + @Query(() => ProfileModel) @Roles('user') public me( @@ -16,6 +22,6 @@ export class ProfileQuery { ): ProfileModel { cache.add(cache.getCacheKey(UserEntity.name, user.id), user) - return new ProfileModel(user) + return new ProfileModel(this.idService.encode(user.id), user) } } diff --git a/src/resolver/profile/profile.update.mutation.ts b/src/resolver/profile/profile.update.mutation.ts index c289c7a..1fbce67 100644 --- a/src/resolver/profile/profile.update.mutation.ts +++ b/src/resolver/profile/profile.update.mutation.ts @@ -5,6 +5,7 @@ import { User } from '../../decorator/user.decorator' import { ProfileModel } from '../../dto/profile/profile.model' import { ProfileUpdateInput } from '../../dto/profile/profile.update.input' import { UserEntity } from '../../entity/user.entity' +import { IdService } from '../../service/id.service' import { ProfileUpdateService } from '../../service/profile/profile.update.service' import { ContextCache } from '../context.cache' @@ -12,6 +13,7 @@ import { ContextCache } from '../context.cache' export class ProfileUpdateMutation { constructor( private readonly updateService: ProfileUpdateService, + private readonly idService: IdService, ) { } @@ -26,7 +28,7 @@ export class ProfileUpdateMutation { cache.add(cache.getCacheKey(UserEntity.name, user.id), user) - return new ProfileModel(user) + return new ProfileModel(this.idService.encode(user.id), user) } @Mutation(() => ProfileModel) @@ -40,6 +42,6 @@ export class ProfileUpdateMutation { cache.add(cache.getCacheKey(UserEntity.name, user.id), user) - return new ProfileModel(user) + return new ProfileModel(this.idService.encode(user.id), user) } } diff --git a/src/resolver/submission/submission.field.resolver.ts b/src/resolver/submission/submission.field.resolver.ts index c310408..509c9f0 100644 --- a/src/resolver/submission/submission.field.resolver.ts +++ b/src/resolver/submission/submission.field.resolver.ts @@ -19,7 +19,7 @@ export class SubmissionFieldResolver { @Context('cache') cache: ContextCache, ): Promise { const submissionField = await cache.get( - cache.getCacheKey(SubmissionFieldEntity.name, parent.id) + cache.getCacheKey(SubmissionFieldEntity.name, parent._id) ) const field = await cache.get( diff --git a/src/resolver/submission/submission.finish.mutation.ts b/src/resolver/submission/submission.finish.mutation.ts index c4e5db2..8bddbab 100644 --- a/src/resolver/submission/submission.finish.mutation.ts +++ b/src/resolver/submission/submission.finish.mutation.ts @@ -2,33 +2,31 @@ import { Injectable } from '@nestjs/common' import { Args, Context, ID, Mutation } from '@nestjs/graphql' import { User } from '../../decorator/user.decorator' import { SubmissionProgressModel } from '../../dto/submission/submission.progress.model' -import { SubmissionSetFieldInput } from '../../dto/submission/submission.set.field.input' import { SubmissionEntity } from '../../entity/submission.entity' import { UserEntity } from '../../entity/user.entity' -import { SubmissionService } from '../../service/submission/submission.service' +import { SubmissionByIdPipe } from '../../pipe/submission/submission.by.id.pipe' +import { IdService } from '../../service/id.service' import { SubmissionSetFieldService } from '../../service/submission/submission.set.field.service' import { ContextCache } from '../context.cache' @Injectable() export class SubmissionFinishMutation { constructor( - private readonly submissionService: SubmissionService, private readonly setFieldService: SubmissionSetFieldService, + private readonly idService: IdService, ) { } @Mutation(() => SubmissionProgressModel) async submissionFinish( @User() user: UserEntity, - @Args({ name: 'submission', type: () => ID }) id: string, + @Args({ name: 'submission', type: () => ID }, SubmissionByIdPipe) submission: SubmissionEntity, @Context('cache') cache: ContextCache, ): Promise { - const submission = await this.submissionService.findById(id) - await this.setFieldService.finishSubmission(submission) cache.add(cache.getCacheKey(SubmissionEntity.name, submission.id), submission) - return new SubmissionProgressModel(submission) + return new SubmissionProgressModel(this.idService.encode(submission.id), submission) } } diff --git a/src/resolver/submission/submission.list.query.ts b/src/resolver/submission/submission.list.query.ts index efca303..cda96bb 100644 --- a/src/resolver/submission/submission.list.query.ts +++ b/src/resolver/submission/submission.list.query.ts @@ -4,31 +4,31 @@ import { User } from '../../decorator/user.decorator' import { SubmissionModel } from '../../dto/submission/submission.model' import { SubmissionPagerFilterInput } from '../../dto/submission/submission.pager.filter.input' import { SubmissionPagerModel } from '../../dto/submission/submission.pager.model' +import { FormEntity } from '../../entity/form.entity' import { SubmissionEntity } from '../../entity/submission.entity' import { UserEntity } from '../../entity/user.entity' -import { FormService } from '../../service/form/form.service' +import { FormByIdPipe } from '../../pipe/form/form.by.id.pipe' +import { IdService } from '../../service/id.service' import { SubmissionService } from '../../service/submission/submission.service' import { ContextCache } from '../context.cache' @Injectable() export class SubmissionListQuery { constructor( - private readonly formService: FormService, private readonly submissionService: SubmissionService, + private readonly idService: IdService, ) { } @Query(() => SubmissionPagerModel) async listSubmissions( @User() user: UserEntity, - @Args('form', {type: () => ID}) id: string, + @Args('form', {type: () => ID}, FormByIdPipe) form: FormEntity, @Args('start', {type: () => Int, defaultValue: 0, nullable: true}) start: number, @Args('limit', {type: () => Int, defaultValue: 50, nullable: true}) limit: number, @Args('filter', {type: () => SubmissionPagerFilterInput, defaultValue: new SubmissionPagerFilterInput()}) filter: SubmissionPagerFilterInput, @Context('cache') cache: ContextCache, ): Promise { - const form = await this.formService.findById(id) - const [submissions, total] = await this.submissionService.find( form, start, @@ -42,7 +42,10 @@ export class SubmissionListQuery { }) return new SubmissionPagerModel( - submissions.map(submission => new SubmissionModel(submission)), + submissions.map(submission => new SubmissionModel( + this.idService.encode(submission.id), + submission + )), total, limit, start, diff --git a/src/resolver/submission/submission.query.ts b/src/resolver/submission/submission.query.ts index 274a5f2..eedd71a 100644 --- a/src/resolver/submission/submission.query.ts +++ b/src/resolver/submission/submission.query.ts @@ -4,8 +4,9 @@ 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 { SubmissionByIdPipe } from '../../pipe/submission/submission.by.id.pipe' import { FormService } from '../../service/form/form.service' -import { SubmissionService } from '../../service/submission/submission.service' +import { IdService } from '../../service/id.service' import { SubmissionTokenService } from '../../service/submission/submission.token.service' import { ContextCache } from '../context.cache' @@ -13,20 +14,18 @@ import { ContextCache } from '../context.cache' export class SubmissionQuery { constructor( private readonly formService: FormService, - private readonly submissionService: SubmissionService, private readonly tokenService: SubmissionTokenService, + private readonly idService: IdService, ) { } @Query(() => SubmissionModel) async getSubmissionById( @User() user: UserEntity, - @Args('id', {type: () => ID}) id: string, + @Args('id', {type: () => ID}, SubmissionByIdPipe) submission: SubmissionEntity, @Args('token', {nullable: true}) token: string, @Context('cache') cache: ContextCache, ): Promise { - const submission = await this.submissionService.findById(id) - if ( !await this.tokenService.verify(token, submission.tokenHash) && !this.formService.isAdmin(submission.form, user) @@ -36,6 +35,6 @@ export class SubmissionQuery { cache.add(cache.getCacheKey(SubmissionEntity.name, submission.id), submission) - return new SubmissionModel(submission) + return new SubmissionModel(this.idService.encode(submission.id), submission) } } diff --git a/src/resolver/submission/submission.resolver.ts b/src/resolver/submission/submission.resolver.ts index cfaaef7..1025244 100644 --- a/src/resolver/submission/submission.resolver.ts +++ b/src/resolver/submission/submission.resolver.ts @@ -5,10 +5,16 @@ import { SubmissionModel } from '../../dto/submission/submission.model' import { SubmissionEntity } from '../../entity/submission.entity' import { SubmissionFieldEntity } from '../../entity/submission.field.entity' import { UserEntity } from '../../entity/user.entity' +import { IdService } from '../../service/id.service' import { ContextCache } from '../context.cache' @Resolver(() => SubmissionModel) export class SubmissionResolver { + constructor( + private readonly idService: IdService, + ) { + } + @ResolveField(() => [SubmissionFieldModel]) async fields( @User() user: UserEntity, @@ -16,12 +22,12 @@ export class SubmissionResolver { @Context('cache') cache: ContextCache, ): Promise { const submission = await cache.get( - cache.getCacheKey(SubmissionEntity.name, parent.id) + cache.getCacheKey(SubmissionEntity.name, parent._id) ) return submission.fields.map(field => { cache.add(cache.getCacheKey(SubmissionFieldEntity.name, field.id), field) - return new SubmissionFieldModel(field) + return new SubmissionFieldModel(this.idService.encode(field.id), field) }) } } diff --git a/src/resolver/submission/submission.set.field.mutation.ts b/src/resolver/submission/submission.set.field.mutation.ts index 3f937f2..c9be89e 100644 --- a/src/resolver/submission/submission.set.field.mutation.ts +++ b/src/resolver/submission/submission.set.field.mutation.ts @@ -5,6 +5,8 @@ import { SubmissionProgressModel } from '../../dto/submission/submission.progres import { SubmissionSetFieldInput } from '../../dto/submission/submission.set.field.input' import { SubmissionEntity } from '../../entity/submission.entity' import { UserEntity } from '../../entity/user.entity' +import { SubmissionByIdPipe } from '../../pipe/submission/submission.by.id.pipe' +import { IdService } from '../../service/id.service' import { SubmissionService } from '../../service/submission/submission.service' import { SubmissionSetFieldService } from '../../service/submission/submission.set.field.service' import { ContextCache } from '../context.cache' @@ -14,18 +16,17 @@ export class SubmissionSetFieldMutation { constructor( private readonly submissionService: SubmissionService, private readonly setFieldService: SubmissionSetFieldService, + private readonly idService: IdService, ) { } @Mutation(() => SubmissionProgressModel) async submissionSetField( @User() user: UserEntity, - @Args({ name: 'submission', type: () => ID }) id: string, + @Args({ name: 'submission', type: () => ID }, SubmissionByIdPipe) submission: SubmissionEntity, @Args({ name: 'field', type: () => SubmissionSetFieldInput }) input: SubmissionSetFieldInput, @Context('cache') cache: ContextCache, ): Promise { - const submission = await this.submissionService.findById(id) - if (!await this.submissionService.isOwner(submission, input.token)) { throw new Error('no access to submission') } @@ -34,6 +35,6 @@ export class SubmissionSetFieldMutation { cache.add(cache.getCacheKey(SubmissionEntity.name, submission.id), submission) - return new SubmissionProgressModel(submission) + return new SubmissionProgressModel(this.idService.encode(submission.id), submission) } } diff --git a/src/resolver/submission/submission.start.mutation.ts b/src/resolver/submission/submission.start.mutation.ts index af8a8c2..23f1267 100644 --- a/src/resolver/submission/submission.start.mutation.ts +++ b/src/resolver/submission/submission.start.mutation.ts @@ -4,9 +4,12 @@ import { IpAddress } from '../../decorator/ip.address.decorator' import { User } from '../../decorator/user.decorator' import { SubmissionProgressModel } from '../../dto/submission/submission.progress.model' import { SubmissionStartInput } from '../../dto/submission/submission.start.input' +import { FormEntity } from '../../entity/form.entity' import { SubmissionEntity } from '../../entity/submission.entity' import { UserEntity } from '../../entity/user.entity' +import { FormByIdPipe } from '../../pipe/form/form.by.id.pipe' import { FormService } from '../../service/form/form.service' +import { IdService } from '../../service/id.service' import { SubmissionStartService } from '../../service/submission/submission.start.service' import { ContextCache } from '../context.cache' @@ -15,19 +18,18 @@ export class SubmissionStartMutation { constructor( private readonly startService: SubmissionStartService, private readonly formService: FormService, + private readonly idService: IdService, ) { } @Mutation(() => SubmissionProgressModel) async submissionStart( @User() user: UserEntity, - @Args({ name: 'form', type: () => ID }) id: string, + @Args({ name: 'form', type: () => ID }, FormByIdPipe) form: FormEntity, @Args({ name: 'submission', type: () => SubmissionStartInput }) input: SubmissionStartInput, @IpAddress() ipAddr: string, @Context('cache') cache: ContextCache, ): Promise { - const form = await this.formService.findById(id) - if (!form.isLive && !this.formService.isAdmin(form, user)) { throw new Error('invalid form') } @@ -36,6 +38,6 @@ export class SubmissionStartMutation { cache.add(cache.getCacheKey(SubmissionEntity.name, submission.id), submission) - return new SubmissionProgressModel(submission) + return new SubmissionProgressModel(this.idService.encode(submission.id), submission) } } diff --git a/src/resolver/user/user.delete.mutation.ts b/src/resolver/user/user.delete.mutation.ts index ce186df..aebedde 100644 --- a/src/resolver/user/user.delete.mutation.ts +++ b/src/resolver/user/user.delete.mutation.ts @@ -4,12 +4,15 @@ import { Roles } from '../../decorator/roles.decorator' import { User } from '../../decorator/user.decorator' import { DeletedModel } from '../../dto/deleted.model' import { UserEntity } from '../../entity/user.entity' +import { UserByIdPipe } from '../../pipe/user/user.by.id.pipe' +import { IdService } from '../../service/id.service' import { UserDeleteService } from '../../service/user/user.delete.service' @Injectable() export class UserDeleteMutation { constructor( private readonly deleteService: UserDeleteService, + private readonly idService: IdService, ) { } @@ -17,14 +20,14 @@ export class UserDeleteMutation { @Roles('admin') async deleteUser( @User() auth: UserEntity, - @Args({ name: 'id', type: () => ID}) id: string, + @Args({ name: 'id', type: () => ID}, UserByIdPipe) user: UserEntity, ): Promise { - if (auth.id.toString() === id) { + if (auth.id === user.id) { throw new Error('cannot delete your own user') } - await this.deleteService.delete(id) + await this.deleteService.delete(user.id) - return new DeletedModel(id) + return new DeletedModel(this.idService.encode(user.id)) } } diff --git a/src/resolver/user/user.list.query.ts b/src/resolver/user/user.list.query.ts index 7ffbdfe..b7631b7 100644 --- a/src/resolver/user/user.list.query.ts +++ b/src/resolver/user/user.list.query.ts @@ -3,6 +3,7 @@ import { Roles } from '../../decorator/roles.decorator' import { UserModel } from '../../dto/user/user.model' import { UserPagerModel } from '../../dto/user/user.pager.model' import { UserEntity } from '../../entity/user.entity' +import { IdService } from '../../service/id.service' import { UserService } from '../../service/user/user.service' import { ContextCache } from '../context.cache' @@ -10,6 +11,7 @@ import { ContextCache } from '../context.cache' export class UserListQuery { constructor( private readonly userService: UserService, + private readonly idService: IdService, ) { } @@ -25,7 +27,7 @@ export class UserListQuery { return new UserPagerModel( entities.map(entity => { cache.add(cache.getCacheKey(UserEntity.name, entity.id), entity) - return new UserModel(entity) + return new UserModel(this.idService.encode(entity.id), entity) }), total, limit, diff --git a/src/resolver/user/user.query.ts b/src/resolver/user/user.query.ts index 77539be..8d6785d 100644 --- a/src/resolver/user/user.query.ts +++ b/src/resolver/user/user.query.ts @@ -3,26 +3,25 @@ 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 { UserByIdPipe } from '../../pipe/user/user.by.id.pipe' +import { IdService } from '../../service/id.service' import { ContextCache } from '../context.cache' @Injectable() export class UserQuery { constructor( - private readonly userService: UserService, + private readonly idService: IdService, ) { } @Query(() => UserModel) @Roles('admin') - public async getUserById( - @Args('id', {type: () => ID}) id: string, + public getUserById( + @Args('id', {type: () => ID}, UserByIdPipe) user: UserEntity, @Context('cache') cache: ContextCache, - ): Promise { - const user = await this.userService.findById(id) - + ): UserModel { cache.add(cache.getCacheKey(UserEntity.name, user.id), user) - return new UserModel(user) + return new UserModel(this.idService.encode(user.id), user) } } diff --git a/src/resolver/user/user.resolver.ts b/src/resolver/user/user.resolver.ts index 9d5af8c..1d47b1f 100644 --- a/src/resolver/user/user.resolver.ts +++ b/src/resolver/user/user.resolver.ts @@ -21,7 +21,7 @@ export class UserResolver { @Context('cache') cache: ContextCache, ): Promise { return this.returnFieldForSuperuser( - await cache.get(cache.getCacheKey(UserEntity.name, parent.id)), + await cache.get(cache.getCacheKey(UserEntity.name, parent._id)), user, c => c.roles ) diff --git a/src/resolver/user/user.update.mutation.ts b/src/resolver/user/user.update.mutation.ts index da07eb4..b401594 100644 --- a/src/resolver/user/user.update.mutation.ts +++ b/src/resolver/user/user.update.mutation.ts @@ -5,6 +5,7 @@ import { User } from '../../decorator/user.decorator' import { UserModel } from '../../dto/user/user.model' import { UserUpdateInput } from '../../dto/user/user.update.input' import { UserEntity } from '../../entity/user.entity' +import { IdService } from '../../service/id.service' import { UserService } from '../../service/user/user.service' import { UserUpdateService } from '../../service/user/user.update.service' import { ContextCache } from '../context.cache' @@ -14,6 +15,7 @@ export class UserUpdateMutation { constructor( private readonly updateService: UserUpdateService, private readonly userService: UserService, + private readonly idService: IdService, ) { } @@ -28,12 +30,12 @@ export class UserUpdateMutation { throw new Error('cannot update your own user') } - const user = await this.userService.findById(input.id) + const user = await this.userService.findById(this.idService.decode(input.id)) await this.updateService.update(user, input) cache.add(cache.getCacheKey(UserEntity.name, user.id), user) - return new UserModel(user) + return new UserModel(this.idService.encode(user.id), user) } } diff --git a/src/service/id.service.ts b/src/service/id.service.ts new file mode 100644 index 0000000..a6b87ef --- /dev/null +++ b/src/service/id.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' +import Hashids from 'hashids' + +@Injectable() +export class IdService { + private readonly hashids: Hashids + + constructor( + readonly config: ConfigService + ) { + this.hashids = new Hashids(config.get('SECRET_KEY'), 6) + } + + public encode(id: number): string { + return this.hashids.encode([ id ]) + } + + public decode(raw: string): number { + if (!this.hashids.isValidId(raw)) { + throw new Error('invalid id passed') + } + + const results: number[] = this.hashids.decode(raw) as number[] + + if (results[0] === undefined) { + throw new Error('invalid id passed') + } + + return results[0] + } +} diff --git a/src/service/index.ts b/src/service/index.ts index 74ae91a..b05082f 100644 --- a/src/service/index.ts +++ b/src/service/index.ts @@ -5,6 +5,7 @@ import Redis from 'ioredis' import { PinoLogger } from 'nestjs-pino' import { authServices } from './auth' import { formServices } from './form' +import { IdService } from './id.service' import { InstallationMetricsService } from './installation.metrics.service' import { MailService } from './mail.service' import { profileServices } from './profile' @@ -44,4 +45,5 @@ export const services = [ }) }, }, + IdService, ] diff --git a/src/service/submission/submission.service.ts b/src/service/submission/submission.service.ts index 5bfc0d3..51fe25c 100644 --- a/src/service/submission/submission.service.ts +++ b/src/service/submission/submission.service.ts @@ -58,7 +58,7 @@ export class SubmissionService { return await qb.getManyAndCount() } - async findById(id: string): Promise { + async findById(id: number): Promise { const submission = await this.submissionRepository.findOne(id); if (!submission) { diff --git a/src/service/user/user.delete.service.ts b/src/service/user/user.delete.service.ts index 1b82771..96c2358 100644 --- a/src/service/user/user.delete.service.ts +++ b/src/service/user/user.delete.service.ts @@ -11,7 +11,7 @@ export class UserDeleteService { ) { } - async delete(id: string): Promise { + async delete(id: number): Promise { await this.userRepository.delete(id) } } diff --git a/src/service/user/user.service.ts b/src/service/user/user.service.ts index c847650..303adab 100644 --- a/src/service/user/user.service.ts +++ b/src/service/user/user.service.ts @@ -26,7 +26,7 @@ export class UserService { return await qb.getManyAndCount() } - async findById(id: string): Promise { + async findById(id: number): Promise { const user = await this.userRepository.findOne(id); if (!user) { diff --git a/yarn.lock b/yarn.lock index 8505112..27509b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4392,6 +4392,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hashids@^2.2.10: + version "2.2.10" + resolved "https://registry.yarnpkg.com/hashids/-/hashids-2.2.10.tgz#82f45538cf03ce73e31b78d1abe78d287cf760c4" + integrity sha512-nXnYums7F8B5Y+GSThutLPlKMaamW1yjWNZVt0WModiJfdjaDZHnhYTWblS+h1OoBx3yjwiBwxldPP3nIbFSSA== + he@1.2.0, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"