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,
|
jest: true,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||||
'@typescript-eslint/no-unsafe-member-access': '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
|
- forms now have multiple notification
|
||||||
- layout for forms
|
- layout for forms
|
||||||
- mariadb / mysql support
|
- mariadb / mysql support
|
||||||
|
- user confirmation tokens
|
||||||
|
- email verification
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@ -20,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- colors object removed the "colors" postfix
|
- colors object removed the "colors" postfix
|
||||||
- if unsupported database engine is used error is thrown during startup
|
- if unsupported database engine is used error is thrown during startup
|
||||||
- improved eslint checks
|
- improved eslint checks
|
||||||
|
- validate submission field data and store it json encoded
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@ -1,43 +1,46 @@
|
|||||||
# Environment Variables
|
# Environment Variables
|
||||||
|
|
||||||
| Name | Default Value | Description |
|
| Name | Default Value | Description |
|
||||||
| ---- | ------------- | ----------- |
|
|------------------------------|----------------------------|---------------------------------------------------------------------------------------|
|
||||||
| DISABLE_INSTALLATION_METRICS | *not set* | Per default installations are [publishing](./installation.metrics.md) their existence |
|
| DISABLE_INSTALLATION_METRICS | *not set* | Per default installations are [publishing](./installation.metrics.md) their existence |
|
||||||
| SECRET_KEY | `changeMe` | JWT Secret for authentication |
|
| SECRET_KEY | `changeMe` | JWT Secret for authentication |
|
||||||
| CLI | *automatically* | activates pretty print for log output |
|
| CLI | *automatically* | activates pretty print for log output |
|
||||||
| NODE_ENV | `production` | |
|
| NODE_ENV | `production` | |
|
||||||
| HIDE_CONTRIB | `false` | decide if backlings to ohmyform should be added |
|
| HIDE_CONTRIB | `false` | decide if backlings to ohmyform should be added |
|
||||||
| SIGNUP_DISABLED | `false` | if users can sign up |
|
| SIGNUP_DISABLED | `false` | if users can sign up |
|
||||||
| LOGIN_NOTE | *not set* | Info box on top of login screen |
|
| LOGIN_NOTE | *not set* | Info box on top of login screen |
|
||||||
| LOCALES_PATH | *not set* | Path to translated elementes in backend like emails |
|
| 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
|
## Default Account
|
||||||
|
|
||||||
*username and email are unique on an instance*
|
*username and email are unique on an instance*
|
||||||
|
|
||||||
| Name | Default Value | Description |
|
| Name | Default Value | Description |
|
||||||
| ---- | ------------- | ----------- |
|
|----------------|----------------------|-------------------------------------|
|
||||||
| CREATE_ADMIN | `false` | if `true` will create a super admin |
|
| CREATE_ADMIN | `false` | if `true` will create a super admin |
|
||||||
| ADMIN_USERNAME | `root` | username for the default admin user |
|
| ADMIN_USERNAME | `root` | username for the default admin user |
|
||||||
| ADMIN_EMAIL | `admin@ohmyform.com` | email to send notifications |
|
| ADMIN_EMAIL | `admin@ohmyform.com` | email to send notifications |
|
||||||
| ADMIN_PASSWORD | `root` | password for user |
|
| ADMIN_PASSWORD | `root` | password for user |
|
||||||
|
|
||||||
## Mailing
|
## Mailing
|
||||||
|
|
||||||
| Name | Default Value | Description |
|
| Name | Default Value | Description |
|
||||||
| ---- | ------------- | ----------- |
|
|-------------|---------------------------------|-----------------------------------------------------------------------------------|
|
||||||
| MAILER_URI | `smtp://localhost:1025` | [Mail Connection](https://nodemailer.com/smtp/) |
|
| 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 |
|
| MAILER_FROM | `OhMyForm <no-reply@localhost>` | Default From path, make sure that your mail server supports the given from addres |
|
||||||
|
|
||||||
## Database Variables
|
## Database Variables
|
||||||
|
|
||||||
| Name | Default Value | Description |
|
| Name | Default Value | Description |
|
||||||
| ---- | ------------- | ----------- |
|
|-----------------------|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| DATABASE_DRIVER | `sqlite` | database driver, either `sqlite` or `postgres` |
|
| 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_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_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_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_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 |
|
| DATABASE_SSL | `false` | if `true` will require ssl database connection |
|
||||||
| REDIS_HOST | *not set* | required in multinode environments |
|
| REDIS_HOST | *not set* | required in multinode environments |
|
||||||
| REDIS_PORT | `6379` | port for redis |
|
| REDIS_PORT | `6379` | port for redis |
|
||||||
|
|||||||
@ -10,12 +10,13 @@
|
|||||||
"src/entity/**/*.ts"
|
"src/entity/**/*.ts"
|
||||||
],
|
],
|
||||||
"migrations": [
|
"migrations": [
|
||||||
"src/migrations/maria/**/*.ts"
|
"src/migrations/mariadb/**/*.ts"
|
||||||
],
|
],
|
||||||
|
"migrationsTransactionMode": "each",
|
||||||
"subscribers": [
|
"subscribers": [
|
||||||
"src/subscriber/**/*.ts"
|
"src/subscriber/**/*.ts"
|
||||||
],
|
],
|
||||||
"cli": {
|
"cli": {
|
||||||
"migrationsDir": "src/migrations/maria"
|
"migrationsDir": "src/migrations/mariadb"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -12,6 +12,7 @@
|
|||||||
"migrations": [
|
"migrations": [
|
||||||
"src/migrations/postgres/**/*.ts"
|
"src/migrations/postgres/**/*.ts"
|
||||||
],
|
],
|
||||||
|
"migrationsTransactionMode": "each",
|
||||||
"subscribers": [
|
"subscribers": [
|
||||||
"src/subscriber/**/*.ts"
|
"src/subscriber/**/*.ts"
|
||||||
],
|
],
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"migrations": [
|
"migrations": [
|
||||||
"src/migrations/sqlite/**/*.ts"
|
"src/migrations/sqlite/**/*.ts"
|
||||||
],
|
],
|
||||||
|
"migrationsTransactionMode": "each",
|
||||||
"cli": {
|
"cli": {
|
||||||
"migrationsDir": "src/migrations/sqlite"
|
"migrationsDir": "src/migrations/sqlite"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"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: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: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": {
|
"dependencies": {
|
||||||
"@nestjs-modules/mailer": "^1.6.0",
|
"@nestjs-modules/mailer": "^1.6.0",
|
||||||
@ -82,6 +82,7 @@
|
|||||||
"@types/html-to-text": "^8.0.1",
|
"@types/html-to-text": "^8.0.1",
|
||||||
"@types/inquirer": "^8.1.3",
|
"@types/inquirer": "^8.1.3",
|
||||||
"@types/jest": "26.0.23",
|
"@types/jest": "26.0.23",
|
||||||
|
"@types/mjml": "^4.7.0",
|
||||||
"@types/node": "^16.11.17",
|
"@types/node": "^16.11.17",
|
||||||
"@types/passport-jwt": "^3.0.6",
|
"@types/passport-jwt": "^3.0.6",
|
||||||
"@types/passport-local": "^1.0.34",
|
"@types/passport-local": "^1.0.34",
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export const imports = [
|
|||||||
}),
|
}),
|
||||||
ServeStaticModule.forRoot({
|
ServeStaticModule.forRoot({
|
||||||
rootPath: join(__dirname, '..', 'public'),
|
rootPath: join(__dirname, '..', 'public'),
|
||||||
exclude: [],
|
exclude: ['/graphql'],
|
||||||
}),
|
}),
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
load: [
|
load: [
|
||||||
@ -122,8 +122,8 @@ export const imports = [
|
|||||||
break
|
break
|
||||||
|
|
||||||
case 'mysql':
|
case 'mysql':
|
||||||
case 'maria':
|
case 'mariadb':
|
||||||
migrationFolder = 'maria'
|
migrationFolder = 'mariadb'
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'sqlite':
|
case 'sqlite':
|
||||||
@ -146,6 +146,7 @@ export const imports = [
|
|||||||
entities,
|
entities,
|
||||||
migrations: [`${__dirname}/**/migrations/${migrationFolder}/**/*{.ts,.js}`],
|
migrations: [`${__dirname}/**/migrations/${migrationFolder}/**/*{.ts,.js}`],
|
||||||
migrationsRun: configService.get<boolean>('DATABASE_MIGRATE', true),
|
migrationsRun: configService.get<boolean>('DATABASE_MIGRATE', true),
|
||||||
|
migrationsTransactionMode: 'each',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Field, ID, InputType } from '@nestjs/graphql'
|
import { Field, ID, InputType } from '@nestjs/graphql'
|
||||||
|
import { FormFieldLogicAction } from '../../entity/form.field.logic.entity'
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class FormFieldLogicInput {
|
export class FormFieldLogicInput {
|
||||||
@ -8,8 +9,9 @@ export class FormFieldLogicInput {
|
|||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
readonly formula: string
|
readonly formula: string
|
||||||
|
|
||||||
@Field({ nullable: true })
|
// TODO verify action value
|
||||||
readonly action: string
|
@Field(() => String, { nullable: true })
|
||||||
|
readonly action: FormFieldLogicAction
|
||||||
|
|
||||||
@Field(() => ID, { nullable: true })
|
@Field(() => ID, { nullable: true })
|
||||||
readonly jumpTo?: string
|
readonly jumpTo?: string
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export class SubmissionFieldModel {
|
|||||||
|
|
||||||
constructor(field: SubmissionFieldEntity) {
|
constructor(field: SubmissionFieldEntity) {
|
||||||
this.id = field.id.toString()
|
this.id = field.id.toString()
|
||||||
this.value = field.fieldValue
|
this.value = JSON.stringify(field.content)
|
||||||
this.type = field.fieldType
|
this.type = field.type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,9 +6,15 @@ export class UserModel {
|
|||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
readonly id: string
|
readonly id: string
|
||||||
|
|
||||||
@Field()
|
/**
|
||||||
|
* @deprecated use emailVerified instead
|
||||||
|
*/
|
||||||
|
@Field({ deprecationReason: 'use emailVerified instead' })
|
||||||
readonly verifiedEmail: boolean
|
readonly verifiedEmail: boolean
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly emailVerified: boolean
|
||||||
|
|
||||||
@Field()
|
@Field()
|
||||||
readonly username: string
|
readonly username: string
|
||||||
|
|
||||||
@ -39,7 +45,8 @@ export class UserModel {
|
|||||||
this.firstName = user.firstName
|
this.firstName = user.firstName
|
||||||
this.lastName = user.lastName
|
this.lastName = user.lastName
|
||||||
|
|
||||||
this.verifiedEmail = !user.token
|
this.verifiedEmail = user.emailVerified
|
||||||
|
this.emailVerified = user.emailVerified
|
||||||
|
|
||||||
this.created = user.created
|
this.created = user.created
|
||||||
this.lastModified = user.lastModified
|
this.lastModified = user.lastModified
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||||
import { FormFieldEntity } from './form.field.entity'
|
import { FormFieldEntity } from './form.field.entity'
|
||||||
|
|
||||||
type LogicAction = 'visible' | 'require' | 'disable' | 'jumpTo'
|
export type FormFieldLogicAction = 'visible' | 'require' | 'disable' | 'jumpTo'
|
||||||
|
|
||||||
@Entity({ name: 'form_field_logic' })
|
@Entity({ name: 'form_field_logic' })
|
||||||
export class FormFieldLogicEntity {
|
export class FormFieldLogicEntity {
|
||||||
@ -15,7 +15,7 @@ export class FormFieldLogicEntity {
|
|||||||
public formula: string
|
public formula: string
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 10 })
|
@Column({ type: 'varchar', length: 10 })
|
||||||
public action: LogicAction
|
public action: FormFieldLogicAction
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public visible?: boolean
|
public visible?: boolean
|
||||||
|
|||||||
@ -2,6 +2,10 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, RelationId } from 't
|
|||||||
import { FormFieldEntity } from './form.field.entity'
|
import { FormFieldEntity } from './form.field.entity'
|
||||||
import { SubmissionEntity } from './submission.entity'
|
import { SubmissionEntity } from './submission.entity'
|
||||||
|
|
||||||
|
export interface SubmissionFieldContent {
|
||||||
|
[key: string]: string | string[] | number | number[] | boolean | boolean[]
|
||||||
|
}
|
||||||
|
|
||||||
@Entity({ name: 'submission_field' })
|
@Entity({ name: 'submission_field' })
|
||||||
export class SubmissionFieldEntity {
|
export class SubmissionFieldEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
@ -17,8 +21,8 @@ export class SubmissionFieldEntity {
|
|||||||
readonly fieldId: number
|
readonly fieldId: number
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
public fieldType: string
|
public type: string
|
||||||
|
|
||||||
@Column()
|
@Column('simple-json')
|
||||||
public fieldValue: string
|
public content: SubmissionFieldContent
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,9 @@ export class UserEntity {
|
|||||||
@Column({ length: 255, unique: true })
|
@Column({ length: 255, unique: true })
|
||||||
public email: string
|
public email: string
|
||||||
|
|
||||||
|
@Column('boolean', { default: false })
|
||||||
|
public emailVerified = false
|
||||||
|
|
||||||
@Column({ length: 255, unique: true })
|
@Column({ length: 255, unique: true })
|
||||||
public username: string
|
public username: string
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import { NestApplicationOptions, ValidationPipe } from '@nestjs/common'
|
import { NestApplicationOptions, ValidationPipe } from '@nestjs/common'
|
||||||
import { NestFactory } from '@nestjs/core'
|
import { NestFactory } from '@nestjs/core'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import { Logger, PinoLogger } from 'nestjs-pino'
|
import { Logger } from 'nestjs-pino'
|
||||||
import { LoggerConfig } from './app.imports'
|
|
||||||
import { AppModule } from './app.module'
|
import { AppModule } from './app.module'
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const options: NestApplicationOptions = {
|
const options: NestApplicationOptions = {
|
||||||
logger: new Logger(new PinoLogger(LoggerConfig), {}),
|
|
||||||
bufferLogs: true,
|
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 "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 "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 "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)');
|
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'
|
name = 'layout1621078163528'
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
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> {
|
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> {
|
): Promise<DeletedModel> {
|
||||||
const form = await this.formService.findById(id)
|
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')
|
throw new Error('invalid form')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export class FormQuery {
|
|||||||
): Promise<FormModel> {
|
): Promise<FormModel> {
|
||||||
const form = await this.formService.findById(id)
|
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')
|
throw new Error('invalid form')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export class FormResolver {
|
|||||||
): Promise<boolean> {
|
): 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 (!await this.formService.isAdmin(form, user)) {
|
if (!this.formService.isAdmin(form, user)) {
|
||||||
throw new Error('no access to field')
|
throw new Error('no access to field')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ export class FormResolver {
|
|||||||
): Promise<FormNotificationModel[]> {
|
): 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 (!await this.formService.isAdmin(form, user)) {
|
if (!this.formService.isAdmin(form, user)) {
|
||||||
throw new Error('no access to field')
|
throw new Error('no access to field')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export class FormUpdateMutation {
|
|||||||
): Promise<FormModel> {
|
): Promise<FormModel> {
|
||||||
const form = await this.formService.findById(input.id)
|
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')
|
throw new Error('invalid form')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,10 +8,10 @@ import { ContextCache } from '../context.cache'
|
|||||||
export class ProfileResolver {
|
export class ProfileResolver {
|
||||||
@Query(() => ProfileModel)
|
@Query(() => ProfileModel)
|
||||||
@Roles('user')
|
@Roles('user')
|
||||||
async me(
|
public me(
|
||||||
@User() user: UserEntity,
|
@User() user: UserEntity,
|
||||||
@Context('cache') cache: ContextCache,
|
@Context('cache') cache: ContextCache,
|
||||||
): Promise<ProfileModel> {
|
): ProfileModel {
|
||||||
cache.add(cache.getCacheKey(UserEntity.name, user.id), user)
|
cache.add(cache.getCacheKey(UserEntity.name, user.id), user)
|
||||||
|
|
||||||
return new ProfileModel(user)
|
return new ProfileModel(user)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
import { Args, Context, Mutation } from '@nestjs/graphql'
|
import { Args, Context, Mutation } from '@nestjs/graphql'
|
||||||
|
import { Roles } from '../../decorator/roles.decorator'
|
||||||
import { User } from '../../decorator/user.decorator'
|
import { User } from '../../decorator/user.decorator'
|
||||||
import { ProfileModel } from '../../dto/profile/profile.model'
|
import { ProfileModel } from '../../dto/profile/profile.model'
|
||||||
import { ProfileUpdateInput } from '../../dto/profile/profile.update.input'
|
import { ProfileUpdateInput } from '../../dto/profile/profile.update.input'
|
||||||
@ -15,6 +16,7 @@ export class ProfileUpdateMutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => ProfileModel)
|
@Mutation(() => ProfileModel)
|
||||||
|
@Roles('user')
|
||||||
async updateProfile(
|
async updateProfile(
|
||||||
@User() user: UserEntity,
|
@User() user: UserEntity,
|
||||||
@Args({ name: 'user', type: () => ProfileUpdateInput }) input: ProfileUpdateInput,
|
@Args({ name: 'user', type: () => ProfileUpdateInput }) input: ProfileUpdateInput,
|
||||||
@ -26,4 +28,18 @@ export class ProfileUpdateMutation {
|
|||||||
|
|
||||||
return new ProfileModel(user)
|
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)
|
@Query(() => SettingModel)
|
||||||
async getSetting(
|
getSetting(
|
||||||
@Args('key', {type: () => ID}) key: string,
|
@Args('key', {type: () => ID}) key: string,
|
||||||
@User() user: UserEntity,
|
@User() user: UserEntity,
|
||||||
): Promise<SettingModel> {
|
): SettingModel {
|
||||||
if (!this.settingService.isPublicKey(key) && !user.roles.includes('superuser')) {
|
if (!this.settingService.isPublicKey(key) && !user.roles.includes('superuser')) {
|
||||||
throw new Error(`no access to key ${key}`)
|
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')
|
await this.submissionRepository.createQueryBuilder('s')
|
||||||
.delete()
|
.delete()
|
||||||
.where('s.form = :form', { form: id })
|
.where('s.form = :form', { form: id })
|
||||||
|
.execute()
|
||||||
|
|
||||||
await this.formRepository.createQueryBuilder('f')
|
await this.formRepository.createQueryBuilder('f')
|
||||||
.delete()
|
.delete()
|
||||||
.where('f.id = :form', { form: id })
|
.where('f.id = :form', { form: id })
|
||||||
|
.execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
import { InjectRepository } from '@nestjs/typeorm'
|
import { InjectRepository } from '@nestjs/typeorm'
|
||||||
|
import { PinoLogger } from 'nestjs-pino'
|
||||||
import { Repository } from 'typeorm'
|
import { Repository } from 'typeorm'
|
||||||
import { FormEntity } from '../../entity/form.entity'
|
import { FormEntity } from '../../entity/form.entity'
|
||||||
import { UserEntity } from '../../entity/user.entity'
|
import { UserEntity } from '../../entity/user.entity'
|
||||||
@ -9,10 +10,12 @@ export class FormService {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(FormEntity)
|
@InjectRepository(FormEntity)
|
||||||
private readonly formRepository: Repository<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) {
|
if (!user) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -24,7 +27,12 @@ export class FormService {
|
|||||||
return form.admin.id === user.id
|
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')
|
const qb = this.formRepository.createQueryBuilder('f')
|
||||||
|
|
||||||
qb.leftJoinAndSelect('f.admin', 'a')
|
qb.leftJoinAndSelect('f.admin', 'a')
|
||||||
@ -34,6 +42,9 @@ export class FormService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO readd sort
|
// TODO readd sort
|
||||||
|
this.logger.debug({
|
||||||
|
sort,
|
||||||
|
}, 'ignored sorting for submissions')
|
||||||
|
|
||||||
qb.skip(start)
|
qb.skip(start)
|
||||||
qb.take(limit)
|
qb.take(limit)
|
||||||
|
|||||||
@ -41,8 +41,12 @@ export class FormUpdateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (input.fields !== undefined) {
|
if (input.fields !== undefined) {
|
||||||
form.fields = await Promise.all(input.fields.map(async (nextField) => {
|
form.fields = input.fields.map((nextField) => {
|
||||||
let field = form.fields.find(field => field.id?.toString() === nextField.id)
|
let field = this.findByIdInList(
|
||||||
|
form.fields,
|
||||||
|
nextField.id,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
if (!field) {
|
if (!field) {
|
||||||
field = new FormFieldEntity()
|
field = new FormFieldEntity()
|
||||||
@ -75,7 +79,11 @@ export class FormUpdateService {
|
|||||||
|
|
||||||
if (nextField.logic !== undefined) {
|
if (nextField.logic !== undefined) {
|
||||||
field.logic = nextField.logic.map(nextLogic => {
|
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
|
logic.field = field
|
||||||
|
|
||||||
@ -83,7 +91,7 @@ export class FormUpdateService {
|
|||||||
logic.formula = nextLogic.formula
|
logic.formula = nextLogic.formula
|
||||||
}
|
}
|
||||||
if (nextLogic.action !== undefined) {
|
if (nextLogic.action !== undefined) {
|
||||||
logic.action = nextLogic.action as any
|
logic.action = nextLogic.action
|
||||||
}
|
}
|
||||||
if (nextLogic.visible !== undefined) {
|
if (nextLogic.visible !== undefined) {
|
||||||
logic.visible = nextLogic.visible
|
logic.visible = nextLogic.visible
|
||||||
@ -95,7 +103,11 @@ export class FormUpdateService {
|
|||||||
logic.disable = nextLogic.disable
|
logic.disable = nextLogic.disable
|
||||||
}
|
}
|
||||||
if (nextLogic.jumpTo !== undefined) {
|
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) {
|
if (nextLogic.enabled !== undefined) {
|
||||||
logic.enabled = nextLogic.enabled
|
logic.enabled = nextLogic.enabled
|
||||||
@ -107,7 +119,11 @@ export class FormUpdateService {
|
|||||||
|
|
||||||
if (nextField.options !== undefined) {
|
if (nextField.options !== undefined) {
|
||||||
field.options = nextField.options.map(nextOption => {
|
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
|
option.field = field
|
||||||
|
|
||||||
@ -124,13 +140,16 @@ export class FormUpdateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return field
|
return field
|
||||||
}))
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.hooks !== undefined) {
|
if (input.hooks !== undefined) {
|
||||||
form.hooks = input.hooks.map((nextHook) => {
|
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
|
// ability for other fields to apply mapping
|
||||||
hook.url = nextHook.url
|
hook.url = nextHook.url
|
||||||
@ -179,7 +198,11 @@ export class FormUpdateService {
|
|||||||
|
|
||||||
if (input.notifications !== undefined) {
|
if (input.notifications !== undefined) {
|
||||||
form.notifications = input.notifications.map(notificationInput => {
|
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.form = form
|
||||||
notification.enabled = notificationInput.enabled
|
notification.enabled = notificationInput.enabled
|
||||||
@ -188,7 +211,11 @@ export class FormUpdateService {
|
|||||||
notification.fromEmail = notificationInput.fromEmail
|
notification.fromEmail = notificationInput.fromEmail
|
||||||
}
|
}
|
||||||
if (notificationInput.fromField !== undefined) {
|
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) {
|
if (notificationInput.subject !== undefined) {
|
||||||
notification.subject = notificationInput.subject
|
notification.subject = notificationInput.subject
|
||||||
@ -200,7 +227,11 @@ export class FormUpdateService {
|
|||||||
notification.toEmail = notificationInput.toEmail
|
notification.toEmail = notificationInput.toEmail
|
||||||
}
|
}
|
||||||
if (notificationInput.toField !== undefined) {
|
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
|
return notification
|
||||||
@ -240,7 +271,11 @@ export class FormUpdateService {
|
|||||||
|
|
||||||
if (input.startPage.buttons !== undefined) {
|
if (input.startPage.buttons !== undefined) {
|
||||||
form.startPage.buttons = input.startPage.buttons.map(buttonInput => {
|
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.page = form.startPage
|
||||||
entity.url = buttonInput.url
|
entity.url = buttonInput.url
|
||||||
entity.action = buttonInput.action
|
entity.action = buttonInput.action
|
||||||
@ -278,7 +313,11 @@ export class FormUpdateService {
|
|||||||
|
|
||||||
if (input.endPage.buttons !== undefined) {
|
if (input.endPage.buttons !== undefined) {
|
||||||
form.endPage.buttons = input.endPage.buttons.map(buttonInput => {
|
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.page = form.endPage
|
||||||
entity.url = buttonInput.url
|
entity.url = buttonInput.url
|
||||||
entity.action = buttonInput.action
|
entity.action = buttonInput.action
|
||||||
@ -296,4 +335,18 @@ export class FormUpdateService {
|
|||||||
|
|
||||||
return form
|
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)
|
logger.setContext(this.constructor.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
async onApplicationBootstrap(): Promise<void> {
|
onApplicationBootstrap(): void {
|
||||||
if (this.configService.get<boolean>('DISABLE_INSTALLATION_METRICS')) {
|
if (this.configService.get<boolean>('DISABLE_INSTALLATION_METRICS')) {
|
||||||
this.logger.info('installation metrics are disabled')
|
this.logger.info('installation metrics are disabled')
|
||||||
return
|
return
|
||||||
@ -42,6 +42,4 @@ export class InstallationMetricsService implements OnApplicationBootstrap {
|
|||||||
})
|
})
|
||||||
}, 24 * 60 * 60 * 1000)
|
}, 24 * 60 * 60 * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import htmlToText from 'html-to-text'
|
|||||||
import mjml2html from 'mjml'
|
import mjml2html from 'mjml'
|
||||||
import { PinoLogger } from 'nestjs-pino'
|
import { PinoLogger } from 'nestjs-pino'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
import { serializeError } from 'serialize-error'
|
||||||
import { defaultLanguage } from '../config/languages'
|
import { defaultLanguage } from '../config/languages'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -19,10 +20,19 @@ export class MailService {
|
|||||||
logger.setContext(this.constructor.name)
|
logger.setContext(this.constructor.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(to: string, template: string, context: { [key: string]: any }, language: string = defaultLanguage): Promise<boolean> {
|
async send(
|
||||||
this.logger.info({
|
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,
|
email: to,
|
||||||
}, `send email ${template}`)
|
template,
|
||||||
|
}, 'try to send email')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const path = this.getTemplatePath(template, language)
|
const path = this.getTemplatePath(template, language)
|
||||||
|
|
||||||
@ -35,23 +45,36 @@ export class MailService {
|
|||||||
}
|
}
|
||||||
).html
|
).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 })
|
await this.nestMailer.sendMail({ to, subject, html, text })
|
||||||
this.logger.info('sent email')
|
this.logger.info({
|
||||||
|
email: to,
|
||||||
|
template,
|
||||||
|
language,
|
||||||
|
}, 'sent email')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
error: error.message,
|
error: serializeError(error),
|
||||||
email: to,
|
email: to,
|
||||||
}, `failed to send email ${template}`)
|
template,
|
||||||
|
}, 'failed to send email')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
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 {
|
private getTemplatePath(template: string, language: string): string {
|
||||||
let templatePath = join(this.configService.get<string>('LOCALES_PATH'), language, 'mail', `${template}.mjml`)
|
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 { ProfileUpdateInput } from '../../dto/profile/profile.update.input'
|
||||||
import { UserEntity } from '../../entity/user.entity'
|
import { UserEntity } from '../../entity/user.entity'
|
||||||
import { PasswordService } from '../auth/password.service'
|
import { PasswordService } from '../auth/password.service'
|
||||||
|
import { UserTokenService } from '../user/user.token.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProfileUpdateService {
|
export class ProfileUpdateService {
|
||||||
@ -11,9 +12,18 @@ export class ProfileUpdateService {
|
|||||||
@InjectRepository(UserEntity)
|
@InjectRepository(UserEntity)
|
||||||
private readonly userRepository: Repository<UserEntity>,
|
private readonly userRepository: Repository<UserEntity>,
|
||||||
private readonly passwordService: PasswordService,
|
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> {
|
async update(user: UserEntity, input: ProfileUpdateInput): Promise<UserEntity> {
|
||||||
if (input.firstName !== undefined) {
|
if (input.firstName !== undefined) {
|
||||||
user.firstName = input.firstName
|
user.firstName = input.firstName
|
||||||
@ -25,6 +35,7 @@ export class ProfileUpdateService {
|
|||||||
|
|
||||||
if (input.email !== undefined && user.email !== input.email) {
|
if (input.email !== undefined && user.email !== input.email) {
|
||||||
user.email = input.email
|
user.email = input.email
|
||||||
|
user.emailVerified = false
|
||||||
// TODO request email verification
|
// TODO request email verification
|
||||||
|
|
||||||
if (undefined !== await this.userRepository.findOne({ email: input.email })) {
|
if (undefined !== await this.userRepository.findOne({ email: input.email })) {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export class SettingService {
|
|||||||
].includes(key)
|
].includes(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByKey(key: string): Promise<SettingModel> {
|
getByKey(key: string): SettingModel {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'SIGNUP_DISABLED':
|
case 'SIGNUP_DISABLED':
|
||||||
case 'LOGIN_NOTE':
|
case 'LOGIN_NOTE':
|
||||||
@ -29,11 +29,11 @@ export class SettingService {
|
|||||||
throw new Error(`no config stored for key ${key}`)
|
throw new Error(`no config stored for key ${key}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async isTrue(key: string): Promise<boolean> {
|
isTrue(key: string): boolean {
|
||||||
return (await this.getByKey(key)).isTrue
|
return this.getByKey(key).isTrue
|
||||||
}
|
}
|
||||||
|
|
||||||
async isFalse(key: string): Promise<boolean> {
|
isFalse(key: string): boolean {
|
||||||
return (await this.getByKey(key)).isFalse
|
return this.getByKey(key).isFalse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { HttpService } from '@nestjs/axios'
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
import handlebars from 'handlebars'
|
import handlebars from 'handlebars'
|
||||||
import { PinoLogger } from 'nestjs-pino'
|
import { PinoLogger } from 'nestjs-pino'
|
||||||
|
import { lastValueFrom } from 'rxjs'
|
||||||
|
import { serializeError } from 'serialize-error'
|
||||||
import { SubmissionEntity } from '../../entity/submission.entity'
|
import { SubmissionEntity } from '../../entity/submission.entity'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -20,15 +22,19 @@ export class SubmissionHookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.httpService.post(
|
const response = await lastValueFrom(this.httpService.post(
|
||||||
hook.url,
|
hook.url,
|
||||||
this.format(submission, hook.format)
|
this.format(submission, hook.format)
|
||||||
).toPromise()
|
))
|
||||||
|
|
||||||
console.log('sent hook', response.data)
|
console.log('sent hook', response.data)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(`failed to post to "${hook.url}: ${e.message}`)
|
this.logger.error({
|
||||||
this.logger.error(e.stack)
|
submission: submission.id,
|
||||||
|
form: submission.formId,
|
||||||
|
webhook: hook.url,
|
||||||
|
error: serializeError(e),
|
||||||
|
}, 'failed to post webhook')
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import handlebars from 'handlebars'
|
|||||||
import htmlToText from 'html-to-text'
|
import htmlToText from 'html-to-text'
|
||||||
import mjml2html from 'mjml'
|
import mjml2html from 'mjml'
|
||||||
import { PinoLogger } from 'nestjs-pino'
|
import { PinoLogger } from 'nestjs-pino'
|
||||||
|
import { serializeError } from 'serialize-error'
|
||||||
import { SubmissionEntity } from '../../entity/submission.entity'
|
import { SubmissionEntity } from '../../entity/submission.entity'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -22,15 +23,25 @@ export class SubmissionNotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const to = this.getEmail(submission.fields.find(field => field.fieldId === notification.toField.id )?.fieldValue, notification.toEmail)
|
const to = this.getEmail(
|
||||||
const from = this.getEmail(submission.fields.find(field => field.fieldId === notification.fromField.id )?.fieldValue, notification.fromEmail)
|
submission,
|
||||||
|
notification.toField.id,
|
||||||
|
notification.toEmail
|
||||||
|
)
|
||||||
|
const from = this.getEmail(
|
||||||
|
submission,
|
||||||
|
notification.fromField.id,
|
||||||
|
notification.fromEmail
|
||||||
|
)
|
||||||
|
|
||||||
const html = mjml2html(
|
const template = handlebars.compile(
|
||||||
handlebars.compile(
|
notification.htmlTemplate
|
||||||
notification.htmlTemplate
|
)
|
||||||
)({
|
|
||||||
|
const html: string = mjml2html(
|
||||||
|
template({
|
||||||
// TODO add variables
|
// TODO add variables
|
||||||
}),
|
}) ,
|
||||||
{
|
{
|
||||||
minify: true,
|
minify: true,
|
||||||
}
|
}
|
||||||
@ -41,29 +52,30 @@ export class SubmissionNotificationService {
|
|||||||
replyTo: from,
|
replyTo: from,
|
||||||
subject: notification.subject,
|
subject: notification.subject,
|
||||||
html,
|
html,
|
||||||
text: htmlToText.fromString(html),
|
text: htmlToText.htmlToText(html),
|
||||||
})
|
})
|
||||||
console.log('sent notification to', to)
|
console.log('sent notification to', to)
|
||||||
} catch (e) {
|
} 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
|
throw e
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEmail(raw: string, fallback: string): string {
|
private getEmail(submission: SubmissionEntity, fieldId: number, fallback: string): string {
|
||||||
if (!raw) {
|
const data = submission.fields.find(field => field.fieldId === fieldId)?.content
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (typeof data.value === 'string') {
|
||||||
const data = JSON.parse(raw)
|
return data.value
|
||||||
|
|
||||||
if (data.value) {
|
|
||||||
return data.value
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.error('could not decode field value', raw)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fallback
|
return fallback
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm'
|
import { InjectRepository } from '@nestjs/typeorm'
|
||||||
|
import { PinoLogger } from 'nestjs-pino'
|
||||||
import { Repository } from 'typeorm'
|
import { Repository } from 'typeorm'
|
||||||
import { FormEntity } from '../../entity/form.entity'
|
import { FormEntity } from '../../entity/form.entity'
|
||||||
import { SubmissionEntity } from '../../entity/submission.entity'
|
import { SubmissionEntity } from '../../entity/submission.entity'
|
||||||
@ -8,15 +9,22 @@ export class SubmissionService {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SubmissionEntity)
|
@InjectRepository(SubmissionEntity)
|
||||||
private readonly submissionRepository: Repository<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> {
|
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')
|
const qb = this.submissionRepository.createQueryBuilder('s')
|
||||||
|
|
||||||
qb.leftJoinAndSelect('s.fields', 'fields')
|
qb.leftJoinAndSelect('s.fields', 'fields')
|
||||||
@ -24,6 +32,9 @@ export class SubmissionService {
|
|||||||
qb.where('s.form = :form', { form: form.id })
|
qb.where('s.form = :form', { form: form.id })
|
||||||
|
|
||||||
// TODO readd sort
|
// TODO readd sort
|
||||||
|
this.logger.debug({
|
||||||
|
sort,
|
||||||
|
}, 'ignored sorting for submissions')
|
||||||
|
|
||||||
qb.skip(start)
|
qb.skip(start)
|
||||||
qb.take(limit)
|
qb.take(limit)
|
||||||
|
|||||||
@ -2,10 +2,11 @@ import { Injectable } from '@nestjs/common'
|
|||||||
import { InjectRepository } from '@nestjs/typeorm'
|
import { InjectRepository } from '@nestjs/typeorm'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { PinoLogger } from 'nestjs-pino'
|
import { PinoLogger } from 'nestjs-pino'
|
||||||
|
import { serializeError } from 'serialize-error'
|
||||||
import { Repository } from 'typeorm'
|
import { Repository } from 'typeorm'
|
||||||
import { SubmissionSetFieldInput } from '../../dto/submission/submission.set.field.input'
|
import { SubmissionSetFieldInput } from '../../dto/submission/submission.set.field.input'
|
||||||
import { SubmissionEntity } from '../../entity/submission.entity'
|
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 { SubmissionHookService } from './submission.hook.service'
|
||||||
import { SubmissionNotificationService } from './submission.notification.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)
|
let field = submission.fields.find(field => field.field.id.toString() === input.field)
|
||||||
|
|
||||||
if (field) {
|
if (field) {
|
||||||
field.fieldValue = JSON.parse(input.data)
|
field.content = this.parseData(field, input.data)
|
||||||
|
|
||||||
await this.submissionFieldRepository.save(field)
|
await this.submissionFieldRepository.save(field)
|
||||||
} else {
|
} else {
|
||||||
@ -35,8 +36,8 @@ export class SubmissionSetFieldService {
|
|||||||
|
|
||||||
field.submission = submission
|
field.submission = submission
|
||||||
field.field = submission.form.fields.find(field => field.id.toString() === input.field)
|
field.field = submission.form.fields.find(field => field.id.toString() === input.field)
|
||||||
field.fieldType = field.field.type
|
field.type = field.field.type
|
||||||
field.fieldValue = JSON.parse(input.data)
|
field.content = this.parseData(field, input.data)
|
||||||
|
|
||||||
field = await this.submissionFieldRepository.save(field)
|
field = await this.submissionFieldRepository.save(field)
|
||||||
|
|
||||||
@ -51,11 +52,103 @@ export class SubmissionSetFieldService {
|
|||||||
|
|
||||||
if (submission.percentageComplete === 1) {
|
if (submission.percentageComplete === 1) {
|
||||||
this.webHook.process(submission).catch(e => {
|
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.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.timeElapsed = 0
|
||||||
submission.percentageComplete = 0
|
submission.percentageComplete = 0
|
||||||
|
|
||||||
|
// TODO set country!
|
||||||
|
|
||||||
submission.device.language = input.device.language
|
submission.device.language = input.device.language
|
||||||
submission.device.name = input.device.name
|
submission.device.name = input.device.name
|
||||||
submission.device.type = input.device.type
|
submission.device.type = input.device.type
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import * as bcrypt from 'bcrypt'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubmissionTokenService {
|
export class SubmissionTokenService {
|
||||||
async hash(token: string): Promise<string> {
|
async hash(token: string): Promise<string> {
|
||||||
return token
|
return bcrypt.hash(token, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify(token: string, hash: string): Promise<boolean> {
|
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 { Injectable, OnApplicationBootstrap } from '@nestjs/common'
|
||||||
import { ConfigService } from '@nestjs/config'
|
import { ConfigService } from '@nestjs/config'
|
||||||
import { PinoLogger } from 'nestjs-pino'
|
import { PinoLogger } from 'nestjs-pino'
|
||||||
|
import { serializeError } from 'serialize-error'
|
||||||
import { UserCreateService } from './user.create.service'
|
import { UserCreateService } from './user.create.service'
|
||||||
import { UserService } from './user.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 email = this.configService.get<string>('ADMIN_EMAIL', 'admin@ohmyform.com')
|
||||||
const password = this.configService.get<string>('ADMIN_PASSWORD', 'root')
|
const password = this.configService.get<string>('ADMIN_PASSWORD', 'root')
|
||||||
|
|
||||||
try {
|
if (await this.userService.usernameInUse(username)) {
|
||||||
await this.userService.findByUsername(username)
|
|
||||||
|
|
||||||
this.logger.info('username already exists, skip creating')
|
this.logger.info('username already exists, skip creating')
|
||||||
return
|
return
|
||||||
} catch (e) {}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
await this.userService.findByEmail(email)
|
|
||||||
|
|
||||||
|
if (await this.userService.emailInUse(email)) {
|
||||||
this.logger.info('email already exists, skip creating')
|
this.logger.info('email already exists, skip creating')
|
||||||
return
|
return
|
||||||
} catch (e) {}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.createService.create({
|
await this.createService.create({
|
||||||
@ -54,7 +51,7 @@ export class BootService implements OnApplicationBootstrap {
|
|||||||
])
|
])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
error: e,
|
error: serializeError(e),
|
||||||
}, 'could not create admin user')
|
}, 'could not create admin user')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { UserCreateService } from './user.create.service'
|
|||||||
import { UserDeleteService } from './user.delete.service'
|
import { UserDeleteService } from './user.delete.service'
|
||||||
import { UserService } from './user.service'
|
import { UserService } from './user.service'
|
||||||
import { UserStatisticService } from './user.statistic.service'
|
import { UserStatisticService } from './user.statistic.service'
|
||||||
|
import { UserTokenService } from './user.token.service'
|
||||||
import { UserUpdateService } from './user.update.service'
|
import { UserUpdateService } from './user.update.service'
|
||||||
|
|
||||||
export const userServices = [
|
export const userServices = [
|
||||||
@ -11,5 +12,6 @@ export const userServices = [
|
|||||||
UserDeleteService,
|
UserDeleteService,
|
||||||
UserService,
|
UserService,
|
||||||
UserStatisticService,
|
UserStatisticService,
|
||||||
|
UserTokenService,
|
||||||
UserUpdateService,
|
UserUpdateService,
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
import { InjectRepository } from '@nestjs/typeorm'
|
import { InjectRepository } from '@nestjs/typeorm'
|
||||||
|
import crypto from 'crypto'
|
||||||
import { PinoLogger } from 'nestjs-pino'
|
import { PinoLogger } from 'nestjs-pino'
|
||||||
import { Repository } from 'typeorm'
|
import { Repository } from 'typeorm'
|
||||||
import { rolesType } from '../../config/roles'
|
import { rolesType } from '../../config/roles'
|
||||||
@ -8,6 +10,7 @@ import { UserEntity } from '../../entity/user.entity'
|
|||||||
import { PasswordService } from '../auth/password.service'
|
import { PasswordService } from '../auth/password.service'
|
||||||
import { MailService } from '../mail.service'
|
import { MailService } from '../mail.service'
|
||||||
import { SettingService } from '../setting.service'
|
import { SettingService } from '../setting.service'
|
||||||
|
import { UserTokenService } from './user.token.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserCreateService {
|
export class UserCreateService {
|
||||||
@ -18,12 +21,14 @@ export class UserCreateService {
|
|||||||
private readonly logger: PinoLogger,
|
private readonly logger: PinoLogger,
|
||||||
private readonly passwordService: PasswordService,
|
private readonly passwordService: PasswordService,
|
||||||
private readonly settingService: SettingService,
|
private readonly settingService: SettingService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly userTokenService: UserTokenService,
|
||||||
) {
|
) {
|
||||||
logger.setContext(this.constructor.name)
|
logger.setContext(this.constructor.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDefaultRoles(): Promise<rolesType> {
|
private getDefaultRoles(): rolesType {
|
||||||
const roleSetting = await this.settingService.getByKey('DEFAULT_ROLE')
|
const roleSetting = this.settingService.getByKey('DEFAULT_ROLE')
|
||||||
|
|
||||||
switch (roleSetting.value) {
|
switch (roleSetting.value) {
|
||||||
case 'superuser':
|
case 'superuser':
|
||||||
@ -49,6 +54,8 @@ export class UserCreateService {
|
|||||||
throw new Error('email already in use')
|
throw new Error('email already in use')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const confirmToken = crypto.randomBytes(30).toString('base64')
|
||||||
|
|
||||||
let user = new UserEntity()
|
let user = new UserEntity()
|
||||||
|
|
||||||
user.provider = 'local'
|
user.provider = 'local'
|
||||||
@ -57,17 +64,25 @@ export class UserCreateService {
|
|||||||
user.firstName = input.firstName
|
user.firstName = input.firstName
|
||||||
user.lastName = input.lastName
|
user.lastName = input.lastName
|
||||||
user.language = input.language ?? 'en'
|
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.passwordHash = await this.passwordService.hash(input.password)
|
||||||
|
user.token = await this.userTokenService.hash(confirmToken)
|
||||||
|
|
||||||
user = await this.userRepository.save(user)
|
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(
|
const sent = await this.mailerService.send(
|
||||||
user.email,
|
user.email,
|
||||||
'user/created',
|
'user/created',
|
||||||
{
|
{
|
||||||
username: user.username,
|
username: user.username,
|
||||||
confirm: 'https://www.google.com', // TODO confirm url
|
confirm: confirmUrl,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -59,4 +59,16 @@ export class UserService {
|
|||||||
|
|
||||||
return user
|
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) {
|
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 })) {
|
||||||
|
throw new Error('email already in use')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.username !== undefined) {
|
if (input.username !== undefined) {
|
||||||
|
|||||||
16
yarn.lock
16
yarn.lock
@ -898,7 +898,7 @@
|
|||||||
tslib "~2.3.0"
|
tslib "~2.3.0"
|
||||||
value-or-promise "1.0.11"
|
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"
|
version "7.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-7.10.0.tgz#07a4cb5d1bec1ff1dc1d47a935919ee6abd38699"
|
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-7.10.0.tgz#07a4cb5d1bec1ff1dc1d47a935919ee6abd38699"
|
||||||
integrity sha512-d334r6bo9mxdSqZW6zWboEnnOOFRrAPVQJ7LkU8/6grglrbcu6WhwCLzHb90E94JI3TD3ricC3YGbUqIi9Xg0w==
|
integrity sha512-d334r6bo9mxdSqZW6zWboEnnOOFRrAPVQJ7LkU8/6grglrbcu6WhwCLzHb90E94JI3TD3ricC3YGbUqIi9Xg0w==
|
||||||
@ -907,7 +907,7 @@
|
|||||||
camel-case "4.1.2"
|
camel-case "4.1.2"
|
||||||
tslib "~2.2.0"
|
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"
|
version "7.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-7.8.0.tgz#74290863b5c84c1bf1d8749e7a05b1b029a0c55e"
|
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-7.8.0.tgz#74290863b5c84c1bf1d8749e7a05b1b029a0c55e"
|
||||||
integrity sha512-nORIltDwBdsc3Ew+vuXISTZw6gpRd3UkK+6HNY3knNca6apTDj7ygcJmgsFKjSyUf7xtukddTcF6Js9kPuJr6g==
|
integrity sha512-nORIltDwBdsc3Ew+vuXISTZw6gpRd3UkK+6HNY3knNca6apTDj7ygcJmgsFKjSyUf7xtukddTcF6Js9kPuJr6g==
|
||||||
@ -1781,6 +1781,18 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256"
|
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256"
|
||||||
integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==
|
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@*":
|
"@types/node@*":
|
||||||
version "14.6.2"
|
version "14.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.2.tgz#264b44c5a28dfa80198fc2f7b6d3c8a054b9491f"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.2.tgz#264b44c5a28dfa80198fc2f7b6d3c8a054b9491f"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user