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,
},
rules: {
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn',
'@typescript-eslint/no-unsafe-argument': '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
- forms now have multiple notification
- layout for forms
- mariadb / mysql support
- mariadb / mysql support (fixes https://github.com/ohmyform/ohmyform/issues/143)
- user confirmation tokens
- email verification

View File

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

View File

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

View File

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

View File

@ -17,7 +17,8 @@ export const fieldTypes = [
export const matchType = {
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: /.+@.+\..+/,
slug: /^[a-z0-9_]+$/,
}

View File

@ -44,8 +44,8 @@ export class FormFieldModel {
this.description = document.description
this.required = document.required
this.value = document.value
this.options = document.options ? document.options.map(option => new FormFieldOptionModel(option)) : []
this.logic = document.logic ? document.logic.map(logic => new FormFieldLogicModel(logic)) : []
this.options = document.options?.map(option => new FormFieldOptionModel(option)) || []
this.logic = document.logic?.map(logic => new FormFieldLogicModel(logic)) || []
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 { AppModule } from './app.module'
(async () => {
void (async () => {
const options: NestApplicationOptions = {
bufferLogs: true,
}

View File

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

View File

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

View File

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

View File

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

View File

@ -18,9 +18,16 @@ export class SubmissionFieldResolver {
@Parent() parent: SubmissionFieldModel,
@Context('cache') cache: ContextCache,
): 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) {
return null

View File

@ -15,7 +15,9 @@ export class SubmissionResolver {
@Parent() parent: SubmissionModel,
@Context('cache') cache: ContextCache,
): 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 => {
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(
submissions.map(submission => new SubmissionModel(submission)),

View File

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

View File

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

View File

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

View File

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