diff --git a/api/package.json b/api/package.json index e5aed3db..899c4cce 100644 --- a/api/package.json +++ b/api/package.json @@ -34,13 +34,15 @@ "class-transformer": "^0.2.3", "class-validator": "^0.9.1", "mongoose": "^5.6.7", + "nestjs-typegoose": "^5.2.1", "passport": "^0.4.0", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", "reflect-metadata": "^0.1.12", "rimraf": "^2.6.2", "rxjs": "^6.3.3", - "swagger-ui-express": "^4.0.7" + "swagger-ui-express": "^4.0.7", + "typegoose": "^5.9.0" }, "devDependencies": { "@nestjs/testing": "6.1.1", diff --git a/api/src/app.controller.ts b/api/src/app.controller.ts index de422d3d..36a7b629 100644 --- a/api/src/app.controller.ts +++ b/api/src/app.controller.ts @@ -1,10 +1,12 @@ import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; +import {ApiExcludeEndpoint} from "@nestjs/swagger" @Controller() export class AppController { constructor(private readonly appService: AppService) {} + @ApiExcludeEndpoint() @Get() getIndex(): object { return { diff --git a/api/src/app.module.ts b/api/src/app.module.ts index ad013674..0e65607a 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -1,21 +1,23 @@ import { Module } from '@nestjs/common'; import { TerminusModule } from '@nestjs/terminus'; import { MongooseModule } from '@nestjs/mongoose'; +import { TypegooseModule } from 'nestjs-typegoose'; import { TerminusOptionsService } from './terminus-options.service'; import { AppController } from './app.controller'; import { AppService } from './app.service'; -import {UsersModule} from "./users/users.module" -import {FormsModule} from "./forms/forms.module" +import { UserModule } from "./user/user.module" +import { FormModule } from "./form/form.module" import { AuthModule } from './auth/auth.module'; @Module({ imports: [ + TypegooseModule.forRoot('mongodb://localhost/ohmyform', { useNewUrlParser: true }), MongooseModule.forRoot('mongodb://localhost/ohmyform'), TerminusModule.forRootAsync({ useClass: TerminusOptionsService, }), - UsersModule, - FormsModule, + UserModule, + FormModule, AuthModule, ], controllers: [AppController], diff --git a/api/src/auth/auth.controllers.ts b/api/src/auth/auth.controllers.ts new file mode 100644 index 00000000..83279f36 --- /dev/null +++ b/api/src/auth/auth.controllers.ts @@ -0,0 +1,9 @@ +import {AuthController} from "./controllers/auth.controller" +import {RecoverController} from "./controllers/recover.controller" +import {RegisterController} from "./controllers/register.controller" + +export default [ + AuthController, + RecoverController, + RegisterController, +] diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts index 7a799daf..381b32f5 100644 --- a/api/src/auth/auth.module.ts +++ b/api/src/auth/auth.module.ts @@ -1,31 +1,21 @@ import { Module } from '@nestjs/common'; -import { AuthService } from './services/auth.service'; -import { UsersModule } from '../users/users.module'; +import { UserModule } from '../user/user.module'; import { PassportModule } from '@nestjs/passport'; -import { PasswordStrategy } from "./strategies/password.strategy" -import { PasswordService } from "./services/password.service" -import { AuthController } from "./controllers/auth.controller" import { jwtConstants } from "./constants" import { JwtModule } from "@nestjs/jwt" -import { JwtStrategy } from "./strategies/jwt.strategy" -import { JwtRefreshStrategy } from "./strategies/jwt.refresh.strategy" +import controllers from './auth.controllers' +import providers from './auth.providers' @Module({ imports: [ - UsersModule, + UserModule, PassportModule, JwtModule.register({ secret: jwtConstants.secret, signOptions: { expiresIn: '12h' }, }), ], - controllers: [AuthController], - providers: [ - AuthService, - PasswordService, - PasswordStrategy, - JwtStrategy, - JwtRefreshStrategy, - ] + controllers, + providers }) export class AuthModule {} diff --git a/api/src/auth/auth.providers.ts b/api/src/auth/auth.providers.ts new file mode 100644 index 00000000..bff9888e --- /dev/null +++ b/api/src/auth/auth.providers.ts @@ -0,0 +1,13 @@ +import { AuthService } from "./services/auth.service" +import { PasswordService } from "./services/password.service" +import { PasswordStrategy } from "./strategies/password.strategy" +import { JwtStrategy } from "./strategies/jwt.strategy" +import { JwtRefreshStrategy } from "./strategies/jwt.refresh.strategy" + +export default [ + AuthService, + PasswordService, + PasswordStrategy, + JwtStrategy, + JwtRefreshStrategy, +] diff --git a/api/src/auth/controllers/recover.controller.ts b/api/src/auth/controllers/recover.controller.ts new file mode 100644 index 00000000..7b601a41 --- /dev/null +++ b/api/src/auth/controllers/recover.controller.ts @@ -0,0 +1,29 @@ +import { Controller, Request, Post, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { AuthService } from "../services/auth.service" +import { AuthJwtDto } from "../dto/auth.jwt.dto" +import {ApiBearerAuth, ApiImplicitBody, ApiImplicitQuery, ApiResponse, ApiUseTags} from "@nestjs/swagger" + +@ApiUseTags('authentication') +@Controller('auth/recover') +export class RecoverController { + constructor(private readonly authService: AuthService) {} + + @ApiResponse({ status: 201, description: 'Successful registration.', type: AuthJwtDto}) + @ApiImplicitQuery({name: 'email', type: String}) + @ApiImplicitQuery({name: 'username', type: String}) + @Post('request') + async request(@Request() req): Promise { + // TODO + return null + } + + @ApiResponse({ status: 201, description: 'Successful registration.', type: AuthJwtDto}) + @ApiImplicitQuery({name: 'token', type: String}) + @ApiImplicitQuery({name: 'password', type: String}) + @Post('finish') + async finish(@Request() req): Promise { + // TODO + return null + } +} diff --git a/api/src/auth/controllers/register.controller.ts b/api/src/auth/controllers/register.controller.ts new file mode 100644 index 00000000..b2254549 --- /dev/null +++ b/api/src/auth/controllers/register.controller.ts @@ -0,0 +1,21 @@ +import { Controller, Request, Post, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { AuthService } from "../services/auth.service" +import { AuthJwtDto } from "../dto/auth.jwt.dto" +import {ApiBearerAuth, ApiImplicitBody, ApiImplicitQuery, ApiResponse, ApiUseTags} from "@nestjs/swagger" + +@ApiUseTags('authentication') +@Controller('auth') +export class RegisterController { + constructor(private readonly authService: AuthService) {} + + @ApiResponse({ status: 201, description: 'Successful registration.', type: AuthJwtDto}) + @ApiImplicitQuery({name: 'email', type: String}) + @ApiImplicitQuery({name: 'username', type: String}) + @ApiImplicitQuery({name: 'password', type: String}) + @Post('register') + async register(@Request() req): Promise { + // TODO + return null + } +} diff --git a/api/src/auth/services/auth.service.ts b/api/src/auth/services/auth.service.ts index 4803c7e5..a06175e9 100644 --- a/api/src/auth/services/auth.service.ts +++ b/api/src/auth/services/auth.service.ts @@ -1,15 +1,15 @@ import { Injectable } from '@nestjs/common'; -import { UsersService } from '../../users/users.service'; +import { UserService } from '../../user/services/user.service'; import { PasswordService } from "./password.service" import { JwtService } from '@nestjs/jwt'; import { AuthUser } from "../interfaces/auth.user.interface" -import { User } from "../../users/interfaces/user.interface" -import {AuthJwtDto} from "../dto/auth.jwt.dto" +import { User } from "../../user/models/user.model" +import { AuthJwtDto } from "../dto/auth.jwt.dto" @Injectable() export class AuthService { constructor( - private readonly usersService: UsersService, + private readonly usersService: UserService, private readonly passwordService: PasswordService, private readonly jwtService: JwtService ) {} @@ -38,12 +38,12 @@ export class AuthService { private static setupAuthUser(user:User):AuthUser { return { - id: user.id, + id: user._id, username: user.username, email: user.email, roles: user.roles, created: user.created, - lastUpdated: user.lastUpdated + lastUpdated: user.lastModified }; } diff --git a/api/src/form/controllers/form.controller.ts b/api/src/form/controllers/form.controller.ts new file mode 100644 index 00000000..150e56d2 --- /dev/null +++ b/api/src/form/controllers/form.controller.ts @@ -0,0 +1,39 @@ +import {Controller, Request, Get, Post, Put, Delete, UseGuards, Param} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiUseTags } from "@nestjs/swagger" +import { FormService } from "../services/form.service" +import {Form} from "../models/form.model" + +@ApiUseTags('forms') +@Controller('forms') +export class FormController { + constructor(private readonly formService: FormService) {} + + @Get() + @UseGuards(AuthGuard('jwt')) + async list(@Request() req): Promise { + return true; + } + @Post() + @UseGuards(AuthGuard('jwt')) + async create(@Request() req): Promise
{ + return null; + } + + @Get(':id') + @UseGuards(AuthGuard('jwt')) + async read(@Param('id') id): Promise { + return this.formService.findById(id); + } + + @Put(':id') + @UseGuards(AuthGuard('jwt')) + async update(@Param('id') id, @Request() req): Promise { + return this.formService.findById(id); + } + + @Delete(':id') + @UseGuards(AuthGuard('jwt')) + async delete(@Param('id') id): Promise { + } +} diff --git a/api/src/form/form.controllers.ts b/api/src/form/form.controllers.ts new file mode 100644 index 00000000..3dafd609 --- /dev/null +++ b/api/src/form/form.controllers.ts @@ -0,0 +1,5 @@ +import { FormController } from "./controllers/form.controller" + +export default [ + FormController, +] diff --git a/api/src/form/form.module.ts b/api/src/form/form.module.ts new file mode 100644 index 00000000..f8e5a4b3 --- /dev/null +++ b/api/src/form/form.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import controllers from './form.controllers'; +import providers from './form.providers' +import {TypegooseModule} from "nestjs-typegoose" +import {Form} from "./models/form.model" + +@Module({ + imports: [ + TypegooseModule.forFeature([Form]), + ], + exports: [], + controllers, + providers, +}) +export class FormModule {} diff --git a/api/src/form/form.providers.ts b/api/src/form/form.providers.ts new file mode 100644 index 00000000..3278e979 --- /dev/null +++ b/api/src/form/form.providers.ts @@ -0,0 +1,5 @@ +import { FormService } from "./services/form.service" + +export default [ + FormService +] diff --git a/api/src/form/interfaces/button.interface.ts b/api/src/form/interfaces/button.interface.ts new file mode 100644 index 00000000..a90ce830 --- /dev/null +++ b/api/src/form/interfaces/button.interface.ts @@ -0,0 +1,5 @@ +import { Document } from 'mongoose'; + +export interface Button extends Document{ + +} diff --git a/api/src/form/interfaces/field.interface.ts b/api/src/form/interfaces/field.interface.ts new file mode 100644 index 00000000..19f05e28 --- /dev/null +++ b/api/src/form/interfaces/field.interface.ts @@ -0,0 +1,5 @@ +import { Document } from 'mongoose'; + +export interface Field extends Document{ + +} diff --git a/api/src/form/interfaces/form.interface.ts b/api/src/form/interfaces/form.interface.ts new file mode 100644 index 00000000..62758e81 --- /dev/null +++ b/api/src/form/interfaces/form.interface.ts @@ -0,0 +1,51 @@ +import { Document } from 'mongoose'; +import { Field } from "./field.interface" +import { Button } from "./button.interface" + +export class Form extends Document{ + readonly firstName: string; + readonly language: string; + readonly analytics: string; + readonly form_fields: Field[]; + readonly admin: any; + readonly startPage: { + readonly showStart: boolean; + readonly introTitle: string; + readonly introParagraph: string; + readonly introButtonText: string; + readonly buttons: Button[]; + }; + readonly endPage: { + readonly showEnd: boolean; + readonly title: string; + readonly paragraph: string; + readonly buttonText: string; + readonly buttons: Button[]; + }; + readonly selfNotifications: { + readonly fromField: string; + readonly toEmails: string; + readonly subject: string; + readonly htmlTemplate: string; + readonly enabled: boolean; + }; + readonly respondentNotifications: { + readonly toField: string; + readonly fromEmails: string; + readonly subject: string; + readonly htmlTemplate: string; + readonly enabled: boolean; + }; + readonly showFooter: boolean; + readonly isLive: boolean; + readonly design: { + readonly colors: { + readonly backgroundColor: string; + readonly questionColor: string; + readonly answerColor: string; + readonly buttonColor: string; + readonly buttonTextColor: string; + }; + readonly font: string; + }; +} diff --git a/api/src/form/models/embedded/analytics.ts b/api/src/form/models/embedded/analytics.ts new file mode 100644 index 00000000..7e5caeb6 --- /dev/null +++ b/api/src/form/models/embedded/analytics.ts @@ -0,0 +1,14 @@ +import {prop} from "typegoose" +import {VisitorData} from "./visitor.data" +import {Exclude} from "class-transformer" + +export class Analytics { + @Exclude() + readonly _id: string; + + @prop() + gaCode: string; + + @prop() + visitors: VisitorData[] +} diff --git a/api/src/form/models/embedded/button.ts b/api/src/form/models/embedded/button.ts new file mode 100644 index 00000000..300ef331 --- /dev/null +++ b/api/src/form/models/embedded/button.ts @@ -0,0 +1,30 @@ +import {Exclude} from "class-transformer" +import {prop} from "typegoose" + +export class Button { + @Exclude() + readonly _id: string; + + @prop({ + match: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/ + }) + url: string; + + @prop() + action: string; + + @prop() + text: string; + + @prop({ + match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], + default: '#5bc0de' + }) + bgColor: string; + + @prop({ + match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], + default: '#ffffff' + }) + color: string; +} diff --git a/api/src/form/models/embedded/colors.ts b/api/src/form/models/embedded/colors.ts new file mode 100644 index 00000000..9b79156c --- /dev/null +++ b/api/src/form/models/embedded/colors.ts @@ -0,0 +1,37 @@ +import {prop, Ref, Typegoose} from "typegoose" +import {Exclude} from "class-transformer" + +export class Colors { + @Exclude() + readonly _id: string; + + @prop({ + match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], + default: '#fff' + }) + readonly backgroundColor: string; + + @prop({ + match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], + default: '#333' + }) + readonly questionColor: string; + + @prop({ + match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], + default: '#333' + }) + readonly answerColor: string; + + @prop({ + match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], + default: '#fff' + }) + readonly buttonColor: string; + + @prop({ + match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], + default: '#333' + }) + readonly buttonTextColor: string; +} diff --git a/api/src/form/models/embedded/design.ts b/api/src/form/models/embedded/design.ts new file mode 100644 index 00000000..e83ff6c9 --- /dev/null +++ b/api/src/form/models/embedded/design.ts @@ -0,0 +1,14 @@ +import {prop} from "typegoose" +import {Colors} from "./colors" +import {Exclude} from "class-transformer" + +export class Design { + @Exclude() + readonly _id: string; + + @prop() + readonly colors: Colors; + + @prop() + readonly font: string; +} diff --git a/api/src/form/models/embedded/end.page.ts b/api/src/form/models/embedded/end.page.ts new file mode 100644 index 00000000..1431e060 --- /dev/null +++ b/api/src/form/models/embedded/end.page.ts @@ -0,0 +1,29 @@ +import {prop} from "typegoose" +import {Button} from "./button" +import {Exclude} from "class-transformer" + +export class EndPage { + @Exclude() + readonly _id: string; + + @prop({ + default: false + }) + readonly showEnd: boolean; + + @prop({ + default: 'Thank you for filling out the form' + }) + readonly title: string; + + @prop() + readonly paragraph: string; + + @prop({ + default: 'Go back to Form' + }) + readonly buttonText: string; + + @prop() + readonly buttons: Button[]; +} diff --git a/api/src/form/models/embedded/field.option.ts b/api/src/form/models/embedded/field.option.ts new file mode 100644 index 00000000..a45cc0c2 --- /dev/null +++ b/api/src/form/models/embedded/field.option.ts @@ -0,0 +1,19 @@ +import {prop} from "typegoose" +import {VisitorData} from "./visitor.data" +import {Exclude} from "class-transformer" + +export class FieldOption { + @Exclude() + readonly _id: string; + + @prop() + option_id: number; + + @prop() + option_title: string; + + @prop({ + trim: true + }) + option_value: string; +} diff --git a/api/src/form/models/embedded/field.ts b/api/src/form/models/embedded/field.ts new file mode 100644 index 00000000..c54d319e --- /dev/null +++ b/api/src/form/models/embedded/field.ts @@ -0,0 +1,83 @@ +import {arrayProp, prop} from "typegoose" +import {LogicJump} from "./logic.jump" +import {RatingField} from "./rating.field" +import {FieldOption} from "./field.option" +import {Exclude} from "class-transformer" + +export class Field { + @Exclude() + readonly _id: string; + + @prop({ + default: false + }) + isSubmission: boolean; + + @prop() + submissionId: string; + + @prop({ + trim: true + }) + title: string; + + @prop({ + default: '' + }) + description: string; + + @prop() + logicJump: LogicJump; + + @prop() + ratingOptions: RatingField; + + @arrayProp({ + items: FieldOption + }) + fieldOptions: FieldOption[]; + + @prop({ + default: true + }) + required: boolean; + + @prop({ + default: false + }) + disabled: boolean; + + @prop({ + default: false + }) + deletePreserved: boolean; + + @arrayProp({ + items: String + }) + validFieldTypes: boolean; + + @prop({ + enum: [ + 'textfield', + 'date', + 'email', + 'legal', + 'textarea', + 'link', + 'statement', + 'dropdown', + 'rating', + 'radio', + 'hidden', + 'yes_no', + 'number' + ] + }) + fieldType: string; + + @prop({ + default: '' + }) + fieldValue: any; +} diff --git a/api/src/form/models/embedded/logic.jump.ts b/api/src/form/models/embedded/logic.jump.ts new file mode 100644 index 00000000..dec93267 --- /dev/null +++ b/api/src/form/models/embedded/logic.jump.ts @@ -0,0 +1,45 @@ +import {prop, Ref} from "typegoose" +import {VisitorData} from "./visitor.data" +import {Exclude} from "class-transformer" +import {Field} from "./field" + +export class LogicJump { + @Exclude() + readonly _id: string; + + @prop({ + enum: [ + 'field == static', + 'field != static', + 'field > static', + 'field >= static', + 'field <= static', + 'field < static', + 'field contains static', + 'field !contains static', + 'field begins static', + 'field !begins static', + 'field ends static', + 'field !ends static' + ] + }) + expressionString: string; + + @prop({ + ref: Field + }) + fieldA: Ref; + + @prop() + valueB: string; + + @prop({ + ref: Field + }) + jumpTo: Ref; + + @prop({ + default: false + }) + enabled: boolean; +} diff --git a/api/src/form/models/embedded/rating.field.ts b/api/src/form/models/embedded/rating.field.ts new file mode 100644 index 00000000..a825a154 --- /dev/null +++ b/api/src/form/models/embedded/rating.field.ts @@ -0,0 +1,38 @@ +import {arrayProp, prop} from "typegoose" +import {VisitorData} from "./visitor.data" +import {Exclude} from "class-transformer" + +export class RatingField { + @Exclude() + readonly _id: string; + + @prop({ + min: 1, + max: 10 + }) + steps: number; + + @prop({ + enum: [ + 'Heart', + 'Star', + 'thumbs-up', + 'thumbs-down', + 'Circle', + 'Square', + 'Check Circle', + 'Smile Outlined', + 'Hourglass', + 'bell', + 'Paper Plane', + 'Comment', + 'Trash' + ] + }) + shape: string; + + @arrayProp({ + items: String + }) + validShapes: [String]; +} diff --git a/api/src/form/models/embedded/respondent.notifications.ts b/api/src/form/models/embedded/respondent.notifications.ts new file mode 100644 index 00000000..e5d215ad --- /dev/null +++ b/api/src/form/models/embedded/respondent.notifications.ts @@ -0,0 +1,30 @@ +import {prop} from "typegoose" +import {Exclude} from "class-transformer" + +export class RespondentNotifications { + @Exclude() + readonly _id: string; + + @prop() + readonly toField: string; + + @prop({ + match: [/.+\@.+\..+/, 'Please fill a valid email address'] + }) + readonly fromEmails: string; + + @prop({ + default: 'OhMyForm: Thank you for filling out this OhMyForm' + }) + readonly subject: string; + + @prop({ + default: 'Hello,

We’ve received your submission.

Thank you & have a nice day!', + }) + readonly htmlTemplate: string; + + @prop({ + default: false + }) + readonly enabled: boolean; +} diff --git a/api/src/form/models/embedded/self.notifications.ts b/api/src/form/models/embedded/self.notifications.ts new file mode 100644 index 00000000..de4f1fe7 --- /dev/null +++ b/api/src/form/models/embedded/self.notifications.ts @@ -0,0 +1,24 @@ +import {prop} from "typegoose" +import {Exclude} from "class-transformer" + +export class SelfNotifications { + @Exclude() + readonly _id: string; + + @prop() + readonly fromField: string; + + @prop() + readonly toEmails: string; + + @prop() + readonly subject: string; + + @prop() + readonly htmlTemplate: string; + + @prop({ + default: false + }) + readonly enabled: boolean; +} diff --git a/api/src/form/models/embedded/start.page.ts b/api/src/form/models/embedded/start.page.ts new file mode 100644 index 00000000..6b70bb89 --- /dev/null +++ b/api/src/form/models/embedded/start.page.ts @@ -0,0 +1,29 @@ +import {prop} from "typegoose" +import {Button} from "./button" +import {Exclude} from "class-transformer" + +export class StartPage { + @Exclude() + readonly _id: string; + + @prop({ + default: false + }) + readonly showStart: boolean; + + @prop({ + default: 'Welcome to Form' + }) + readonly introTitle: string; + + @prop() + readonly introParagraph: string; + + @prop({ + default: 'Start' + }) + readonly introButtonText: string; + + @prop() + readonly buttons: Button[]; +} diff --git a/api/src/form/models/embedded/visitor.data.ts b/api/src/form/models/embedded/visitor.data.ts new file mode 100644 index 00000000..d8e33b4b --- /dev/null +++ b/api/src/form/models/embedded/visitor.data.ts @@ -0,0 +1,44 @@ +import {Exclude} from "class-transformer" +import {arrayProp, prop} from "typegoose" +import {Field} from "./field" +import {Ref} from "typegoose/lib/prop" + +export class VisitorData { + @Exclude() + readonly _id: string; + + @prop() + readonly introParagraph: string; + + @prop() + readonly referrer: string; + + @arrayProp({ + itemsRef: Field + }) + readonly filledOutFields: Ref[]; + + @prop() + readonly timeElapsed: number; + + @prop() + readonly isSubmitted: boolean; + + @prop({ + enum: ['en', 'fr', 'es', 'it', 'de'], + default: 'en', + }) + readonly language: string; + + @prop() + readonly ipAddr: string; + + @prop({ + enum: ['desktop', 'phone', 'tablet', 'other'], + default: 'other' + }) + readonly deviceType: string; + + @prop() + readonly userAgent: string; +} diff --git a/api/src/form/models/form.model.ts b/api/src/form/models/form.model.ts new file mode 100644 index 00000000..50c1b4ac --- /dev/null +++ b/api/src/form/models/form.model.ts @@ -0,0 +1,63 @@ +import {prop, Ref, Typegoose} from "typegoose" +import {Analytics} from "./embedded/analytics" +import {Field} from "./embedded/field" +import {StartPage} from "./embedded/start.page" +import {EndPage} from "./embedded/end.page" +import {SelfNotifications} from "./embedded/self.notifications" +import {RespondentNotifications} from "./embedded/respondent.notifications" +import {Design} from "./embedded/design" +import {User} from "../../user/models/user.model" + +export class Form extends Typegoose{ + @prop({ + trim: true, + required: 'Form Title cannot be blank' + }) + readonly firstName: string; + + @prop({ + enum: ['en', 'fr', 'es', 'it', 'de'], + default: 'en', + required: 'Form must have a language' + }) + readonly language: string; + + @prop() + readonly analytics: Analytics; + + @prop({ + default: [] + }) + readonly form_fields: Field[]; + + @prop({ + ref: User, + required: 'Form must have an Admin' + }) + readonly admin: Ref; + + @prop() + readonly startPage: StartPage; + + @prop() + readonly endPage: EndPage; + + @prop() + readonly selfNotifications: SelfNotifications; + + @prop() + readonly respondentNotifications: RespondentNotifications; + + @prop({ + default: true + }) + readonly showFooter: boolean; + + @prop({ + default: true + }) + readonly isLive: boolean; + + @prop() + readonly design: Design; +} diff --git a/api/src/form/services/form.service.ts b/api/src/form/services/form.service.ts new file mode 100644 index 00000000..b592a317 --- /dev/null +++ b/api/src/form/services/form.service.ts @@ -0,0 +1,13 @@ +import {Injectable} from '@nestjs/common'; +import { InjectModel } from 'nestjs-typegoose'; +import { ModelType } from 'typegoose'; +import {Form} from "../models/form.model" + +@Injectable() +export class FormService { + constructor(@InjectModel(Form) private readonly formModel: ModelType) {} + + async findById(id: string): Promise { + return await this.formModel.findById(id).exec() + } +} diff --git a/api/src/forms/forms.module.ts b/api/src/forms/forms.module.ts deleted file mode 100644 index ad6d579c..00000000 --- a/api/src/forms/forms.module.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Module } from '@nestjs/common'; - -@Module({ - providers: [], - exports: [], -}) -export class FormsModule {} diff --git a/api/src/forms/schemas/button.schema.ts b/api/src/forms/schemas/button.schema.ts deleted file mode 100644 index 5b4d1230..00000000 --- a/api/src/forms/schemas/button.schema.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as mongoose from 'mongoose'; - -export const ButtonSchema = new mongoose.Schema({ - url: { - type: String, - match: [/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/], - }, - action: String, - text: String, - bgColor: { - type: String, - match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], - default: '#5bc0de' - }, - color: { - type: String, - match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], - default: '#ffffff' - } -}); diff --git a/api/src/forms/schemas/field.option.schema.ts b/api/src/forms/schemas/field.option.schema.ts deleted file mode 100644 index aedb04d5..00000000 --- a/api/src/forms/schemas/field.option.schema.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as mongoose from 'mongoose'; - -export const FieldOptionSchema = new mongoose.Schema({ - option_id: { - type: Number - }, - - option_title: { - type: String - }, - - option_value: { - type: String, - trim: true - } -}); diff --git a/api/src/forms/schemas/field.schema.ts b/api/src/forms/schemas/field.schema.ts deleted file mode 100644 index 22a08212..00000000 --- a/api/src/forms/schemas/field.schema.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as mongoose from 'mongoose'; -import {RatingFieldSchema} from "./rating.field.schema" -import {FieldOptionSchema} from "./field.option.schema" -import {LogicJumpSchema} from "./logic.jump.schema" - -export const FieldSchema = new mongoose.Schema({ - isSubmission: { - type: Boolean, - default: false - }, - submissionId: { - type: mongoose.Schema.Types.ObjectId - }, - title: { - type: String, - trim: true - }, - description: { - type: String, - default: '' - }, - - logicJump: LogicJumpSchema, - - ratingOptions: RatingFieldSchema, - fieldOptions: [FieldOptionSchema], - - required: { - type: Boolean, - default: true - }, - disabled: { - type: Boolean, - default: false - }, - - deletePreserved: { - type: Boolean, - default: false - }, - validFieldTypes: { - type: [String] - }, - fieldType: { - type: String, - enum: [ - 'textfield', - 'date', - 'email', - 'legal', - 'textarea', - 'link', - 'statement', - 'dropdown', - 'rating', - 'radio', - 'hidden', - 'yes_no', - 'number' - ] - }, - fieldValue: { - type: mongoose.Schema.Types.Mixed, - default: '' - } -}); diff --git a/api/src/forms/schemas/form.schema.ts b/api/src/forms/schemas/form.schema.ts deleted file mode 100644 index 18ab73a8..00000000 --- a/api/src/forms/schemas/form.schema.ts +++ /dev/null @@ -1,151 +0,0 @@ -import * as mongoose from 'mongoose'; -import {VisitorDataSchema} from "./visitor.data.schema" -import {ButtonSchema} from "./button.schema" -import {FieldSchema} from "./field.schema" - -export const FormSchema = new mongoose.Schema({ - title: { - type: String, - trim: true, - required: 'Form Title cannot be blank' - }, - language: { - type: String, - enum: ['en', 'fr', 'es', 'it', 'de'], - default: 'en', - required: 'Form must have a language' - }, - analytics:{ - gaCode: { - type: String - }, - visitors: [VisitorDataSchema] - }, - form_fields: { - type: [FieldSchema], - default: [] - }, - admin: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: 'Form must have an Admin' - }, - startPage: { - showStart:{ - type: Boolean, - default: false - }, - introTitle:{ - type: String, - default: 'Welcome to Form' - }, - introParagraph:{ - type: String - }, - introButtonText:{ - type: String, - default: 'Start' - }, - buttons:[ButtonSchema] - }, - endPage: { - showEnd:{ - type: Boolean, - default: false - }, - title:{ - type: String, - default: 'Thank you for filling out the form' - }, - paragraph:{ - type: String - }, - buttonText:{ - type: String, - default: 'Go back to Form' - }, - buttons:[ButtonSchema] - }, - - selfNotifications: { - fromField: { - type: String - }, - toEmails: { - type: String - }, - subject: { - type: String - }, - htmlTemplate: { - type: String - }, - enabled: { - type: Boolean, - default: false - } - }, - - respondentNotifications: { - toField: { - type: String - }, - fromEmails: { - type: String, - match: [/.+\@.+\..+/, 'Please fill a valid email address'] - }, - subject: { - type: String, - default: 'OhMyForm: Thank you for filling out this OhMyForm' - }, - htmlTemplate: { - type: String, - default: 'Hello,

We’ve received your submission.

Thank you & have a nice day!', - }, - enabled: { - type: Boolean, - default: false - } - }, - - showFooter: { - type: Boolean, - default: true - }, - - isLive: { - type: Boolean, - default: true - }, - - design: { - colors: { - backgroundColor: { - type: String, - match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], - default: '#fff' - }, - questionColor: { - type: String, - match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], - default: '#333' - }, - answerColor: { - type: String, - match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], - default: '#333' - }, - buttonColor: { - type: String, - match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], - default: '#fff' - }, - buttonTextColor: { - type: String, - match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], - default: '#333' - } - }, - font: String - } -}); diff --git a/api/src/forms/schemas/logic.jump.schema.ts b/api/src/forms/schemas/logic.jump.schema.ts deleted file mode 100644 index 9f3b7a83..00000000 --- a/api/src/forms/schemas/logic.jump.schema.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as mongoose from 'mongoose'; - -export const LogicJumpSchema = new mongoose.Schema({ - expressionString: { - type: String, - enum: [ - 'field == static', - 'field != static', - 'field > static', - 'field >= static', - 'field <= static', - 'field < static', - 'field contains static', - 'field !contains static', - 'field begins static', - 'field !begins static', - 'field ends static', - 'field !ends static' - ] - }, - fieldA: { - type: mongoose.Schema.Types.ObjectId, - ref: 'FormField' - }, - valueB: { - type: mongoose.Schema.Types.String - }, - jumpTo: { - type: mongoose.Schema.Types.ObjectId, - ref: 'FormField' - }, - enabled: { - type: mongoose.Schema.Types.Boolean, - default: false - } -}); diff --git a/api/src/forms/schemas/rating.field.schema.ts b/api/src/forms/schemas/rating.field.schema.ts deleted file mode 100644 index e6b63b50..00000000 --- a/api/src/forms/schemas/rating.field.schema.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as mongoose from 'mongoose'; - -export const RatingFieldSchema = new mongoose.Schema({ - steps: { - type: Number, - min: 1, - max: 10 - }, - shape: { - type: String, - enum: [ - 'Heart', - 'Star', - 'thumbs-up', - 'thumbs-down', - 'Circle', - 'Square', - 'Check Circle', - 'Smile Outlined', - 'Hourglass', - 'bell', - 'Paper Plane', - 'Comment', - 'Trash' - ] - }, - validShapes: { - type: [String] - } -}); diff --git a/api/src/forms/schemas/visitor.data.schema.ts b/api/src/forms/schemas/visitor.data.schema.ts deleted file mode 100644 index ee2e83eb..00000000 --- a/api/src/forms/schemas/visitor.data.schema.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as mongoose from 'mongoose'; - -export const VisitorDataSchema = new mongoose.Schema({ - socketId: { - type: String - }, - referrer: { - type: String - }, - filledOutFields: { - type: [mongoose.Schema.Types.ObjectId] - }, - timeElapsed: { - type: Number - }, - isSubmitted: { - type: Boolean - }, - language: { - type: String, - enum: ['en', 'fr', 'es', 'it', 'de'], - default: 'en', - }, - ipAddr: { - type: String - }, - deviceType: { - type: String, - enum: ['desktop', 'phone', 'tablet', 'other'], - default: 'other' - }, - userAgent: { - type: String - } -}); diff --git a/api/src/main.ts b/api/src/main.ts index 8bd56150..4c0a7a2e 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -1,7 +1,7 @@ -import { NestFactory } from '@nestjs/core'; +import {NestFactory, Reflector} from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; -import { ValidationPipe } from '@nestjs/common'; +import {ClassSerializerInterceptor, ValidationPipe} from '@nestjs/common'; const pkg = require('../package.json') async function bootstrap() { @@ -15,10 +15,13 @@ async function bootstrap() { transform: true, })); + // app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))) + const options = new DocumentBuilder() .setTitle('OhMyForm') .setDescription('API documentation') .setVersion(pkg.version) + .addBearerAuth() .build(); const document = SwaggerModule.createDocument(app, options); diff --git a/api/src/user/controllers/users.controller.ts b/api/src/user/controllers/users.controller.ts new file mode 100644 index 00000000..e69de29b diff --git a/api/src/user/models/user.model.ts b/api/src/user/models/user.model.ts new file mode 100644 index 00000000..a424e943 --- /dev/null +++ b/api/src/user/models/user.model.ts @@ -0,0 +1,93 @@ +import {arrayProp, prop, Typegoose} from 'typegoose'; +import { IsString } from 'class-validator'; +import {Exclude} from "class-transformer" + +export class User extends Typegoose { + @Exclude() + readonly _id: string; + + @prop({ + trim: true, + default: '' + }) + readonly firstName: string; + + @prop({ + trim: true, + default: '' + }) + readonly lastName: string; + + @prop({ + trim: true, + lowercase: true, + unique: true, // 'Account already exists with this email', + match: [ + /^(([^<>()\[\]\\.,;:\s@']+(\.[^<>()\[\]\\.,;:\s@']+)*)|('.+'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + 'Please fill a valid email address' + ], + required: [true, 'Email is required'] + }) + readonly email: string; + + @prop({ + unique: true, + lowercase: true, + match: [ + /^[a-zA-Z0-9\-]+$/, + 'Username can only contain alphanumeric characters and \'-\'' + ], + required: [true, 'Username is required'] + }) + readonly username: string; + + @prop({ + default: '' + }) + readonly passwordHash: string; + + @prop() + readonly salt: string; + + @prop({ + default: 'local' + }) + readonly provider: string; + + @arrayProp({ + items: String, + enum: ['user', 'admin', 'superuser'], + default: ['user'] + }) + readonly roles: [string]; + + @prop({ + enum: ['en', 'fr', 'es', 'it', 'de'], + default: 'en', + }) + readonly language: string; + + @prop({ + default: Date.now + }) + readonly created: Date; + + @prop() + readonly lastModified: Date; + + @prop() + readonly resetPasswordToken: string; + + @prop() + readonly resetPasswordExpires: Date; + + @prop() + readonly token: string; + + @prop({ + unique: true, + index: true, + sparse: true + }) + readonly apiKey: string; +} diff --git a/api/src/users/users.service.spec.ts b/api/src/user/services/user.service.spec.ts similarity index 63% rename from api/src/users/users.service.spec.ts rename to api/src/user/services/user.service.spec.ts index 62815ba6..c0591989 100644 --- a/api/src/users/users.service.spec.ts +++ b/api/src/user/services/user.service.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { UsersService } from './users.service'; +import { UserService } from './users.service'; describe('UsersService', () => { - let service: UsersService; + let service: UserService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [UsersService], + providers: [UserService], }).compile(); - service = module.get(UsersService); + service = module.get(UserService); }); it('should be defined', () => { diff --git a/api/src/users/users.service.ts b/api/src/user/services/user.service.ts similarity index 60% rename from api/src/users/users.service.ts rename to api/src/user/services/user.service.ts index ced6a32f..afcff462 100644 --- a/api/src/users/users.service.ts +++ b/api/src/user/services/user.service.ts @@ -1,13 +1,11 @@ -import { Model } from 'mongoose'; import {Injectable, NotFoundException} from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import {User} from "./interfaces/user.interface" - -export type User = any; +import { InjectModel } from 'nestjs-typegoose'; +import { ModelType } from 'typegoose'; +import { User } from "../models/user.model" @Injectable() -export class UsersService { - constructor(@InjectModel('User') private readonly userModel: Model) {} +export class UserService { + constructor(@InjectModel(User) private readonly userModel: ModelType) {} async findOneByIdentifier(identifier: string): Promise { const results = await this.userModel.find().or([ diff --git a/api/src/user/user.controllers.ts b/api/src/user/user.controllers.ts new file mode 100644 index 00000000..e69de29b diff --git a/api/src/user/user.exports.ts b/api/src/user/user.exports.ts new file mode 100644 index 00000000..c7c27178 --- /dev/null +++ b/api/src/user/user.exports.ts @@ -0,0 +1,5 @@ +import {UserService} from "./services/user.service" + +export default [ + UserService +] diff --git a/api/src/user/user.module.ts b/api/src/user/user.module.ts new file mode 100644 index 00000000..12481031 --- /dev/null +++ b/api/src/user/user.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypegooseModule } from 'nestjs-typegoose'; +import providers from './user.providers' +import exportList from './user.exports' +import { User } from "./models/user.model" + +@Module({ + imports: [ + TypegooseModule.forFeature([User]), + ], + providers, + exports: exportList, +}) +export class UserModule {} diff --git a/api/src/user/user.providers.ts b/api/src/user/user.providers.ts new file mode 100644 index 00000000..c7c27178 --- /dev/null +++ b/api/src/user/user.providers.ts @@ -0,0 +1,5 @@ +import {UserService} from "./services/user.service" + +export default [ + UserService +] diff --git a/api/src/users/interfaces/user.interface.ts b/api/src/users/interfaces/user.interface.ts deleted file mode 100644 index 1cd168c3..00000000 --- a/api/src/users/interfaces/user.interface.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Document } from 'mongoose'; - -export interface User extends Document{ - readonly firstName: string; - readonly lastName: string; - readonly email: string; - readonly username: string; - readonly passwordHash: string; - readonly salt: string; - readonly provider: string; - readonly roles: [string]; - readonly language: string; - readonly created: Date; - readonly lastUpdated: Date; - readonly resetPasswordToken: string; - readonly resetPasswordExpires: Date; - readonly token: string; - readonly apiKey: string; -} diff --git a/api/src/users/schemas/user.schema.ts b/api/src/users/schemas/user.schema.ts deleted file mode 100644 index 7cd7cd47..00000000 --- a/api/src/users/schemas/user.schema.ts +++ /dev/null @@ -1,80 +0,0 @@ -import * as mongoose from 'mongoose'; - -export const UserSchema = new mongoose.Schema({ - firstName: { - type: String, - trim: true, - default: '' - }, - lastName: { - type: String, - trim: true, - default: '' - }, - email: { - type: String, - trim: true, - lowercase: true, - unique: 'Account already exists with this email', - match: [ - /^(([^<>()\[\]\\.,;:\s@']+(\.[^<>()\[\]\\.,;:\s@']+)*)|('.+'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, - 'Please fill a valid email address' - ], - required: [true, 'Email is required'] - }, - username: { - type: String, - unique: true, - lowercase: true, - match: [ - /^[a-zA-Z0-9\-]+$/, - 'Username can only contain alphanumeric characters and \'-\'' - ], - required: [true, 'Username is required'] - }, - passwordHash: { - type: String, - default: '' - }, - salt: { - type: String - }, - provider: { - type: String, - default: 'local' - }, - roles: { - type: [{ - type: String, - enum: ['user', 'admin', 'superuser'] - }], - default: ['user'] - }, - language: { - type: String, - enum: ['en', 'fr', 'es', 'it', 'de'], - default: 'en', - }, - lastModified: { - type: Date - }, - created: { - type: Date, - default: Date.now - }, - - /* For reset password */ - resetPasswordToken: { - type: String - }, - resetPasswordExpires: { - type: Date - }, - token: String, - apiKey: { - type: String, - unique: true, - index: true, - sparse: true - } -}); diff --git a/api/src/users/users.module.ts b/api/src/users/users.module.ts deleted file mode 100644 index 82398af1..00000000 --- a/api/src/users/users.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; -import { UsersService } from './users.service'; -import {UserSchema} from "./schemas/user.schema" - -@Module({ - imports: [MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])], - providers: [UsersService], - exports: [UsersService], -}) -export class UsersModule {} diff --git a/api/tslint.json b/api/tslint.json index e66f0a4a..e815f45e 100644 --- a/api/tslint.json +++ b/api/tslint.json @@ -49,6 +49,10 @@ ], "one-variable-per-declaration": [ false + ], + "whitespace": [ + true, + "check-postbrace" ] }, "rulesDirectory": []