mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
11 changed files with 1248 additions and 1 deletions
@ -0,0 +1,20 @@ |
|||||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||||
|
import { ProjectUsersController } from './project-users.controller'; |
||||||
|
import { ProjectUsersService } from './project-users.service'; |
||||||
|
|
||||||
|
describe('ProjectUsersController', () => { |
||||||
|
let controller: ProjectUsersController; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||||
|
controllers: [ProjectUsersController], |
||||||
|
providers: [ProjectUsersService], |
||||||
|
}).compile(); |
||||||
|
|
||||||
|
controller = module.get<ProjectUsersController>(ProjectUsersController); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should be defined', () => { |
||||||
|
expect(controller).toBeDefined(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,106 @@ |
|||||||
|
import { |
||||||
|
Controller, |
||||||
|
Get, |
||||||
|
Param, |
||||||
|
Post, |
||||||
|
UseGuards, |
||||||
|
Request, |
||||||
|
Body, |
||||||
|
Patch, |
||||||
|
Delete, |
||||||
|
} from '@nestjs/common'; |
||||||
|
import { ProjectUserReqType } from 'nocodb-sdk'; |
||||||
|
import { |
||||||
|
Acl, |
||||||
|
ExtractProjectIdMiddleware, |
||||||
|
} from '../../middlewares/extract-project-id/extract-project-id.middleware'; |
||||||
|
import { ProjectUsersService } from './project-users.service'; |
||||||
|
import { AuthGuard } from '@nestjs/passport'; |
||||||
|
|
||||||
|
@UseGuards(ExtractProjectIdMiddleware, AuthGuard('jwt')) |
||||||
|
@Controller('project-users') |
||||||
|
export class ProjectUsersController { |
||||||
|
constructor(private readonly projectUsersService: ProjectUsersService) {} |
||||||
|
|
||||||
|
@Get('/api/v1/db/meta/projects/:projectId/users') |
||||||
|
@Acl('userList') |
||||||
|
async userList(@Param('projectId') projectId: string, @Request() req) { |
||||||
|
return { |
||||||
|
users: await this.projectUsersService.userList({ |
||||||
|
projectId, |
||||||
|
query: req.query, |
||||||
|
}), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
@Post('/api/v1/db/meta/projects/:projectId/users') |
||||||
|
@Acl('userInvite') |
||||||
|
async userInvite( |
||||||
|
@Param('projectId') projectId: string, |
||||||
|
@Request() req, |
||||||
|
@Body() body: ProjectUserReqType, |
||||||
|
): Promise<any> { |
||||||
|
return await this.projectUsersService.userInvite({ |
||||||
|
projectId, |
||||||
|
projectUser: body, |
||||||
|
req, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Patch('/api/v1/db/meta/projects/:projectId/users/:userId') |
||||||
|
async projectUserUpdate( |
||||||
|
@Param('projectId') projectId: string, |
||||||
|
@Param('userId') userId: string, |
||||||
|
@Request() req, |
||||||
|
@Body() |
||||||
|
body: ProjectUserReqType & { |
||||||
|
project_id: string; |
||||||
|
}, |
||||||
|
): Promise<any> { |
||||||
|
await this.projectUsersService.projectUserUpdate({ |
||||||
|
projectUser: body, |
||||||
|
projectId, |
||||||
|
userId, |
||||||
|
req, |
||||||
|
}); |
||||||
|
return { |
||||||
|
msg: 'The user has been updated successfully', |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
@Delete('/api/v1/db/meta/projects/:projectId/users/:userId') |
||||||
|
@Acl('projectUserDelete') |
||||||
|
async projectUserDelete( |
||||||
|
@Param('projectId') projectId: string, |
||||||
|
@Param('userId') userId: string, |
||||||
|
@Request() req, |
||||||
|
): Promise<any> { |
||||||
|
await this.projectUsersService.projectUserDelete({ |
||||||
|
projectId, |
||||||
|
userId, |
||||||
|
req, |
||||||
|
}); |
||||||
|
return { |
||||||
|
msg: 'The user has been deleted successfully', |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
@Post('/api/v1/db/meta/projects/:projectId/users/:userId/resend-invite') |
||||||
|
@Acl('projectUserInviteResend') |
||||||
|
async projectUserInviteResend( |
||||||
|
@Param('projectId') projectId: string, |
||||||
|
@Param('userId') userId: string, |
||||||
|
@Request() req, |
||||||
|
@Body() body: ProjectUserReqType, |
||||||
|
): Promise<any> { |
||||||
|
await this.projectUsersService.projectUserInviteResend({ |
||||||
|
projectId: projectId, |
||||||
|
userId: userId, |
||||||
|
projectUser: body, |
||||||
|
req, |
||||||
|
}); |
||||||
|
return { |
||||||
|
msg: 'The invitation has been sent to the user', |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
import { Module } from '@nestjs/common'; |
||||||
|
import { ProjectUsersService } from './project-users.service'; |
||||||
|
import { ProjectUsersController } from './project-users.controller'; |
||||||
|
|
||||||
|
@Module({ |
||||||
|
controllers: [ProjectUsersController], |
||||||
|
providers: [ProjectUsersService] |
||||||
|
}) |
||||||
|
export class ProjectUsersModule {} |
@ -0,0 +1,18 @@ |
|||||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||||
|
import { ProjectUsersService } from './project-users.service'; |
||||||
|
|
||||||
|
describe('ProjectUsersService', () => { |
||||||
|
let service: ProjectUsersService; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||||
|
providers: [ProjectUsersService], |
||||||
|
}).compile(); |
||||||
|
|
||||||
|
service = module.get<ProjectUsersService>(ProjectUsersService); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should be defined', () => { |
||||||
|
expect(service).toBeDefined(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,329 @@ |
|||||||
|
import { Injectable } from '@nestjs/common'; |
||||||
|
import { OrgUserRoles, PluginCategory, ProjectUserReqType } from 'nocodb-sdk'; |
||||||
|
import NocoCache from '../../cache/NocoCache'; |
||||||
|
import { validatePayload } from '../../helpers'; |
||||||
|
import { NcError } from '../../helpers/catchError'; |
||||||
|
import NcPluginMgrv2 from '../../helpers/NcPluginMgrv2'; |
||||||
|
import { PagedResponseImpl } from '../../helpers/PagedResponse'; |
||||||
|
import { randomTokenString } from '../../helpers/stringHelpers'; |
||||||
|
import { Audit, ProjectUser, User } from '../../models'; |
||||||
|
import { T } from 'nc-help'; |
||||||
|
import { v4 as uuidv4 } from 'uuid'; |
||||||
|
import * as ejs from 'ejs'; |
||||||
|
|
||||||
|
import validator from 'validator'; |
||||||
|
import Noco from '../../Noco'; |
||||||
|
import { CacheGetType, CacheScope, MetaTable } from '../../utils/globals'; |
||||||
|
|
||||||
|
@Injectable() |
||||||
|
export class ProjectUsersService { |
||||||
|
async userList(param: { projectId: string; query: any }) { |
||||||
|
return new PagedResponseImpl( |
||||||
|
await ProjectUser.getUsersList({ |
||||||
|
...param.query, |
||||||
|
project_id: param.projectId, |
||||||
|
}), |
||||||
|
{ |
||||||
|
...param.query, |
||||||
|
count: await ProjectUser.getUsersCount(param.query), |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
async userInvite(param: { |
||||||
|
projectId: string; |
||||||
|
projectUser: ProjectUserReqType; |
||||||
|
req: any; |
||||||
|
}): Promise<any> { |
||||||
|
validatePayload( |
||||||
|
'swagger.json#/components/schemas/ProjectUserReq', |
||||||
|
param.projectUser, |
||||||
|
); |
||||||
|
|
||||||
|
const emails = (param.projectUser.email || '') |
||||||
|
.toLowerCase() |
||||||
|
.split(/\s*,\s*/) |
||||||
|
.map((v) => v.trim()); |
||||||
|
|
||||||
|
// check for invalid emails
|
||||||
|
const invalidEmails = emails.filter((v) => !validator.isEmail(v)); |
||||||
|
if (!emails.length) { |
||||||
|
return NcError.badRequest('Invalid email address'); |
||||||
|
} |
||||||
|
if (invalidEmails.length) { |
||||||
|
NcError.badRequest('Invalid email address : ' + invalidEmails.join(', ')); |
||||||
|
} |
||||||
|
|
||||||
|
const invite_token = uuidv4(); |
||||||
|
const error = []; |
||||||
|
|
||||||
|
for (const email of emails) { |
||||||
|
// add user to project if user already exist
|
||||||
|
const user = await User.getByEmail(email); |
||||||
|
|
||||||
|
if (user) { |
||||||
|
// check if this user has been added to this project
|
||||||
|
const projectUser = await ProjectUser.get(param.projectId, user.id); |
||||||
|
if (projectUser) { |
||||||
|
NcError.badRequest( |
||||||
|
`${user.email} with role ${projectUser.roles} already exists in this project`, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
await ProjectUser.insert({ |
||||||
|
project_id: param.projectId, |
||||||
|
fk_user_id: user.id, |
||||||
|
roles: param.projectUser.roles || 'editor', |
||||||
|
}); |
||||||
|
|
||||||
|
const cachedUser = await NocoCache.get( |
||||||
|
`${CacheScope.USER}:${email}___${param.projectId}`, |
||||||
|
CacheGetType.TYPE_OBJECT, |
||||||
|
); |
||||||
|
|
||||||
|
if (cachedUser) { |
||||||
|
cachedUser.roles = param.projectUser.roles || 'editor'; |
||||||
|
await NocoCache.set( |
||||||
|
`${CacheScope.USER}:${email}___${param.projectId}`, |
||||||
|
cachedUser, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
await Audit.insert({ |
||||||
|
project_id: param.projectId, |
||||||
|
op_type: 'AUTHENTICATION', |
||||||
|
op_sub_type: 'INVITE', |
||||||
|
user: param.req.user.email, |
||||||
|
description: `invited ${email} to ${param.projectId} project `, |
||||||
|
ip: param.req.clientIp, |
||||||
|
}); |
||||||
|
} else { |
||||||
|
try { |
||||||
|
// create new user with invite token
|
||||||
|
const { id } = await User.insert({ |
||||||
|
invite_token, |
||||||
|
invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), |
||||||
|
email, |
||||||
|
roles: OrgUserRoles.VIEWER, |
||||||
|
token_version: randomTokenString(), |
||||||
|
}); |
||||||
|
|
||||||
|
// add user to project
|
||||||
|
await ProjectUser.insert({ |
||||||
|
project_id: param.projectId, |
||||||
|
fk_user_id: id, |
||||||
|
roles: param.projectUser.roles, |
||||||
|
}); |
||||||
|
|
||||||
|
const count = await User.count(); |
||||||
|
T.emit('evt', { evt_type: 'project:invite', count }); |
||||||
|
|
||||||
|
await Audit.insert({ |
||||||
|
project_id: param.projectId, |
||||||
|
op_type: 'AUTHENTICATION', |
||||||
|
op_sub_type: 'INVITE', |
||||||
|
user: param.req.user.email, |
||||||
|
description: `invited ${email} to ${param.projectId} project `, |
||||||
|
ip: param.req.clientIp, |
||||||
|
}); |
||||||
|
// in case of single user check for smtp failure
|
||||||
|
// and send back token if failed
|
||||||
|
if ( |
||||||
|
emails.length === 1 && |
||||||
|
!(await this.sendInviteEmail(email, invite_token, param.req)) |
||||||
|
) { |
||||||
|
return { invite_token, email }; |
||||||
|
} else { |
||||||
|
this.sendInviteEmail(email, invite_token, param.req); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
console.log(e); |
||||||
|
if (emails.length === 1) { |
||||||
|
throw e; |
||||||
|
} else { |
||||||
|
error.push({ email, error: e.message }); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (emails.length === 1) { |
||||||
|
return { |
||||||
|
msg: 'The user has been invited successfully', |
||||||
|
}; |
||||||
|
} else { |
||||||
|
return { invite_token, emails, error }; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async projectUserUpdate(param: { |
||||||
|
userId: string; |
||||||
|
// todo: update swagger
|
||||||
|
projectUser: ProjectUserReqType & { project_id: string }; |
||||||
|
// todo: refactor
|
||||||
|
req: any; |
||||||
|
projectId: string; |
||||||
|
}): Promise<any> { |
||||||
|
validatePayload( |
||||||
|
'swagger.json#/components/schemas/ProjectUserReq', |
||||||
|
param.projectUser, |
||||||
|
); |
||||||
|
|
||||||
|
// todo: use param.projectId
|
||||||
|
if (!param.projectUser?.project_id) { |
||||||
|
NcError.badRequest('Missing project id in request body.'); |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
param.req.session?.passport?.user?.roles?.owner && |
||||||
|
param.req.session?.passport?.user?.id === param.userId && |
||||||
|
param.projectUser.roles.indexOf('owner') === -1 |
||||||
|
) { |
||||||
|
NcError.badRequest("Super admin can't remove Super role themselves"); |
||||||
|
} |
||||||
|
const user = await User.get(param.userId); |
||||||
|
|
||||||
|
if (!user) { |
||||||
|
NcError.badRequest(`User with id '${param.userId}' doesn't exist`); |
||||||
|
} |
||||||
|
|
||||||
|
// todo: handle roles which contains super
|
||||||
|
if ( |
||||||
|
!param.req.session?.passport?.user?.roles?.owner && |
||||||
|
param.projectUser.roles.indexOf('owner') > -1 |
||||||
|
) { |
||||||
|
NcError.forbidden('Insufficient privilege to add super admin role.'); |
||||||
|
} |
||||||
|
|
||||||
|
await ProjectUser.update( |
||||||
|
param.projectId, |
||||||
|
param.userId, |
||||||
|
param.projectUser.roles, |
||||||
|
); |
||||||
|
|
||||||
|
await Audit.insert({ |
||||||
|
op_type: 'AUTHENTICATION', |
||||||
|
op_sub_type: 'ROLES_MANAGEMENT', |
||||||
|
user: param.req.user.email, |
||||||
|
description: `updated roles for ${user.email} with ${param.projectUser.roles} `, |
||||||
|
ip: param.req.clientIp, |
||||||
|
}); |
||||||
|
|
||||||
|
return { |
||||||
|
msg: 'User has been updated successfully', |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
async projectUserDelete(param: { |
||||||
|
projectId: string; |
||||||
|
userId: string; |
||||||
|
// todo: refactor
|
||||||
|
req: any; |
||||||
|
}): Promise<any> { |
||||||
|
const project_id = param.projectId; |
||||||
|
|
||||||
|
if (param.req.session?.passport?.user?.id === param.userId) { |
||||||
|
NcError.badRequest("Admin can't delete themselves!"); |
||||||
|
} |
||||||
|
|
||||||
|
if (!param.req.session?.passport?.user?.roles?.owner) { |
||||||
|
const user = await User.get(param.userId); |
||||||
|
if (user.roles?.split(',').includes('super')) |
||||||
|
NcError.forbidden( |
||||||
|
'Insufficient privilege to delete a super admin user.', |
||||||
|
); |
||||||
|
|
||||||
|
const projectUser = await ProjectUser.get(project_id, param.userId); |
||||||
|
if (projectUser?.roles?.split(',').includes('super')) |
||||||
|
NcError.forbidden('Insufficient privilege to delete a owner user.'); |
||||||
|
} |
||||||
|
|
||||||
|
await ProjectUser.delete(project_id, param.userId); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
async projectUserInviteResend(param: { |
||||||
|
userId: string; |
||||||
|
projectUser: ProjectUserReqType; |
||||||
|
projectId: string; |
||||||
|
// todo: refactor
|
||||||
|
req: any; |
||||||
|
}): Promise<any> { |
||||||
|
const user = await User.get(param.userId); |
||||||
|
|
||||||
|
if (!user) { |
||||||
|
NcError.badRequest(`User with id '${param.userId}' not found`); |
||||||
|
} |
||||||
|
|
||||||
|
const invite_token = uuidv4(); |
||||||
|
|
||||||
|
await User.update(user.id, { |
||||||
|
invite_token, |
||||||
|
invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), |
||||||
|
}); |
||||||
|
|
||||||
|
const pluginData = await Noco.ncMeta.metaGet2( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.PLUGIN, |
||||||
|
{ |
||||||
|
category: PluginCategory.EMAIL, |
||||||
|
active: true, |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
if (!pluginData) { |
||||||
|
NcError.badRequest( |
||||||
|
`No Email Plugin is found. Please go to App Store to configure first or copy the invitation URL to users instead.`, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
await this.sendInviteEmail(user.email, invite_token, param.req); |
||||||
|
|
||||||
|
await Audit.insert({ |
||||||
|
op_type: 'AUTHENTICATION', |
||||||
|
op_sub_type: 'RESEND_INVITE', |
||||||
|
user: user.email, |
||||||
|
description: `resent a invite to ${user.email} `, |
||||||
|
ip: param.req.clientIp, |
||||||
|
project_id: param.projectId, |
||||||
|
}); |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
// todo: refactor the whole function
|
||||||
|
async sendInviteEmail(email: string, token: string, req: any): Promise<any> { |
||||||
|
try { |
||||||
|
const template = (await import('./ui/emailTemplates/invite')) |
||||||
|
.default; |
||||||
|
|
||||||
|
const emailAdapter = await NcPluginMgrv2.emailAdapter(); |
||||||
|
|
||||||
|
if (emailAdapter) { |
||||||
|
await emailAdapter.mailSend({ |
||||||
|
to: email, |
||||||
|
subject: 'Verify email', |
||||||
|
html: ejs.render(template, { |
||||||
|
signupLink: `${req.ncSiteUrl}${ |
||||||
|
Noco.getConfig()?.dashboardPath |
||||||
|
}#/signup/${token}`,
|
||||||
|
projectName: req.body?.projectName, |
||||||
|
roles: (req.body?.roles || '') |
||||||
|
.split(',') |
||||||
|
.map((r) => r.replace(/^./, (m) => m.toUpperCase())) |
||||||
|
.join(', '), |
||||||
|
adminEmail: req.session?.passport?.user?.email, |
||||||
|
}), |
||||||
|
}); |
||||||
|
return true; |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
console.log( |
||||||
|
'Warning : `mailSend` failed, Please configure emailClient configuration.', |
||||||
|
e.message, |
||||||
|
); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
export default `<!DOCTYPE html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>NocoDB - Verify Email</title> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/fonts.roboto.css" rel="stylesheet"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/materialdesignicons.5.x.min.css" rel="stylesheet"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/vuetify.2.x.min.css" rel="stylesheet"> |
||||||
|
<script src="<%- ncPublicUrl %>/js/vue.2.6.14.min.js"></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="app"> |
||||||
|
<v-app> |
||||||
|
<v-container> |
||||||
|
<v-row class="justify-center"> |
||||||
|
<v-col class="col-12 col-md-6"> |
||||||
|
<v-alert v-if="valid" type="success"> |
||||||
|
Email verified successfully! |
||||||
|
</v-alert> |
||||||
|
<v-alert v-else-if="errMsg" type="error"> |
||||||
|
{{errMsg}} |
||||||
|
</v-alert> |
||||||
|
|
||||||
|
<template v-else> |
||||||
|
|
||||||
|
<v-skeleton-loader type="heading"></v-skeleton-loader> |
||||||
|
|
||||||
|
</template> |
||||||
|
</v-col> |
||||||
|
</v-row> |
||||||
|
</v-container> |
||||||
|
</v-app> |
||||||
|
</div> |
||||||
|
<script src="<%- ncPublicUrl %>/js/vuetify.2.x.min.js"></script> |
||||||
|
<script src="<%- ncPublicUrl %>/js/axios.0.19.2.min.js"></script> |
||||||
|
|
||||||
|
<script> |
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
vuetify: new Vuetify(), |
||||||
|
data: { |
||||||
|
valid: null, |
||||||
|
errMsg: null, |
||||||
|
validForm: false, |
||||||
|
token: <%- token %>, |
||||||
|
greeting: 'Password Reset', |
||||||
|
formdata: { |
||||||
|
password: '', |
||||||
|
newPassword: '' |
||||||
|
}, |
||||||
|
success: false |
||||||
|
}, |
||||||
|
methods: {}, |
||||||
|
async created() { |
||||||
|
try { |
||||||
|
const valid = (await axios.post('<%- baseUrl %>/api/v1/auth/email/validate/' + this.token)).data; |
||||||
|
this.valid = !!valid; |
||||||
|
} catch (e) { |
||||||
|
this.valid = false; |
||||||
|
if(e.response && e.response.data && e.response.data.msg){ |
||||||
|
this.errMsg = e.response.data.msg; |
||||||
|
}else{ |
||||||
|
this.errMsg = 'Some error occurred'; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html>`;
|
@ -0,0 +1,108 @@ |
|||||||
|
export default `<!DOCTYPE html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>NocoDB - Reset Password</title> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/fonts.roboto.css" rel="stylesheet"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/materialdesignicons.5.x.min.css" rel="stylesheet"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/vuetify.2.x.min.css" rel="stylesheet"> |
||||||
|
<script src="<%- ncPublicUrl %>/js/vue.2.6.14.min.js"></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="app"> |
||||||
|
<v-app> |
||||||
|
<v-container> |
||||||
|
<v-row class="justify-center"> |
||||||
|
<v-col class="col-12 col-md-6"> |
||||||
|
<v-alert v-if="success" type="success"> |
||||||
|
Password reset successful! |
||||||
|
</v-alert> |
||||||
|
<template v-else> |
||||||
|
|
||||||
|
<v-form ref="form" v-model="validForm" v-if="valid === true" ref="formType" class="ma-auto" |
||||||
|
lazy-validation> |
||||||
|
|
||||||
|
|
||||||
|
<v-text-field |
||||||
|
name="input-10-2" |
||||||
|
label="New password" |
||||||
|
type="password" |
||||||
|
v-model="formdata.password" |
||||||
|
:rules="[v => !!v || 'Password is required']" |
||||||
|
></v-text-field> |
||||||
|
|
||||||
|
<v-text-field |
||||||
|
name="input-10-2" |
||||||
|
type="password" |
||||||
|
label="Confirm new password" |
||||||
|
v-model="formdata.newPassword" |
||||||
|
:rules="[v => !!v || 'Password is required', v => v === formdata.password || 'Password mismatch']" |
||||||
|
></v-text-field> |
||||||
|
|
||||||
|
<v-btn |
||||||
|
:disabled="!validForm" |
||||||
|
large |
||||||
|
@click="resetPassword" |
||||||
|
> |
||||||
|
RESET PASSWORD |
||||||
|
</v-btn> |
||||||
|
|
||||||
|
</v-form> |
||||||
|
<div v-else-if="valid === false">Not a valid url</div> |
||||||
|
<div v-else> |
||||||
|
<v-skeleton-loader type="actions"></v-skeleton-loader> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</v-col> |
||||||
|
</v-row> |
||||||
|
</v-container> |
||||||
|
</v-app> |
||||||
|
</div> |
||||||
|
<script src="<%- ncPublicUrl %>/js/vuetify.2.x.min.js"></script> |
||||||
|
<script src="<%- ncPublicUrl %>/js/axios.0.19.2.min.js"></script> |
||||||
|
|
||||||
|
<script> |
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
vuetify: new Vuetify(), |
||||||
|
data: { |
||||||
|
valid: null, |
||||||
|
validForm: false, |
||||||
|
token: <%- token %>, |
||||||
|
greeting: 'Password Reset', |
||||||
|
formdata: { |
||||||
|
password: '', |
||||||
|
newPassword: '' |
||||||
|
}, |
||||||
|
success: false |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
async resetPassword() { |
||||||
|
if (this.$refs.form.validate()) { |
||||||
|
try { |
||||||
|
const res = await axios.post('<%- baseUrl %>api/v1/db/auth/password/reset/' + this.token, { |
||||||
|
...this.formdata |
||||||
|
}); |
||||||
|
this.success = true; |
||||||
|
} catch (e) { |
||||||
|
if (e.response && e.response.data && e.response.data.msg) { |
||||||
|
alert('Failed to reset password: ' + e.response.data.msg) |
||||||
|
} else { |
||||||
|
alert('Some error occurred') |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
async created() { |
||||||
|
try { |
||||||
|
const valid = (await axios.post('<%- baseUrl %>api/v1/db/auth/token/validate/' + this.token)).data; |
||||||
|
this.valid = !!valid; |
||||||
|
} catch (e) { |
||||||
|
this.valid = false; |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html>`;
|
@ -0,0 +1,171 @@ |
|||||||
|
export default `<!doctype html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta name="viewport" content="width=device-width"> |
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
||||||
|
<title>Simple Transactional Email</title> |
||||||
|
<style> |
||||||
|
@media only screen and (max-width: 620px) { |
||||||
|
table[class=body] h1 { |
||||||
|
font-size: 28px !important; |
||||||
|
margin-bottom: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] p, |
||||||
|
table[class=body] ul, |
||||||
|
table[class=body] ol, |
||||||
|
table[class=body] td, |
||||||
|
table[class=body] span, |
||||||
|
table[class=body] a { |
||||||
|
font-size: 16px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .wrapper, |
||||||
|
table[class=body] .article { |
||||||
|
padding: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .content { |
||||||
|
padding: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .container { |
||||||
|
padding: 0 !important; |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .main { |
||||||
|
border-left-width: 0 !important; |
||||||
|
border-radius: 0 !important; |
||||||
|
border-right-width: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn table { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn a { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .img-responsive { |
||||||
|
height: auto !important; |
||||||
|
max-width: 100% !important; |
||||||
|
width: auto !important; |
||||||
|
} |
||||||
|
} |
||||||
|
@media all { |
||||||
|
.ExternalClass { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.ExternalClass, |
||||||
|
.ExternalClass p, |
||||||
|
.ExternalClass span, |
||||||
|
.ExternalClass font, |
||||||
|
.ExternalClass td, |
||||||
|
.ExternalClass div { |
||||||
|
line-height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.apple-link a { |
||||||
|
color: inherit !important; |
||||||
|
font-family: inherit !important; |
||||||
|
font-size: inherit !important; |
||||||
|
font-weight: inherit !important; |
||||||
|
line-height: inherit !important; |
||||||
|
text-decoration: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
#MessageViewBody a { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
font-size: inherit; |
||||||
|
font-family: inherit; |
||||||
|
font-weight: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary table td:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary a:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
border-color: #34495e !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"> |
||||||
|
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;" width="100%" bgcolor="#f6f6f6"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" width="580" valign="top"> |
||||||
|
<div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;"> |
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER --> |
||||||
|
<table role="presentation" class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" width="100%"> |
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA --> |
||||||
|
<tr> |
||||||
|
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">Hi,</p> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">To change your NocoDB account password click the following link.</p> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" width="100%"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" valign="top" align="center" bgcolor="#1088ff"> <a href="<%- resetLink %>" target="_blank" style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Reset Password</a> </td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">Thanks regards NocoDB.</p> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA --> |
||||||
|
</table> |
||||||
|
<!-- END CENTERED WHITE CONTAINER --> |
||||||
|
|
||||||
|
<!-- START FOOTER --> |
||||||
|
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%"> |
||||||
|
<tr> |
||||||
|
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" valign="top" align="center"> |
||||||
|
<span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;"></span> |
||||||
|
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" valign="top" align="center"> |
||||||
|
<a href="http://nocodb.com/">NocoDB</a> |
||||||
|
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<!-- END FOOTER --> |
||||||
|
|
||||||
|
</div> |
||||||
|
</td> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
`;
|
@ -0,0 +1,208 @@ |
|||||||
|
export default `<!doctype html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta name="viewport" content="width=device-width"> |
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
||||||
|
<title>Simple Transactional Email</title> |
||||||
|
<style> |
||||||
|
@media only screen and (max-width: 620px) { |
||||||
|
table[class=body] h1 { |
||||||
|
font-size: 28px !important; |
||||||
|
margin-bottom: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] p, |
||||||
|
table[class=body] ul, |
||||||
|
table[class=body] ol, |
||||||
|
table[class=body] td, |
||||||
|
table[class=body] span, |
||||||
|
table[class=body] a { |
||||||
|
font-size: 16px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .wrapper, |
||||||
|
table[class=body] .article { |
||||||
|
padding: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .content { |
||||||
|
padding: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .container { |
||||||
|
padding: 0 !important; |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .main { |
||||||
|
border-left-width: 0 !important; |
||||||
|
border-radius: 0 !important; |
||||||
|
border-right-width: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn table { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn a { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .img-responsive { |
||||||
|
height: auto !important; |
||||||
|
max-width: 100% !important; |
||||||
|
width: auto !important; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@media all { |
||||||
|
.ExternalClass { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.ExternalClass, |
||||||
|
.ExternalClass p, |
||||||
|
.ExternalClass span, |
||||||
|
.ExternalClass font, |
||||||
|
.ExternalClass td, |
||||||
|
.ExternalClass div { |
||||||
|
line-height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.apple-link a { |
||||||
|
color: inherit !important; |
||||||
|
font-family: inherit !important; |
||||||
|
font-size: inherit !important; |
||||||
|
font-weight: inherit !important; |
||||||
|
line-height: inherit !important; |
||||||
|
text-decoration: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
#MessageViewBody a { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
font-size: inherit; |
||||||
|
font-family: inherit; |
||||||
|
font-weight: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary table td:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary a:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
border-color: #34495e !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body class="" |
||||||
|
style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"> |
||||||
|
<span class="preheader" |
||||||
|
style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;" |
||||||
|
width="100%" bgcolor="#f6f6f6"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
<td class="container" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" |
||||||
|
width="580" valign="top"> |
||||||
|
<div class="content" |
||||||
|
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;"> |
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER --> |
||||||
|
<table role="presentation" class="main" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA --> |
||||||
|
<tr> |
||||||
|
<td class="wrapper" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" |
||||||
|
valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" |
||||||
|
valign="top"> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Hi,</p> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
I invited you to be "<%- roles -%>" of the NocoDB project "<%- projectName %>". |
||||||
|
Click the button below to to accept my invitation.</p> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
class="btn btn-primary" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td align="left" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" |
||||||
|
valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" |
||||||
|
cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" |
||||||
|
valign="top" align="center" bgcolor="#1088ff"><a |
||||||
|
href="<%- signupLink %>" target="_blank" |
||||||
|
style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Signup</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Thanks regards <%- adminEmail %>.</p> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA --> |
||||||
|
</table> |
||||||
|
<!-- END CENTERED WHITE CONTAINER --> |
||||||
|
|
||||||
|
<!-- START FOOTER --> |
||||||
|
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tr> |
||||||
|
<td class="content-block" |
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
||||||
|
valign="top" align="center"> |
||||||
|
<span class="apple-link" |
||||||
|
style="color: #999999; font-size: 12px; text-align: center;"></span> |
||||||
|
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<td class="content-block powered-by" |
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
||||||
|
valign="top" align="center"> |
||||||
|
<a href="http://nocodb.com/">NocoDB</a> |
||||||
|
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<!-- END FOOTER --> |
||||||
|
|
||||||
|
</div> |
||||||
|
</td> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
`;
|
@ -0,0 +1,207 @@ |
|||||||
|
export default `<!doctype html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta name="viewport" content="width=device-width"> |
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
||||||
|
<title>Simple Transactional Email</title> |
||||||
|
<style> |
||||||
|
@media only screen and (max-width: 620px) { |
||||||
|
table[class=body] h1 { |
||||||
|
font-size: 28px !important; |
||||||
|
margin-bottom: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] p, |
||||||
|
table[class=body] ul, |
||||||
|
table[class=body] ol, |
||||||
|
table[class=body] td, |
||||||
|
table[class=body] span, |
||||||
|
table[class=body] a { |
||||||
|
font-size: 16px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .wrapper, |
||||||
|
table[class=body] .article { |
||||||
|
padding: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .content { |
||||||
|
padding: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .container { |
||||||
|
padding: 0 !important; |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .main { |
||||||
|
border-left-width: 0 !important; |
||||||
|
border-radius: 0 !important; |
||||||
|
border-right-width: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn table { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn a { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .img-responsive { |
||||||
|
height: auto !important; |
||||||
|
max-width: 100% !important; |
||||||
|
width: auto !important; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@media all { |
||||||
|
.ExternalClass { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.ExternalClass, |
||||||
|
.ExternalClass p, |
||||||
|
.ExternalClass span, |
||||||
|
.ExternalClass font, |
||||||
|
.ExternalClass td, |
||||||
|
.ExternalClass div { |
||||||
|
line-height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.apple-link a { |
||||||
|
color: inherit !important; |
||||||
|
font-family: inherit !important; |
||||||
|
font-size: inherit !important; |
||||||
|
font-weight: inherit !important; |
||||||
|
line-height: inherit !important; |
||||||
|
text-decoration: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
#MessageViewBody a { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
font-size: inherit; |
||||||
|
font-family: inherit; |
||||||
|
font-weight: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary table td:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary a:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
border-color: #34495e !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body class="" |
||||||
|
style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"> |
||||||
|
<span class="preheader" |
||||||
|
style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;" |
||||||
|
width="100%" bgcolor="#f6f6f6"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
<td class="container" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" |
||||||
|
width="580" valign="top"> |
||||||
|
<div class="content" |
||||||
|
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;"> |
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER --> |
||||||
|
<table role="presentation" class="main" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA --> |
||||||
|
<tr> |
||||||
|
<td class="wrapper" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" |
||||||
|
valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" |
||||||
|
valign="top"> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Hi,</p> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Please verify your email address by clicking the following button.</p> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
class="btn btn-primary" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td align="left" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" |
||||||
|
valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" |
||||||
|
cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" |
||||||
|
valign="top" align="center" bgcolor="#1088ff"><a |
||||||
|
href="<%- verifyLink %>" target="_blank" |
||||||
|
style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Verify</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Thanks regards NocoDB.</p> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA --> |
||||||
|
</table> |
||||||
|
<!-- END CENTERED WHITE CONTAINER --> |
||||||
|
|
||||||
|
<!-- START FOOTER --> |
||||||
|
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tr> |
||||||
|
<td class="content-block" |
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
||||||
|
valign="top" align="center"> |
||||||
|
<span class="apple-link" |
||||||
|
style="color: #999999; font-size: 12px; text-align: center;"></span> |
||||||
|
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<td class="content-block powered-by" |
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
||||||
|
valign="top" align="center"> |
||||||
|
<a href="http://nocodb.com/">NocoDB</a> |
||||||
|
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<!-- END FOOTER --> |
||||||
|
|
||||||
|
</div> |
||||||
|
</td> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
`;
|
Loading…
Reference in new issue