From 1b1028679da18887f894e43b25235bfb746b382f Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Sun, 13 Sep 2020 17:13:14 +0200 Subject: [PATCH] registration email, handlebars templates --- .env | 5 ++ .vscode/settings.json | 2 + nest-cli.json | 10 ++- package-lock.json | 86 +++++++++++++++++-- package.json | 5 ++ src/app.module.ts | 2 + src/crags/services/crags.service.ts | 1 - .../interfaces/mail-options.interface.ts | 6 ++ src/notification/notification.module.ts | 11 +++ .../services/mail.service.spec.ts | 18 ++++ src/notification/services/mail.service.ts | 57 ++++++++++++ .../services/notification.service.spec.ts | 18 ++++ .../services/notification.service.ts | 19 ++++ .../templates/account-confirmation.html.hbs | 14 +++ .../templates/account-confirmation.plain.hbs | 8 ++ src/notification/templates/layout.html.hbs | 53 ++++++++++++ src/users/resolvers/users.resolver.ts | 22 ++++- src/users/services/users.service.ts | 12 ++- src/users/users.module.ts | 3 +- tsconfig.json | 2 +- 20 files changed, 340 insertions(+), 14 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/notification/interfaces/mail-options.interface.ts create mode 100644 src/notification/notification.module.ts create mode 100644 src/notification/services/mail.service.spec.ts create mode 100644 src/notification/services/mail.service.ts create mode 100644 src/notification/services/notification.service.spec.ts create mode 100644 src/notification/services/notification.service.ts create mode 100644 src/notification/templates/account-confirmation.html.hbs create mode 100644 src/notification/templates/account-confirmation.plain.hbs create mode 100644 src/notification/templates/layout.html.hbs diff --git a/.env b/.env index 60d6720a..df09ee67 100644 --- a/.env +++ b/.env @@ -2,3 +2,8 @@ DATABASE_USER=test DATABASE_PASSWORD=test JWT_SECRET=1i03RjH85jL4HTk1XHXmhwJjM-8ZfDxzOaN1Hnjx4XzDm0RcH6xmnGBlO + +SMTP_HOST=smtp.gmail.com +SMTP_PORT=465 +SMTP_USERNAME=demshy@gmail.com +SMTP_PASSWORD=zvbquyhagnjchutw \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7a73a41b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/nest-cli.json b/nest-cli.json index 56167b36..5337193e 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,4 +1,10 @@ { "collection": "@nestjs/schematics", - "sourceRoot": "src" -} + "sourceRoot": "src", + "compilerOptions": { + "assets": [ + "**/*.hbs" + ], + "watchAssets": true + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2d9b8f24..82e86b12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3414,6 +3414,14 @@ "@types/node": "*" } }, + "@types/nodemailer": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.0.tgz", + "integrity": "sha512-KY7bFWB0MahRZvVW4CuW83qcCDny59pJJ0MQ5ifvfcjNwPlIT0vW4uARO4u1gtkYnWdhSvURegecY/tzcukJcA==", + "requires": { + "@types/node": "*" + } + }, "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -5874,6 +5882,13 @@ "integrity": "sha512-FFLcLtraisj5eteosnX1gf01qYDCOc4fDy0+euOt8Kn9YBY2NtXL/pCoYPavw24NIQkQqm5ZOLsGD5Zzj0gyew==", "requires": { "node-fetch": "2.6.0" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + } } }, "cross-spawn": { @@ -7571,6 +7586,11 @@ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "dev": true }, + "foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY=" + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -7673,6 +7693,11 @@ } } }, + "fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" + }, "fs-capacitor": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-2.0.4.tgz", @@ -8040,6 +8065,25 @@ "dev": true, "optional": true }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -8208,6 +8252,15 @@ "minimalistic-assert": "^1.0.1" } }, + "hbs": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/hbs/-/hbs-4.1.1.tgz", + "integrity": "sha512-6QsbB4RwbpL4cb4DNyjEEPF+suwp+3yZqFVlhILEn92ScC0U4cDCR+FDX53jkfKJPhutcqhAvs+rOLZw5sQrDA==", + "requires": { + "handlebars": "4.7.6", + "walk": "2.3.14" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -12480,8 +12533,7 @@ "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "next-tick": { "version": "1.0.0", @@ -12525,9 +12577,9 @@ } }, "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, "node-int64": { "version": "0.4.0", @@ -12738,6 +12790,11 @@ } } }, + "nodemailer": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.11.tgz", + "integrity": "sha512-BVZBDi+aJV4O38rxsUh164Dk1NCqgh6Cm0rQSb9SK/DHGll/DrCMnycVDD7msJgZCnmVa8ASo8EZzR7jsgTukQ==" + }, "nopt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", @@ -15892,6 +15949,12 @@ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz", "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==" }, + "uglify-js": { + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.4.tgz", + "integrity": "sha512-kBFT3U4Dcj4/pJ52vfjCSfyLyvG9VYYuGYPmrPvAxRw/i7xHiT4VvCev+uiEMcEEiu6UNB6KgWmGtSUYIWScbw==", + "optional": true + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -16168,6 +16231,14 @@ "xml-name-validator": "^3.0.0" } }, + "walk": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.14.tgz", + "integrity": "sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg==", + "requires": { + "foreachasync": "^3.0.0" + } + }, "walker": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", @@ -16671,6 +16742,11 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, "worker-farm": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", diff --git a/package.json b/package.json index c2ddcf3c..7e43f1ea 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,18 @@ "@nestjs/passport": "^7.1.0", "@nestjs/platform-express": "^7.0.0", "@nestjs/typeorm": "^7.1.0", + "@types/nodemailer": "^6.4.0", "apollo-server-express": "^2.16.1", "bcrypt": "^5.0.0", "class-transformer": "^0.3.1", "class-validator": "^0.12.2", "dotenv": "^8.2.0", + "fs": "0.0.1-security", "graphql": "^15.3.0", "graphql-tools": "^6.0.18", + "handlebars": "^4.7.6", + "hbs": "^4.1.1", + "nodemailer": "^6.4.11", "passport": "^0.4.1", "passport-jwt": "^4.0.0", "pg": "^8.3.0", diff --git a/src/app.module.ts b/src/app.module.ts index 20a460ae..25e68092 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,6 +17,7 @@ import { Route } from './crags/entities/route.entity'; import { Area } from './crags/entities/area.entity'; import { Book } from './crags/entities/book.entity'; import { Grade } from './crags/entities/grade.entity'; +import { NotificationModule } from './notification/notification.module'; @Module({ imports: [ @@ -41,6 +42,7 @@ import { Grade } from './crags/entities/grade.entity'; UsersModule, CragsModule, AuditModule, + NotificationModule, ], controllers: [], providers: [], diff --git a/src/crags/services/crags.service.ts b/src/crags/services/crags.service.ts index d5270e47..617d07c9 100644 --- a/src/crags/services/crags.service.ts +++ b/src/crags/services/crags.service.ts @@ -123,7 +123,6 @@ export class CragsService { .addOrderBy('route.grade', 'DESC') .getOne().then((route) => { if (route != null && route.grade != null) { - console.log(route.id, route.grade); return route.grade; } diff --git a/src/notification/interfaces/mail-options.interface.ts b/src/notification/interfaces/mail-options.interface.ts new file mode 100644 index 00000000..289c7278 --- /dev/null +++ b/src/notification/interfaces/mail-options.interface.ts @@ -0,0 +1,6 @@ +export interface MailOptions { + to: string; + subject: string; + template: string; + templateParams?: any; +} \ No newline at end of file diff --git a/src/notification/notification.module.ts b/src/notification/notification.module.ts new file mode 100644 index 00000000..12db2cb2 --- /dev/null +++ b/src/notification/notification.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { NotificationService } from './services/notification.service'; +import { MailService } from './services/mail.service'; + +@Module({ + imports: [ConfigModule], + providers: [NotificationService, MailService, ConfigService], + exports: [NotificationService] +}) +export class NotificationModule { } diff --git a/src/notification/services/mail.service.spec.ts b/src/notification/services/mail.service.spec.ts new file mode 100644 index 00000000..4297913d --- /dev/null +++ b/src/notification/services/mail.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MailService } from './mail.service'; + +describe('MailService', () => { + let service: MailService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MailService], + }).compile(); + + service = module.get(MailService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/notification/services/mail.service.ts b/src/notification/services/mail.service.ts new file mode 100644 index 00000000..8431ab43 --- /dev/null +++ b/src/notification/services/mail.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +import * as nodemailer from 'nodemailer'; +import Mail from 'nodemailer/lib/mailer'; +import { SentMessageInfo } from 'nodemailer/lib/smtp-transport'; +import { MailOptions } from '../interfaces/mail-options.interface'; +import { readFileSync } from 'fs'; + +import { compile } from 'handlebars'; + +@Injectable() +export class MailService { + + transporter: Mail; + + constructor(configService: ConfigService) { + this.transporter = nodemailer.createTransport({ + host: configService.get("SMTP_HOST"), + port: configService.get("SMTP_PORT"), + secure: true, + auth: { + user: configService.get("SMTP_USERNAME"), + pass: configService.get("SMTP_PASSWORD"), + }, + }); + } + + send(options: MailOptions): Promise { + return this.transporter.sendMail({ + from: '"Plezanje.net" ', + to: options.to, + subject: options.subject, + text: this.render("plain", options.template, options.templateParams), + html: this.render("html", options.template, options.templateParams) + }) + } + + render(type: string, templateName: string, params: any = {}): string { + const template = compile( + readFileSync(__dirname + '/../templates/' + templateName + '.' + type + '.hbs').toString() + ) + + if (type == "plain") { + return template(params); + } + + const layout = compile( + readFileSync(__dirname + '/../templates/layout.html.hbs').toString() + ) + + return layout({ + content: template(params) + }); + } + +} diff --git a/src/notification/services/notification.service.spec.ts b/src/notification/services/notification.service.spec.ts new file mode 100644 index 00000000..65bd59d4 --- /dev/null +++ b/src/notification/services/notification.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NotificationService } from './notification.service'; + +describe('NotificationService', () => { + let service: NotificationService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [NotificationService], + }).compile(); + + service = module.get(NotificationService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/notification/services/notification.service.ts b/src/notification/services/notification.service.ts new file mode 100644 index 00000000..8586165e --- /dev/null +++ b/src/notification/services/notification.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { User } from 'src/users/entities/user.entity'; +import { MailService } from './mail.service'; + +@Injectable() +export class NotificationService { + constructor(private readonly mailService: MailService) { } + + public accountConfirmation(user: User): void { + this.mailService.send({ + to: user.email, + subject: 'Aktivacija računa', + template: 'account-confirmation', + templateParams: { + user: user + } + }); + } +} diff --git a/src/notification/templates/account-confirmation.html.hbs b/src/notification/templates/account-confirmation.html.hbs new file mode 100644 index 00000000..383c219a --- /dev/null +++ b/src/notification/templates/account-confirmation.html.hbs @@ -0,0 +1,14 @@ +

Aktivacija uporabniškega računa

+ +

+ Na portalu plezanje.net ste + ustvarili nov račun, ki ga je pred uporabo + potrebno še aktivirati. +

+

+ To naredite s klikom na spodnjo povezavo: +

+

+ https://www.plezanje.net/aktivacija/{{ user.id }}/{{ user.confirmationToken }} +

\ No newline at end of file diff --git a/src/notification/templates/account-confirmation.plain.hbs b/src/notification/templates/account-confirmation.plain.hbs new file mode 100644 index 00000000..4f7df739 --- /dev/null +++ b/src/notification/templates/account-confirmation.plain.hbs @@ -0,0 +1,8 @@ +Aktivacija uporabniškega računa + +Na portalu plezanje.net ste +ustvarili nov račun, ki ga je pred uporabo +potrebno še aktivirati. + +To naredite s klikom na spodnjo povezavo: +https://www.plezanje.net/aktivacija/{{ user.id }}/{{ user.confirmationToken }} \ No newline at end of file diff --git a/src/notification/templates/layout.html.hbs b/src/notification/templates/layout.html.hbs new file mode 100644 index 00000000..e3be9f38 --- /dev/null +++ b/src/notification/templates/layout.html.hbs @@ -0,0 +1,53 @@ + + + + + + + + + + + + +
+

+ +

+ +
+ {{{ content }}} +
+
+ + + \ No newline at end of file diff --git a/src/users/resolvers/users.resolver.ts b/src/users/resolvers/users.resolver.ts index 585f33d4..8529753b 100644 --- a/src/users/resolvers/users.resolver.ts +++ b/src/users/resolvers/users.resolver.ts @@ -9,13 +9,18 @@ import { AuthService } from '../../auth/services/auth.service'; import { TokenResponse } from '../interfaces/token-response.class'; import { Role } from '../entities/role.entity'; import { Roles } from '../../auth/decorators/roles.decorator'; +import { NotFoundFilter } from 'src/crags/filters/not-found.filter'; +import { UseFilters } from '@nestjs/common'; +import { ConflictFilter } from 'src/crags/filters/conflict.filter'; +import { NotificationService } from 'src/notification/services/notification.service'; @Resolver(() => User) export class UsersResolver { constructor( private usersService: UsersService, - private authService: AuthService + private authService: AuthService, + private notificationService: NotificationService ) { } @Roles('admin') @@ -31,8 +36,13 @@ export class UsersResolver { } @Mutation(() => Boolean) + @UseFilters(ConflictFilter) async register(@Args('input', { type: () => RegisterInput }) input: RegisterInput): Promise { - return this.usersService.register(input); + const user = await this.usersService.register(input); + + this.notificationService.accountConfirmation(user); + + return true; } @Mutation(() => Boolean) @@ -40,6 +50,14 @@ export class UsersResolver { return this.usersService.confirm(input) } + @Mutation(() => Boolean) + @UseFilters(NotFoundFilter) + async recover(@Args('email') email: string): Promise { + const user = await this.usersService.recover(email) + + return false; + } + @Mutation(() => TokenResponse) async login(@Args('input', { type: () => LoginInput }) input: LoginInput): Promise { return this.authService.login(input) diff --git a/src/users/services/users.service.ts b/src/users/services/users.service.ts index 546c8ebb..67241bfa 100644 --- a/src/users/services/users.service.ts +++ b/src/users/services/users.service.ts @@ -41,7 +41,7 @@ export class UsersService { }); } - async register(data: RegisterInput): Promise { + async register(data: RegisterInput): Promise { const user = new User this.usersRepository.merge(user, data); @@ -49,7 +49,7 @@ export class UsersService { user.password = await bcrypt.hash(data.password, 10) user.confirmationToken = randomBytes(20).toString('hex') - return this.usersRepository.save(user).then(() => true) + return this.usersRepository.save(user).then(() => user) } async confirm(data: ConfirmInput): Promise { @@ -69,4 +69,12 @@ export class UsersService { return this.usersRepository.save(user).then(() => true) } + + async recover(email: string): Promise { + const user = await this.usersRepository.findOneOrFail({ email: email }) + + user.passwordToken = randomBytes(20).toString('hex') + + return this.usersRepository.save(user).then(() => user) + } } diff --git a/src/users/users.module.ts b/src/users/users.module.ts index b9036265..7931f0bf 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -5,9 +5,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AuthModule } from '../auth/auth.module'; import { User } from './entities/user.entity'; import { Role } from './entities/role.entity'; +import { NotificationModule } from 'src/notification/notification.module'; @Module({ - imports: [TypeOrmModule.forFeature([User, Role]), forwardRef(() => AuthModule)], + imports: [TypeOrmModule.forFeature([User, Role]), forwardRef(() => AuthModule), NotificationModule], providers: [UsersService, UsersResolver], exports: [UsersService] }) diff --git a/tsconfig.json b/tsconfig.json index bf10a239..e75ef106 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,4 +12,4 @@ "baseUrl": "./", "incremental": true } -} +} \ No newline at end of file