mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Feat/add new user email (#793)
* feat: send email when adding a new user * fix: rename method * fix: create welcome email * fix: update email templates * fix: add name to templates * refactor: reduce database calls to one * fix: alter tests * fix: remove console logs
This commit is contained in:
		
							parent
							
								
									b0e6d8c363
								
							
						
					
					
						commit
						c58612fc8f
					
				| @ -81,6 +81,14 @@ export class ResetTokenStore { | |||||||
|         return rowToResetToken(row); |         return rowToResetToken(row); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async getActiveTokens(): Promise<IResetToken[]> { | ||||||
|  |         const rows = await this.db<IResetTokenTable>(TABLE) | ||||||
|  |             .whereNull('used_at') | ||||||
|  |             .andWhere('expires_at', '>', new Date()); | ||||||
|  | 
 | ||||||
|  |         return rows.map(rowToResetToken); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     async insert(newToken: IResetTokenCreate): Promise<IResetToken> { |     async insert(newToken: IResetTokenCreate): Promise<IResetToken> { | ||||||
|         const [row] = await this.db<IResetTokenTable>(TABLE) |         const [row] = await this.db<IResetTokenTable>(TABLE) | ||||||
|             .insert(newToken) |             .insert(newToken) | ||||||
|  | |||||||
| @ -5,6 +5,8 @@ import { AccessService } from '../../services/access-service'; | |||||||
| import { Logger } from '../../logger'; | import { Logger } from '../../logger'; | ||||||
| import { handleErrors } from './util'; | import { handleErrors } from './util'; | ||||||
| import { IUnleashConfig } from '../../types/option'; | import { IUnleashConfig } from '../../types/option'; | ||||||
|  | import { EmailService, MAIL_ACCEPTED } from '../../services/email-service'; | ||||||
|  | import ResetTokenService from '../../services/reset-token-service'; | ||||||
| 
 | 
 | ||||||
| const getCreatorUsernameOrPassword = req => req.user.username || req.user.email; | const getCreatorUsernameOrPassword = req => req.user.username || req.user.email; | ||||||
| 
 | 
 | ||||||
| @ -15,11 +17,20 @@ export default class UserAdminController extends Controller { | |||||||
| 
 | 
 | ||||||
|     private readonly logger: Logger; |     private readonly logger: Logger; | ||||||
| 
 | 
 | ||||||
|     constructor(config: IUnleashConfig, { userService, accessService }) { |     private emailService: EmailService; | ||||||
|  | 
 | ||||||
|  |     private resetTokenService: ResetTokenService; | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         config: IUnleashConfig, | ||||||
|  |         { userService, accessService, emailService, resetTokenService }, | ||||||
|  |     ) { | ||||||
|         super(config); |         super(config); | ||||||
|         this.userService = userService; |         this.userService = userService; | ||||||
|         this.accessService = accessService; |         this.accessService = accessService; | ||||||
|         this.logger = config.getLogger('routes/user-controller.ts'); |         this.logger = config.getLogger('routes/user-controller.ts'); | ||||||
|  |         this.emailService = emailService; | ||||||
|  |         this.resetTokenService = resetTokenService; | ||||||
| 
 | 
 | ||||||
|         this.get('/', this.getUsers, ADMIN); |         this.get('/', this.getUsers, ADMIN); | ||||||
|         this.get('/search', this.search); |         this.get('/search', this.search); | ||||||
| @ -51,8 +62,14 @@ export default class UserAdminController extends Controller { | |||||||
|         try { |         try { | ||||||
|             const users = await this.userService.getAll(); |             const users = await this.userService.getAll(); | ||||||
|             const rootRoles = await this.accessService.getRootRoles(); |             const rootRoles = await this.accessService.getRootRoles(); | ||||||
|  |             const inviteLinks = await this.resetTokenService.getActiveInvitations(); | ||||||
| 
 | 
 | ||||||
|             res.json({ users, rootRoles }); |             const usersWithInviteLinks = users.map(user => { | ||||||
|  |                 const inviteLink = inviteLinks[user.id] || ''; | ||||||
|  |                 return { ...user, inviteLink }; | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             res.json({ users: usersWithInviteLinks, rootRoles }); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             this.logger.error(error); |             this.logger.error(error); | ||||||
|             res.status(500).send({ msg: 'server errors' }); |             res.status(500).send({ msg: 'server errors' }); | ||||||
| @ -75,15 +92,44 @@ export default class UserAdminController extends Controller { | |||||||
|     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 |     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | ||||||
|     async createUser(req, res): Promise<void> { |     async createUser(req, res): Promise<void> { | ||||||
|         const { username, email, name, rootRole } = req.body; |         const { username, email, name, rootRole } = req.body; | ||||||
|  |         const { user } = req; | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             const user = await this.userService.createUser({ |             const createdUser = await this.userService.createUser({ | ||||||
|                 username, |                 username, | ||||||
|                 email, |                 email, | ||||||
|                 name, |                 name, | ||||||
|                 rootRole: Number(rootRole), |                 rootRole: Number(rootRole), | ||||||
|             }); |             }); | ||||||
|             res.status(201).send({ ...user, rootRole }); | 
 | ||||||
|  |             const inviteLink = await this.resetTokenService.createNewUserUrl( | ||||||
|  |                 createdUser.id, | ||||||
|  |                 user.email, | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             const emailConfigured = this.emailService.configured(); | ||||||
|  |             let sentMetaData = null; | ||||||
|  |             if (emailConfigured) { | ||||||
|  |                 sentMetaData = await this.emailService.sendGettingStartedMail( | ||||||
|  |                     createdUser.name, | ||||||
|  |                     createdUser.email, | ||||||
|  |                     inviteLink.toString(), | ||||||
|  |                 ); | ||||||
|  |             } else { | ||||||
|  |                 this.logger.warn( | ||||||
|  |                     'email was not sent to the user because email configuration is lacking', | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const emailSent = | ||||||
|  |                 sentMetaData?.response.includes(MAIL_ACCEPTED) || false; | ||||||
|  | 
 | ||||||
|  |             res.status(201).send({ | ||||||
|  |                 ...createdUser, | ||||||
|  |                 inviteLink, | ||||||
|  |                 emailSent, | ||||||
|  |                 rootRole, | ||||||
|  |             }); | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             this.logger.warn(e.message); |             this.logger.warn(e.message); | ||||||
|             res.status(400).send([{ msg: e.message }]); |             res.status(400).send([{ msg: e.message }]); | ||||||
|  | |||||||
| @ -33,6 +33,8 @@ export interface IEmailOptions { | |||||||
| const RESET_MAIL_SUBJECT = 'Unleash - Reset your password'; | const RESET_MAIL_SUBJECT = 'Unleash - Reset your password'; | ||||||
| const GETTING_STARTED_SUBJECT = 'Welcome to Unleash'; | const GETTING_STARTED_SUBJECT = 'Welcome to Unleash'; | ||||||
| 
 | 
 | ||||||
|  | export const MAIL_ACCEPTED = '250 Accepted'; | ||||||
|  | 
 | ||||||
| export class EmailService { | export class EmailService { | ||||||
|     private logger: Logger; |     private logger: Logger; | ||||||
| 
 | 
 | ||||||
| @ -66,7 +68,7 @@ export class EmailService { | |||||||
|         recipient: string, |         recipient: string, | ||||||
|         resetLink: string, |         resetLink: string, | ||||||
|     ): Promise<SentMessageInfo> { |     ): Promise<SentMessageInfo> { | ||||||
|         if (this.mailer !== undefined) { |         if (this.configured()) { | ||||||
|             const year = new Date().getFullYear(); |             const year = new Date().getFullYear(); | ||||||
|             const bodyHtml = await this.compileTemplate( |             const bodyHtml = await this.compileTemplate( | ||||||
|                 'reset-password', |                 'reset-password', | ||||||
| @ -108,7 +110,7 @@ export class EmailService { | |||||||
|         recipient: string, |         recipient: string, | ||||||
|         passwordLink: string, |         passwordLink: string, | ||||||
|     ): Promise<SentMessageInfo> { |     ): Promise<SentMessageInfo> { | ||||||
|         if (this.mailer !== undefined) { |         if (this.configured()) { | ||||||
|             const year = new Date().getFullYear(); |             const year = new Date().getFullYear(); | ||||||
|             const context = { passwordLink, name, year }; |             const context = { passwordLink, name, year }; | ||||||
|             const bodyHtml = await this.compileTemplate( |             const bodyHtml = await this.compileTemplate( | ||||||
| @ -174,4 +176,11 @@ export class EmailService { | |||||||
|         } |         } | ||||||
|         throw new NotFoundError('Could not find template'); |         throw new NotFoundError('Could not find template'); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     configured(): boolean { | ||||||
|  |         if (this.sender !== 'not-configured' && this.mailer !== undefined) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -19,6 +19,10 @@ interface IStores { | |||||||
|     userStore: UserStore; |     userStore: UserStore; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | interface IInviteLinks { | ||||||
|  |     [key: string]: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export default class ResetTokenService { | export default class ResetTokenService { | ||||||
|     private store: ResetTokenStore; |     private store: ResetTokenStore; | ||||||
| 
 | 
 | ||||||
| @ -45,6 +49,25 @@ export default class ResetTokenService { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async getActiveInvitations(): Promise<IInviteLinks> { | ||||||
|  |         try { | ||||||
|  |             const tokens = await this.store.getActiveTokens(); | ||||||
|  |             const links = tokens.reduce((acc, token) => { | ||||||
|  |                 const inviteLink = this.getExistingInvitationUrl( | ||||||
|  |                     token, | ||||||
|  |                 ).toString(); | ||||||
|  | 
 | ||||||
|  |                 acc[token.userId] = inviteLink; | ||||||
|  | 
 | ||||||
|  |                 return acc; | ||||||
|  |             }, {}); | ||||||
|  | 
 | ||||||
|  |             return links; | ||||||
|  |         } catch (e) { | ||||||
|  |             return {}; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     async isValid(token: string): Promise<IResetToken> { |     async isValid(token: string): Promise<IResetToken> { | ||||||
|         let t; |         let t; | ||||||
|         try { |         try { | ||||||
| @ -58,6 +81,10 @@ export default class ResetTokenService { | |||||||
|         throw new UsedTokenError(t.usedAt); |         throw new UsedTokenError(t.usedAt); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private getExistingInvitationUrl(token: IResetToken) { | ||||||
|  |         return new URL(`/#/new-user?token=${token.token}`, this.unleashBase); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private async createResetUrl( |     private async createResetUrl( | ||||||
|         forUser: number, |         forUser: number, | ||||||
|         creator: string, |         creator: string, | ||||||
| @ -69,11 +96,6 @@ export default class ResetTokenService { | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async createWelcomeUrl(forUser: number, creator: string): Promise<URL> { |  | ||||||
|         const path = '/#/new-user'; |  | ||||||
|         return this.createResetUrl(forUser, creator, path); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async createResetPasswordUrl( |     async createResetPasswordUrl( | ||||||
|         forUser: number, |         forUser: number, | ||||||
|         creator: string, |         creator: string, | ||||||
| @ -82,6 +104,11 @@ export default class ResetTokenService { | |||||||
|         return this.createResetUrl(forUser, creator, path); |         return this.createResetUrl(forUser, creator, path); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async createNewUserUrl(forUser: number, creator: string): Promise<URL> { | ||||||
|  |         const path = '/#/new-user'; | ||||||
|  |         return this.createResetUrl(forUser, creator, path); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     async createToken( |     async createToken( | ||||||
|         tokenUser: number, |         tokenUser: number, | ||||||
|         creator: string, |         creator: string, | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ export interface IUser { | |||||||
|     name?: string; |     name?: string; | ||||||
|     username?: string; |     username?: string; | ||||||
|     email?: string; |     email?: string; | ||||||
|  |     inviteLink?: string; | ||||||
|     createdAt: Date; |     createdAt: Date; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,21 +1,540 @@ | |||||||
| <!DOCTYPE html> | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | ||||||
| <html xmlns="http://www.w3.org/1999/xhtml"> | <html xmlns="http://www.w3.org/1999/xhtml"> | ||||||
| <head> | 	<head> | ||||||
|     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> |         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | ||||||
|     <meta name="viewport" content="width=device-width" /> |         <title>*|MC:SUBJECT|*</title> | ||||||
|     <title>Welcome to Unleash - {{ name }}</title> |         <style type="text/css"> | ||||||
| </head> | 			/* /\/\/\/\/\/\/\/\/ CLIENT-SPECIFIC STYLES /\/\/\/\/\/\/\/\/ */ | ||||||
| <body> | 			#outlook a{padding:0;} /* Force Outlook to provide a "view in browser" message */ | ||||||
| <header> | 			.ReadMsgBody{width:100%;} .ExternalClass{width:100%;} /* Force Hotmail to display emails at full width */ | ||||||
|     Welcome to Unleash | 			.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;} /* Force Hotmail to display normal line spacing */ | ||||||
| </header> | 			body, table, td, p, a, li, blockquote{-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%;} /* Prevent WebKit and Windows mobile changing default text sizes */ | ||||||
| <section> | 			table, td{mso-table-lspace:0pt; mso-table-rspace:0pt;} /* Remove spacing between tables in Outlook 2007 and up */ | ||||||
|     First step: Setup your password by visiting <a href="{{ passwordLink }}" title="Set Password link">{{ passwordLink }}</a> | 			img{-ms-interpolation-mode:bicubic;} /* Allow smoother rendering of resized image in Internet Explorer */ | ||||||
|     Second step: Visit your instance at {{ unleashUrl }} |  | ||||||
| </section> |  | ||||||
| 
 | 
 | ||||||
| <footer> | 			/* /\/\/\/\/\/\/\/\/ RESET STYLES /\/\/\/\/\/\/\/\/ */ | ||||||
|     © {{ year }} - Unleash | 			body{margin:0; padding:0;} | ||||||
| </footer> | 			img{border:0; height:auto; line-height:100%; outline:none; text-decoration:none;} | ||||||
| </body> | 			table{border-collapse:collapse !important;} | ||||||
|  | 			body, #bodyTable, #bodyCell{height:100% !important; margin:0; padding:0; width:100% !important;} | ||||||
|  | 
 | ||||||
|  | 			/* /\/\/\/\/\/\/\/\/ TEMPLATE STYLES /\/\/\/\/\/\/\/\/ */ | ||||||
|  | 
 | ||||||
|  | 			/* ========== Page Styles ========== */ | ||||||
|  | 
 | ||||||
|  | 			#bodyCell{padding:20px;} | ||||||
|  | 			#templateContainer{width:600px;} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Page | ||||||
|  | 			* @section background style | ||||||
|  | 			* @tip Set the background color and top border for your email. You may want to choose colors that match your company's branding. | ||||||
|  | 			* @theme page | ||||||
|  | 			*/ | ||||||
|  | 			body, #bodyTable{ | ||||||
|  | 				/*@editable*/ background-color:#DEE0E2; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Page | ||||||
|  | 			* @section background style | ||||||
|  | 			* @tip Set the background color and top border for your email. You may want to choose colors that match your company's branding. | ||||||
|  | 			* @theme page | ||||||
|  | 			*/ | ||||||
|  | 			#bodyCell{ | ||||||
|  | 				/*@editable*/ border-top:4px solid #BBBBBB; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Page | ||||||
|  | 			* @section email border | ||||||
|  | 			* @tip Set the border for your email. | ||||||
|  | 			*/ | ||||||
|  | 			#templateContainer{ | ||||||
|  | 				/*@editable*/ border:1px solid #BBBBBB; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Page | ||||||
|  | 			* @section heading 1 | ||||||
|  | 			* @tip Set the styling for all first-level headings in your emails. These should be the largest of your headings. | ||||||
|  | 			* @style heading 1 | ||||||
|  | 			*/ | ||||||
|  | 			h1{ | ||||||
|  | 				/*@editable*/ color:#202020 !important; | ||||||
|  | 				display:block; | ||||||
|  | 				/*@editable*/ font-family:Helvetica; | ||||||
|  | 				/*@editable*/ font-size:26px; | ||||||
|  | 				/*@editable*/ font-style:normal; | ||||||
|  | 				/*@editable*/ font-weight:bold; | ||||||
|  | 				/*@editable*/ line-height:100%; | ||||||
|  | 				/*@editable*/ letter-spacing:normal; | ||||||
|  | 				margin-top:0; | ||||||
|  | 				margin-right:0; | ||||||
|  | 				margin-bottom:10px; | ||||||
|  | 				margin-left:0; | ||||||
|  | 				/*@editable*/ text-align:left; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Page | ||||||
|  | 			* @section heading 2 | ||||||
|  | 			* @tip Set the styling for all second-level headings in your emails. | ||||||
|  | 			* @style heading 2 | ||||||
|  | 			*/ | ||||||
|  | 			h2{ | ||||||
|  | 				/*@editable*/ color:#404040 !important; | ||||||
|  | 				display:block; | ||||||
|  | 				/*@editable*/ font-family:Helvetica; | ||||||
|  | 				/*@editable*/ font-size:20px; | ||||||
|  | 				/*@editable*/ font-style:normal; | ||||||
|  | 				/*@editable*/ font-weight:bold; | ||||||
|  | 				/*@editable*/ line-height:100%; | ||||||
|  | 				/*@editable*/ letter-spacing:normal; | ||||||
|  | 				margin-top:0; | ||||||
|  | 				margin-right:0; | ||||||
|  | 				margin-bottom:10px; | ||||||
|  | 				margin-left:0; | ||||||
|  | 				/*@editable*/ text-align:left; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Page | ||||||
|  | 			* @section heading 3 | ||||||
|  | 			* @tip Set the styling for all third-level headings in your emails. | ||||||
|  | 			* @style heading 3 | ||||||
|  | 			*/ | ||||||
|  | 			h3{ | ||||||
|  | 				/*@editable*/ color:#606060 !important; | ||||||
|  | 				display:block; | ||||||
|  | 				/*@editable*/ font-family:Helvetica; | ||||||
|  | 				/*@editable*/ font-size:16px; | ||||||
|  | 				/*@editable*/ font-style:italic; | ||||||
|  | 				/*@editable*/ font-weight:normal; | ||||||
|  | 				/*@editable*/ line-height:100%; | ||||||
|  | 				/*@editable*/ letter-spacing:normal; | ||||||
|  | 				margin-top:0; | ||||||
|  | 				margin-right:0; | ||||||
|  | 				margin-bottom:10px; | ||||||
|  | 				margin-left:0; | ||||||
|  | 				/*@editable*/ text-align:left; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Page | ||||||
|  | 			* @section heading 4 | ||||||
|  | 			* @tip Set the styling for all fourth-level headings in your emails. These should be the smallest of your headings. | ||||||
|  | 			* @style heading 4 | ||||||
|  | 			*/ | ||||||
|  | 			h4{ | ||||||
|  | 				/*@editable*/ color:#808080 !important; | ||||||
|  | 				display:block; | ||||||
|  | 				/*@editable*/ font-family:Helvetica; | ||||||
|  | 				/*@editable*/ font-size:14px; | ||||||
|  | 				/*@editable*/ font-style:italic; | ||||||
|  | 				/*@editable*/ font-weight:normal; | ||||||
|  | 				/*@editable*/ line-height:100%; | ||||||
|  | 				/*@editable*/ letter-spacing:normal; | ||||||
|  | 				margin-top:0; | ||||||
|  | 				margin-right:0; | ||||||
|  | 				margin-bottom:10px; | ||||||
|  | 				margin-left:0; | ||||||
|  | 				/*@editable*/ text-align:left; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/* ========== Header Styles ========== */ | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Header | ||||||
|  | 			* @section preheader style | ||||||
|  | 			* @tip Set the background color and bottom border for your email's preheader area. | ||||||
|  | 			* @theme header | ||||||
|  | 			*/ | ||||||
|  | 			#templatePreheader{ | ||||||
|  | 				/*@editable*/ background-color:#fff; | ||||||
|  | 				/*@editable*/ border-bottom:1px solid #CCCCCC; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Header | ||||||
|  | 			* @section preheader text | ||||||
|  | 			* @tip Set the styling for your email's preheader text. Choose a size and color that is easy to read. | ||||||
|  | 			*/ | ||||||
|  | 			.preheaderContent{ | ||||||
|  | 				/*@editable*/ color:#808080; | ||||||
|  | 				/*@editable*/ font-family:Helvetica; | ||||||
|  | 				/*@editable*/ font-size:10px; | ||||||
|  | 				/*@editable*/ line-height:125%; | ||||||
|  | 				/*@editable*/ text-align:left; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Header | ||||||
|  | 			* @section preheader link | ||||||
|  | 			* @tip Set the styling for your email's preheader links. Choose a color that helps them stand out from your text. | ||||||
|  | 			*/ | ||||||
|  | 			.preheaderContent a:link, .preheaderContent a:visited, /* Yahoo! Mail Override */ .preheaderContent a .yshortcuts /* Yahoo! Mail Override */{ | ||||||
|  | 				/*@editable*/ color:#fff; | ||||||
|  | 				/*@editable*/ font-weight:normal; | ||||||
|  | 				/*@editable*/ text-decoration:underline; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Header | ||||||
|  | 			* @section header style | ||||||
|  | 			* @tip Set the background color and borders for your email's header area. | ||||||
|  | 			* @theme header | ||||||
|  | 			*/ | ||||||
|  | 			#templateHeader{ | ||||||
|  | 				/*@editable*/ background-color:#fff; | ||||||
|  | 				/*@editable*/ border-top:1px solid #FFFFFF; | ||||||
|  | 				/*@editable*/ border-bottom:1px solid #CCCCCC; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Header | ||||||
|  | 			* @section header text | ||||||
|  | 			* @tip Set the styling for your email's header text. Choose a size and color that is easy to read. | ||||||
|  | 			*/ | ||||||
|  | 			.headerContent{ | ||||||
|  | 				/*@editable*/ color:#505050; | ||||||
|  | 				/*@editable*/ font-family:Helvetica; | ||||||
|  | 				/*@editable*/ font-size:20px; | ||||||
|  | 				/*@editable*/ font-weight:bold; | ||||||
|  | 				/*@editable*/ line-height:100%; | ||||||
|  | 				/*@editable*/ padding-top:0; | ||||||
|  | 				/*@editable*/ padding-right:0; | ||||||
|  | 				/*@editable*/ padding-bottom:0; | ||||||
|  | 				/*@editable*/ padding-left:0; | ||||||
|  | 				/*@editable*/ text-align:left; | ||||||
|  | 				/*@editable*/ vertical-align:middle; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Header | ||||||
|  | 			* @section header link | ||||||
|  | 			* @tip Set the styling for your email's header links. Choose a color that helps them stand out from your text. | ||||||
|  | 			*/ | ||||||
|  | 			.headerContent a:link, .headerContent a:visited, /* Yahoo! Mail Override */ .headerContent a .yshortcuts /* Yahoo! Mail Override */{ | ||||||
|  | 				/*@editable*/ color:#fff; | ||||||
|  | 				/*@editable*/ font-weight:normal; | ||||||
|  | 				/*@editable*/ text-decoration:underline; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			#headerImage{ | ||||||
|  | 				height:auto; | ||||||
|  | 				max-width:600px; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/* ========== Body Styles ========== */ | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Body | ||||||
|  | 			* @section body style | ||||||
|  | 			* @tip Set the background color and borders for your email's body area. | ||||||
|  | 			*/ | ||||||
|  | 			#templateBody{ | ||||||
|  | 				/*@editable*/ background-color:#fff; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Body | ||||||
|  | 			* @section body text | ||||||
|  | 			* @tip Set the styling for your email's main content text. Choose a size and color that is easy to read. | ||||||
|  | 			* @theme main | ||||||
|  | 			*/ | ||||||
|  | 			.bodyContent{ | ||||||
|  | 				/*@editable*/ color:#505050; | ||||||
|  | 				/*@editable*/ font-family:Helvetica; | ||||||
|  | 				/*@editable*/ font-size:14px; | ||||||
|  | 				/*@editable*/ line-height:150%; | ||||||
|  |                 /*@editable*/ border-bottom: 1px solid #CCCCCC; | ||||||
|  | 				padding-top:20px; | ||||||
|  | 				padding-right:20px; | ||||||
|  | 				padding-bottom:20px; | ||||||
|  | 				padding-left:20px; | ||||||
|  | 				/*@editable*/ text-align:left; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  | 			.bodyContent img{ | ||||||
|  | 				display:inline; | ||||||
|  | 				height:auto; | ||||||
|  | 				max-width:560px; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  |             .resetPasswordLink { | ||||||
|  |                 text-decoration: none; | ||||||
|  |                 color: #fff; | ||||||
|  |                 background-color: #607d8b; | ||||||
|  |                 border-radius: 25px; | ||||||
|  |                 padding: 0.75rem; | ||||||
|  |                 text-align: center; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             .resetPasswordLink:hover { | ||||||
|  |                 text-decoration: none; | ||||||
|  |                 color: #fff; | ||||||
|  |             }  | ||||||
|  | 
 | ||||||
|  | 			/* ========== Footer Styles ========== */ | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Footer | ||||||
|  | 			* @section footer style | ||||||
|  | 			* @tip Set the background color and borders for your email's footer area. | ||||||
|  | 			* @theme footer | ||||||
|  | 			*/ | ||||||
|  | 			#templateFooter{ | ||||||
|  | 				/*@editable*/ background-color:#fff; | ||||||
|  | 				/*@editable*/ border-top:1px solid #FFFFFF; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Footer | ||||||
|  | 			* @section footer text | ||||||
|  | 			* @tip Set the styling for your email's footer text. Choose a size and color that is easy to read. | ||||||
|  | 			* @theme footer | ||||||
|  | 			*/ | ||||||
|  | 			.footerContent{ | ||||||
|  | 				/*@editable*/ color:#808080; | ||||||
|  | 				/*@editable*/ font-family:Helvetica; | ||||||
|  | 				/*@editable*/ font-size:10px; | ||||||
|  | 				/*@editable*/ line-height:150%; | ||||||
|  | 				padding-top:20px; | ||||||
|  | 				padding-right:20px; | ||||||
|  | 				padding-bottom:20px; | ||||||
|  | 				padding-left:20px; | ||||||
|  | 				/*@editable*/ text-align:left; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/** | ||||||
|  | 			* @tab Footer | ||||||
|  | 			* @section footer link | ||||||
|  | 			* @tip Set the styling for your email's footer links. Choose a color that helps them stand out from your text. | ||||||
|  | 			*/ | ||||||
|  | 			.footerContent a:link, .footerContent a:visited, /* Yahoo! Mail Override */ .footerContent a .yshortcuts, .footerContent a span /* Yahoo! Mail Override */{ | ||||||
|  | 				/*@editable*/ color:#606060; | ||||||
|  | 				/*@editable*/ font-weight:normal; | ||||||
|  | 				/*@editable*/ text-decoration:underline; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/* /\/\/\/\/\/\/\/\/ MOBILE STYLES /\/\/\/\/\/\/\/\/ */ | ||||||
|  | 
 | ||||||
|  |             @media only screen and (max-width: 480px){ | ||||||
|  | 				/* /\/\/\/\/\/\/ CLIENT-SPECIFIC MOBILE STYLES /\/\/\/\/\/\/ */ | ||||||
|  | 				body, table, td, p, a, li, blockquote{-webkit-text-size-adjust:none !important;} /* Prevent Webkit platforms from changing default text sizes */ | ||||||
|  |                 body{width:100% !important; min-width:100% !important;} /* Prevent iOS Mail from adding padding to the body */ | ||||||
|  | 
 | ||||||
|  | 				/* /\/\/\/\/\/\/ MOBILE RESET STYLES /\/\/\/\/\/\/ */ | ||||||
|  | 				#bodyCell{padding:10px !important;} | ||||||
|  | 
 | ||||||
|  | 				/* /\/\/\/\/\/\/ MOBILE TEMPLATE STYLES /\/\/\/\/\/\/ */ | ||||||
|  | 
 | ||||||
|  | 				/* ======== Page Styles ======== */ | ||||||
|  | 
 | ||||||
|  | 				/** | ||||||
|  | 				* @tab Mobile Styles | ||||||
|  | 				* @section template width | ||||||
|  | 				* @tip Make the template fluid for portrait or landscape view adaptability. If a fluid layout doesn't work for you, set the width to 300px instead. | ||||||
|  | 				*/ | ||||||
|  | 				#templateContainer{ | ||||||
|  | 					max-width:600px !important; | ||||||
|  | 					/*@editable*/ width:100% !important; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				/** | ||||||
|  | 				* @tab Mobile Styles | ||||||
|  | 				* @section heading 1 | ||||||
|  | 				* @tip Make the first-level headings larger in size for better readability on small screens. | ||||||
|  | 				*/ | ||||||
|  | 				h1{ | ||||||
|  | 					/*@editable*/ font-size:24px !important; | ||||||
|  | 					/*@editable*/ line-height:100% !important; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				/** | ||||||
|  | 				* @tab Mobile Styles | ||||||
|  | 				* @section heading 2 | ||||||
|  | 				* @tip Make the second-level headings larger in size for better readability on small screens. | ||||||
|  | 				*/ | ||||||
|  | 				h2{ | ||||||
|  | 					/*@editable*/ font-size:20px !important; | ||||||
|  | 					/*@editable*/ line-height:100% !important; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				/** | ||||||
|  | 				* @tab Mobile Styles | ||||||
|  | 				* @section heading 3 | ||||||
|  | 				* @tip Make the third-level headings larger in size for better readability on small screens. | ||||||
|  | 				*/ | ||||||
|  | 				h3{ | ||||||
|  | 					/*@editable*/ font-size:18px !important; | ||||||
|  | 					/*@editable*/ line-height:100% !important; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				/** | ||||||
|  | 				* @tab Mobile Styles | ||||||
|  | 				* @section heading 4 | ||||||
|  | 				* @tip Make the fourth-level headings larger in size for better readability on small screens. | ||||||
|  | 				*/ | ||||||
|  | 				h4{ | ||||||
|  | 					/*@editable*/ font-size:16px !important; | ||||||
|  | 					/*@editable*/ line-height:100% !important; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				/* ======== Header Styles ======== */ | ||||||
|  | 
 | ||||||
|  | 				#templatePreheader{display:none !important;} /* Hide the template preheader to save space */ | ||||||
|  | 
 | ||||||
|  | 				/** | ||||||
|  | 				* @tab Mobile Styles | ||||||
|  | 				* @section header image | ||||||
|  | 				* @tip Make the main header image fluid for portrait or landscape view adaptability, and set the image's original width as the max-width. If a fluid setting doesn't work, set the image width to half its original size instead. | ||||||
|  | 				*/ | ||||||
|  | 				#headerImage{ | ||||||
|  | 					height:auto !important; | ||||||
|  | 					/*@editable*/ max-width:600px !important; | ||||||
|  | 					/*@editable*/ width:100% !important; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				/** | ||||||
|  | 				* @tab Mobile Styles | ||||||
|  | 				* @section header text | ||||||
|  | 				* @tip Make the header content text larger in size for better readability on small screens. We recommend a font size of at least 16px. | ||||||
|  | 				*/ | ||||||
|  | 				.headerContent{ | ||||||
|  | 					/*@editable*/ font-size:20px !important; | ||||||
|  | 					/*@editable*/ line-height:125% !important; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				/* ======== Body Styles ======== */ | ||||||
|  | 
 | ||||||
|  | 				/** | ||||||
|  | 				* @tab Mobile Styles | ||||||
|  | 				* @section body text | ||||||
|  | 				* @tip Make the body content text larger in size for better readability on small screens. We recommend a font size of at least 16px. | ||||||
|  | 				*/ | ||||||
|  | 				.bodyContent{ | ||||||
|  | 					/*@editable*/ font-size:18px !important; | ||||||
|  | 					/*@editable*/ line-height:125% !important; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				/* ======== Footer Styles ======== */ | ||||||
|  | 
 | ||||||
|  | 				/** | ||||||
|  | 				* @tab Mobile Styles | ||||||
|  | 				* @section footer text | ||||||
|  | 				* @tip Make the body content text larger in size for better readability on small screens. | ||||||
|  | 				*/ | ||||||
|  | 				.footerContent{ | ||||||
|  | 					/*@editable*/ font-size:14px !important; | ||||||
|  | 					/*@editable*/ line-height:115% !important; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				.footerContent a{display:block !important;} /* Place footer social and utility links on their own lines, for easier access */ | ||||||
|  | 			} | ||||||
|  | 		</style> | ||||||
|  |     </head> | ||||||
|  |     <body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0"> | ||||||
|  |     	<center> | ||||||
|  |         	<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable"> | ||||||
|  |             	<tr> | ||||||
|  |                 	<td align="center" valign="top" id="bodyCell"> | ||||||
|  |                     	<!-- BEGIN TEMPLATE // --> | ||||||
|  |                     	<table border="0" cellpadding="0" cellspacing="0" id="templateContainer"> | ||||||
|  |                         	<tr> | ||||||
|  |                             	<td align="center" valign="top"> | ||||||
|  |                                 	<!-- BEGIN PREHEADER // --> | ||||||
|  |                                     <table border="0" cellpadding="0" cellspacing="0" width="100%" id="templatePreheader"> | ||||||
|  |                                         <tr> | ||||||
|  |                                             <td valign="top" class="preheaderContent" style="padding-top:10px; padding-right:20px; padding-bottom:10px; padding-left:20px;" mc:edit="preheader_content00"> | ||||||
|  |                                                 Welcome to Unleash! | ||||||
|  |                                             </td> | ||||||
|  |                                             <!-- *|IFNOT:ARCHIVE_PAGE|* --> | ||||||
|  |                                             <td valign="top" width="180" class="preheaderContent" style="padding-top:10px; padding-right:20px; padding-bottom:10px; padding-left:0;" mc:edit="preheader_content01"> | ||||||
|  |                                                 Email not displaying correctly?<br /><a href="*|ARCHIVE|*" target="_blank">View it in your browser</a>. | ||||||
|  |                                             </td> | ||||||
|  |                                             <!-- *|END:IF|* --> | ||||||
|  |                                         </tr> | ||||||
|  |                                     </table> | ||||||
|  |                                     <!-- // END PREHEADER --> | ||||||
|  |                                 </td> | ||||||
|  |                             </tr> | ||||||
|  |                         	<tr> | ||||||
|  |                             	<td align="center" valign="top"> | ||||||
|  |                                 	<!-- BEGIN HEADER // --> | ||||||
|  |                                     <table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateHeader"> | ||||||
|  |                                         <tr> | ||||||
|  |                                             <td valign="top" class="headerContent"> | ||||||
|  |                                             	<img src="https://www.unleash-hosted.com/wp-content/uploads/2019/12/unleash-hosted-logo-v2-mini-fit.png" style="max-width:600px;" id="headerImage" mc:label="header_image" mc:edit="header_image" mc:allowdesigner mc:allowtext /> | ||||||
|  |                                             </td> | ||||||
|  |                                         </tr> | ||||||
|  |                                     </table> | ||||||
|  |                                     <!-- // END HEADER --> | ||||||
|  |                                 </td> | ||||||
|  |                             </tr> | ||||||
|  |                         	<tr> | ||||||
|  |                             	<td align="center" valign="top"> | ||||||
|  |                                 	<!-- BEGIN BODY // --> | ||||||
|  |                                     <table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateBody"> | ||||||
|  |                                         <tr> | ||||||
|  |                                             <td valign="top" class="bodyContent" mc:edit="body_content"> | ||||||
|  |                                                 <h1>Welcome to Unleash {{ name }}!</h1> | ||||||
|  | 												 | ||||||
|  |                                                 <br /> | ||||||
|  |                                                 You have been invited to your organizations Unleash account. Below is your magic sign-in link. Just click the button and follow the steps provided to update your password and get started using Unleash. | ||||||
|  |                                                 <br /> | ||||||
|  |                                                 <br /> | ||||||
|  | 												<br /> | ||||||
|  |                                                 <a class="resetPasswordLink" href="{{{ passwordLink }}}" target="_blank" rel="noopener noreferrer">Setup account<a/> | ||||||
|  |                                             </td> | ||||||
|  |                                         </tr> | ||||||
|  |                                         <tr> | ||||||
|  |                                             <td valign="top" class="bodyContent" mc:edit="body_content"> | ||||||
|  |                                                 <h2>Useful resources</h2> | ||||||
|  | 
 | ||||||
|  |                                                 <p>Once you are up and running, you might want to take a look at the following resources:</p> | ||||||
|  |                                                 <ul> | ||||||
|  |                                                     <li><a href="https://docs.getunleash.io" target="_blank" rel="noopener noreferrer">Documentation</a></li> | ||||||
|  |                                                     <li><a href="https://github.com/unleash" target="_blank" rel="noopener noreferrer">Code examples</a></li> | ||||||
|  |                                                     <li><a href="https://docs.getunleash.io/docs/user_guide/connect_sdk" target="_blank" rel="noopener noreferrer">SDKs</a></li> | ||||||
|  |                                                 </ul> | ||||||
|  |                                             </td> | ||||||
|  |                                         </tr> | ||||||
|  |                                     </table> | ||||||
|  |                                     <!-- // END BODY --> | ||||||
|  |                                 </td> | ||||||
|  |                             </tr> | ||||||
|  |                         	<tr> | ||||||
|  |                             	<td align="center" valign="top"> | ||||||
|  |                                 	<!-- BEGIN FOOTER // --> | ||||||
|  |                                     <table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateFooter"> | ||||||
|  |                                         <tr> | ||||||
|  |                                             <td valign="top" class="footerContent" mc:edit="footer_content00"> | ||||||
|  |                                                 <a href="https://github.com/Unleash/unleash">Follow us on Github</a> | ||||||
|  |                                             </td> | ||||||
|  |                                         </tr> | ||||||
|  |                                         <tr> | ||||||
|  |                                             <td valign="top" class="footerContent" style="padding-top:0;" mc:edit="footer_content01"> | ||||||
|  |                                                 <em>Copyright © {{ year }} | Unleash | All rights reserved.</em> | ||||||
|  |                                                 <br /> | ||||||
|  |                      | ||||||
|  |                                                 <br /> | ||||||
|  |                                                 <strong>Our mailing address is: team@getunleash.io</strong> | ||||||
|  |                                                 <br /> | ||||||
|  |                      | ||||||
|  |                                             </td> | ||||||
|  |                                         </tr> | ||||||
|  | 
 | ||||||
|  |                                     </table> | ||||||
|  |                                     <!-- // END FOOTER --> | ||||||
|  |                                 </td> | ||||||
|  |                             </tr> | ||||||
|  |                         </table> | ||||||
|  |                         <!-- // END TEMPLATE --> | ||||||
|  |                     </td> | ||||||
|  |                 </tr> | ||||||
|  |             </table> | ||||||
|  |         </center> | ||||||
|  |     </body> | ||||||
| </html> | </html> | ||||||
| @ -0,0 +1,11 @@ | |||||||
|  | Welcome to Unleash {{ name }}! | ||||||
|  | 
 | ||||||
|  | You have been invited to your organizations Unleash account. Below is your magic sign-in link. Just click the button and follow the steps provided to update your password and get started using Unleash. | ||||||
|  | 
 | ||||||
|  | Visit {{{ passwordLink }}} to get started. | ||||||
|  | 
 | ||||||
|  | Useful resources:  | ||||||
|  | 
 | ||||||
|  | - https://docs.getunleash.io | ||||||
|  | - https://github.com/unleash | ||||||
|  | - https://docs.getunleash.io/docs/user_guide/connect_sdk | ||||||
| @ -499,7 +499,7 @@ | |||||||
|                                     <table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateFooter"> |                                     <table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateFooter"> | ||||||
|                                         <tr> |                                         <tr> | ||||||
|                                             <td valign="top" class="footerContent" mc:edit="footer_content00"> |                                             <td valign="top" class="footerContent" mc:edit="footer_content00"> | ||||||
|                                                 <a href="https://github.com/Unleash/unleash">Follow us on Github</a>   <a href="*|FACEBOOK:PROFILEURL|*">Friend on Facebook</a>   <a href="*|FORWARD|*">Forward to Friend</a>  |                                                 <a href="https://github.com/Unleash/unleash">Follow us on Github</a> | ||||||
|                                             </td> |                                             </td> | ||||||
|                                         </tr> |                                         </tr> | ||||||
|                                         <tr> |                                         <tr> | ||||||
|  | |||||||
| @ -218,3 +218,27 @@ test.serial('should search for users', async t => { | |||||||
|             t.true(res.body.some(u => u.email === 'another@mail.com')); |             t.true(res.body.some(u => u.email === 'another@mail.com')); | ||||||
|         }); |         }); | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | test.serial( | ||||||
|  |     'Creates a user and includes inviteLink and emailConfigured', | ||||||
|  |     async t => { | ||||||
|  |         t.plan(5); | ||||||
|  |         const request = await setupApp(stores); | ||||||
|  |         return request | ||||||
|  |             .post('/api/admin/user-admin') | ||||||
|  |             .send({ | ||||||
|  |                 email: 'some@getunelash.ai', | ||||||
|  |                 name: 'Some Name', | ||||||
|  |                 rootRole: editorRole.id, | ||||||
|  |             }) | ||||||
|  |             .set('Content-Type', 'application/json') | ||||||
|  |             .expect(201) | ||||||
|  |             .expect(res => { | ||||||
|  |                 t.is(res.body.email, 'some@getunelash.ai'); | ||||||
|  |                 t.is(res.body.rootRole, editorRole.id); | ||||||
|  |                 t.truthy(res.body.inviteLink); | ||||||
|  |                 t.falsy(res.body.emailSent); | ||||||
|  |                 t.truthy(res.body.id); | ||||||
|  |             }); | ||||||
|  |     }, | ||||||
|  | ); | ||||||
|  | |||||||
| @ -60,7 +60,7 @@ test.serial('Should create a reset link', async t => { | |||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.serial('Should create a welcome link', async t => { | test.serial('Should create a welcome link', async t => { | ||||||
|     const url = await resetTokenService.createWelcomeUrl( |     const url = await resetTokenService.createNewUserUrl( | ||||||
|         userIdToCreateResetFor, |         userIdToCreateResetFor, | ||||||
|         adminUser.username, |         adminUser.username, | ||||||
|     ); |     ); | ||||||
| @ -94,3 +94,15 @@ test.serial('Creating a new token should expire older tokens', async t => { | |||||||
|     const validToken = await resetTokenService.isValid(secondToken.token); |     const validToken = await resetTokenService.isValid(secondToken.token); | ||||||
|     t.is(secondToken.token, validToken.token); |     t.is(secondToken.token, validToken.token); | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | test.serial( | ||||||
|  |     'Retrieving valid invitation links should retrieve an object with userid key and token value', | ||||||
|  |     async t => { | ||||||
|  |         await resetTokenService.createToken(userIdToCreateResetFor, adminUser); | ||||||
|  | 
 | ||||||
|  |         const activeInvitations = await resetTokenService.getActiveInvitations(); | ||||||
|  |         t.true(Object.keys(activeInvitations).length === 1); | ||||||
|  |         t.true(+Object.keys(activeInvitations)[0] === userIdToCreateResetFor); | ||||||
|  |         t.truthy(activeInvitations[userIdToCreateResetFor]); | ||||||
|  |     }, | ||||||
|  | ); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user