add user confirmation, add validation for submission data
This commit is contained in:
parent
11e95cb9c2
commit
29a74ea9c9
@ -24,6 +24,7 @@ module.exports = {
|
||||
jest: true,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
|
||||
@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- forms now have multiple notification
|
||||
- layout for forms
|
||||
- mariadb / mysql support
|
||||
- user confirmation tokens
|
||||
- email verification
|
||||
|
||||
### Changed
|
||||
|
||||
@ -20,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- colors object removed the "colors" postfix
|
||||
- if unsupported database engine is used error is thrown during startup
|
||||
- improved eslint checks
|
||||
- validate submission field data and store it json encoded
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@ -1,43 +1,46 @@
|
||||
# Environment Variables
|
||||
|
||||
| Name | Default Value | Description |
|
||||
| ---- | ------------- | ----------- |
|
||||
| DISABLE_INSTALLATION_METRICS | *not set* | Per default installations are [publishing](./installation.metrics.md) their existence |
|
||||
| SECRET_KEY | `changeMe` | JWT Secret for authentication |
|
||||
| CLI | *automatically* | activates pretty print for log output |
|
||||
| NODE_ENV | `production` | |
|
||||
| HIDE_CONTRIB | `false` | decide if backlings to ohmyform should be added |
|
||||
| SIGNUP_DISABLED | `false` | if users can sign up |
|
||||
| LOGIN_NOTE | *not set* | Info box on top of login screen |
|
||||
| LOCALES_PATH | *not set* | Path to translated elementes in backend like emails |
|
||||
| Name | Default Value | Description |
|
||||
|------------------------------|----------------------------|---------------------------------------------------------------------------------------|
|
||||
| DISABLE_INSTALLATION_METRICS | *not set* | Per default installations are [publishing](./installation.metrics.md) their existence |
|
||||
| SECRET_KEY | `changeMe` | JWT Secret for authentication |
|
||||
| CLI | *automatically* | activates pretty print for log output |
|
||||
| NODE_ENV | `production` | |
|
||||
| HIDE_CONTRIB | `false` | decide if backlings to ohmyform should be added |
|
||||
| SIGNUP_DISABLED | `false` | if users can sign up |
|
||||
| LOGIN_NOTE | *not set* | Info box on top of login screen |
|
||||
| LOCALES_PATH | *not set* | Path to translated elementes in backend like emails |
|
||||
| LOCALE | `en` | Default Locale |
|
||||
| BASE_URL | `http://localhost` | Url to Frontend root |
|
||||
| USER_CONFIRM_PATH | `/confirm?token={{token}}` | Path to confirm user |
|
||||
|
||||
## Default Account
|
||||
|
||||
*username and email are unique on an instance*
|
||||
|
||||
| Name | Default Value | Description |
|
||||
| ---- | ------------- | ----------- |
|
||||
| CREATE_ADMIN | `false` | if `true` will create a super admin |
|
||||
| ADMIN_USERNAME | `root` | username for the default admin user |
|
||||
| ADMIN_EMAIL | `admin@ohmyform.com` | email to send notifications |
|
||||
| ADMIN_PASSWORD | `root` | password for user |
|
||||
| Name | Default Value | Description |
|
||||
|----------------|----------------------|-------------------------------------|
|
||||
| CREATE_ADMIN | `false` | if `true` will create a super admin |
|
||||
| ADMIN_USERNAME | `root` | username for the default admin user |
|
||||
| ADMIN_EMAIL | `admin@ohmyform.com` | email to send notifications |
|
||||
| ADMIN_PASSWORD | `root` | password for user |
|
||||
|
||||
## Mailing
|
||||
|
||||
| Name | Default Value | Description |
|
||||
| ---- | ------------- | ----------- |
|
||||
| MAILER_URI | `smtp://localhost:1025` | [Mail Connection](https://nodemailer.com/smtp/) |
|
||||
| Name | Default Value | Description |
|
||||
|-------------|---------------------------------|-----------------------------------------------------------------------------------|
|
||||
| MAILER_URI | `smtp://localhost:1025` | [Mail Connection](https://nodemailer.com/smtp/) |
|
||||
| MAILER_FROM | `OhMyForm <no-reply@localhost>` | Default From path, make sure that your mail server supports the given from addres |
|
||||
|
||||
## Database Variables
|
||||
|
||||
| Name | Default Value | Description |
|
||||
| ---- | ------------- | ----------- |
|
||||
| DATABASE_DRIVER | `sqlite` | database driver, either `sqlite` or `postgres` |
|
||||
| DATABASE_URL | `sqlite://data.sqlite` | url in the format `TYPE://USER:PASS@HOST:PORT/NAME?EXTRA` ([read more](https://typeorm.io/#/connection-options/common-connection-options)) |
|
||||
| DATABASE_TABLE_PREFIX | *empty* | prefix all tables if used within same database as other applications. |
|
||||
| DATABASE_LOGGING | `false` | if `true` all db interactions will be logged to stdout |
|
||||
| DATABASE_MIGRATE | `true` | can be used in load balanced environments to only allow one container to perform migrations / manually execute migrations
|
||||
| DATABASE_SSL | `false` | if `true` will require ssl database connection |
|
||||
| REDIS_HOST | *not set* | required in multinode environments |
|
||||
| REDIS_PORT | `6379` | port for redis |
|
||||
| Name | Default Value | Description |
|
||||
|-----------------------|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| DATABASE_DRIVER | `sqlite` | database driver, either `sqlite` or `postgres` |
|
||||
| DATABASE_URL | `sqlite://data.sqlite` | url in the format `TYPE://USER:PASS@HOST:PORT/NAME?EXTRA` ([read more](https://typeorm.io/#/connection-options/common-connection-options)) |
|
||||
| DATABASE_TABLE_PREFIX | *empty* | prefix all tables if used within same database as other applications. |
|
||||
| DATABASE_LOGGING | `false` | if `true` all db interactions will be logged to stdout |
|
||||
| DATABASE_MIGRATE | `true` | can be used in load balanced environments to only allow one container to perform migrations / manually execute migrations |
|
||||
| DATABASE_SSL | `false` | if `true` will require ssl database connection |
|
||||
| REDIS_HOST | *not set* | required in multinode environments |
|
||||
| REDIS_PORT | `6379` | port for redis |
|
||||
|
||||
@ -10,12 +10,13 @@
|
||||
"src/entity/**/*.ts"
|
||||
],
|
||||
"migrations": [
|
||||
"src/migrations/maria/**/*.ts"
|
||||
"src/migrations/mariadb/**/*.ts"
|
||||
],
|
||||
"migrationsTransactionMode": "each",
|
||||
"subscribers": [
|
||||
"src/subscriber/**/*.ts"
|
||||
],
|
||||
"cli": {
|
||||
"migrationsDir": "src/migrations/maria"
|
||||
"migrationsDir": "src/migrations/mariadb"
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@
|
||||
"migrations": [
|
||||
"src/migrations/postgres/**/*.ts"
|
||||
],
|
||||
"migrationsTransactionMode": "each",
|
||||
"subscribers": [
|
||||
"src/subscriber/**/*.ts"
|
||||
],
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
"migrations": [
|
||||
"src/migrations/sqlite/**/*.ts"
|
||||
],
|
||||
"migrationsTransactionMode": "each",
|
||||
"cli": {
|
||||
"migrationsDir": "src/migrations/sqlite"
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"typeorm:sqlite": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -f ormconfig_sqlite.json",
|
||||
"typeorm:postgres": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -f ormconfig_postgres.json",
|
||||
"typeorm:maria": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -f ormconfig_maria.json"
|
||||
"typeorm:mariadb": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -f ormconfig_mariadb.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/mailer": "^1.6.0",
|
||||
@ -82,6 +82,7 @@
|
||||
"@types/html-to-text": "^8.0.1",
|
||||
"@types/inquirer": "^8.1.3",
|
||||
"@types/jest": "26.0.23",
|
||||
"@types/mjml": "^4.7.0",
|
||||
"@types/node": "^16.11.17",
|
||||
"@types/passport-jwt": "^3.0.6",
|
||||
"@types/passport-local": "^1.0.34",
|
||||
|
||||
@ -49,7 +49,7 @@ export const imports = [
|
||||
}),
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '..', 'public'),
|
||||
exclude: [],
|
||||
exclude: ['/graphql'],
|
||||
}),
|
||||
ConfigModule.forRoot({
|
||||
load: [
|
||||
@ -122,8 +122,8 @@ export const imports = [
|
||||
break
|
||||
|
||||
case 'mysql':
|
||||
case 'maria':
|
||||
migrationFolder = 'maria'
|
||||
case 'mariadb':
|
||||
migrationFolder = 'mariadb'
|
||||
break
|
||||
|
||||
case 'sqlite':
|
||||
@ -146,6 +146,7 @@ export const imports = [
|
||||
entities,
|
||||
migrations: [`${__dirname}/**/migrations/${migrationFolder}/**/*{.ts,.js}`],
|
||||
migrationsRun: configService.get<boolean>('DATABASE_MIGRATE', true),
|
||||
migrationsTransactionMode: 'each',
|
||||
})
|
||||
},
|
||||
}),
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Field, ID, InputType } from '@nestjs/graphql'
|
||||
import { FormFieldLogicAction } from '../../entity/form.field.logic.entity'
|
||||
|
||||
@InputType()
|
||||
export class FormFieldLogicInput {
|
||||
@ -8,8 +9,9 @@ export class FormFieldLogicInput {
|
||||
@Field({ nullable: true })
|
||||
readonly formula: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly action: string
|
||||
// TODO verify action value
|
||||
@Field(() => String, { nullable: true })
|
||||
readonly action: FormFieldLogicAction
|
||||
|
||||
@Field(() => ID, { nullable: true })
|
||||
readonly jumpTo?: string
|
||||
|
||||
@ -14,7 +14,7 @@ export class SubmissionFieldModel {
|
||||
|
||||
constructor(field: SubmissionFieldEntity) {
|
||||
this.id = field.id.toString()
|
||||
this.value = field.fieldValue
|
||||
this.type = field.fieldType
|
||||
this.value = JSON.stringify(field.content)
|
||||
this.type = field.type
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,9 +6,15 @@ export class UserModel {
|
||||
@Field(() => ID)
|
||||
readonly id: string
|
||||
|
||||
@Field()
|
||||
/**
|
||||
* @deprecated use emailVerified instead
|
||||
*/
|
||||
@Field({ deprecationReason: 'use emailVerified instead' })
|
||||
readonly verifiedEmail: boolean
|
||||
|
||||
@Field()
|
||||
readonly emailVerified: boolean
|
||||
|
||||
@Field()
|
||||
readonly username: string
|
||||
|
||||
@ -39,7 +45,8 @@ export class UserModel {
|
||||
this.firstName = user.firstName
|
||||
this.lastName = user.lastName
|
||||
|
||||
this.verifiedEmail = !user.token
|
||||
this.verifiedEmail = user.emailVerified
|
||||
this.emailVerified = user.emailVerified
|
||||
|
||||
this.created = user.created
|
||||
this.lastModified = user.lastModified
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { FormFieldEntity } from './form.field.entity'
|
||||
|
||||
type LogicAction = 'visible' | 'require' | 'disable' | 'jumpTo'
|
||||
export type FormFieldLogicAction = 'visible' | 'require' | 'disable' | 'jumpTo'
|
||||
|
||||
@Entity({ name: 'form_field_logic' })
|
||||
export class FormFieldLogicEntity {
|
||||
@ -15,7 +15,7 @@ export class FormFieldLogicEntity {
|
||||
public formula: string
|
||||
|
||||
@Column({ type: 'varchar', length: 10 })
|
||||
public action: LogicAction
|
||||
public action: FormFieldLogicAction
|
||||
|
||||
@Column({ nullable: true })
|
||||
public visible?: boolean
|
||||
|
||||
@ -2,6 +2,10 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, RelationId } from 't
|
||||
import { FormFieldEntity } from './form.field.entity'
|
||||
import { SubmissionEntity } from './submission.entity'
|
||||
|
||||
export interface SubmissionFieldContent {
|
||||
[key: string]: string | string[] | number | number[] | boolean | boolean[]
|
||||
}
|
||||
|
||||
@Entity({ name: 'submission_field' })
|
||||
export class SubmissionFieldEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@ -17,8 +21,8 @@ export class SubmissionFieldEntity {
|
||||
readonly fieldId: number
|
||||
|
||||
@Column()
|
||||
public fieldType: string
|
||||
public type: string
|
||||
|
||||
@Column()
|
||||
public fieldValue: string
|
||||
@Column('simple-json')
|
||||
public content: SubmissionFieldContent
|
||||
}
|
||||
|
||||
@ -15,6 +15,9 @@ export class UserEntity {
|
||||
@Column({ length: 255, unique: true })
|
||||
public email: string
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
public emailVerified = false
|
||||
|
||||
@Column({ length: 255, unique: true })
|
||||
public username: string
|
||||
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { NestApplicationOptions, ValidationPipe } from '@nestjs/common'
|
||||
import { NestFactory } from '@nestjs/core'
|
||||
import cors from 'cors'
|
||||
import { Logger, PinoLogger } from 'nestjs-pino'
|
||||
import { LoggerConfig } from './app.imports'
|
||||
import { Logger } from 'nestjs-pino'
|
||||
import { AppModule } from './app.module'
|
||||
|
||||
(async () => {
|
||||
const options: NestApplicationOptions = {
|
||||
logger: new Logger(new PinoLogger(LoggerConfig), {}),
|
||||
bufferLogs: true,
|
||||
}
|
||||
|
||||
|
||||
17
src/migrations/mariadb/1641124349039-submission.ts
Normal file
17
src/migrations/mariadb/1641124349039-submission.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class submission1641124349039 implements MigrationInterface {
|
||||
name = 'submission1641124349039'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `submission_field` RENAME COLUMN `fieldType` TO `type`');
|
||||
await queryRunner.query('ALTER TABLE `submission_field` RENAME COLUMN `fieldValue` TO `content`');
|
||||
await queryRunner.query('ALTER TABLE `submission_field` MODIFY COLUMN `content` text NOT NULL');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `submission_field` MODIFY COLUMN `content` varchar(255) NOT NULL');
|
||||
await queryRunner.query('ALTER TABLE `submission_field` RENAME COLUMN `content` TO `fieldValue`');
|
||||
await queryRunner.query('ALTER TABLE `submission_field` RENAME COLUMN `type` TO `fieldType`');
|
||||
}
|
||||
}
|
||||
13
src/migrations/mariadb/1641132645227-confirm.ts
Normal file
13
src/migrations/mariadb/1641132645227-confirm.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class confirm1641132645227 implements MigrationInterface {
|
||||
name = 'confirm1641132645227'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `user` ADD `emailVerified` tinyint NOT NULL DEFAULT 0');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `user` DROP COLUMN `emailVerified`');
|
||||
}
|
||||
}
|
||||
17
src/migrations/postgres/1641124349039-submission.ts
Normal file
17
src/migrations/postgres/1641124349039-submission.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class submission1641124349039 implements MigrationInterface {
|
||||
name = 'submission1641124349039'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "submission_field" RENAME COLUMN "fieldType" TO "type"');
|
||||
await queryRunner.query('ALTER TABLE "submission_field" RENAME COLUMN "fieldValue" TO "content"');
|
||||
await queryRunner.query('ALTER TABLE "submission_field" ALTER COLUMN "content" type text');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "submission_field" ALTER COLUMN "content" type character varying');
|
||||
await queryRunner.query('ALTER TABLE "submission_field" RENAME COLUMN "content" TO "fieldValue"');
|
||||
await queryRunner.query('ALTER TABLE "submission_field" RENAME COLUMN "type" TO "fieldType"');
|
||||
}
|
||||
}
|
||||
13
src/migrations/postgres/1641132645227-confirm.ts
Normal file
13
src/migrations/postgres/1641132645227-confirm.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class confirm1641132645227 implements MigrationInterface {
|
||||
name = 'confirm1641132645227'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" ADD "emailVerified" boolean NOT NULL DEFAULT false');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" DROP COLUMN "emailVerified"');
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,10 @@ export class initial1619723437787 implements MigrationInterface {
|
||||
await queryRunner.query('CREATE TABLE "submission_field" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "fieldType" varchar NOT NULL, "fieldValue" varchar NOT NULL, "submissionId" integer, "fieldId" integer, CONSTRAINT "FK_16fae661ce5b10f27abe2e524a0" FOREIGN KEY ("submissionId") REFERENCES "submission" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_5befa92da2370b7eb1cab6ae30a" FOREIGN KEY ("fieldId") REFERENCES "form_field" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)');
|
||||
await queryRunner.query('CREATE TABLE "form_visitor" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "referrer" varchar, "ipAddr" varchar NOT NULL, "created" datetime NOT NULL DEFAULT (datetime(\'now\')), "updated" datetime NOT NULL DEFAULT (datetime(\'now\')), "formId" integer, "geoLocationCountry" varchar, "geoLocationCity" varchar, "deviceLanguage" varchar, "deviceType" varchar, "deviceName" varchar, CONSTRAINT "FK_72ade6c3a3e55d1fce94300f8b6" FOREIGN KEY ("formId") REFERENCES "form" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)');
|
||||
await queryRunner.query('CREATE TABLE "submission" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "ipAddr" varchar NOT NULL, "tokenHash" varchar NOT NULL, "timeElapsed" numeric NOT NULL, "percentageComplete" numeric NOT NULL, "created" datetime NOT NULL DEFAULT (datetime(\'now\')), "lastModified" datetime NOT NULL DEFAULT (datetime(\'now\')), "formId" integer, "visitorId" integer, "userId" integer, "geoLocationCountry" varchar, "geoLocationCity" varchar, "deviceLanguage" varchar, "deviceType" varchar, "deviceName" varchar, CONSTRAINT "FK_6090e1d5cbf3433ffd14e3b53e7" FOREIGN KEY ("formId") REFERENCES "form" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_95b73c7faf2c199f005fda5e8c8" FOREIGN KEY ("visitorId") REFERENCES "form_visitor" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_7bd626272858ef6464aa2579094" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)');
|
||||
|
||||
|
||||
|
||||
|
||||
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, 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)');
|
||||
}
|
||||
|
||||
|
||||
@ -4,11 +4,17 @@ export class layout1621078163528 implements MigrationInterface {
|
||||
name = 'layout1621078163528'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "form" ADD "designLayout" character varying');
|
||||
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, CONSTRAINT "FK_e5d158932e43cfbf9958931ee01" FOREIGN KEY ("endPageId") REFERENCES "page" ("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_a7cb33580bca2b362e5e34fdfcd" FOREIGN KEY ("adminId") REFERENCES "user" ("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") SELECT "id", "title", "language", "showFooter", "isLive", "created", "lastModified", "adminId", "startPageId", "endPageId", "analyticsGacode", "designFont", "designColorsBackground", "designColorsQuestion", "designColorsAnswer", "designColorsButton", "designColorsButtonactive", "designColorsButtontext" FROM "form"');
|
||||
await queryRunner.query('DROP TABLE "form"');
|
||||
await queryRunner.query('ALTER TABLE "temporary_form" RENAME TO "form"');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "form" DROP COLUMN "designLayout"');
|
||||
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, CONSTRAINT "FK_e5d158932e43cfbf9958931ee01" FOREIGN KEY ("endPageId") REFERENCES "page" ("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_a7cb33580bca2b362e5e34fdfcd" FOREIGN KEY ("adminId") REFERENCES "user" ("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") SELECT "id", "title", "language", "showFooter", "isLive", "created", "lastModified", "adminId", "startPageId", "endPageId", "analyticsGacode", "designFont", "designColorsBackground", "designColorsQuestion", "designColorsAnswer", "designColorsButton", "designColorsButtonactive", "designColorsButtontext" FROM "temporary_form"');
|
||||
await queryRunner.query('DROP TABLE "temporary_form"');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
20
src/migrations/sqlite/1641124349039-submission.ts
Normal file
20
src/migrations/sqlite/1641124349039-submission.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class submission1641124349039 implements MigrationInterface {
|
||||
name = 'submission1641124349039'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('CREATE TABLE "temporary_submission_field" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "submissionId" integer, "fieldId" integer, "type" varchar NOT NULL, "content" text NOT NULL, CONSTRAINT "FK_5befa92da2370b7eb1cab6ae30a" FOREIGN KEY ("fieldId") REFERENCES "form_field" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_16fae661ce5b10f27abe2e524a0" FOREIGN KEY ("submissionId") REFERENCES "submission" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)');
|
||||
await queryRunner.query('INSERT INTO "temporary_submission_field"("id", "submissionId", "type", "content", "fieldId") SELECT "id", "submissionId", "fieldType", "fieldValue", "fieldId" FROM "submission_field"');
|
||||
await queryRunner.query('DROP TABLE "submission_field"');
|
||||
await queryRunner.query('ALTER TABLE "temporary_submission_field" RENAME TO "submission_field"');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "submission_field" RENAME TO "temporary_submission_field"');
|
||||
await queryRunner.query('CREATE TABLE "submission_field" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "fieldType" varchar NOT NULL, "fieldValue" varchar NOT NULL, "submissionId" integer, "fieldId" integer, CONSTRAINT "FK_5befa92da2370b7eb1cab6ae30a" FOREIGN KEY ("fieldId") REFERENCES "form_field" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_16fae661ce5b10f27abe2e524a0" FOREIGN KEY ("submissionId") REFERENCES "submission" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)');
|
||||
await queryRunner.query('INSERT INTO "submission_field"("id", "submissionId", "fieldType", "fieldValue", "fieldId") SELECT "id", "submissionId", "type", "content", "fieldId" FROM "temporary_submission_field"');
|
||||
await queryRunner.query('DROP TABLE "temporary_submission_field"');
|
||||
}
|
||||
|
||||
}
|
||||
19
src/migrations/sqlite/1641132645227-confirm.ts
Normal file
19
src/migrations/sqlite/1641132645227-confirm.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class confirm1641132645227 implements MigrationInterface {
|
||||
name = 'confirm1641132645227'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "firstName" varchar, "lastName" varchar, "email" varchar(255) NOT NULL, "username" varchar(255) NOT NULL, "passwordHash" varchar NOT NULL, "salt" varchar, "provider" varchar NOT NULL, "roles" text NOT NULL, "language" varchar NOT NULL, "resetPasswordToken" varchar, "resetPasswordExpires" datetime, "token" varchar, "apiKey" varchar, "created" datetime NOT NULL DEFAULT (datetime(\'now\')), "lastModified" datetime NOT NULL DEFAULT (datetime(\'now\')), "emailVerified" boolean NOT NULL DEFAULT (0), CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE ("username"), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))');
|
||||
await queryRunner.query('INSERT INTO "temporary_user"("id", "firstName", "lastName", "email", "username", "passwordHash", "salt", "provider", "roles", "language", "resetPasswordToken", "resetPasswordExpires", "token", "apiKey", "created", "lastModified") SELECT "id", "firstName", "lastName", "email", "username", "passwordHash", "salt", "provider", "roles", "language", "resetPasswordToken", "resetPasswordExpires", "token", "apiKey", "created", "lastModified" FROM "user"');
|
||||
await queryRunner.query('DROP TABLE "user"');
|
||||
await queryRunner.query('ALTER TABLE "temporary_user" RENAME TO "user"');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" RENAME TO "temporary_user"');
|
||||
await queryRunner.query('CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "firstName" varchar, "lastName" varchar, "email" varchar(255) NOT NULL, "username" varchar(255) NOT NULL, "passwordHash" varchar NOT NULL, "salt" varchar, "provider" varchar NOT NULL, "roles" text NOT NULL, "language" varchar NOT NULL, "resetPasswordToken" varchar, "resetPasswordExpires" datetime, "token" varchar, "apiKey" varchar, "created" datetime NOT NULL DEFAULT (datetime(\'now\')), "lastModified" datetime NOT NULL DEFAULT (datetime(\'now\')), CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE ("username"), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))');
|
||||
await queryRunner.query('INSERT INTO "user"("id", "firstName", "lastName", "email", "username", "passwordHash", "salt", "provider", "roles", "language", "resetPasswordToken", "resetPasswordExpires", "token", "apiKey", "created", "lastModified") SELECT "id", "firstName", "lastName", "email", "username", "passwordHash", "salt", "provider", "roles", "language", "resetPasswordToken", "resetPasswordExpires", "token", "apiKey", "created", "lastModified" FROM "temporary_user"');
|
||||
await queryRunner.query('DROP TABLE "temporary_user"');
|
||||
}
|
||||
}
|
||||
@ -23,7 +23,7 @@ export class FormDeleteMutation {
|
||||
): Promise<DeletedModel> {
|
||||
const form = await this.formService.findById(id)
|
||||
|
||||
if (!form.isLive && !await this.formService.isAdmin(form, user)) {
|
||||
if (!form.isLive && !this.formService.isAdmin(form, user)) {
|
||||
throw new Error('invalid form')
|
||||
}
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ export class FormQuery {
|
||||
): Promise<FormModel> {
|
||||
const form = await this.formService.findById(id)
|
||||
|
||||
if (!form.isLive && !await this.formService.isAdmin(form, user)) {
|
||||
if (!form.isLive && !this.formService.isAdmin(form, user)) {
|
||||
throw new Error('invalid form')
|
||||
}
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ export class FormResolver {
|
||||
): Promise<boolean> {
|
||||
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
|
||||
|
||||
if (!await this.formService.isAdmin(form, user)) {
|
||||
if (!this.formService.isAdmin(form, user)) {
|
||||
throw new Error('no access to field')
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ export class FormResolver {
|
||||
): Promise<FormNotificationModel[]> {
|
||||
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
|
||||
|
||||
if (!await this.formService.isAdmin(form, user)) {
|
||||
if (!this.formService.isAdmin(form, user)) {
|
||||
throw new Error('no access to field')
|
||||
}
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ export class FormUpdateMutation {
|
||||
): Promise<FormModel> {
|
||||
const form = await this.formService.findById(input.id)
|
||||
|
||||
if (!form.isLive && !await this.formService.isAdmin(form, user)) {
|
||||
if (!form.isLive && !this.formService.isAdmin(form, user)) {
|
||||
throw new Error('invalid form')
|
||||
}
|
||||
|
||||
|
||||
@ -8,10 +8,10 @@ import { ContextCache } from '../context.cache'
|
||||
export class ProfileResolver {
|
||||
@Query(() => ProfileModel)
|
||||
@Roles('user')
|
||||
async me(
|
||||
public me(
|
||||
@User() user: UserEntity,
|
||||
@Context('cache') cache: ContextCache,
|
||||
): Promise<ProfileModel> {
|
||||
): ProfileModel {
|
||||
cache.add(cache.getCacheKey(UserEntity.name, user.id), user)
|
||||
|
||||
return new ProfileModel(user)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { Args, Context, Mutation } from '@nestjs/graphql'
|
||||
import { Roles } from '../../decorator/roles.decorator'
|
||||
import { User } from '../../decorator/user.decorator'
|
||||
import { ProfileModel } from '../../dto/profile/profile.model'
|
||||
import { ProfileUpdateInput } from '../../dto/profile/profile.update.input'
|
||||
@ -15,6 +16,7 @@ export class ProfileUpdateMutation {
|
||||
}
|
||||
|
||||
@Mutation(() => ProfileModel)
|
||||
@Roles('user')
|
||||
async updateProfile(
|
||||
@User() user: UserEntity,
|
||||
@Args({ name: 'user', type: () => ProfileUpdateInput }) input: ProfileUpdateInput,
|
||||
@ -26,4 +28,18 @@ export class ProfileUpdateMutation {
|
||||
|
||||
return new ProfileModel(user)
|
||||
}
|
||||
|
||||
@Mutation(() => ProfileModel)
|
||||
@Roles('user')
|
||||
async verifyEmail(
|
||||
@User() user: UserEntity,
|
||||
@Args({ name: 'token' }) token: string,
|
||||
@Context('cache') cache: ContextCache,
|
||||
): Promise<ProfileModel> {
|
||||
await this.updateService.verifyEmail(user, token)
|
||||
|
||||
cache.add(cache.getCacheKey(UserEntity.name, user.id), user)
|
||||
|
||||
return new ProfileModel(user)
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,14 +27,14 @@ export class SettingResolver {
|
||||
}
|
||||
|
||||
@Query(() => SettingModel)
|
||||
async getSetting(
|
||||
getSetting(
|
||||
@Args('key', {type: () => ID}) key: string,
|
||||
@User() user: UserEntity,
|
||||
): Promise<SettingModel> {
|
||||
): SettingModel {
|
||||
if (!this.settingService.isPublicKey(key) && !user.roles.includes('superuser')) {
|
||||
throw new Error(`no access to key ${key}`)
|
||||
}
|
||||
|
||||
return await this.settingService.getByKey(key)
|
||||
return this.settingService.getByKey(key)
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,9 +18,11 @@ export class FormDeleteService {
|
||||
await this.submissionRepository.createQueryBuilder('s')
|
||||
.delete()
|
||||
.where('s.form = :form', { form: id })
|
||||
.execute()
|
||||
|
||||
await this.formRepository.createQueryBuilder('f')
|
||||
.delete()
|
||||
.where('f.id = :form', { form: id })
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { InjectRepository } from '@nestjs/typeorm'
|
||||
import { PinoLogger } from 'nestjs-pino'
|
||||
import { Repository } from 'typeorm'
|
||||
import { FormEntity } from '../../entity/form.entity'
|
||||
import { UserEntity } from '../../entity/user.entity'
|
||||
@ -9,10 +10,12 @@ export class FormService {
|
||||
constructor(
|
||||
@InjectRepository(FormEntity)
|
||||
private readonly formRepository: Repository<FormEntity>,
|
||||
private readonly logger: PinoLogger,
|
||||
) {
|
||||
logger.setContext(this.constructor.name)
|
||||
}
|
||||
|
||||
async isAdmin(form: FormEntity, user: UserEntity): Promise<boolean> {
|
||||
isAdmin(form: FormEntity, user: UserEntity): boolean {
|
||||
if (!user) {
|
||||
return false
|
||||
}
|
||||
@ -24,7 +27,12 @@ export class FormService {
|
||||
return form.admin.id === user.id
|
||||
}
|
||||
|
||||
async find(start: number, limit: number, sort: any = {}, user?: UserEntity): Promise<[FormEntity[], number]> {
|
||||
async find(
|
||||
start: number,
|
||||
limit: number,
|
||||
sort: any = {},
|
||||
user?: UserEntity
|
||||
): Promise<[FormEntity[], number]> {
|
||||
const qb = this.formRepository.createQueryBuilder('f')
|
||||
|
||||
qb.leftJoinAndSelect('f.admin', 'a')
|
||||
@ -34,6 +42,9 @@ export class FormService {
|
||||
}
|
||||
|
||||
// TODO readd sort
|
||||
this.logger.debug({
|
||||
sort,
|
||||
}, 'ignored sorting for submissions')
|
||||
|
||||
qb.skip(start)
|
||||
qb.take(limit)
|
||||
|
||||
@ -41,8 +41,12 @@ export class FormUpdateService {
|
||||
}
|
||||
|
||||
if (input.fields !== undefined) {
|
||||
form.fields = await Promise.all(input.fields.map(async (nextField) => {
|
||||
let field = form.fields.find(field => field.id?.toString() === nextField.id)
|
||||
form.fields = input.fields.map((nextField) => {
|
||||
let field = this.findByIdInList(
|
||||
form.fields,
|
||||
nextField.id,
|
||||
null
|
||||
)
|
||||
|
||||
if (!field) {
|
||||
field = new FormFieldEntity()
|
||||
@ -75,7 +79,11 @@ export class FormUpdateService {
|
||||
|
||||
if (nextField.logic !== undefined) {
|
||||
field.logic = nextField.logic.map(nextLogic => {
|
||||
const logic = field.logic?.find(logic => logic.id?.toString() === nextLogic.id) || new FormFieldLogicEntity()
|
||||
const logic = this.findByIdInList(
|
||||
field.logic,
|
||||
nextLogic.id,
|
||||
new FormFieldLogicEntity()
|
||||
)
|
||||
|
||||
logic.field = field
|
||||
|
||||
@ -83,7 +91,7 @@ export class FormUpdateService {
|
||||
logic.formula = nextLogic.formula
|
||||
}
|
||||
if (nextLogic.action !== undefined) {
|
||||
logic.action = nextLogic.action as any
|
||||
logic.action = nextLogic.action
|
||||
}
|
||||
if (nextLogic.visible !== undefined) {
|
||||
logic.visible = nextLogic.visible
|
||||
@ -95,7 +103,11 @@ export class FormUpdateService {
|
||||
logic.disable = nextLogic.disable
|
||||
}
|
||||
if (nextLogic.jumpTo !== undefined) {
|
||||
logic.jumpTo = form.fields.find(value => value.id?.toString() === nextLogic.jumpTo)
|
||||
logic.jumpTo = this.findByIdInList(
|
||||
form.fields,
|
||||
nextLogic.jumpTo,
|
||||
null
|
||||
)
|
||||
}
|
||||
if (nextLogic.enabled !== undefined) {
|
||||
logic.enabled = nextLogic.enabled
|
||||
@ -107,7 +119,11 @@ export class FormUpdateService {
|
||||
|
||||
if (nextField.options !== undefined) {
|
||||
field.options = nextField.options.map(nextOption => {
|
||||
const option = field.options?.find(option => option.id?.toString() === nextOption.id) || new FormFieldOptionEntity()
|
||||
const option = this.findByIdInList(
|
||||
field.options,
|
||||
nextOption.id,
|
||||
new FormFieldOptionEntity()
|
||||
)
|
||||
|
||||
option.field = field
|
||||
|
||||
@ -124,13 +140,16 @@ export class FormUpdateService {
|
||||
}
|
||||
|
||||
return field
|
||||
}))
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
if (input.hooks !== undefined) {
|
||||
form.hooks = input.hooks.map((nextHook) => {
|
||||
const hook = form.hooks?.find(hook => hook.id?.toString() === nextHook.id) || new FormHookEntity()
|
||||
const hook = this.findByIdInList(
|
||||
form.hooks,
|
||||
nextHook.id,
|
||||
new FormHookEntity()
|
||||
)
|
||||
|
||||
// ability for other fields to apply mapping
|
||||
hook.url = nextHook.url
|
||||
@ -179,7 +198,11 @@ export class FormUpdateService {
|
||||
|
||||
if (input.notifications !== undefined) {
|
||||
form.notifications = input.notifications.map(notificationInput => {
|
||||
const notification = form.notifications?.find(value => value.id?.toString() === notificationInput.id) || new FormNotificationEntity()
|
||||
const notification = this.findByIdInList(
|
||||
form.notifications,
|
||||
notificationInput.id,
|
||||
new FormNotificationEntity()
|
||||
)
|
||||
|
||||
notification.form = form
|
||||
notification.enabled = notificationInput.enabled
|
||||
@ -188,7 +211,11 @@ export class FormUpdateService {
|
||||
notification.fromEmail = notificationInput.fromEmail
|
||||
}
|
||||
if (notificationInput.fromField !== undefined) {
|
||||
notification.fromField = form.fields.find(value => value.id?.toString() === notificationInput.fromField)
|
||||
notification.fromField = this.findByIdInList(
|
||||
form.fields,
|
||||
notificationInput.fromField,
|
||||
null
|
||||
)
|
||||
}
|
||||
if (notificationInput.subject !== undefined) {
|
||||
notification.subject = notificationInput.subject
|
||||
@ -200,7 +227,11 @@ export class FormUpdateService {
|
||||
notification.toEmail = notificationInput.toEmail
|
||||
}
|
||||
if (notificationInput.toField !== undefined) {
|
||||
notification.toField = form.fields.find(value => value.id?.toString() === notificationInput.toField)
|
||||
notification.toField = this.findByIdInList(
|
||||
form.fields,
|
||||
notificationInput.toField,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
return notification
|
||||
@ -240,7 +271,11 @@ export class FormUpdateService {
|
||||
|
||||
if (input.startPage.buttons !== undefined) {
|
||||
form.startPage.buttons = input.startPage.buttons.map(buttonInput => {
|
||||
const entity = form.startPage?.buttons?.find(value => value.id?.toString() === buttonInput.id) || new PageButtonEntity()
|
||||
const entity = this.findByIdInList(
|
||||
form.startPage?.buttons,
|
||||
buttonInput.id,
|
||||
new PageButtonEntity()
|
||||
)
|
||||
entity.page = form.startPage
|
||||
entity.url = buttonInput.url
|
||||
entity.action = buttonInput.action
|
||||
@ -278,7 +313,11 @@ export class FormUpdateService {
|
||||
|
||||
if (input.endPage.buttons !== undefined) {
|
||||
form.endPage.buttons = input.endPage.buttons.map(buttonInput => {
|
||||
const entity = form.endPage?.buttons?.find(value => value.id?.toString() === buttonInput.id) || new PageButtonEntity()
|
||||
const entity = this.findByIdInList(
|
||||
form.endPage?.buttons,
|
||||
buttonInput.id,
|
||||
new PageButtonEntity()
|
||||
)
|
||||
entity.page = form.endPage
|
||||
entity.url = buttonInput.url
|
||||
entity.action = buttonInput.action
|
||||
@ -296,4 +335,18 @@ export class FormUpdateService {
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
private findByIdInList<T>(list: T[], id: string, fallback: T): T {
|
||||
if (!list) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const found = list.find((value: any) => String(value.id) === String(id))
|
||||
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ export class InstallationMetricsService implements OnApplicationBootstrap {
|
||||
logger.setContext(this.constructor.name)
|
||||
}
|
||||
|
||||
async onApplicationBootstrap(): Promise<void> {
|
||||
onApplicationBootstrap(): void {
|
||||
if (this.configService.get<boolean>('DISABLE_INSTALLATION_METRICS')) {
|
||||
this.logger.info('installation metrics are disabled')
|
||||
return
|
||||
@ -42,6 +42,4 @@ export class InstallationMetricsService implements OnApplicationBootstrap {
|
||||
})
|
||||
}, 24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import htmlToText from 'html-to-text'
|
||||
import mjml2html from 'mjml'
|
||||
import { PinoLogger } from 'nestjs-pino'
|
||||
import { join } from 'path'
|
||||
import { serializeError } from 'serialize-error'
|
||||
import { defaultLanguage } from '../config/languages'
|
||||
|
||||
@Injectable()
|
||||
@ -19,10 +20,19 @@ export class MailService {
|
||||
logger.setContext(this.constructor.name)
|
||||
}
|
||||
|
||||
async send(to: string, template: string, context: { [key: string]: any }, language: string = defaultLanguage): Promise<boolean> {
|
||||
this.logger.info({
|
||||
async send(
|
||||
to: string,
|
||||
template: string,
|
||||
context: { [key: string]: any },
|
||||
forceLanguage?: string
|
||||
): Promise<boolean> {
|
||||
const language = forceLanguage || this.configService.get('LOCALE', defaultLanguage)
|
||||
|
||||
this.logger.debug({
|
||||
email: to,
|
||||
}, `send email ${template}`)
|
||||
template,
|
||||
}, 'try to send email')
|
||||
|
||||
try {
|
||||
const path = this.getTemplatePath(template, language)
|
||||
|
||||
@ -35,23 +45,36 @@ export class MailService {
|
||||
}
|
||||
).html
|
||||
|
||||
const text = htmlToText.fromString(html)
|
||||
const text = htmlToText.htmlToText(html)
|
||||
|
||||
const subject = /<title>(.*?)<\/title>/gi.test(html) ? /<title>(.*?)<\/title>/gi.exec(html)[1] : template
|
||||
const subject = this.extractSubject(html, template)
|
||||
|
||||
await this.nestMailer.sendMail({ to, subject, html, text })
|
||||
this.logger.info('sent email')
|
||||
this.logger.info({
|
||||
email: to,
|
||||
template,
|
||||
language,
|
||||
}, 'sent email')
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
error: error.message,
|
||||
error: serializeError(error),
|
||||
email: to,
|
||||
}, `failed to send email ${template}`)
|
||||
template,
|
||||
}, 'failed to send email')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private extractSubject(html: string, template: string): string {
|
||||
if (/<title>(.*?)<\/title>/gi.test(html)) {
|
||||
return /<title>(.*?)<\/title>/gi.exec(html)[1]
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
private getTemplatePath(template: string, language: string): string {
|
||||
let templatePath = join(this.configService.get<string>('LOCALES_PATH'), language, 'mail', `${template}.mjml`)
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { Repository } from 'typeorm'
|
||||
import { ProfileUpdateInput } from '../../dto/profile/profile.update.input'
|
||||
import { UserEntity } from '../../entity/user.entity'
|
||||
import { PasswordService } from '../auth/password.service'
|
||||
import { UserTokenService } from '../user/user.token.service'
|
||||
|
||||
@Injectable()
|
||||
export class ProfileUpdateService {
|
||||
@ -11,9 +12,18 @@ export class ProfileUpdateService {
|
||||
@InjectRepository(UserEntity)
|
||||
private readonly userRepository: Repository<UserEntity>,
|
||||
private readonly passwordService: PasswordService,
|
||||
private readonly userTokenService: UserTokenService,
|
||||
) {
|
||||
}
|
||||
|
||||
async verifyEmail(user: UserEntity, token: string): Promise<UserEntity> {
|
||||
if (!await this.userTokenService.verify(token, user.token)) {
|
||||
throw new Error('invalid token')
|
||||
}
|
||||
|
||||
return await this.userRepository.save(user)
|
||||
}
|
||||
|
||||
async update(user: UserEntity, input: ProfileUpdateInput): Promise<UserEntity> {
|
||||
if (input.firstName !== undefined) {
|
||||
user.firstName = input.firstName
|
||||
@ -25,6 +35,7 @@ export class ProfileUpdateService {
|
||||
|
||||
if (input.email !== undefined && user.email !== input.email) {
|
||||
user.email = input.email
|
||||
user.emailVerified = false
|
||||
// TODO request email verification
|
||||
|
||||
if (undefined !== await this.userRepository.findOne({ email: input.email })) {
|
||||
|
||||
@ -17,7 +17,7 @@ export class SettingService {
|
||||
].includes(key)
|
||||
}
|
||||
|
||||
async getByKey(key: string): Promise<SettingModel> {
|
||||
getByKey(key: string): SettingModel {
|
||||
switch (key) {
|
||||
case 'SIGNUP_DISABLED':
|
||||
case 'LOGIN_NOTE':
|
||||
@ -29,11 +29,11 @@ export class SettingService {
|
||||
throw new Error(`no config stored for key ${key}`)
|
||||
}
|
||||
|
||||
async isTrue(key: string): Promise<boolean> {
|
||||
return (await this.getByKey(key)).isTrue
|
||||
isTrue(key: string): boolean {
|
||||
return this.getByKey(key).isTrue
|
||||
}
|
||||
|
||||
async isFalse(key: string): Promise<boolean> {
|
||||
return (await this.getByKey(key)).isFalse
|
||||
isFalse(key: string): boolean {
|
||||
return this.getByKey(key).isFalse
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ import { HttpService } from '@nestjs/axios'
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import handlebars from 'handlebars'
|
||||
import { PinoLogger } from 'nestjs-pino'
|
||||
import { lastValueFrom } from 'rxjs'
|
||||
import { serializeError } from 'serialize-error'
|
||||
import { SubmissionEntity } from '../../entity/submission.entity'
|
||||
|
||||
@Injectable()
|
||||
@ -20,15 +22,19 @@ export class SubmissionHookService {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.httpService.post(
|
||||
const response = await lastValueFrom(this.httpService.post(
|
||||
hook.url,
|
||||
this.format(submission, hook.format)
|
||||
).toPromise()
|
||||
))
|
||||
|
||||
console.log('sent hook', response.data)
|
||||
} catch (e) {
|
||||
this.logger.error(`failed to post to "${hook.url}: ${e.message}`)
|
||||
this.logger.error(e.stack)
|
||||
this.logger.error({
|
||||
submission: submission.id,
|
||||
form: submission.formId,
|
||||
webhook: hook.url,
|
||||
error: serializeError(e),
|
||||
}, 'failed to post webhook')
|
||||
throw e
|
||||
}
|
||||
}))
|
||||
|
||||
@ -4,6 +4,7 @@ import handlebars from 'handlebars'
|
||||
import htmlToText from 'html-to-text'
|
||||
import mjml2html from 'mjml'
|
||||
import { PinoLogger } from 'nestjs-pino'
|
||||
import { serializeError } from 'serialize-error'
|
||||
import { SubmissionEntity } from '../../entity/submission.entity'
|
||||
|
||||
@Injectable()
|
||||
@ -22,15 +23,25 @@ export class SubmissionNotificationService {
|
||||
}
|
||||
|
||||
try {
|
||||
const to = this.getEmail(submission.fields.find(field => field.fieldId === notification.toField.id )?.fieldValue, notification.toEmail)
|
||||
const from = this.getEmail(submission.fields.find(field => field.fieldId === notification.fromField.id )?.fieldValue, notification.fromEmail)
|
||||
const to = this.getEmail(
|
||||
submission,
|
||||
notification.toField.id,
|
||||
notification.toEmail
|
||||
)
|
||||
const from = this.getEmail(
|
||||
submission,
|
||||
notification.fromField.id,
|
||||
notification.fromEmail
|
||||
)
|
||||
|
||||
const html = mjml2html(
|
||||
handlebars.compile(
|
||||
notification.htmlTemplate
|
||||
)({
|
||||
const template = handlebars.compile(
|
||||
notification.htmlTemplate
|
||||
)
|
||||
|
||||
const html: string = mjml2html(
|
||||
template({
|
||||
// TODO add variables
|
||||
}),
|
||||
}) ,
|
||||
{
|
||||
minify: true,
|
||||
}
|
||||
@ -41,29 +52,30 @@ export class SubmissionNotificationService {
|
||||
replyTo: from,
|
||||
subject: notification.subject,
|
||||
html,
|
||||
text: htmlToText.fromString(html),
|
||||
text: htmlToText.htmlToText(html),
|
||||
})
|
||||
console.log('sent notification to', to)
|
||||
} catch (e) {
|
||||
this.logger.error(e.stack)
|
||||
this.logger.error({
|
||||
form: submission.formId,
|
||||
submission: submission.id,
|
||||
notification: notification.id,
|
||||
error: serializeError(e),
|
||||
}, 'failed to process notification')
|
||||
throw e
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
private getEmail(raw: string, fallback: string): string {
|
||||
if (!raw) {
|
||||
private getEmail(submission: SubmissionEntity, fieldId: number, fallback: string): string {
|
||||
const data = submission.fields.find(field => field.fieldId === fieldId)?.content
|
||||
|
||||
if (!data) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
|
||||
if (data.value) {
|
||||
return data.value
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error('could not decode field value', raw)
|
||||
if (typeof data.value === 'string') {
|
||||
return data.value
|
||||
}
|
||||
|
||||
return fallback
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm'
|
||||
import { PinoLogger } from 'nestjs-pino'
|
||||
import { Repository } from 'typeorm'
|
||||
import { FormEntity } from '../../entity/form.entity'
|
||||
import { SubmissionEntity } from '../../entity/submission.entity'
|
||||
@ -8,15 +9,22 @@ export class SubmissionService {
|
||||
constructor(
|
||||
@InjectRepository(SubmissionEntity)
|
||||
private readonly submissionRepository: Repository<SubmissionEntity>,
|
||||
private readonly tokenService: SubmissionTokenService
|
||||
private readonly tokenService: SubmissionTokenService,
|
||||
private readonly logger: PinoLogger,
|
||||
) {
|
||||
this.logger.setContext(this.constructor.name)
|
||||
}
|
||||
|
||||
async isOwner(submission: SubmissionEntity, token: string): Promise<boolean> {
|
||||
return await this.tokenService.verify(token, submission.tokenHash)
|
||||
return this.tokenService.verify(token, submission.tokenHash)
|
||||
}
|
||||
|
||||
async find(form: FormEntity, start: number, limit: number, sort: any = {}): Promise<[SubmissionEntity[], number]> {
|
||||
async find(
|
||||
form: FormEntity,
|
||||
start: number,
|
||||
limit: number,
|
||||
sort: any = {}
|
||||
): Promise<[SubmissionEntity[], number]> {
|
||||
const qb = this.submissionRepository.createQueryBuilder('s')
|
||||
|
||||
qb.leftJoinAndSelect('s.fields', 'fields')
|
||||
@ -24,6 +32,9 @@ export class SubmissionService {
|
||||
qb.where('s.form = :form', { form: form.id })
|
||||
|
||||
// TODO readd sort
|
||||
this.logger.debug({
|
||||
sort,
|
||||
}, 'ignored sorting for submissions')
|
||||
|
||||
qb.skip(start)
|
||||
qb.take(limit)
|
||||
|
||||
@ -2,10 +2,11 @@ import { Injectable } from '@nestjs/common'
|
||||
import { InjectRepository } from '@nestjs/typeorm'
|
||||
import dayjs from 'dayjs'
|
||||
import { PinoLogger } from 'nestjs-pino'
|
||||
import { serializeError } from 'serialize-error'
|
||||
import { Repository } from 'typeorm'
|
||||
import { SubmissionSetFieldInput } from '../../dto/submission/submission.set.field.input'
|
||||
import { SubmissionEntity } from '../../entity/submission.entity'
|
||||
import { SubmissionFieldEntity } from '../../entity/submission.field.entity'
|
||||
import { SubmissionFieldContent, SubmissionFieldEntity } from '../../entity/submission.field.entity'
|
||||
import { SubmissionHookService } from './submission.hook.service'
|
||||
import { SubmissionNotificationService } from './submission.notification.service'
|
||||
|
||||
@ -27,7 +28,7 @@ export class SubmissionSetFieldService {
|
||||
let field = submission.fields.find(field => field.field.id.toString() === input.field)
|
||||
|
||||
if (field) {
|
||||
field.fieldValue = JSON.parse(input.data)
|
||||
field.content = this.parseData(field, input.data)
|
||||
|
||||
await this.submissionFieldRepository.save(field)
|
||||
} else {
|
||||
@ -35,8 +36,8 @@ export class SubmissionSetFieldService {
|
||||
|
||||
field.submission = submission
|
||||
field.field = submission.form.fields.find(field => field.id.toString() === input.field)
|
||||
field.fieldType = field.field.type
|
||||
field.fieldValue = JSON.parse(input.data)
|
||||
field.type = field.field.type
|
||||
field.content = this.parseData(field, input.data)
|
||||
|
||||
field = await this.submissionFieldRepository.save(field)
|
||||
|
||||
@ -51,11 +52,103 @@ export class SubmissionSetFieldService {
|
||||
|
||||
if (submission.percentageComplete === 1) {
|
||||
this.webHook.process(submission).catch(e => {
|
||||
this.logger.error(`failed to send webhooks: ${e.message}`)
|
||||
this.logger.error({
|
||||
submission: submission.id,
|
||||
form: submission.formId,
|
||||
error: serializeError(e),
|
||||
}, 'failed to send webhooks')
|
||||
})
|
||||
this.notifications.process(submission).catch(e => {
|
||||
this.logger.error(`failed to send notifications: ${e.message}`)
|
||||
this.logger.error({
|
||||
submission: submission.id,
|
||||
form: submission.formId,
|
||||
error: serializeError(e),
|
||||
}, 'failed to send notifications')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private parseData(
|
||||
field: SubmissionFieldEntity,
|
||||
data: string
|
||||
): SubmissionFieldContent {
|
||||
let raw: { [key: string]: unknown }
|
||||
|
||||
const context = {
|
||||
field: field.fieldId,
|
||||
type: field.type,
|
||||
}
|
||||
|
||||
try {
|
||||
raw = JSON.parse(data) as { [key: string]: unknown }
|
||||
} catch (e) {
|
||||
this.logger.warn(context, 'received invalid data for field')
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
||||
this.logger.warn(context, 'only object supported for data')
|
||||
return null
|
||||
}
|
||||
|
||||
// now ensure data structure
|
||||
const result = {
|
||||
value: null,
|
||||
}
|
||||
|
||||
let valid = true
|
||||
|
||||
Object.keys(raw).forEach((key) => {
|
||||
const value = raw[String(key)]
|
||||
|
||||
switch (typeof value) {
|
||||
case 'number':
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
result[String(key)] = value
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
result[String(key)] = value.map((row: unknown, index) => {
|
||||
switch (typeof value) {
|
||||
case 'number':
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
case 'undefined':
|
||||
return row
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
return row
|
||||
}
|
||||
|
||||
this.logger.warn({
|
||||
...context,
|
||||
path: `${key}/${index}`,
|
||||
}, 'invalid data in array')
|
||||
valid = false
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.warn({
|
||||
...context,
|
||||
path: String(key),
|
||||
|
||||
}, 'invalid data in entry')
|
||||
|
||||
valid = false
|
||||
})
|
||||
|
||||
if (!valid) {
|
||||
this.logger.warn(context, 'invalid data in object entries')
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,8 @@ export class SubmissionStartService {
|
||||
submission.timeElapsed = 0
|
||||
submission.percentageComplete = 0
|
||||
|
||||
// TODO set country!
|
||||
|
||||
submission.device.language = input.device.language
|
||||
submission.device.name = input.device.name
|
||||
submission.device.type = input.device.type
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import * as bcrypt from 'bcrypt'
|
||||
|
||||
@Injectable()
|
||||
export class SubmissionTokenService {
|
||||
async hash(token: string): Promise<string> {
|
||||
return token
|
||||
return bcrypt.hash(token, 4)
|
||||
}
|
||||
|
||||
async verify(token: string, hash: string): Promise<boolean> {
|
||||
return token == hash
|
||||
return await bcrypt.compare(token, hash)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Injectable, OnApplicationBootstrap } from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import { PinoLogger } from 'nestjs-pino'
|
||||
import { serializeError } from 'serialize-error'
|
||||
import { UserCreateService } from './user.create.service'
|
||||
import { UserService } from './user.service'
|
||||
|
||||
@ -30,19 +31,15 @@ export class BootService implements OnApplicationBootstrap {
|
||||
const email = this.configService.get<string>('ADMIN_EMAIL', 'admin@ohmyform.com')
|
||||
const password = this.configService.get<string>('ADMIN_PASSWORD', 'root')
|
||||
|
||||
try {
|
||||
await this.userService.findByUsername(username)
|
||||
|
||||
if (await this.userService.usernameInUse(username)) {
|
||||
this.logger.info('username already exists, skip creating')
|
||||
return
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
await this.userService.findByEmail(email)
|
||||
}
|
||||
|
||||
if (await this.userService.emailInUse(email)) {
|
||||
this.logger.info('email already exists, skip creating')
|
||||
return
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.createService.create({
|
||||
@ -54,7 +51,7 @@ export class BootService implements OnApplicationBootstrap {
|
||||
])
|
||||
} catch (e) {
|
||||
this.logger.error({
|
||||
error: e,
|
||||
error: serializeError(e),
|
||||
}, 'could not create admin user')
|
||||
return
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { UserCreateService } from './user.create.service'
|
||||
import { UserDeleteService } from './user.delete.service'
|
||||
import { UserService } from './user.service'
|
||||
import { UserStatisticService } from './user.statistic.service'
|
||||
import { UserTokenService } from './user.token.service'
|
||||
import { UserUpdateService } from './user.update.service'
|
||||
|
||||
export const userServices = [
|
||||
@ -11,5 +12,6 @@ export const userServices = [
|
||||
UserDeleteService,
|
||||
UserService,
|
||||
UserStatisticService,
|
||||
UserTokenService,
|
||||
UserUpdateService,
|
||||
]
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import { InjectRepository } from '@nestjs/typeorm'
|
||||
import crypto from 'crypto'
|
||||
import { PinoLogger } from 'nestjs-pino'
|
||||
import { Repository } from 'typeorm'
|
||||
import { rolesType } from '../../config/roles'
|
||||
@ -8,6 +10,7 @@ import { UserEntity } from '../../entity/user.entity'
|
||||
import { PasswordService } from '../auth/password.service'
|
||||
import { MailService } from '../mail.service'
|
||||
import { SettingService } from '../setting.service'
|
||||
import { UserTokenService } from './user.token.service'
|
||||
|
||||
@Injectable()
|
||||
export class UserCreateService {
|
||||
@ -18,12 +21,14 @@ export class UserCreateService {
|
||||
private readonly logger: PinoLogger,
|
||||
private readonly passwordService: PasswordService,
|
||||
private readonly settingService: SettingService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly userTokenService: UserTokenService,
|
||||
) {
|
||||
logger.setContext(this.constructor.name)
|
||||
}
|
||||
|
||||
private async getDefaultRoles(): Promise<rolesType> {
|
||||
const roleSetting = await this.settingService.getByKey('DEFAULT_ROLE')
|
||||
private getDefaultRoles(): rolesType {
|
||||
const roleSetting = this.settingService.getByKey('DEFAULT_ROLE')
|
||||
|
||||
switch (roleSetting.value) {
|
||||
case 'superuser':
|
||||
@ -49,6 +54,8 @@ export class UserCreateService {
|
||||
throw new Error('email already in use')
|
||||
}
|
||||
|
||||
const confirmToken = crypto.randomBytes(30).toString('base64')
|
||||
|
||||
let user = new UserEntity()
|
||||
|
||||
user.provider = 'local'
|
||||
@ -57,17 +64,25 @@ export class UserCreateService {
|
||||
user.firstName = input.firstName
|
||||
user.lastName = input.lastName
|
||||
user.language = input.language ?? 'en'
|
||||
user.roles = roles ? roles : await this.getDefaultRoles()
|
||||
user.roles = roles ? roles : this.getDefaultRoles()
|
||||
user.passwordHash = await this.passwordService.hash(input.password)
|
||||
user.token = await this.userTokenService.hash(confirmToken)
|
||||
|
||||
user = await this.userRepository.save(user)
|
||||
|
||||
const confirmUrl = [
|
||||
this.configService.get('BASE_URL', 'http://localhost'),
|
||||
this.configService.get('USER_CONFIRM_PATH', '/confirm?token={{token}}'),
|
||||
]
|
||||
.join('')
|
||||
.replace('{{token}}', confirmToken)
|
||||
|
||||
const sent = await this.mailerService.send(
|
||||
user.email,
|
||||
'user/created',
|
||||
{
|
||||
username: user.username,
|
||||
confirm: 'https://www.google.com', // TODO confirm url
|
||||
confirm: confirmUrl,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -59,4 +59,16 @@ export class UserService {
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
async usernameInUse(username: string): Promise<boolean> {
|
||||
return 0 !== await this.userRepository.count({
|
||||
username,
|
||||
})
|
||||
}
|
||||
|
||||
async emailInUse(email: string): Promise<boolean> {
|
||||
return 0 !== await this.userRepository.count({
|
||||
email,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
13
src/service/user/user.token.service.ts
Normal file
13
src/service/user/user.token.service.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import * as bcrypt from 'bcrypt'
|
||||
|
||||
@Injectable()
|
||||
export class UserTokenService {
|
||||
async hash(token: string): Promise<string> {
|
||||
return bcrypt.hash(token, 4)
|
||||
}
|
||||
|
||||
async verify(token: string, hash: string): Promise<boolean> {
|
||||
return await bcrypt.compare(token, hash)
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,12 @@ export class UserUpdateService {
|
||||
|
||||
if (input.email !== undefined) {
|
||||
user.email = input.email
|
||||
user.emailVerified = false
|
||||
// TODO request email verification
|
||||
|
||||
if (undefined !== await this.userRepository.findOne({ email: input.email })) {
|
||||
throw new Error('email already in use')
|
||||
}
|
||||
}
|
||||
|
||||
if (input.username !== undefined) {
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@ -898,7 +898,7 @@
|
||||
tslib "~2.3.0"
|
||||
value-or-promise "1.0.11"
|
||||
|
||||
"@graphql-tools/utils@7.10.0":
|
||||
"@graphql-tools/utils@7.10.0", "@graphql-tools/utils@^7.0.0":
|
||||
version "7.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-7.10.0.tgz#07a4cb5d1bec1ff1dc1d47a935919ee6abd38699"
|
||||
integrity sha512-d334r6bo9mxdSqZW6zWboEnnOOFRrAPVQJ7LkU8/6grglrbcu6WhwCLzHb90E94JI3TD3ricC3YGbUqIi9Xg0w==
|
||||
@ -907,7 +907,7 @@
|
||||
camel-case "4.1.2"
|
||||
tslib "~2.2.0"
|
||||
|
||||
"@graphql-tools/utils@^7.0.0", "@graphql-tools/utils@^7.1.2":
|
||||
"@graphql-tools/utils@^7.1.2":
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-7.8.0.tgz#74290863b5c84c1bf1d8749e7a05b1b029a0c55e"
|
||||
integrity sha512-nORIltDwBdsc3Ew+vuXISTZw6gpRd3UkK+6HNY3knNca6apTDj7ygcJmgsFKjSyUf7xtukddTcF6Js9kPuJr6g==
|
||||
@ -1781,6 +1781,18 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256"
|
||||
integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==
|
||||
|
||||
"@types/mjml-core@*":
|
||||
version "4.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/mjml-core/-/mjml-core-4.7.1.tgz#c2627499045b54eccfca38e2b532566fb0689189"
|
||||
integrity sha512-k5IRafi93tyZBGF+0BTrcBDvG47OueI+Q7TC4V4UjGQn0AMVvL3Y+S26QF/UHMmMJW5r1hxLyv3StX2/+FatFg==
|
||||
|
||||
"@types/mjml@^4.7.0":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/mjml/-/mjml-4.7.0.tgz#ea31b58008f54119efda9e673af674757d35981b"
|
||||
integrity sha512-aWWu8Lxq2SexXGs+lBPRUpN3kFf0sDRo3Y4jz7BQ15cQvMfyZOadgFJsNlHmDqI6D2Qjx0PIK+1f9IMXgq9vTA==
|
||||
dependencies:
|
||||
"@types/mjml-core" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "14.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.2.tgz#264b44c5a28dfa80198fc2f7b6d3c8a054b9491f"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user