diff --git a/api/package.json b/api/package.json index 5a9d7612..d595251a 100644 --- a/api/package.json +++ b/api/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@godaddy/terminus": "^4.1.2", + "@nest-modules/mailer": "^1.1.3", "@nestjs/common": "^6.5.2", "@nestjs/core": "^6.5.2", "@nestjs/jwt": "^6.1.1", diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 5305c02c..e9b408d1 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -9,6 +9,7 @@ import { UserModule } from "./user/user.module" import { FormModule } from "./form/form.module" import { AuthModule } from './auth/auth.module'; import { MailModule } from "./mail/mail.module" +import { MailerModule } from "@nest-modules/mailer" @Module({ imports: [ @@ -17,6 +18,12 @@ import { MailModule } from "./mail/mail.module" TerminusModule.forRootAsync({ useClass: TerminusOptionsService, }), + MailerModule.forRoot({ + transport: 'smtp://localhost:1025', + defaults: { + from:'"OhMyForm" ', + } + }), UserModule, FormModule, AuthModule, diff --git a/api/src/auth/dto/register.dto.ts b/api/src/auth/dto/register.dto.ts index 1670d2fd..64a4e892 100644 --- a/api/src/auth/dto/register.dto.ts +++ b/api/src/auth/dto/register.dto.ts @@ -1,12 +1,20 @@ import { ApiModelProperty } from "@nestjs/swagger" +import { IsEmail, IsNotEmpty, Validate } from "class-validator" +import { UsernameAlreadyInUse } from "../../user/validators/UsernameAlreadyInUse" +import { EmailAlreadyInUse } from "../../user/validators/EmailAlreadyInUse" export class RegisterDto { @ApiModelProperty() + @IsNotEmpty() + @Validate(UsernameAlreadyInUse) readonly username: string; @ApiModelProperty() + @IsNotEmpty() readonly password: string; @ApiModelProperty() + @Validate(EmailAlreadyInUse) + @IsEmail() readonly email: string; } diff --git a/api/src/auth/services/password.service.ts b/api/src/auth/services/password.service.ts index 548926b9..68a4d1c0 100644 --- a/api/src/auth/services/password.service.ts +++ b/api/src/auth/services/password.service.ts @@ -24,7 +24,7 @@ export class PasswordService { ).toString('base64'); } - hash (password): Promise { + hash (password): Promise { return bcrypt.hash(password, 4) } } diff --git a/api/src/auth/services/register.service.ts b/api/src/auth/services/register.service.ts index 507a92f9..66734a41 100644 --- a/api/src/auth/services/register.service.ts +++ b/api/src/auth/services/register.service.ts @@ -1,16 +1,30 @@ import { Injectable } from "@nestjs/common" import { MailService } from "../../mail/services/mail.service" +import { User } from "../../user/models/user.model" +import { PasswordService } from "./password.service" +import { UserService } from "../../user/services/user.service" @Injectable() export class RegisterService { - constructor(private readonly mailService: MailService) {} + constructor( + private readonly mailService: MailService, + private readonly passwordService: PasswordService, + private readonly userService: UserService + ) {} async register (username: string, email: string, password: string): Promise { // TODO actually create user + let user = new User() + user.email = email + user.username = username + user.passwordHash = await this.passwordService.hash(password) + + await this.userService.save(user) + await this.mailService.sendEmail( { - template: 'auth/register.hbs', + template: 'auth/register', to: email }, { diff --git a/api/src/form/models/embedded/rating.field.ts b/api/src/form/models/embedded/rating.field.ts index a825a154..77281aa3 100644 --- a/api/src/form/models/embedded/rating.field.ts +++ b/api/src/form/models/embedded/rating.field.ts @@ -34,5 +34,5 @@ export class RatingField { @arrayProp({ items: String }) - validShapes: [String]; + validShapes: [string]; } diff --git a/api/src/mail/mail.module.ts b/api/src/mail/mail.module.ts index d9d7c3db..5ea6d846 100644 --- a/api/src/mail/mail.module.ts +++ b/api/src/mail/mail.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; import providers from './mail.providers' import exportList from './mail.exports' +import { HandlebarsAdapter, MailerModule } from "@nest-modules/mailer" @Module({ - imports: [], + imports: [ + ], providers, exports: exportList, }) diff --git a/api/src/mail/services/mail.service.ts b/api/src/mail/services/mail.service.ts index 875373f5..2a7e2ae1 100644 --- a/api/src/mail/services/mail.service.ts +++ b/api/src/mail/services/mail.service.ts @@ -1,10 +1,35 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import {Inject, Injectable, NotFoundException} from '@nestjs/common'; import { OptionsDto } from "../dto/options.dto" +import { MailerService } from '@nest-modules/mailer'; +import * as Handlebars from 'handlebars' +import * as fs from 'fs'; @Injectable() export class MailService { - // TODO + constructor( + private readonly nodeMailer: MailerService + ) {} + async sendEmail(options:OptionsDto, placeholders:any): Promise { + const template = fs.readFileSync(`${__dirname}/../../../views/en/mail/${options.template}.hbs`, 'UTF-8'); + + const parts = Handlebars + .compile(template) + (placeholders) + .split('---') + + const mail:any = { + to: options.to, + subject: parts[0], + text: parts[1], + } + + if (parts.length > 2) { + mail.html = parts[2] + } + + await this.nodeMailer.sendMail(mail) + return false } } diff --git a/api/src/mail/views/en/auth/recover.hbs b/api/src/mail/views/en/auth/recover.hbs deleted file mode 100644 index bd821196..00000000 --- a/api/src/mail/views/en/auth/recover.hbs +++ /dev/null @@ -1,6 +0,0 @@ -Hi, - -if you have not requested a new password you can ignore this email. To set a new password for your account -just follow this link: {{ recover }} - -See you soon diff --git a/api/src/mail/views/en/auth/register.hbs b/api/src/mail/views/en/auth/register.hbs deleted file mode 100644 index 759399f2..00000000 --- a/api/src/mail/views/en/auth/register.hbs +++ /dev/null @@ -1,5 +0,0 @@ -Welcome to OhMyForm! - -please confirm your account by following the following link: {{ confirm }} - -enjoy! diff --git a/api/src/main.ts b/api/src/main.ts index 7c882026..022d3a5f 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -1,7 +1,8 @@ import {NestFactory, Reflector} from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; -import {ClassSerializerInterceptor, ValidationPipe} from '@nestjs/common'; +import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common'; +import { useContainer } from "class-validator" const pkg = require('../package.json') async function bootstrap() { @@ -10,6 +11,8 @@ async function bootstrap() { // app.enableCors({ origin: '*' }); // app.getHttpAdapter().options('*', cors()); + useContainer(app.select(AppModule), { fallbackOnErrors: true }); + app.useGlobalPipes(new ValidationPipe({ disableErrorMessages: false, transform: true, diff --git a/api/src/user/models/user.model.ts b/api/src/user/models/user.model.ts index a424e943..9590dd09 100644 --- a/api/src/user/models/user.model.ts +++ b/api/src/user/models/user.model.ts @@ -1,7 +1,11 @@ -import {arrayProp, prop, Typegoose} from 'typegoose'; +import {arrayProp, pre, prop, Typegoose} from 'typegoose'; import { IsString } from 'class-validator'; import {Exclude} from "class-transformer" +@pre('save', (next) => { + this.lastModified = new Date() + next() +}) export class User extends Typegoose { @Exclude() readonly _id: string; @@ -10,13 +14,13 @@ export class User extends Typegoose { trim: true, default: '' }) - readonly firstName: string; + firstName: string; @prop({ trim: true, default: '' }) - readonly lastName: string; + lastName: string; @prop({ trim: true, @@ -28,7 +32,7 @@ export class User extends Typegoose { ], required: [true, 'Email is required'] }) - readonly email: string; + email: string; @prop({ unique: true, @@ -39,33 +43,33 @@ export class User extends Typegoose { ], required: [true, 'Username is required'] }) - readonly username: string; + username: string; @prop({ default: '' }) - readonly passwordHash: string; + passwordHash: string; @prop() - readonly salt: string; + salt: string; @prop({ default: 'local' }) - readonly provider: string; + provider: string; @arrayProp({ items: String, enum: ['user', 'admin', 'superuser'], default: ['user'] }) - readonly roles: [string]; + roles: [string]; @prop({ enum: ['en', 'fr', 'es', 'it', 'de'], default: 'en', }) - readonly language: string; + language: string; @prop({ default: Date.now @@ -76,18 +80,18 @@ export class User extends Typegoose { readonly lastModified: Date; @prop() - readonly resetPasswordToken: string; + resetPasswordToken: string; @prop() - readonly resetPasswordExpires: Date; + resetPasswordExpires: Date; @prop() - readonly token: string; + token: string; @prop({ unique: true, index: true, sparse: true }) - readonly apiKey: string; + apiKey: string; } diff --git a/api/src/user/services/user.service.ts b/api/src/user/services/user.service.ts index fe22c3b3..e349446a 100644 --- a/api/src/user/services/user.service.ts +++ b/api/src/user/services/user.service.ts @@ -28,4 +28,13 @@ export class UserService { async findById(id: string): Promise { return await this.userModel.findById(id).exec() } + + async findOneBy(conditions): Promise { + return await this.userModel.findOne(conditions).exec() + } + + async save(user: User): Promise { + let model = new this.userModel(user) + return await model.save() + } } diff --git a/api/src/user/user.exports.ts b/api/src/user/user.exports.ts index c7c27178..9f880f97 100644 --- a/api/src/user/user.exports.ts +++ b/api/src/user/user.exports.ts @@ -1,5 +1,9 @@ -import {UserService} from "./services/user.service" +import { UserService } from "./services/user.service" +import { UsernameAlreadyInUse } from "./validators/UsernameAlreadyInUse" +import { EmailAlreadyInUse } from "./validators/EmailAlreadyInUse" export default [ - UserService + UserService, + UsernameAlreadyInUse, + EmailAlreadyInUse, ] diff --git a/api/src/user/user.providers.ts b/api/src/user/user.providers.ts index c7c27178..9f880f97 100644 --- a/api/src/user/user.providers.ts +++ b/api/src/user/user.providers.ts @@ -1,5 +1,9 @@ -import {UserService} from "./services/user.service" +import { UserService } from "./services/user.service" +import { UsernameAlreadyInUse } from "./validators/UsernameAlreadyInUse" +import { EmailAlreadyInUse } from "./validators/EmailAlreadyInUse" export default [ - UserService + UserService, + UsernameAlreadyInUse, + EmailAlreadyInUse, ] diff --git a/api/src/user/validators/EmailAlreadyInUse.ts b/api/src/user/validators/EmailAlreadyInUse.ts new file mode 100644 index 00000000..4f1d3980 --- /dev/null +++ b/api/src/user/validators/EmailAlreadyInUse.ts @@ -0,0 +1,20 @@ +import { ValidationArguments, ValidatorConstraint } from "class-validator" +import { Inject, Injectable } from "@nestjs/common" +import { UserService } from "../services/user.service" + +@ValidatorConstraint({ name: 'EmailAlreadyInUse', async: true }) +@Injectable() +export class EmailAlreadyInUse { + constructor( + @Inject('UserService') private readonly userService: UserService, + ) {} + + async validate(text: string) { + const user = await this.userService.findOneBy({email: text}); + return !user; + } + + defaultMessage(args: ValidationArguments) { + return 'User with this email already exists.'; + } +} diff --git a/api/src/user/validators/UsernameAlreadyInUse.ts b/api/src/user/validators/UsernameAlreadyInUse.ts new file mode 100644 index 00000000..2dca12c8 --- /dev/null +++ b/api/src/user/validators/UsernameAlreadyInUse.ts @@ -0,0 +1,20 @@ +import { ValidationArguments, ValidatorConstraint } from "class-validator" +import { Inject, Injectable } from "@nestjs/common" +import { UserService } from "../services/user.service" + +@ValidatorConstraint({ name: 'UsernameAlreadyInUse', async: true }) +@Injectable() +export class UsernameAlreadyInUse { + constructor( + @Inject('UserService') private readonly userService: UserService, + ) {} + + async validate(text: string) { + const user = await this.userService.findOneBy({username: text}); + return !user; + } + + defaultMessage(args: ValidationArguments) { + return 'User with this username already exists.'; + } +} diff --git a/api/views/en/mail/auth/recover.hbs b/api/views/en/mail/auth/recover.hbs new file mode 100644 index 00000000..596981bb --- /dev/null +++ b/api/views/en/mail/auth/recover.hbs @@ -0,0 +1,19 @@ +Password Reset Request +--- +Hi, + +if you have not requested a new password you can ignore this email. To set a new password for your account +just follow this link: {{ recover }} + +See you soon +--- +

Hi,

+ +

+ if you have not requested a new password you can ignore this email. To set a new password for your account + just follow this link: {{ recover }} +

+ +

+ See you soon +

diff --git a/api/views/en/mail/auth/register.hbs b/api/views/en/mail/auth/register.hbs new file mode 100644 index 00000000..7a9c920e --- /dev/null +++ b/api/views/en/mail/auth/register.hbs @@ -0,0 +1,14 @@ +Welcome to OhMyForm +--- +Welcome to OhMyForm! + +please confirm your account by following the following link: {{ confirm }} + +enjoy! +--- +

Welcome to OhMyForm!

+

+ please confirm your account by following the following link: {{ confirm }} +

+ +

enjoy!