add registration and emails

This commit is contained in:
wodka 2019-08-30 17:58:19 +02:00
parent 70859f6871
commit aaa546bccd
19 changed files with 180 additions and 37 deletions

View File

@ -20,6 +20,7 @@
}, },
"dependencies": { "dependencies": {
"@godaddy/terminus": "^4.1.2", "@godaddy/terminus": "^4.1.2",
"@nest-modules/mailer": "^1.1.3",
"@nestjs/common": "^6.5.2", "@nestjs/common": "^6.5.2",
"@nestjs/core": "^6.5.2", "@nestjs/core": "^6.5.2",
"@nestjs/jwt": "^6.1.1", "@nestjs/jwt": "^6.1.1",

View File

@ -9,6 +9,7 @@ import { UserModule } from "./user/user.module"
import { FormModule } from "./form/form.module" import { FormModule } from "./form/form.module"
import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module';
import { MailModule } from "./mail/mail.module" import { MailModule } from "./mail/mail.module"
import { MailerModule } from "@nest-modules/mailer"
@Module({ @Module({
imports: [ imports: [
@ -17,6 +18,12 @@ import { MailModule } from "./mail/mail.module"
TerminusModule.forRootAsync({ TerminusModule.forRootAsync({
useClass: TerminusOptionsService, useClass: TerminusOptionsService,
}), }),
MailerModule.forRoot({
transport: 'smtp://localhost:1025',
defaults: {
from:'"OhMyForm" <noreply@ohmyform.com>',
}
}),
UserModule, UserModule,
FormModule, FormModule,
AuthModule, AuthModule,

View File

@ -1,12 +1,20 @@
import { ApiModelProperty } from "@nestjs/swagger" 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 { export class RegisterDto {
@ApiModelProperty() @ApiModelProperty()
@IsNotEmpty()
@Validate(UsernameAlreadyInUse)
readonly username: string; readonly username: string;
@ApiModelProperty() @ApiModelProperty()
@IsNotEmpty()
readonly password: string; readonly password: string;
@ApiModelProperty() @ApiModelProperty()
@Validate(EmailAlreadyInUse)
@IsEmail()
readonly email: string; readonly email: string;
} }

View File

@ -24,7 +24,7 @@ export class PasswordService {
).toString('base64'); ).toString('base64');
} }
hash (password): Promise<String> { hash (password): Promise<string> {
return bcrypt.hash(password, 4) return bcrypt.hash(password, 4)
} }
} }

View File

@ -1,16 +1,30 @@
import { Injectable } from "@nestjs/common" import { Injectable } from "@nestjs/common"
import { MailService } from "../../mail/services/mail.service" 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() @Injectable()
export class RegisterService { 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<void> { async register (username: string, email: string, password: string): Promise<void> {
// TODO actually create user // 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( await this.mailService.sendEmail(
{ {
template: 'auth/register.hbs', template: 'auth/register',
to: email to: email
}, },
{ {

View File

@ -34,5 +34,5 @@ export class RatingField {
@arrayProp({ @arrayProp({
items: String items: String
}) })
validShapes: [String]; validShapes: [string];
} }

View File

@ -1,9 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import providers from './mail.providers' import providers from './mail.providers'
import exportList from './mail.exports' import exportList from './mail.exports'
import { HandlebarsAdapter, MailerModule } from "@nest-modules/mailer"
@Module({ @Module({
imports: [], imports: [
],
providers, providers,
exports: exportList, exports: exportList,
}) })

View File

@ -1,10 +1,35 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import {Inject, Injectable, NotFoundException} from '@nestjs/common';
import { OptionsDto } from "../dto/options.dto" import { OptionsDto } from "../dto/options.dto"
import { MailerService } from '@nest-modules/mailer';
import * as Handlebars from 'handlebars'
import * as fs from 'fs';
@Injectable() @Injectable()
export class MailService { export class MailService {
// TODO constructor(
private readonly nodeMailer: MailerService
) {}
async sendEmail(options:OptionsDto, placeholders:any): Promise<boolean> { async sendEmail(options:OptionsDto, placeholders:any): Promise<boolean> {
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 return false
} }
} }

View File

@ -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

View File

@ -1,5 +0,0 @@
Welcome to OhMyForm!
please confirm your account by following the following link: {{ confirm }}
enjoy!

View File

@ -1,7 +1,8 @@
import {NestFactory, Reflector} from '@nestjs/core'; import {NestFactory, Reflector} from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module'; 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') const pkg = require('../package.json')
async function bootstrap() { async function bootstrap() {
@ -10,6 +11,8 @@ async function bootstrap() {
// app.enableCors({ origin: '*' }); // app.enableCors({ origin: '*' });
// app.getHttpAdapter().options('*', cors()); // app.getHttpAdapter().options('*', cors());
useContainer(app.select(AppModule), { fallbackOnErrors: true });
app.useGlobalPipes(new ValidationPipe({ app.useGlobalPipes(new ValidationPipe({
disableErrorMessages: false, disableErrorMessages: false,
transform: true, transform: true,

View File

@ -1,7 +1,11 @@
import {arrayProp, prop, Typegoose} from 'typegoose'; import {arrayProp, pre, prop, Typegoose} from 'typegoose';
import { IsString } from 'class-validator'; import { IsString } from 'class-validator';
import {Exclude} from "class-transformer" import {Exclude} from "class-transformer"
@pre<User>('save', (next) => {
this.lastModified = new Date()
next()
})
export class User extends Typegoose { export class User extends Typegoose {
@Exclude() @Exclude()
readonly _id: string; readonly _id: string;
@ -10,13 +14,13 @@ export class User extends Typegoose {
trim: true, trim: true,
default: '' default: ''
}) })
readonly firstName: string; firstName: string;
@prop({ @prop({
trim: true, trim: true,
default: '' default: ''
}) })
readonly lastName: string; lastName: string;
@prop({ @prop({
trim: true, trim: true,
@ -28,7 +32,7 @@ export class User extends Typegoose {
], ],
required: [true, 'Email is required'] required: [true, 'Email is required']
}) })
readonly email: string; email: string;
@prop({ @prop({
unique: true, unique: true,
@ -39,33 +43,33 @@ export class User extends Typegoose {
], ],
required: [true, 'Username is required'] required: [true, 'Username is required']
}) })
readonly username: string; username: string;
@prop({ @prop({
default: '' default: ''
}) })
readonly passwordHash: string; passwordHash: string;
@prop() @prop()
readonly salt: string; salt: string;
@prop({ @prop({
default: 'local' default: 'local'
}) })
readonly provider: string; provider: string;
@arrayProp({ @arrayProp({
items: String, items: String,
enum: ['user', 'admin', 'superuser'], enum: ['user', 'admin', 'superuser'],
default: ['user'] default: ['user']
}) })
readonly roles: [string]; roles: [string];
@prop({ @prop({
enum: ['en', 'fr', 'es', 'it', 'de'], enum: ['en', 'fr', 'es', 'it', 'de'],
default: 'en', default: 'en',
}) })
readonly language: string; language: string;
@prop({ @prop({
default: Date.now default: Date.now
@ -76,18 +80,18 @@ export class User extends Typegoose {
readonly lastModified: Date; readonly lastModified: Date;
@prop() @prop()
readonly resetPasswordToken: string; resetPasswordToken: string;
@prop() @prop()
readonly resetPasswordExpires: Date; resetPasswordExpires: Date;
@prop() @prop()
readonly token: string; token: string;
@prop({ @prop({
unique: true, unique: true,
index: true, index: true,
sparse: true sparse: true
}) })
readonly apiKey: string; apiKey: string;
} }

View File

@ -28,4 +28,13 @@ export class UserService {
async findById(id: string): Promise<User> { async findById(id: string): Promise<User> {
return await this.userModel.findById(id).exec() return await this.userModel.findById(id).exec()
} }
async findOneBy(conditions): Promise<User> {
return await this.userModel.findOne(conditions).exec()
}
async save(user: User): Promise<User> {
let model = new this.userModel(user)
return await model.save()
}
} }

View File

@ -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 [ export default [
UserService UserService,
UsernameAlreadyInUse,
EmailAlreadyInUse,
] ]

View File

@ -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 [ export default [
UserService UserService,
UsernameAlreadyInUse,
EmailAlreadyInUse,
] ]

View File

@ -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.';
}
}

View File

@ -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.';
}
}

View File

@ -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
---
<h1>Hi, </h1>
<p>
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: <a href="{{ recover }}">{{ recover }}</a>
</p>
<p>
See you soon
</p>

View File

@ -0,0 +1,14 @@
Welcome to OhMyForm
---
Welcome to OhMyForm!
please confirm your account by following the following link: {{ confirm }}
enjoy!
---
<h1>Welcome to OhMyForm!</h1>
<p>
please confirm your account by following the following link: <a href="{{ confirm }}">{{ confirm }}</a>
</p>
<p>enjoy!</p>