diff --git a/CHANGELOG.md b/CHANGELOG.md index 78bcd44..7c9bed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - email verification - idx for fields and logic to have stable order - ability to load submission by id if token is present +- anonymous form submissions (fixes https://github.com/ohmyform/ohmyform/issues/108) ### Changed diff --git a/package.json b/package.json index 4920cc6..8ee92ab 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "html-to-text": "^8.1.0", "inquirer": "^8.2.0", "ioredis": "^4.28.2", + "ip-anonymize": "^0.1.0", "matomo-tracker": "^2.2.4", "mjml": "^4.11.0", "mysql2": "^2.3.3", diff --git a/src/dto/form/form.create.input.ts b/src/dto/form/form.create.input.ts index a05afdd..1152b18 100644 --- a/src/dto/form/form.create.input.ts +++ b/src/dto/form/form.create.input.ts @@ -11,6 +11,9 @@ export class FormCreateInput { @Field({ nullable: true }) readonly showFooter: boolean + @Field({ nullable: true }) + readonly anonymousSubmission: boolean + @Field({ nullable: true }) readonly isLive: boolean diff --git a/src/dto/form/form.model.ts b/src/dto/form/form.model.ts index e10ab13..59060d6 100644 --- a/src/dto/form/form.model.ts +++ b/src/dto/form/form.model.ts @@ -21,6 +21,9 @@ export class FormModel { @Field() readonly showFooter: boolean + @Field() + readonly anonymousSubmission: boolean + constructor(form: FormEntity) { this.id = form.id.toString() this.title = form.title @@ -28,5 +31,6 @@ export class FormModel { this.lastModified = form.lastModified this.language = form.language this.showFooter = form.showFooter + this.anonymousSubmission = form.anonymousSubmission } } diff --git a/src/dto/form/form.update.input.ts b/src/dto/form/form.update.input.ts index 68e4060..87675cd 100644 --- a/src/dto/form/form.update.input.ts +++ b/src/dto/form/form.update.input.ts @@ -19,6 +19,9 @@ export class FormUpdateInput { @Field({ nullable: true }) readonly showFooter: boolean + @Field({ nullable: true }) + readonly anonymousSubmission: boolean + @Field({ nullable: true }) readonly isLive: boolean diff --git a/src/entity/form.entity.ts b/src/entity/form.entity.ts index 4382489..2c6f713 100644 --- a/src/entity/form.entity.ts +++ b/src/entity/form.entity.ts @@ -61,6 +61,9 @@ export class FormEntity { @Column() public isLive: boolean; + @Column({ default: false }) + public anonymousSubmission: boolean; + @Column(() => DesignEmbedded) public design: DesignEmbedded = new DesignEmbedded(); @@ -69,5 +72,4 @@ export class FormEntity { @UpdateDateColumn() public lastModified: Date - } diff --git a/src/migrations/mariadb/1641193946192-anonymous.ts b/src/migrations/mariadb/1641193946192-anonymous.ts new file mode 100644 index 0000000..5f86bfc --- /dev/null +++ b/src/migrations/mariadb/1641193946192-anonymous.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class anonymous1641193946192 implements MigrationInterface { + name = 'anonymous1641193946192' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE `form` ADD `anonymousSubmission` tinyint NOT NULL DEFAULT 0'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE `form` DROP COLUMN `anonymousSubmission`'); + } +} diff --git a/src/migrations/postgres/1641193946192-anonymous.ts b/src/migrations/postgres/1641193946192-anonymous.ts new file mode 100644 index 0000000..e9a2904 --- /dev/null +++ b/src/migrations/postgres/1641193946192-anonymous.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class anonymous1641193946192 implements MigrationInterface { + name = 'anonymous1641193946192' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "form" ADD "anonymousSubmission" boolean NOT NULL DEFAULT false'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "form" DROP COLUMN "anonymousSubmission"'); + } +} diff --git a/src/migrations/sqlite/1641193946192-anonymous.ts b/src/migrations/sqlite/1641193946192-anonymous.ts new file mode 100644 index 0000000..5504d0e --- /dev/null +++ b/src/migrations/sqlite/1641193946192-anonymous.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class anonymous1641193946192 implements MigrationInterface { + name = 'anonymous1641193946192' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('CREATE TABLE "temporary_form" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar NOT NULL, "language" varchar(10) NOT NULL, "showFooter" boolean NOT NULL, "isLive" boolean NOT NULL, "created" datetime NOT NULL DEFAULT (datetime(\'now\')), "lastModified" datetime NOT NULL DEFAULT (datetime(\'now\')), "adminId" integer, "startPageId" integer, "endPageId" integer, "analyticsGacode" varchar, "designFont" varchar, "designColorsBackground" varchar, "designColorsQuestion" varchar, "designColorsAnswer" varchar, "designColorsButton" varchar, "designColorsButtonactive" varchar, "designColorsButtontext" varchar, "designLayout" varchar, "anonymousSubmission" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_a7cb33580bca2b362e5e34fdfcd" FOREIGN KEY ("adminId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_023d9cf1d97e93facc96c86ca70" FOREIGN KEY ("startPageId") REFERENCES "page" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_e5d158932e43cfbf9958931ee01" FOREIGN KEY ("endPageId") REFERENCES "page" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)'); + await queryRunner.query('INSERT INTO "temporary_form"("id", "title", "language", "showFooter", "isLive", "created", "lastModified", "adminId", "startPageId", "endPageId", "analyticsGacode", "designFont", "designColorsBackground", "designColorsQuestion", "designColorsAnswer", "designColorsButton", "designColorsButtonactive", "designColorsButtontext", "designLayout") SELECT "id", "title", "language", "showFooter", "isLive", "created", "lastModified", "adminId", "startPageId", "endPageId", "analyticsGacode", "designFont", "designColorsBackground", "designColorsQuestion", "designColorsAnswer", "designColorsButton", "designColorsButtonactive", "designColorsButtontext", "designLayout" FROM "form"'); + await queryRunner.query('DROP TABLE "form"'); + await queryRunner.query('ALTER TABLE "temporary_form" RENAME TO "form"'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "form" RENAME TO "temporary_form"'); + await queryRunner.query('CREATE TABLE "form" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar NOT NULL, "language" varchar(10) NOT NULL, "showFooter" boolean NOT NULL, "isLive" boolean NOT NULL, "created" datetime NOT NULL DEFAULT (datetime(\'now\')), "lastModified" datetime NOT NULL DEFAULT (datetime(\'now\')), "adminId" integer, "startPageId" integer, "endPageId" integer, "analyticsGacode" varchar, "designFont" varchar, "designColorsBackground" varchar, "designColorsQuestion" varchar, "designColorsAnswer" varchar, "designColorsButton" varchar, "designColorsButtonactive" varchar, "designColorsButtontext" varchar, "designLayout" varchar, CONSTRAINT "FK_a7cb33580bca2b362e5e34fdfcd" FOREIGN KEY ("adminId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_023d9cf1d97e93facc96c86ca70" FOREIGN KEY ("startPageId") REFERENCES "page" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_e5d158932e43cfbf9958931ee01" FOREIGN KEY ("endPageId") REFERENCES "page" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)'); + await queryRunner.query('INSERT INTO "form"("id", "title", "language", "showFooter", "isLive", "created", "lastModified", "adminId", "startPageId", "endPageId", "analyticsGacode", "designFont", "designColorsBackground", "designColorsQuestion", "designColorsAnswer", "designColorsButton", "designColorsButtonactive", "designColorsButtontext", "designLayout") SELECT "id", "title", "language", "showFooter", "isLive", "created", "lastModified", "adminId", "startPageId", "endPageId", "analyticsGacode", "designFont", "designColorsBackground", "designColorsQuestion", "designColorsAnswer", "designColorsButton", "designColorsButtonactive", "designColorsButtontext", "designLayout" FROM "temporary_form"'); + await queryRunner.query('DROP TABLE "temporary_form"'); + } +} diff --git a/src/service/submission/submission.start.service.ts b/src/service/submission/submission.start.service.ts index c885bc6..6d4492c 100644 --- a/src/service/submission/submission.start.service.ts +++ b/src/service/submission/submission.start.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' +import anonymize from 'ip-anonymize' import { Repository } from 'typeorm' import { SubmissionStartInput } from '../../dto/submission/submission.start.input' import { FormEntity } from '../../entity/form.entity' @@ -24,9 +25,12 @@ export class SubmissionStartService { ): Promise { const submission = new SubmissionEntity() + if (!form.anonymousSubmission) { + submission.user = user + } + submission.form = form - submission.user = user - submission.ipAddr = ipAddr || '?' + submission.ipAddr = anonymize(ipAddr, 16, 16) || '?' submission.timeElapsed = 0 submission.percentageComplete = 0 diff --git a/yarn.lock b/yarn.lock index 1f44d71..63c1d13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4551,6 +4551,11 @@ ioredis@^4.17.3, ioredis@^4.28.2: redis-parser "^3.0.0" standard-as-callback "^2.1.0" +ip-anonymize@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ip-anonymize/-/ip-anonymize-0.1.0.tgz#5ead504d01871c5c28189a25382f852036b57f7e" + integrity sha512-cZJu+N5JKKFGMK0eEQWNaQMn2EhCysciVM6eotCJwfqotj16BTfVchKsJCH6mQAT9N0GC7oWRcsZ6Lb8dDiwTA== + ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"