Browse Source

feat: project user apis

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/5444/head
Pranav C 2 years ago
parent
commit
81295b57ae
  1. 3
      packages/nocodb-nest/src/app.module.ts
  2. 20
      packages/nocodb-nest/src/modules/project-users/project-users.controller.spec.ts
  3. 106
      packages/nocodb-nest/src/modules/project-users/project-users.controller.ts
  4. 9
      packages/nocodb-nest/src/modules/project-users/project-users.module.ts
  5. 18
      packages/nocodb-nest/src/modules/project-users/project-users.service.spec.ts
  6. 329
      packages/nocodb-nest/src/modules/project-users/project-users.service.ts
  7. 70
      packages/nocodb-nest/src/modules/project-users/ui/auth/emailVerify.ts
  8. 108
      packages/nocodb-nest/src/modules/project-users/ui/auth/resetPassword.ts
  9. 171
      packages/nocodb-nest/src/modules/project-users/ui/emailTemplates/forgotPassword.ts
  10. 208
      packages/nocodb-nest/src/modules/project-users/ui/emailTemplates/invite.ts
  11. 207
      packages/nocodb-nest/src/modules/project-users/ui/emailTemplates/verify.ts

3
packages/nocodb-nest/src/app.module.ts

@ -25,9 +25,10 @@ import { GalleriesModule } from './modules/galleries/galleries.module';
import { FormColumnsModule } from './modules/form-columns/form-columns.module'; import { FormColumnsModule } from './modules/form-columns/form-columns.module';
import { GridColumnsModule } from './modules/grid-columns/grid-columns.module'; import { GridColumnsModule } from './modules/grid-columns/grid-columns.module';
import { MapsModule } from './modules/maps/maps.module'; import { MapsModule } from './modules/maps/maps.module';
import { ProjectUsersModule } from './modules/project-users/project-users.module';
@Module({ @Module({
imports: [AuthModule, UsersModule, UtilsModule, ProjectsModule, TablesModule, ViewsModule, FiltersModule, SortsModule, ColumnsModule, ViewColumnsModule, BasesModule, HooksModule, SharedBasesModule, FormsModule, GridsModule, KanbansModule, GalleriesModule, FormColumnsModule, GridColumnsModule, MapsModule], imports: [AuthModule, UsersModule, UtilsModule, ProjectsModule, TablesModule, ViewsModule, FiltersModule, SortsModule, ColumnsModule, ViewColumnsModule, BasesModule, HooksModule, SharedBasesModule, FormsModule, GridsModule, KanbansModule, GalleriesModule, FormColumnsModule, GridColumnsModule, MapsModule, ProjectUsersModule],
controllers: [], controllers: [],
providers: [Connection, MetaService, JwtStrategy, ExtractProjectIdMiddleware], providers: [Connection, MetaService, JwtStrategy, ExtractProjectIdMiddleware],
exports: [Connection, MetaService], exports: [Connection, MetaService],

20
packages/nocodb-nest/src/modules/project-users/project-users.controller.spec.ts

@ -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();
});
});

106
packages/nocodb-nest/src/modules/project-users/project-users.controller.ts

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

9
packages/nocodb-nest/src/modules/project-users/project-users.module.ts

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

18
packages/nocodb-nest/src/modules/project-users/project-users.service.spec.ts

@ -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();
});
});

329
packages/nocodb-nest/src/modules/project-users/project-users.service.ts

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

70
packages/nocodb-nest/src/modules/project-users/ui/auth/emailVerify.ts

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

108
packages/nocodb-nest/src/modules/project-users/ui/auth/resetPassword.ts

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

171
packages/nocodb-nest/src/modules/project-users/ui/emailTemplates/forgotPassword.ts

@ -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">&nbsp;</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">&nbsp;</td>
</tr>
</table>
</body>
</html>
`;

208
packages/nocodb-nest/src/modules/project-users/ui/emailTemplates/invite.ts

@ -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">&nbsp;</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">&nbsp;</td>
</tr>
</table>
</body>
</html>
`;

207
packages/nocodb-nest/src/modules/project-users/ui/emailTemplates/verify.ts

@ -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">&nbsp;</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">&nbsp;</td>
</tr>
</table>
</body>
</html>
`;
Loading…
Cancel
Save