From 396e8f8b402f94ef4fbd9f8cf7f7aae8fc2c5175 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 10 Nov 2022 16:56:20 +0000 Subject: [PATCH] fix email --- Accounts/src/App.tsx | 4 + Common/Types/Email/EmailMessage.ts | 11 +++ .../MailServer.ts => Email/EmailServer.ts} | 2 +- Common/Types/Email/EmailSubjects.ts | 26 ------ Common/Types/Email/EmailTemplateType.ts | 13 +-- Common/Types/Mail/Mail.ts | 11 --- CommonServer/Config.ts | 27 ++++++ CommonServer/Services/MailService.ts | 8 +- Identity/API/AuthenticationAPI.ts | 82 +++++++++++-------- Mail/API/Mail.ts | 10 +-- Mail/Services/MailService.ts | 51 +++++++----- ...t_password_body.hbs => ForgotPassword.hbs} | 2 +- config.tpl.env | 5 ++ 13 files changed, 137 insertions(+), 115 deletions(-) create mode 100644 Common/Types/Email/EmailMessage.ts rename Common/Types/{Mail/MailServer.ts => Email/EmailServer.ts} (86%) delete mode 100644 Common/Types/Email/EmailSubjects.ts delete mode 100644 Common/Types/Mail/Mail.ts rename Mail/Templates/{forgot_password_body.hbs => ForgotPassword.hbs} (99%) diff --git a/Accounts/src/App.tsx b/Accounts/src/App.tsx index ea87eb032d..fc90f796d0 100644 --- a/Accounts/src/App.tsx +++ b/Accounts/src/App.tsx @@ -35,6 +35,10 @@ function App(): ReactElement { path="/accounts/forgot-password" element={} /> + } + /> } /> } /> ; + body?: string; +} diff --git a/Common/Types/Mail/MailServer.ts b/Common/Types/Email/EmailServer.ts similarity index 86% rename from Common/Types/Mail/MailServer.ts rename to Common/Types/Email/EmailServer.ts index 38e95ea8a0..271b53ce23 100644 --- a/Common/Types/Mail/MailServer.ts +++ b/Common/Types/Email/EmailServer.ts @@ -2,7 +2,7 @@ import Email from '../Email'; import Port from '../Port'; import Hostname from '../API/Hostname'; -export default interface MailServer { +export default interface EmailServer { host: Hostname; port: Port; username: string; diff --git a/Common/Types/Email/EmailSubjects.ts b/Common/Types/Email/EmailSubjects.ts deleted file mode 100644 index 930962a3a0..0000000000 --- a/Common/Types/Email/EmailSubjects.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Dictionary from '../Dictionary'; -import BadOperationException from '../Exception/BadOperationException'; -import EmailTemplateType from './EmailTemplateType'; - -class EmailSubjects { - private subjectMap: Dictionary = {}; - - public constructor() { - this.subjectMap[EmailTemplateType.SIGNUP_WELCOME_EMAIL] = - 'Welcome to OneUptime.'; - this.subjectMap[EmailTemplateType.SIGNUP_VERIFICATION_EMAIL] = - 'Welcome to OneUptime. Please verify your email.'; - } - - public getSubjectByType(emailTemplateType: EmailTemplateType): string { - if (this.subjectMap[emailTemplateType]) { - return this.subjectMap[emailTemplateType] as string; - } - - throw new BadOperationException( - `Subject for ${emailTemplateType} not found.` - ); - } -} - -export default new EmailSubjects(); diff --git a/Common/Types/Email/EmailTemplateType.ts b/Common/Types/Email/EmailTemplateType.ts index 8c01a237d7..5a5689fc34 100644 --- a/Common/Types/Email/EmailTemplateType.ts +++ b/Common/Types/Email/EmailTemplateType.ts @@ -1,15 +1,6 @@ enum EmailTemplateType { - SIGNUP_WELCOME_EMAIL = 'SignupWelcomeEmail', - SIGNUP_VERIFICATION_EMAIL = 'SignupVerificationEmail', - SUBSCRIBER_INCIDENT_CREATED = 'Subscriber Incident Created', - SUBSCRIBER_INCIDENT_ACKNOWLEDGED = 'Subscriber Incident Acknowledged', - SUBSCRIBER_INCIDENT_RESOLVED = 'Subscriber Incident Resolved', - INVESTIGATION_NOTE_CREATED = 'Investigation Note Created', - SUBSCRIBER_SCHEDULED_MAINTENANCE_CREATED = 'Subscriber Scheduled Maintenance Created', - SUBSCRIBER_SCHEDULED_MAINTENANCE_NOTE_CREATED = 'Subscriber Scheduled Maintenance Note Created', - SUBSCRIBER_SCHEDULED_MAINTENANCE_RESOLVED = 'Subscriber Scheduled Maintenance Resolved', - SUBSCRIBER_SCHEDULED_MAINTENANCE_CANCELLED = 'Subscriber Scheduled Maintenance Cancelled', - SUBSCRIBER_ANNOUNCEMENT_NOTIFICATION_CREATED = 'Subscriber Announcement Notification Created', + ForgotPassword = "ForgotPassword.hbs", + WelomeEmail="WelcomeEmail.hbs" } export default EmailTemplateType; diff --git a/Common/Types/Mail/Mail.ts b/Common/Types/Mail/Mail.ts deleted file mode 100644 index 372a3ac8e4..0000000000 --- a/Common/Types/Mail/Mail.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Email from '../Email'; -import Dictionary from '../Dictionary'; -import EmailTemplateType from '../Email/EmailTemplateType'; - -export default interface Mail { - toEmail: Email; - subject: string; - templateType: EmailTemplateType; - vars: Dictionary; - body: string; -} diff --git a/CommonServer/Config.ts b/CommonServer/Config.ts index 23388cc121..423a58bba7 100644 --- a/CommonServer/Config.ts +++ b/CommonServer/Config.ts @@ -2,6 +2,7 @@ import Protocol from 'Common/Types/API/Protocol'; import ObjectID from 'Common/Types/ObjectID'; import Port from 'Common/Types/Port'; import Hostname from 'Common/Types/API/Hostname'; +import Route from 'Common/Types/API/Route'; export const DisableSignup: boolean = process.env['DISABLE_SIGNUP'] === 'true'; @@ -33,6 +34,10 @@ export const ClusterKey: ObjectID = new ObjectID( process.env['ONEUPTIME_SECRET'] || '' ); +export const Domain: Hostname = Hostname.fromString( + process.env['DOMAIN'] || '' +); + export const RealtimeHostname: Hostname = Hostname.fromString( process.env['REALTIME_HOSTNAME'] || '' ); @@ -79,3 +84,25 @@ export const HttpProtocol: Protocol = ( export const RedisHostname: string = process.env['REDIS_HOST'] || ''; export const RedisPassword: string = process.env['REDIS_PASSWORD'] || ''; export const RedisPort: Port = new Port(process.env['REDIS_PORT'] || ''); + + +export const DashboardApiRoute: Route = new Route(process.env['DASHBOARD_API_ROUTE'] || ''); + +export const IdentityRoute: Route = new Route(process.env['IDENTITY_ROUTE'] || ''); + +export const FileRoute: Route = new Route(process.env['FILE_ROUTE'] || ''); + +export const StausPageRoute: Route = new Route(process.env['STATUS_PAGE_ROUTE'] || ''); + +export const DashboardRoute: Route = new Route(process.env['DASHBOARD_ROUTE'] || ''); + +export const IntegrationRoute: Route = new Route(process.env['INTEGRATION_ROUTE'] || ''); + +export const HelmRoute: Route = new Route(process.env['HELMCHART_ROUTE'] || ''); +export const AccountsRoute: Route = new Route(process.env['ACCOUNTS_ROUTE'] || ''); + +export const ApiDocsRoute: Route = new Route(process.env['APIDOCS_ROUTE'] || ''); + +export const AdminDashboardRoute: Route = new Route( + process.env['ADMINDASHBOARD_ROUTE'] || '' +); diff --git a/CommonServer/Services/MailService.ts b/CommonServer/Services/MailService.ts index 41311115c3..03052ad7dd 100644 --- a/CommonServer/Services/MailService.ts +++ b/CommonServer/Services/MailService.ts @@ -5,13 +5,13 @@ import URL from 'Common/Types/API/URL'; import { JSONObject } from 'Common/Types/JSON'; import API from 'Common/Utils/API'; import { ClusterKey, HttpProtocol, MailHostname } from '../Config'; -import Mail from 'Common/Types/Mail/Mail'; -import MailServer from 'Common/Types/Mail/MailServer'; +import Email from 'Common/Types/Email/EmailMessage'; +import EmailServer from 'Common/Types/Email/EmailServer'; export default class MailService { public static async sendMail( - mail: Mail, - mailServer?: MailServer + mail: Email, + mailServer?: EmailServer ): Promise> { const body: JSONObject = { ...mail, diff --git a/Identity/API/AuthenticationAPI.ts b/Identity/API/AuthenticationAPI.ts index 205636a81b..51bf329278 100644 --- a/Identity/API/AuthenticationAPI.ts +++ b/Identity/API/AuthenticationAPI.ts @@ -1,11 +1,12 @@ import { - AccountsHostname, - DashboardHostname, DisableSignup, HomeHostname, HttpProtocol, IsSaaSService, EncryptionSecret, + Domain, + AccountsRoute, + DashboardRoute, } from 'CommonServer/Config'; import Express, { ExpressRequest, @@ -23,13 +24,13 @@ import EmailVerificationToken from 'Model/Models/EmailVerificationToken'; import BadDataException from 'Common/Types/Exception/BadDataException'; import MailService from 'CommonServer/Services/MailService'; import EmailTemplateType from 'Common/Types/Email/EmailTemplateType'; -import EmailSubjects from 'Common/Types/Email/EmailSubjects'; import URL from 'Common/Types/API/URL'; import Response from 'CommonServer/Utils/Response'; import JSONWebToken from 'CommonServer/Utils/JsonWebToken'; import OneUptimeDate from 'Common/Types/Date'; import PositiveNumber from 'Common/Types/PositiveNumber'; import BaseModel from 'Common/Models/BaseModel'; +import Route from 'Common/Types/API/Route'; const router: ExpressRouter = Express.getRouter(); @@ -85,7 +86,7 @@ router.post( emailVerificationToken && user && alreadySavedUser?.id?.toString() === - emailVerificationToken?.userId?.toString() + emailVerificationToken?.userId?.toString() ) { user.isEmailVerified = true; } @@ -122,37 +123,31 @@ router.post( if (alreadySavedUser) { // Send Welcome Mail - await MailService.sendMail( - user.email!, - EmailSubjects.getSubjectByType( - EmailTemplateType.SIGNUP_WELCOME_EMAIL - ), - EmailTemplateType.SIGNUP_WELCOME_EMAIL, - { + await MailService.sendMail({ + toEmail: user.email!, + subject: "Welcome to OneUptime.", + templateType: EmailTemplateType.WelomeEmail, + vars: { name: user.name!.toString(), dashboardUrl: new URL( HttpProtocol, - DashboardHostname + Domain, DashboardRoute ).toString(), homeUrl: new URL(HttpProtocol, HomeHostname).toString(), } - ); + }); } else { // Send EmailVerification Link because this is a new user. - await MailService.sendMail( - user.email!, - EmailSubjects.getSubjectByType( - EmailTemplateType.SIGNUP_WELCOME_EMAIL - ), - EmailTemplateType.SIGNUP_WELCOME_EMAIL, - { + MailService.sendMail({ + toEmail: user.email!, + subject: 'Welcome to OneUptime. Please verify your email.', + templateType: EmailTemplateType.WelomeEmail, + vars: { name: user.name!.toString(), - emailVerificationUrl: new URL( - HttpProtocol, - AccountsHostname - ).toString(), + emailVerificationUrl: new URL(HttpProtocol, Domain, new Route(AccountsRoute.toString()).addRoute("/reset-password/")).toString(), homeUrl: new URL(HttpProtocol, HomeHostname).toString(), } + } ); } @@ -187,10 +182,8 @@ router.post( const user: User = User.fromJSON(data as JSONObject, User) as User; - await user.password?.hashValue(EncryptionSecret); - const alreadySavedUser: User | null = await UserService.findOneBy({ - query: { email: user.email!, password: user.password! }, + query: { email: user.email! }, select: { _id: true, password: true, @@ -204,19 +197,38 @@ router.post( }); if (alreadySavedUser) { - const token: string = JSONWebToken.sign( - alreadySavedUser, - OneUptimeDate.getSecondsInDays(new PositiveNumber(30)) - ); - return Response.sendJsonObjectResponse(req, res, { - token: token, - user: BaseModel.toJSON(alreadySavedUser, User), + const token = ObjectID.generate().toString(); + await UserService.updateOneBy({ + query: { + _id: user._id! + }, + data: { + resetPasswordToken: token, + resetPasswordExpires: OneUptimeDate.getOneDayAfter() + }, + props: { + isRoot: true + } }); + + MailService.sendMail({ + toEmail: user.email!, + subject: "Password Reset Request for OneUptime", + templateType: EmailTemplateType.ForgotPassword, + vars: { + homeURL: new URL(HttpProtocol, Domain).toString(), + tokenVerifyUrl: new URL(HttpProtocol, Domain, new Route(AccountsRoute.toString()).addRoute("/reset-password/" + token)).toString(), + } + }); + + return Response.sendEmptyResponse(req, res); } + throw new BadDataException( - 'Invalid login: Email or password does not match.' + `No user is registered with ${user.email?.toString()}` ); + } catch (err) { return next(err); } diff --git a/Mail/API/Mail.ts b/Mail/API/Mail.ts index 18245abe45..150b207e35 100644 --- a/Mail/API/Mail.ts +++ b/Mail/API/Mail.ts @@ -7,12 +7,12 @@ const router: ExpressRouter = Express.getRouter(); import Response from 'CommonServer/Utils/Response'; import ClusterKeyAuthorization from 'CommonServer/Middleware/ClusterKeyAuthorization'; import MailService from '../Services/MailService'; -import Mail from 'Common/Types/Mail/Mail'; +import EmailMessage from 'Common/Types/Email/EmailMessage'; import EmailTemplateType from 'Common/Types/Email/EmailTemplateType'; import { JSONObject } from 'Common/Types/JSON'; import Email from 'Common/Types/Email'; import Dictionary from 'Common/Types/Dictionary'; -import MailServer from 'Common/Types/Mail/MailServer'; +import EmailServer from 'Common/Types/Email/EmailServer'; router.post( '/send', @@ -20,7 +20,7 @@ router.post( async (req: ExpressRequest, res: ExpressResponse) => { const body: JSONObject = req.body; - const mail: Mail = { + const mail: EmailMessage = { templateType: body['template-name'] as EmailTemplateType, toEmail: new Email(body['to-email'] as string), subject: body['subject'] as string, @@ -28,10 +28,10 @@ router.post( body: body['body'] as string || '', }; - let mailServer: MailServer | undefined = undefined; + let mailServer: EmailServer | undefined = undefined; if (hasMailServerSettingsInBody(body)) { - mailServer = MailService.getMailServer(req.body); + mailServer = MailService.getEmailServer(req.body); } await MailService.send( diff --git a/Mail/Services/MailService.ts b/Mail/Services/MailService.ts index 36371b0348..10abd6602c 100755 --- a/Mail/Services/MailService.ts +++ b/Mail/Services/MailService.ts @@ -2,11 +2,11 @@ import nodemailer, { Transporter } from 'nodemailer'; import hbs from 'nodemailer-express-handlebars'; import Handlebars from 'handlebars'; import fsp from 'fs/promises'; -import Mail from 'Common/Types/Mail/Mail'; +import EmailMessage from 'Common/Types/Email/EmailMessage'; import Path from 'path'; import Email from 'Common/Types/Email'; import BadDataException from 'Common/Types/Exception/BadDataException'; -import MailServer from 'Common/Types/Mail/MailServer'; +import EmailServer from 'Common/Types/Email/EmailServer'; import LocalCache from 'CommonServer/Infrastructure/LocalCache'; import OneUptimeDate from 'Common/Types/Date'; import EmailTemplateType from 'Common/Types/Email/EmailTemplateType'; @@ -72,7 +72,7 @@ export default class MailService { return true; } - public static getMailServer(obj : JSONObject): MailServer { + public static getEmailServer(obj : JSONObject): EmailServer { if (!this.isSMTPConfigValid(obj)) { throw new BadDataException("SMTP Config is not valid"); } @@ -89,8 +89,8 @@ export default class MailService { }; } - private static getGlobalSmtpSettings(): MailServer { - return this.getMailServer(process.env); + private static getGlobalSmtpSettings(): EmailServer { + return this.getEmailServer(process.env); } @@ -111,7 +111,7 @@ export default class MailService { Path.resolve( process.cwd(), 'Templates', - `${emailTemplateType}.hbs` + `${emailTemplateType}` ), { encoding: 'utf8', flag: 'r' } ); @@ -136,7 +136,7 @@ export default class MailService { return subjectHandlebars(vars).toString(); } - private static createMailer(mailServer: MailServer): Transporter { + private static createMailer(EmailServer: EmailServer): Transporter { const helpers: Dictionary = { year: OneUptimeDate.getCurrentYear().toString(), }; @@ -154,12 +154,12 @@ export default class MailService { }; const privateMailer: Transporter = nodemailer.createTransport({ - host: mailServer.host.toString(), - port: mailServer.port.toNumber(), - secure: mailServer.secure, + host: EmailServer.host.toString(), + port: EmailServer.port.toNumber(), + secure: EmailServer.secure, auth: { - user: mailServer.username, - pass: mailServer.password, + user: EmailServer.username, + pass: EmailServer.password, }, }); @@ -169,24 +169,33 @@ export default class MailService { } private static async transportMail( - mail: Mail, - mailServer: MailServer + mail: EmailMessage, + EmailServer: EmailServer ): Promise { - const mailer: Transporter = this.createMailer(mailServer); + const mailer: Transporter = this.createMailer(EmailServer); await mailer.sendMail(mail); } public static async send( - mail: Mail, - mailServer?: MailServer + mail: EmailMessage, + EmailServer?: EmailServer ): Promise { - if (!mailServer) { - mailServer = this.getGlobalSmtpSettings(); + if (!EmailServer) { + EmailServer = this.getGlobalSmtpSettings(); } - mail.body = mail.templateType ? await this.compileEmailBody(mail.templateType, mail.vars) : this.compileText(mail.body, mail.vars); + // default vars. + if (!mail.vars) { + mail.vars = {}; + } + + if (!mail.vars['year']) { + mail.vars['year'] = OneUptimeDate.getCurrentYear().toString(); + } + + mail.body = mail.templateType ? await this.compileEmailBody(mail.templateType, mail.vars) : this.compileText(mail.body || '', mail.vars); mail.subject = this.compileText(mail.subject, mail.vars); - await this.transportMail(mail, mailServer); + await this.transportMail(mail, EmailServer); } } diff --git a/Mail/Templates/forgot_password_body.hbs b/Mail/Templates/ForgotPassword.hbs similarity index 99% rename from Mail/Templates/forgot_password_body.hbs rename to Mail/Templates/ForgotPassword.hbs index 036206bba6..face25b812 100755 --- a/Mail/Templates/forgot_password_body.hbs +++ b/Mail/Templates/ForgotPassword.hbs @@ -429,7 +429,7 @@ style="border: 0; margin: 0; padding: 0; color: #000000 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;"> - If you need any help using OneUptime, please send us an email at support@oneuptime.com and let us know. diff --git a/config.tpl.env b/config.tpl.env index f3679a5920..bc8a9ab036 100644 --- a/config.tpl.env +++ b/config.tpl.env @@ -5,6 +5,11 @@ APP_TAG=latest # This will be generated automatically during install. ONEUPTIME_SECRET={{ .Env.ONEUPTIME_SECRET }} +# Which domain is this server hosted on? +DOMAIN=oneuptime.com +# Is this server hosted with a TLS cert. If yes, this should be "https" +HTTP_PROTOCOL=http + # This supports test | production | development | ci. # Development is used for local development. Test is used for insider / beta / staging builds. Production is used for production ready app. ci is for testing in the CI/CD. ENVIRONMENT=production