add user confirmation, add validation for submission data

This commit is contained in:
Michael Schramm 2022-01-02 16:14:00 +01:00
parent 29a74ea9c9
commit 1f413c2949
19 changed files with 43 additions and 27 deletions

View File

@ -24,6 +24,7 @@ module.exports = {
jest: true, jest: true,
}, },
rules: { rules: {
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn', '@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',

View File

@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- logic backend components - logic backend components
- forms now have multiple notification - forms now have multiple notification
- layout for forms - layout for forms
- mariadb / mysql support - mariadb / mysql support (fixes https://github.com/ohmyform/ohmyform/issues/143)
- user confirmation tokens - user confirmation tokens
- email verification - email verification

View File

@ -64,7 +64,7 @@ export const imports = [
JwtModule.registerAsync({ JwtModule.registerAsync({
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
useFactory: async (configService: ConfigService): Promise<JwtModuleOptions> => ({ useFactory: (configService: ConfigService): JwtModuleOptions => ({
secret: configService.get<string>('SECRET_KEY'), secret: configService.get<string>('SECRET_KEY'),
signOptions: { signOptions: {
expiresIn: '4h', expiresIn: '4h',
@ -154,7 +154,7 @@ export const imports = [
MailerModule.forRootAsync({ MailerModule.forRootAsync({
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({ useFactory: (configService: ConfigService) => ({
transport: configService.get<string>('MAILER_URI', 'smtp://localhost:1025'), transport: configService.get<string>('MAILER_URI', 'smtp://localhost:1025'),
defaults: { defaults: {
from: configService.get<string>('MAILER_FROM', 'OhMyForm <no-reply@localhost>'), from: configService.get<string>('MAILER_FROM', 'OhMyForm <no-reply@localhost>'),

View File

@ -5,7 +5,7 @@ const bootstrap = new BootstrapConsole({
module: AppModule, module: AppModule,
useDecorators: true, useDecorators: true,
}); });
bootstrap.init().then(async (app) => { void bootstrap.init().then(async (app) => {
try { try {
await app.init(); await app.init();
await bootstrap.boot(); await bootstrap.boot();

View File

@ -59,7 +59,7 @@ export class UserCommand {
@Command({ @Command({
command: 'activate <username>', command: 'activate <username>',
}) })
async activate(username: string): Promise<void> { activate(username: string): void {
console.log(`activate user ${username}`) console.log(`activate user ${username}`)
} }
} }

View File

@ -17,7 +17,8 @@ export const fieldTypes = [
export const matchType = { export const matchType = {
color: /^#([A-F0-9]{6}|[A-F0-9]{3})$/i, color: /^#([A-F0-9]{6}|[A-F0-9]{3})$/i,
url: /((([A-Z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/i, // eslint-disable-next-line max-len
url: /((([A-Z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/i,
email: /.+@.+\..+/, email: /.+@.+\..+/,
slug: /^[a-z0-9_]+$/, slug: /^[a-z0-9_]+$/,
} }

View File

@ -44,8 +44,8 @@ export class FormFieldModel {
this.description = document.description this.description = document.description
this.required = document.required this.required = document.required
this.value = document.value this.value = document.value
this.options = document.options ? document.options.map(option => new FormFieldOptionModel(option)) : [] this.options = document.options?.map(option => new FormFieldOptionModel(option)) || []
this.logic = document.logic ? document.logic.map(logic => new FormFieldLogicModel(logic)) : [] this.logic = document.logic?.map(logic => new FormFieldLogicModel(logic)) || []
this.rating = document.rating ? new FormFieldRatingModel(document.rating) : null this.rating = document.rating ? new FormFieldRatingModel(document.rating) : null
} }
} }

View File

@ -4,7 +4,7 @@ import cors from 'cors'
import { Logger } from 'nestjs-pino' import { Logger } from 'nestjs-pino'
import { AppModule } from './app.module' import { AppModule } from './app.module'
(async () => { void (async () => {
const options: NestApplicationOptions = { const options: NestApplicationOptions = {
bufferLogs: true, bufferLogs: true,
} }

View File

@ -22,7 +22,7 @@ export class AuthRegisterResolver {
async authRegister( async authRegister(
@Args({ name: 'user' }) data: UserCreateInput, @Args({ name: 'user' }) data: UserCreateInput,
): Promise<AuthJwtModel> { ): Promise<AuthJwtModel> {
if (await this.settingService.isTrue('SIGNUP_DISABLED')) { if (this.settingService.isTrue('SIGNUP_DISABLED')) {
throw new Error('signup disabled') throw new Error('signup disabled')
} }

View File

@ -12,7 +12,7 @@ export class FormStatisticResolver {
} }
@Query(() => FormStatisticModel) @Query(() => FormStatisticModel)
async getFormStatistic(): Promise<FormStatisticModel> { getFormStatistic(): FormStatisticModel {
return new FormStatisticModel() return new FormStatisticModel()
} }

View File

@ -16,7 +16,7 @@ export class SettingResolver {
@Query(() => SettingPagerModel) @Query(() => SettingPagerModel)
@Roles('superuser') @Roles('superuser')
async getSettings(): Promise<SettingPagerModel> { getSettings(): SettingPagerModel {
// TODO https://github.com/ohmyform/api/issues/3 // TODO https://github.com/ohmyform/api/issues/3
return new SettingPagerModel( return new SettingPagerModel(
[], [],

View File

@ -4,7 +4,7 @@ import { StatusModel } from '../dto/status.model'
@Resolver(() => StatusModel) @Resolver(() => StatusModel)
export class StatusResolver { export class StatusResolver {
@Query(() => StatusModel) @Query(() => StatusModel)
async status(): Promise<StatusModel> { status(): StatusModel {
return new StatusModel({ return new StatusModel({
version: process.env.version || 'dev', version: process.env.version || 'dev',
}) })

View File

@ -18,9 +18,16 @@ export class SubmissionFieldResolver {
@Parent() parent: SubmissionFieldModel, @Parent() parent: SubmissionFieldModel,
@Context('cache') cache: ContextCache, @Context('cache') cache: ContextCache,
): Promise<FormFieldModel> { ): Promise<FormFieldModel> {
const submissionField = await cache.get<SubmissionFieldEntity>(cache.getCacheKey(SubmissionFieldEntity.name, parent.id)) const submissionField = await cache.get<SubmissionFieldEntity>(
cache.getCacheKey(SubmissionFieldEntity.name, parent.id)
)
const field = await cache.get<FormFieldEntity>(cache.getCacheKey(FormFieldEntity.name, submissionField.fieldId), () => this.formFieldService.findById(submissionField.fieldId, submissionField.field)) const field = await cache.get<FormFieldEntity>(
cache.getCacheKey(
FormFieldEntity.name,
submissionField.fieldId),
() => this.formFieldService.findById(submissionField.fieldId, submissionField.field)
)
if (!field) { if (!field) {
return null return null

View File

@ -15,7 +15,9 @@ export class SubmissionResolver {
@Parent() parent: SubmissionModel, @Parent() parent: SubmissionModel,
@Context('cache') cache: ContextCache, @Context('cache') cache: ContextCache,
): Promise<SubmissionFieldModel[]> { ): Promise<SubmissionFieldModel[]> {
const submission = await cache.get<SubmissionEntity>(cache.getCacheKey(SubmissionEntity.name, parent.id)) const submission = await cache.get<SubmissionEntity>(
cache.getCacheKey(SubmissionEntity.name, parent.id)
)
return submission.fields.map(field => { return submission.fields.map(field => {
cache.add(cache.getCacheKey(SubmissionFieldEntity.name, field.id), field) cache.add(cache.getCacheKey(SubmissionFieldEntity.name, field.id), field)

View File

@ -34,7 +34,9 @@ export class SubmissionSearchResolver {
{}, {},
) )
submissions.forEach(submission => cache.add(cache.getCacheKey(SubmissionEntity.name, submission.id), submission)) submissions.forEach(submission => {
cache.add(cache.getCacheKey(SubmissionEntity.name, submission.id), submission)
})
return new SubmissionPagerModel( return new SubmissionPagerModel(
submissions.map(submission => new SubmissionModel(submission)), submissions.map(submission => new SubmissionModel(submission)),

View File

@ -13,7 +13,7 @@ export class SubmissionStatisticResolver {
@Query(() => SubmissionStatisticModel) @Query(() => SubmissionStatisticModel)
async getSubmissionStatistic(): Promise<SubmissionStatisticModel> { getSubmissionStatistic(): SubmissionStatisticModel {
return new SubmissionStatisticModel() return new SubmissionStatisticModel()
} }

View File

@ -16,7 +16,7 @@ export class UserResolver {
@Query(() => UserModel) @Query(() => UserModel)
@Roles('admin') @Roles('admin')
public async getUserById( public async getUserById(
@Args('id', {type: () => ID}) id, @Args('id', {type: () => ID}) id: string,
@Context('cache') cache: ContextCache, @Context('cache') cache: ContextCache,
): Promise<UserModel> { ): Promise<UserModel> {
const user = await this.userService.findById(id) const user = await this.userService.findById(id)
@ -33,18 +33,18 @@ export class UserResolver {
@Parent() parent: UserModel, @Parent() parent: UserModel,
@Context('cache') cache: ContextCache, @Context('cache') cache: ContextCache,
): Promise<string[]> { ): Promise<string[]> {
return await this.returnFieldForSuperuser( return this.returnFieldForSuperuser(
await cache.get<UserEntity>(cache.getCacheKey(UserEntity.name, parent.id)), await cache.get<UserEntity>(cache.getCacheKey(UserEntity.name, parent.id)),
user, user,
c => c.roles c => c.roles
) )
} }
async returnFieldForSuperuser<T>( returnFieldForSuperuser<T>(
parent: UserEntity, parent: UserEntity,
user: UserEntity, user: UserEntity,
callback: (user: UserEntity) => T callback: (user: UserEntity) => T
): Promise<T> { ): T {
if (user.id !== parent.id && !this.userService.isSuperuser(user)) { if (user.id !== parent.id && !this.userService.isSuperuser(user)) {
throw new Error('No access to roles') throw new Error('No access to roles')
} }

View File

@ -1,5 +1,4 @@
import { Query, ResolveField, Resolver } from '@nestjs/graphql' import { Int, Query, ResolveField, Resolver } from '@nestjs/graphql'
import { GraphQLInt } from 'graphql'
import { Roles } from '../../decorator/roles.decorator' import { Roles } from '../../decorator/roles.decorator'
import { UserStatisticModel } from '../../dto/user/user.statistic.model' import { UserStatisticModel } from '../../dto/user/user.statistic.model'
import { UserStatisticService } from '../../service/user/user.statistic.service' import { UserStatisticService } from '../../service/user/user.statistic.service'
@ -12,11 +11,11 @@ export class UserStatisticResolver {
} }
@Query(() => UserStatisticModel) @Query(() => UserStatisticModel)
async getUserStatistic(): Promise<UserStatisticModel> { getUserStatistic(): UserStatisticModel {
return new UserStatisticModel() return new UserStatisticModel()
} }
@ResolveField('total', () => GraphQLInt) @ResolveField('total', () => Int)
@Roles('admin') @Roles('admin')
getTotal(): Promise<number> { getTotal(): Promise<number> {
return this.statisticService.getTotal() return this.statisticService.getTotal()

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt' import { JwtService } from '@nestjs/jwt'
import { PinoLogger } from 'nestjs-pino' import { PinoLogger } from 'nestjs-pino'
import { serializeError } from 'serialize-error'
import { AuthJwtModel } from '../../dto/auth/auth.jwt.model' import { AuthJwtModel } from '../../dto/auth/auth.jwt.model'
import { UserEntity } from '../../entity/user.entity' import { UserEntity } from '../../entity/user.entity'
import { UserService } from '../user/user.service' import { UserService } from '../user/user.service'
@ -26,7 +27,10 @@ export class AuthService {
return user; return user;
} }
} catch (e) { } catch (e) {
this.logger.error(`failed to verify user? ${e.message}`) this.logger.error({
error: serializeError(e),
username,
},'failed to verify user')
} }
return null; return null;