stop exposing internal ids and switch over to hashids

This commit is contained in:
Michael Schramm 2022-02-28 22:50:20 +01:00
parent 2d3589e13a
commit fe3821ad42
45 changed files with 274 additions and 97 deletions

View File

@ -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

View File

@ -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",

View File

@ -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,
]

View File

@ -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) {

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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<SubmissionEntity>) {
this.id = submission.id.toString()
constructor(id: string, submission: Partial<SubmissionEntity>) {
this._id = submission.id
this.id = id
this.timeElapsed = submission.timeElapsed
this.percentageComplete = submission.percentageComplete

View File

@ -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

View File

@ -72,4 +72,10 @@ export class FormEntity {
@UpdateDateColumn()
public lastModified: Date
constructor(partial?: Partial<FormEntity>) {
if (partial) {
Object.assign(this, partial)
}
}
}

View File

@ -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<string, Promise<FormEntity>> {
constructor(
private readonly formService: FormService,
private readonly idService: IdService,
) {
}
async transform(value: string, metadata: ArgumentMetadata): Promise<FormEntity> {
const id = this.idService.decode(value)
console.log({
id,
value,
})
return await this.formService.findById(id)
}
}

3
src/pipe/form/index.ts Normal file
View File

@ -0,0 +1,3 @@
import { FormByIdPipe } from './form.by.id.pipe'
export const formPipes = [FormByIdPipe]

9
src/pipe/index.ts Normal file
View File

@ -0,0 +1,9 @@
import { formPipes } from './form'
import { submissionPipes } from './submission'
import { userPipes } from './user'
export const pipes = [
...formPipes,
...submissionPipes,
...userPipes,
]

View File

@ -0,0 +1,3 @@
import { SubmissionByIdPipe } from './submission.by.id.pipe'
export const submissionPipes = [SubmissionByIdPipe]

View File

@ -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<string, Promise<SubmissionEntity>> {
constructor(
private readonly submissionService: SubmissionService,
private readonly idService: IdService,
) {
}
async transform(value: string, metadata: ArgumentMetadata): Promise<SubmissionEntity> {
const id = this.idService.decode(value)
return await this.submissionService.findById(id)
}
}

3
src/pipe/user/index.ts Normal file
View File

@ -0,0 +1,3 @@
import { UserByIdPipe } from './user.by.id.pipe'
export const userPipes = [UserByIdPipe]

View File

@ -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<string, Promise<UserEntity>> {
constructor(
private readonly userService: UserService,
private readonly idService: IdService,
) {
}
async transform(value: string, metadata: ArgumentMetadata): Promise<UserEntity> {
const id = this.idService.decode(value)
return await this.userService.findById(id)
}
}

View File

@ -1,12 +1,10 @@
type ID = string | number
export class ContextCache<A = any> {
private cache: {
[key: string]: any
} = {}
public getCacheKey(type: string, id: ID): string {
public getCacheKey(type: string, id: number): string {
return `${type}:${id}`
}

View File

@ -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)
}
}

View File

@ -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<DeletedModel> {
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))
}
}

View File

@ -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,

View File

@ -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<FormModel> {
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)
}
}

View File

@ -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<FormFieldModel[]> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(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<FormHookModel[]> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(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<boolean> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(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<FormNotificationModel[]> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(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<DesignModel> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(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<PageModel> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(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<PageModel> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(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<UserModel> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(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)
}
}

View File

@ -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<FormModel> {
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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -19,7 +19,7 @@ export class SubmissionFieldResolver {
@Context('cache') cache: ContextCache,
): Promise<FormFieldModel> {
const submissionField = await cache.get<SubmissionFieldEntity>(
cache.getCacheKey(SubmissionFieldEntity.name, parent.id)
cache.getCacheKey(SubmissionFieldEntity.name, parent._id)
)
const field = await cache.get<FormFieldEntity>(

View File

@ -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<SubmissionProgressModel> {
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)
}
}

View File

@ -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<SubmissionPagerModel> {
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,

View File

@ -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<SubmissionModel> {
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)
}
}

View File

@ -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<SubmissionFieldModel[]> {
const submission = await cache.get<SubmissionEntity>(
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)
})
}
}

View File

@ -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<SubmissionProgressModel> {
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)
}
}

View File

@ -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<SubmissionProgressModel> {
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)
}
}

View File

@ -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<DeletedModel> {
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))
}
}

View File

@ -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,

View File

@ -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<UserModel> {
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)
}
}

View File

@ -21,7 +21,7 @@ export class UserResolver {
@Context('cache') cache: ContextCache,
): Promise<string[]> {
return this.returnFieldForSuperuser(
await cache.get<UserEntity>(cache.getCacheKey(UserEntity.name, parent.id)),
await cache.get<UserEntity>(cache.getCacheKey(UserEntity.name, parent._id)),
user,
c => c.roles
)

View File

@ -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)
}
}

32
src/service/id.service.ts Normal file
View File

@ -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]
}
}

View File

@ -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,
]

View File

@ -58,7 +58,7 @@ export class SubmissionService {
return await qb.getManyAndCount()
}
async findById(id: string): Promise<SubmissionEntity> {
async findById(id: number): Promise<SubmissionEntity> {
const submission = await this.submissionRepository.findOne(id);
if (!submission) {

View File

@ -11,7 +11,7 @@ export class UserDeleteService {
) {
}
async delete(id: string): Promise<void> {
async delete(id: number): Promise<void> {
await this.userRepository.delete(id)
}
}

View File

@ -26,7 +26,7 @@ export class UserService {
return await qb.getManyAndCount()
}
async findById(id: string): Promise<UserEntity> {
async findById(id: number): Promise<UserEntity> {
const user = await this.userRepository.findOne(id);
if (!user) {

View File

@ -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"