Browse Source

fix: corrections

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/5239/head
Pranav C 2 years ago
parent
commit
4e67fd6128
  1. 2
      packages/nocodb/src/lib/Noco.ts
  2. 20
      packages/nocodb/src/lib/controllers/dataApis/helpers.ts
  3. 2
      packages/nocodb/src/lib/controllers/dataApis/index.ts
  4. 2
      packages/nocodb/src/lib/controllers/dataController/dataAliasNestedApis.ts
  5. 2
      packages/nocodb/src/lib/controllers/projectUserController.ts
  6. 2
      packages/nocodb/src/lib/controllers/publicApis/publicDataApis.ts
  7. 0
      packages/nocodb/src/lib/controllers/swagger/helpers/getPaths.ts
  8. 0
      packages/nocodb/src/lib/controllers/swagger/helpers/getSchemas.ts
  9. 0
      packages/nocodb/src/lib/controllers/swagger/helpers/getSwaggerColumnMetas.ts
  10. 0
      packages/nocodb/src/lib/controllers/swagger/helpers/getSwaggerJSON.ts
  11. 0
      packages/nocodb/src/lib/controllers/swagger/helpers/templates/headers.ts
  12. 0
      packages/nocodb/src/lib/controllers/swagger/helpers/templates/params.ts
  13. 93
      packages/nocodb/src/lib/controllers/swagger/redocHtml.ts
  14. 61
      packages/nocodb/src/lib/controllers/swagger/swaggerApis.ts
  15. 87
      packages/nocodb/src/lib/controllers/swagger/swaggerHtml.ts
  16. 222
      packages/nocodb/src/lib/controllers/sync/helpers/EntityMap.ts
  17. 6
      packages/nocodb/src/lib/controllers/sync/helpers/NocoSyncDestAdapter.ts
  18. 7
      packages/nocodb/src/lib/controllers/sync/helpers/NocoSyncSourceAdapter.ts
  19. 238
      packages/nocodb/src/lib/controllers/sync/helpers/fetchAT.ts
  20. 2435
      packages/nocodb/src/lib/controllers/sync/helpers/job.ts
  21. 338
      packages/nocodb/src/lib/controllers/sync/helpers/readAndProcessData.ts
  22. 31
      packages/nocodb/src/lib/controllers/sync/helpers/syncMap.ts
  23. 138
      packages/nocodb/src/lib/controllers/sync/importApis.ts
  24. 69
      packages/nocodb/src/lib/controllers/sync/syncSourceApis.ts
  25. 12
      packages/nocodb/src/lib/controllers/tableController.ts
  26. 23
      packages/nocodb/src/lib/controllers/userApi/helpers.ts
  27. 1
      packages/nocodb/src/lib/controllers/userApi/index.ts
  28. 283
      packages/nocodb/src/lib/controllers/userApi/initAdminFromEnv.ts
  29. 336
      packages/nocodb/src/lib/controllers/userApi/initStrategies.ts
  30. 70
      packages/nocodb/src/lib/controllers/userApi/ui/auth/emailVerify.ts
  31. 108
      packages/nocodb/src/lib/controllers/userApi/ui/auth/resetPassword.ts
  32. 171
      packages/nocodb/src/lib/controllers/userApi/ui/emailTemplates/forgotPassword.ts
  33. 208
      packages/nocodb/src/lib/controllers/userApi/ui/emailTemplates/invite.ts
  34. 207
      packages/nocodb/src/lib/controllers/userApi/ui/emailTemplates/verify.ts
  35. 12
      packages/nocodb/src/lib/meta/api/index.ts
  36. 3
      packages/nocodb/src/lib/services/attachmentService.ts
  37. 2
      packages/nocodb/src/lib/services/projectUserService.ts
  38. 109
      packages/nocodb/src/lib/services/tableService.ts
  39. 2
      packages/nocodb/src/lib/services/userService/helpers.ts

2
packages/nocodb/src/lib/Noco.ts

@ -45,7 +45,7 @@ import User from './models/User';
import * as http from 'http'; import * as http from 'http';
import weAreHiring from './utils/weAreHiring'; import weAreHiring from './utils/weAreHiring';
import getInstance from './utils/getInstance'; import getInstance from './utils/getInstance';
import initAdminFromEnv from './controllers/userApi/initAdminFromEnv'; import initAdminFromEnv from './services/userService/initAdminFromEnv';
const log = debug('nc:app'); const log = debug('nc:app');
require('dotenv').config(); require('dotenv').config();

20
packages/nocodb/src/lib/controllers/dataApis/helpers.ts

@ -1,19 +1,19 @@
import Project from '../../../models/Project'; import { NcError } from '../../meta/helpers/catchError';
import Model from '../../../models/Model'; import Project from '../../models/Project';
import View from '../../../models/View'; import Model from '../../models/Model';
import { NcError } from '../../helpers/catchError'; import View from '../../models/View';
import { Request } from 'express'; import { Request } from 'express';
import Base from '../../../models/Base'; import Base from '../../models/Base';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import { isSystemColumn, UITypes } from 'nocodb-sdk'; import { isSystemColumn, UITypes } from 'nocodb-sdk';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import Column from '../../../models/Column'; import Column from '../../models/Column';
import LookupColumn from '../../../models/LookupColumn'; import LookupColumn from '../../models/LookupColumn';
import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import papaparse from 'papaparse'; import papaparse from 'papaparse';
import { dataService } from '../../../services'; import { dataService } from '../../services';
export async function getViewAndModelFromRequestByAliasOrId( export async function getViewAndModelFromRequestByAliasOrId(
req: req:
| Request<{ projectName: string; tableName: string; viewName?: string }> | Request<{ projectName: string; tableName: string; viewName?: string }>

2
packages/nocodb/src/lib/controllers/dataApis/index.ts

@ -2,7 +2,7 @@ import dataApis from './dataApis';
import oldDataApis from './oldDataApis'; import oldDataApis from './oldDataApis';
import dataAliasApis from './dataAliasApis'; import dataAliasApis from './dataAliasApis';
import bulkDataAliasApis from './bulkDataAliasApis'; import bulkDataAliasApis from './bulkDataAliasApis';
import dataAliasNestedApis from '../../../controllers/dataController/dataAliasNestedApis'; import dataAliasNestedApis from '../../controllers/dataController/dataAliasNestedApis';
import dataAliasExportApis from './dataAliasExportApis'; import dataAliasExportApis from './dataAliasExportApis';
export { export {

2
packages/nocodb/src/lib/controllers/dataController/dataAliasNestedApis.ts

@ -7,7 +7,7 @@ import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import { import {
getColumnByIdOrName, getColumnByIdOrName,
getViewAndModelFromRequestByAliasOrId, getViewAndModelFromRequestByAliasOrId,
} from '../../meta/api/dataApis/helpers'; } from '../dataApis/helpers';
import { NcError } from '../../meta/helpers/catchError'; import { NcError } from '../../meta/helpers/catchError';
import apiMetrics from '../../meta/helpers/apiMetrics'; import apiMetrics from '../../meta/helpers/apiMetrics';

2
packages/nocodb/src/lib/controllers/projectUserController.ts

@ -270,7 +270,7 @@ export async function sendInviteEmail(
req: any req: any
): Promise<any> { ): Promise<any> {
try { try {
const template = (await import('./userApi/ui/emailTemplates/invite')) const template = (await import('./userController/ui/emailTemplates/invite'))
.default; .default;
const emailAdapter = await NcPluginMgrv2.emailAdapter(); const emailAdapter = await NcPluginMgrv2.emailAdapter();

2
packages/nocodb/src/lib/controllers/publicApis/publicDataApis.ts

@ -15,7 +15,7 @@ import path from 'path';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { mimeIcons } from '../../utils/mimeTypes'; import { mimeIcons } from '../../utils/mimeTypes';
import slash from 'slash'; import slash from 'slash';
import { sanitizeUrlPath } from '../attachmentController'; import { sanitizeUrlPath } from '../../services/attachmentService';
import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst'; import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst';
import { getColumnByIdOrName } from '../dataApis/helpers'; import { getColumnByIdOrName } from '../dataApis/helpers';
import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants'; import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants';

0
packages/nocodb/src/lib/controllers/swagger/helpers/getPaths.ts

0
packages/nocodb/src/lib/controllers/swagger/helpers/getSchemas.ts

0
packages/nocodb/src/lib/controllers/swagger/helpers/getSwaggerColumnMetas.ts

0
packages/nocodb/src/lib/controllers/swagger/helpers/getSwaggerJSON.ts

0
packages/nocodb/src/lib/controllers/swagger/helpers/templates/headers.ts

0
packages/nocodb/src/lib/controllers/swagger/helpers/templates/params.ts

93
packages/nocodb/src/lib/controllers/swagger/redocHtml.ts

@ -1,93 +0,0 @@
export default ({
ncSiteUrl,
}: {
ncSiteUrl: string;
}): string => `<!DOCTYPE html>
<html>
<head>
<title>NocoDB API Documentation</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="${ncSiteUrl}/css/fonts.montserrat.css" rel="stylesheet">
<!--
Redoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="redoc"></div>
<script src="${ncSiteUrl}/js/redoc.standalone.min.js"></script>
<script>
let initialLocalStorage = {}
try {
initialLocalStorage = JSON.parse(localStorage.getItem('nocodb-gui-v2') || '{}');
} catch (e) {
console.error('Failed to parse local storage', e);
}
const xhttp = new XMLHttpRequest();
xhttp.open("GET", "./swagger.json");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.setRequestHeader("xc-auth", initialLocalStorage && initialLocalStorage.token);
xhttp.onload = function () {
const swaggerJson = this.responseText;
const swagger = JSON.parse(swaggerJson);
Redoc.init(swagger, {
scrollYOffset: 50
}, document.getElementById('redoc'))
};
xhttp.send();
</script>
<script>
console.log('%c🚀 We are Hiring!!! 🚀%c\\n%cJoin the forces http://careers.nocodb.com', 'color:#1348ba;font-size:3rem;padding:20px;', 'display:none', 'font-size:1.5rem;padding:20px')
const linkEl = document.createElement('a')
linkEl.setAttribute('href', "http://careers.nocodb.com")
linkEl.setAttribute('target', '_blank')
linkEl.setAttribute('class', 'we-are-hiring')
linkEl.innerHTML = '🚀 We are Hiring!!! 🚀'
const styleEl = document.createElement('style');
styleEl.innerHTML = \`
.we-are-hiring {
position: fixed;
bottom: 50px;
right: -250px;
opacity: 0;
background: orange;
border-radius: 4px;
padding: 19px;
z-index: 200;
text-decoration: none;
text-transform: uppercase;
color: black;
transition: 1s opacity, 1s right;
display: block;
font-weight: bold;
}
.we-are-hiring.active {
opacity: 1;
right:25px;
}
@media only screen and (max-width: 600px) {
.we-are-hiring {
display: none;
}
}
\`
document.body.appendChild(linkEl, document.body.firstChild)
document.body.appendChild(styleEl, document.body.firstChild)
setTimeout(() => linkEl.classList.add('active'), 2000)
</script>
</body>
</html>`;

61
packages/nocodb/src/lib/controllers/swagger/swaggerApis.ts

@ -1,61 +0,0 @@
// @ts-ignore
import catchError, { NcError } from '../../meta/helpers/catchError';
import { Router } from 'express';
import Model from '../../models/Model';
import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import getSwaggerJSON from './helpers/getSwaggerJSON';
import Project from '../../models/Project';
import getSwaggerHtml from './swaggerHtml';
import getRedocHtml from './redocHtml';
async function swaggerJson(req, res) {
const project = await Project.get(req.params.projectId);
if (!project) NcError.notFound();
const models = await Model.list({
project_id: req.params.projectId,
base_id: null,
});
const swagger = await getSwaggerJSON(project, models);
swagger.servers = [
{
url: req.ncSiteUrl,
},
{
url: '{customUrl}',
variables: {
customUrl: {
default: req.ncSiteUrl,
description: 'Provide custom nocodb app base url',
},
},
},
] as any;
res.json(swagger);
}
function swaggerHtml(_, res) {
res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
function redocHtml(_, res) {
res.send(getRedocHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
const router = Router({ mergeParams: true });
// todo: auth
router.get(
'/api/v1/db/meta/projects/:projectId/swagger.json',
ncMetaAclMw(swaggerJson, 'swaggerJson')
);
router.get('/api/v1/db/meta/projects/:projectId/swagger', swaggerHtml);
router.get('/api/v1/db/meta/projects/:projectId/redoc', redocHtml);
export default router;

87
packages/nocodb/src/lib/controllers/swagger/swaggerHtml.ts

@ -1,87 +0,0 @@
export default ({
ncSiteUrl,
}: {
ncSiteUrl: string;
}): string => `<!DOCTYPE html>
<html>
<head>
<title>NocoDB : API Docs</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<link rel="shortcut icon" href="${ncSiteUrl}/favicon.ico" />
<link rel="stylesheet" href="${ncSiteUrl}/css/swagger-ui-bundle.4.5.2.min.css"/>
<script src="${ncSiteUrl}/js/swagger-ui-bundle.4.5.2.min.js"></script>
</head>
<body>
<div id="app"></div>
<script>
let initialLocalStorage = {}
try {
initialLocalStorage = JSON.parse(localStorage.getItem('nocodb-gui-v2') || '{}');
} catch (e) {
console.error('Failed to parse local storage', e);
}
var xmlhttp = new XMLHttpRequest(); // new HttpRequest instance
xmlhttp.open("GET", "./swagger.json");
xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xmlhttp.setRequestHeader("xc-auth", initialLocalStorage && initialLocalStorage.token);
xmlhttp.onload = function () {
const ui = SwaggerUIBundle({
// url: ,
spec: JSON.parse(xmlhttp.responseText),
dom_id: '#app',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
})
}
xmlhttp.send();
console.log('%c🚀 We are Hiring!!! 🚀%c\\n%cJoin the forces http://careers.nocodb.com', 'color:#1348ba;font-size:3rem;padding:20px;', 'display:none', 'font-size:1.5rem;padding:20px');
const linkEl = document.createElement('a')
linkEl.setAttribute('href', "http://careers.nocodb.com")
linkEl.setAttribute('target', '_blank')
linkEl.setAttribute('class', 'we-are-hiring')
linkEl.innerHTML = '🚀 We are Hiring!!! 🚀'
const styleEl = document.createElement('style');
styleEl.innerHTML = \`
.we-are-hiring {
position: fixed;
bottom: 50px;
right: -250px;
opacity: 0;
background: orange;
border-radius: 4px;
padding: 19px;
z-index: 200;
text-decoration: none;
text-transform: uppercase;
color: black;
transition: 1s opacity, 1s right;
display: block;
font-weight: bold;
}
.we-are-hiring.active {
opacity: 1;
right:25px;
}
@media only screen and (max-width: 600px) {
.we-are-hiring {
display: none;
}
}
\`
document.body.appendChild(linkEl, document.body.firstChild)
document.body.appendChild(styleEl, document.body.firstChild)
setTimeout(() => linkEl.classList.add('active'), 2000)
</script>
</body>
</html>
`;

222
packages/nocodb/src/lib/controllers/sync/helpers/EntityMap.ts

@ -1,222 +0,0 @@
import sqlite3 from 'sqlite3';
import { Readable } from 'stream';
class EntityMap {
initialized: boolean;
cols: string[];
db: any;
constructor(...args) {
this.initialized = false;
this.cols = args.map((arg) => processKey(arg));
this.db = new Promise((resolve, reject) => {
const db = new sqlite3.Database(':memory:');
const colStatement =
this.cols.length > 0
? this.cols.join(' TEXT, ') + ' TEXT'
: 'mappingPlaceholder TEXT';
db.run(`CREATE TABLE mapping (${colStatement})`, (err) => {
if (err) {
console.log(err);
reject(err);
}
resolve(db);
});
});
}
async init() {
if (!this.initialized) {
this.db = await this.db;
this.initialized = true;
}
}
destroy() {
if (this.initialized && this.db) {
this.db.close();
}
}
async addRow(row) {
if (!this.initialized) {
throw 'Please initialize first!';
}
const cols = Object.keys(row).map((key) => processKey(key));
const colStatement = cols.map((key) => `'${key}'`).join(', ');
const questionMarks = cols.map(() => '?').join(', ');
const promises = [];
for (const col of cols.filter((col) => !this.cols.includes(col))) {
promises.push(
new Promise((resolve, reject) => {
this.db.run(`ALTER TABLE mapping ADD '${col}' TEXT;`, (err) => {
if (err) {
console.log(err);
reject(err);
}
this.cols.push(col);
resolve(true);
});
})
);
}
await Promise.all(promises);
const values = Object.values(row).map((val) => {
if (typeof val === 'object') {
return `JSON::${JSON.stringify(val)}`;
}
return val;
});
return new Promise((resolve, reject) => {
this.db.run(
`INSERT INTO mapping (${colStatement}) VALUES (${questionMarks})`,
values,
(err) => {
if (err) {
console.log(err);
reject(err);
}
resolve(true);
}
);
});
}
getRow(col, val, res = []): Promise<Record<string, any>> {
if (!this.initialized) {
throw 'Please initialize first!';
}
return new Promise((resolve, reject) => {
col = processKey(col);
res = res.map((r) => processKey(r));
this.db.get(
`SELECT ${
res.length ? res.join(', ') : '*'
} FROM mapping WHERE ${col} = ?`,
[val],
(err, rs) => {
if (err) {
console.log(err);
reject(err);
}
if (rs) {
rs = processResponseRow(rs);
}
resolve(rs);
}
);
});
}
getCount(): Promise<number> {
if (!this.initialized) {
throw 'Please initialize first!';
}
return new Promise((resolve, reject) => {
this.db.get(`SELECT COUNT(*) as count FROM mapping`, (err, rs) => {
if (err) {
console.log(err);
reject(err);
}
resolve(rs.count);
});
});
}
getStream(res = []): DBStream {
if (!this.initialized) {
throw 'Please initialize first!';
}
res = res.map((r) => processKey(r));
return new DBStream(
this.db,
`SELECT ${res.length ? res.join(', ') : '*'} FROM mapping`
);
}
getLimit(limit, offset, res = []): Promise<Record<string, any>[]> {
if (!this.initialized) {
throw 'Please initialize first!';
}
return new Promise((resolve, reject) => {
res = res.map((r) => processKey(r));
this.db.all(
`SELECT ${
res.length ? res.join(', ') : '*'
} FROM mapping LIMIT ${limit} OFFSET ${offset}`,
(err, rs) => {
if (err) {
console.log(err);
reject(err);
}
for (let row of rs) {
row = processResponseRow(row);
}
resolve(rs);
}
);
});
}
}
class DBStream extends Readable {
db: any;
stmt: any;
sql: any;
constructor(db, sql) {
super({ objectMode: true });
this.db = db;
this.sql = sql;
this.stmt = this.db.prepare(this.sql);
this.on('end', () => this.stmt.finalize());
}
_read() {
let stream = this;
this.stmt.get(function (err, result) {
if (err) {
stream.emit('error', err);
} else {
if (result) {
result = processResponseRow(result);
}
stream.push(result || null);
}
});
}
}
function processResponseRow(res: any) {
for (const key of Object.keys(res)) {
if (res[key] && res[key].startsWith('JSON::')) {
try {
res[key] = JSON.parse(res[key].replace('JSON::', ''));
} catch (e) {
console.log(e);
}
}
if (revertKey(key) !== key) {
res[revertKey(key)] = res[key];
delete res[key];
}
}
return res;
}
function processKey(key) {
return key.replace(/'/g, "''").replace(/[A-Z]/g, (match) => `_${match}`);
}
function revertKey(key) {
return key.replace(/''/g, "'").replace(/_[A-Z]/g, (match) => match[1]);
}
export default EntityMap;

6
packages/nocodb/src/lib/controllers/sync/helpers/NocoSyncDestAdapter.ts

@ -1,6 +0,0 @@
export abstract class NocoSyncSourceAdapter {
public abstract init(): Promise<void>;
public abstract destProjectWrite(): Promise<any>;
public abstract destSchemaWrite(): Promise<any>;
public abstract destDataWrite(): Promise<any>;
}

7
packages/nocodb/src/lib/controllers/sync/helpers/NocoSyncSourceAdapter.ts

@ -1,7 +0,0 @@
export abstract class NocoSyncSourceAdapter {
public abstract init(): Promise<void>;
public abstract srcSchemaGet(): Promise<any>;
public abstract srcDataLoad(): Promise<any>;
public abstract srcDataListen(): Promise<any>;
public abstract srcDataPoll(): Promise<any>;
}

238
packages/nocodb/src/lib/controllers/sync/helpers/fetchAT.ts

@ -1,238 +0,0 @@
const axios = require('axios').default;
const info: any = {
initialized: false,
};
async function initialize(shareId) {
info.cookie = '';
const url = `https://airtable.com/${shareId}`;
try {
const hreq = await axios
.get(url, {
headers: {
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-language': 'en-US,en;q=0.9',
'sec-ch-ua':
'" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'none',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'User-Agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36',
},
referrerPolicy: 'strict-origin-when-cross-origin',
body: null,
method: 'GET',
})
.then((response) => {
for (const ck of response.headers['set-cookie']) {
info.cookie += ck.split(';')[0] + '; ';
}
return response.data;
})
.catch(() => {
throw {
message:
'Invalid Shared Base ID :: Ensure www.airtable.com/<SharedBaseID> is accessible. Refer https://bit.ly/3x0OdXI for details',
};
});
info.headers = JSON.parse(
hreq.match(/(?<=var headers =)(.*)(?=;)/g)[0].trim()
);
info.link = unicodeToChar(hreq.match(/(?<=fetch\(")(.*)(?=")/g)[0].trim());
info.baseInfo = decodeURIComponent(info.link)
.match(/{(.*)}/g)[0]
.split('&')
.reduce((result, el) => {
try {
return Object.assign(
result,
JSON.parse(el.includes('=') ? el.split('=')[1] : el)
);
} catch (e) {
if (el.includes('=')) {
return Object.assign(result, {
[el.split('=')[0]]: el.split('=')[1],
});
}
}
}, {});
info.baseId = info.baseInfo.applicationId;
info.initialized = true;
} catch (e) {
console.log(e);
info.initialized = false;
if (e.message) {
throw e;
} else {
throw {
message:
'Error processing Shared Base :: Ensure www.airtable.com/<SharedBaseID> is accessible. Refer https://bit.ly/3x0OdXI for details',
};
}
}
}
async function read() {
if (info.initialized) {
const resreq = await axios('https://airtable.com' + info.link, {
headers: {
accept: '*/*',
'accept-language': 'en-US,en;q=0.9',
'sec-ch-ua':
'" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'User-Agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36',
'x-time-zone': 'Europe/Berlin',
cookie: info.cookie,
...info.headers,
},
referrerPolicy: 'no-referrer',
body: null,
method: 'GET',
})
.then((response) => {
return response.data;
})
.catch(() => {
throw {
message:
'Error Reading :: Ensure www.airtable.com/<SharedBaseID> is accessible. Refer https://bit.ly/3x0OdXI for details',
};
});
return {
schema: resreq.data,
baseId: info.baseId,
baseInfo: info.baseInfo,
};
} else {
throw {
message: 'Error Initializing :: please try again !!',
};
}
}
async function readView(viewId) {
if (info.initialized) {
const resreq = await axios(
`https://airtable.com/v0.3/view/${viewId}/readData?` +
`stringifiedObjectParams=${encodeURIComponent('{}')}&requestId=${
info.baseInfo.requestId
}&accessPolicy=${encodeURIComponent(
JSON.stringify({
allowedActions: info.baseInfo.allowedActions,
shareId: info.baseInfo.shareId,
applicationId: info.baseInfo.applicationId,
generationNumber: info.baseInfo.generationNumber,
expires: info.baseInfo.expires,
signature: info.baseInfo.signature,
})
)}`,
{
headers: {
accept: '*/*',
'accept-language': 'en-US,en;q=0.9',
'sec-ch-ua':
'" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'User-Agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36',
'x-time-zone': 'Europe/Berlin',
cookie: info.cookie,
...info.headers,
},
referrerPolicy: 'no-referrer',
body: null,
method: 'GET',
}
)
.then((response) => {
return response.data;
})
.catch(() => {
throw {
message:
'Error Reading View :: Ensure www.airtable.com/<SharedBaseID> is accessible. Refer https://bit.ly/3x0OdXI for details',
};
});
return { view: resreq.data };
} else {
throw {
message: 'Error Initializing :: please try again !!',
};
}
}
async function readTemplate(templateId) {
if (!info.initialized) {
await initialize('shrO8aYf3ybwSdDKn');
}
const resreq = await axios(
`https://www.airtable.com/v0.3/exploreApplications/${templateId}`,
{
headers: {
accept: '*/*',
'accept-language': 'en-US,en;q=0.9',
'sec-ch-ua':
'" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'User-Agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36',
'x-time-zone': 'Europe/Berlin',
cookie: info.cookie,
...info.headers,
},
referrer: 'https://www.airtable.com/',
referrerPolicy: 'same-origin',
body: null,
method: 'GET',
mode: 'cors',
credentials: 'include',
}
)
.then((response) => {
return response.data;
})
.catch(() => {
throw {
message:
'Error Fetching :: Ensure www.airtable.com/templates/featured/<TemplateID> is accessible.',
};
});
return { template: resreq };
}
function unicodeToChar(text) {
return text.replace(/\\u[\dA-F]{4}/gi, function (match) {
return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16));
});
}
export default {
initialize,
read,
readView,
readTemplate,
};

2435
packages/nocodb/src/lib/controllers/sync/helpers/job.ts

File diff suppressed because it is too large Load Diff

338
packages/nocodb/src/lib/controllers/sync/helpers/readAndProcessData.ts

@ -1,338 +0,0 @@
import { AirtableBase } from 'airtable/lib/airtable_base';
import { Api, RelationTypes, TableType, UITypes } from 'nocodb-sdk';
import EntityMap from './EntityMap';
const BULK_DATA_BATCH_SIZE = 500;
const ASSOC_BULK_DATA_BATCH_SIZE = 1000;
const BULK_PARALLEL_PROCESS = 5;
async function readAllData({
table,
fields,
base,
logBasic = (_str) => {},
}: {
table: { title?: string };
fields?;
base: AirtableBase;
logBasic?: (string) => void;
logDetailed?: (string) => void;
}): Promise<EntityMap> {
return new Promise((resolve, reject) => {
let data = null;
const selectParams: any = {
pageSize: 100,
};
if (fields) selectParams.fields = fields;
base(table.title)
.select(selectParams)
.eachPage(
async function page(records, fetchNextPage) {
if (!data) {
data = new EntityMap();
await data.init();
}
for await (const record of records) {
await data.addRow({ id: record.id, ...record.fields });
}
const tmpLength = await data.getCount();
logBasic(
`:: Reading '${table.title}' data :: ${Math.max(
1,
tmpLength - records.length
)} - ${tmpLength}`
);
// To fetch the next page of records, call `fetchNextPage`.
// If there are more records, `page` will get called again.
// If there are no more records, `done` will get called.
fetchNextPage();
},
async function done(err) {
if (err) {
console.error(err);
return reject(err);
}
resolve(data);
}
);
});
}
export async function importData({
projectName,
table,
base,
api,
nocoBaseDataProcessing_v2,
sDB,
logDetailed = (_str) => {},
logBasic = (_str) => {},
}: {
projectName: string;
table: { title?: string; id?: string };
fields?;
base: AirtableBase;
logBasic: (string) => void;
logDetailed: (string) => void;
api: Api<any>;
nocoBaseDataProcessing_v2;
sDB;
}): Promise<EntityMap> {
try {
// @ts-ignore
const records = await readAllData({
table,
base,
logDetailed,
logBasic,
});
await new Promise(async (resolve) => {
const readable = records.getStream();
const allRecordsCount = await records.getCount();
const promises = [];
let tempData = [];
let importedCount = 0;
let activeProcess = 0;
readable.on('data', async (record) => {
promises.push(
new Promise(async (resolve) => {
activeProcess++;
if (activeProcess >= BULK_PARALLEL_PROCESS) readable.pause();
const { id: rid, ...fields } = record;
const r = await nocoBaseDataProcessing_v2(sDB, table, {
id: rid,
fields,
});
tempData.push(r);
if (tempData.length >= BULK_DATA_BATCH_SIZE) {
let insertArray = tempData.splice(0, tempData.length);
await api.dbTableRow.bulkCreate(
'nc',
projectName,
table.id,
insertArray
);
logBasic(
`:: Importing '${
table.title
}' data :: ${importedCount} - ${Math.min(
importedCount + BULK_DATA_BATCH_SIZE,
allRecordsCount
)}`
);
importedCount += insertArray.length;
insertArray = [];
}
activeProcess--;
if (activeProcess < BULK_PARALLEL_PROCESS) readable.resume();
resolve(true);
})
);
});
readable.on('end', async () => {
await Promise.all(promises);
if (tempData.length > 0) {
await api.dbTableRow.bulkCreate(
'nc',
projectName,
table.id,
tempData
);
logBasic(
`:: Importing '${
table.title
}' data :: ${importedCount} - ${Math.min(
importedCount + BULK_DATA_BATCH_SIZE,
allRecordsCount
)}`
);
importedCount += tempData.length;
tempData = [];
}
resolve(true);
});
});
return records;
} catch (e) {
console.log(e);
return null;
}
}
export async function importLTARData({
table,
fields,
base,
api,
projectName,
insertedAssocRef = {},
logDetailed = (_str) => {},
logBasic = (_str) => {},
records,
atNcAliasRef,
ncLinkMappingTable,
}: {
projectName: string;
table: { title?: string; id?: string };
fields;
base: AirtableBase;
logDetailed: (string) => void;
logBasic: (string) => void;
api: Api<any>;
insertedAssocRef: { [assocTableId: string]: boolean };
records?: EntityMap;
atNcAliasRef: {
[ncTableId: string]: {
[ncTitle: string]: string;
};
};
ncLinkMappingTable: Record<string, Record<string, any>>[];
}) {
const assocTableMetas: Array<{
modelMeta: { id?: string; title?: string };
colMeta: { title?: string };
curCol: { title?: string };
refCol: { title?: string };
}> = [];
const allData =
records ||
(await readAllData({
table,
fields,
base,
logDetailed,
logBasic,
}));
const modelMeta: any = await api.dbTable.read(table.id);
for (const colMeta of modelMeta.columns) {
// skip columns which are not LTAR and Many to many
if (
colMeta.uidt !== UITypes.LinkToAnotherRecord ||
colMeta.colOptions.type !== RelationTypes.MANY_TO_MANY
) {
continue;
}
// skip if already inserted
if (colMeta.colOptions.fk_mm_model_id in insertedAssocRef) continue;
// self links: skip if the column under consideration is the add-on column NocoDB creates
if (ncLinkMappingTable.every((a) => a.nc.title !== colMeta.title)) continue;
// mark as inserted
insertedAssocRef[colMeta.colOptions.fk_mm_model_id] = true;
const assocModelMeta: TableType = (await api.dbTable.read(
colMeta.colOptions.fk_mm_model_id
)) as any;
// extract associative table and columns meta
assocTableMetas.push({
modelMeta: assocModelMeta,
colMeta,
curCol: assocModelMeta.columns.find(
(c) => c.id === colMeta.colOptions.fk_mm_child_column_id
),
refCol: assocModelMeta.columns.find(
(c) => c.id === colMeta.colOptions.fk_mm_parent_column_id
),
});
}
let nestedLinkCnt = 0;
// Iterate over all related M2M associative table
for await (const assocMeta of assocTableMetas) {
let assocTableData = [];
let importedCount = 0;
// extract insert data from records
await new Promise((resolve) => {
const promises = [];
const readable = allData.getStream();
let activeProcess = 0;
readable.on('data', async (record) => {
promises.push(
new Promise(async (resolve) => {
activeProcess++;
if (activeProcess >= BULK_PARALLEL_PROCESS) readable.pause();
const { id: _atId, ...rec } = record;
// todo: use actual alias instead of sanitized
assocTableData.push(
...(
rec?.[atNcAliasRef[table.id][assocMeta.colMeta.title]] || []
).map((id) => ({
[assocMeta.curCol.title]: record.id,
[assocMeta.refCol.title]: id,
}))
);
if (assocTableData.length >= ASSOC_BULK_DATA_BATCH_SIZE) {
let insertArray = assocTableData.splice(0, assocTableData.length);
logBasic(
`:: Importing '${
table.title
}' LTAR data :: ${importedCount} - ${Math.min(
importedCount + ASSOC_BULK_DATA_BATCH_SIZE,
insertArray.length
)}`
);
await api.dbTableRow.bulkCreate(
'nc',
projectName,
assocMeta.modelMeta.id,
insertArray
);
importedCount += insertArray.length;
insertArray = [];
}
activeProcess--;
if (activeProcess < BULK_PARALLEL_PROCESS) readable.resume();
resolve(true);
})
);
});
readable.on('end', async () => {
await Promise.all(promises);
if (assocTableData.length >= 0) {
logBasic(
`:: Importing '${
table.title
}' LTAR data :: ${importedCount} - ${Math.min(
importedCount + ASSOC_BULK_DATA_BATCH_SIZE,
assocTableData.length
)}`
);
await api.dbTableRow.bulkCreate(
'nc',
projectName,
assocMeta.modelMeta.id,
assocTableData
);
importedCount += assocTableData.length;
assocTableData = [];
}
resolve(true);
});
});
nestedLinkCnt += importedCount;
}
return nestedLinkCnt;
}

31
packages/nocodb/src/lib/controllers/sync/helpers/syncMap.ts

@ -1,31 +0,0 @@
export const mapTbl = {};
// static mapping records between aTblId && ncId
export const addToMappingTbl = function addToMappingTbl(
aTblId,
ncId,
ncName,
parent?
) {
mapTbl[aTblId] = {
ncId: ncId,
ncParent: parent,
// name added to assist in quick debug
ncName: ncName,
};
};
// get NcID from airtable ID
export const getNcIdFromAtId = function getNcIdFromAtId(aId) {
return mapTbl[aId]?.ncId;
};
// get nc Parent from airtable ID
export const getNcParentFromAtId = function getNcParentFromAtId(aId) {
return mapTbl[aId]?.ncParent;
};
// get nc-title from airtable ID
export const getNcNameFromAtId = function getNcNameFromAtId(aId) {
return mapTbl[aId]?.ncName;
};

138
packages/nocodb/src/lib/controllers/sync/importApis.ts

@ -1,138 +0,0 @@
import { Request, Router } from 'express';
// import { Queue } from 'bullmq';
// import axios from 'axios';
import catchError, { NcError } from '../../meta/helpers/catchError';
import { Server } from 'socket.io';
import NocoJobs from '../../jobs/NocoJobs';
import job, { AirtableSyncConfig } from './helpers/job';
import SyncSource from '../../models/SyncSource';
import Noco from '../../Noco';
import { genJwt } from '../userApi/helpers';
const AIRTABLE_IMPORT_JOB = 'AIRTABLE_IMPORT_JOB';
const AIRTABLE_PROGRESS_JOB = 'AIRTABLE_PROGRESS_JOB';
enum SyncStatus {
PROGRESS = 'PROGRESS',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
}
export default (
router: Router,
sv: Server,
jobs: { [id: string]: { last_message: any } }
) => {
// add importer job handler and progress notification job handler
NocoJobs.jobsMgr.addJobWorker(AIRTABLE_IMPORT_JOB, job);
NocoJobs.jobsMgr.addJobWorker(
AIRTABLE_PROGRESS_JOB,
({ payload, progress }) => {
sv.to(payload?.id).emit('progress', {
msg: progress?.msg,
level: progress?.level,
status: progress?.status,
});
if (payload?.id in jobs) {
jobs[payload?.id].last_message = {
msg: progress?.msg,
level: progress?.level,
status: progress?.status,
};
}
}
);
NocoJobs.jobsMgr.addProgressCbk(AIRTABLE_IMPORT_JOB, (payload, progress) => {
NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
payload,
progress: {
msg: progress?.msg,
level: progress?.level,
status: progress?.status,
},
});
});
NocoJobs.jobsMgr.addSuccessCbk(AIRTABLE_IMPORT_JOB, (payload) => {
NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
payload,
progress: {
msg: 'Complete!',
status: SyncStatus.COMPLETED,
},
});
delete jobs[payload?.id];
});
NocoJobs.jobsMgr.addFailureCbk(AIRTABLE_IMPORT_JOB, (payload, error: any) => {
NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
payload,
progress: {
msg: error?.message || 'Failed due to some internal error',
status: SyncStatus.FAILED,
},
});
delete jobs[payload?.id];
});
router.post(
'/api/v1/db/meta/import/airtable',
catchError((req, res) => {
NocoJobs.jobsMgr.add(AIRTABLE_IMPORT_JOB, {
id: req.query.id,
...req.body,
});
res.json({});
})
);
router.post(
'/api/v1/db/meta/syncs/:syncId/trigger',
catchError(async (req: Request, res) => {
if (req.params.syncId in jobs) {
NcError.badRequest('Sync already in progress');
}
const syncSource = await SyncSource.get(req.params.syncId);
const user = await syncSource.getUser();
const token = genJwt(user, Noco.getConfig());
// Treat default baseUrl as siteUrl from req object
let baseURL = (req as any).ncSiteUrl;
// if environment value avail use it
// or if it's docker construct using `PORT`
if (process.env.NC_BASEURL_INTERNAL) {
baseURL = process.env.NC_BASEURL_INTERNAL;
} else if (process.env.NC_DOCKER) {
baseURL = `http://localhost:${process.env.PORT || 8080}`;
}
setTimeout(() => {
NocoJobs.jobsMgr.add<AirtableSyncConfig>(AIRTABLE_IMPORT_JOB, {
id: req.params.syncId,
...(syncSource?.details || {}),
projectId: syncSource.project_id,
baseId: syncSource.base_id,
authToken: token,
baseURL,
});
}, 1000);
jobs[req.params.syncId] = {
last_message: {
msg: 'Sync started',
},
};
res.json({});
})
);
router.post(
'/api/v1/db/meta/syncs/:syncId/abort',
catchError(async (req: Request, res) => {
if (req.params.syncId in jobs) {
delete jobs[req.params.syncId];
}
res.json({});
})
);
};

69
packages/nocodb/src/lib/controllers/sync/syncSourceApis.ts

@ -1,69 +0,0 @@
import { Request, Response, Router } from 'express';
import SyncSource from '../../../models/SyncSource';
import { T } from 'nc-help';
import { PagedResponseImpl } from '../../helpers/PagedResponse';
import ncMetaAclMw from '../../helpers/ncMetaAclMw';
import Project from '../../../models/Project';
export async function syncSourceList(req: Request, res: Response) {
// todo: pagination
res.json(
new PagedResponseImpl(
await SyncSource.list(req.params.projectId, req.params.baseId)
)
);
}
export async function syncCreate(req: Request, res: Response) {
T.emit('evt', { evt_type: 'webhooks:created' });
const project = await Project.getWithInfo(req.params.projectId);
const sync = await SyncSource.insert({
...req.body,
fk_user_id: (req as any).user.id,
base_id: req.params.baseId ? req.params.baseId : project.bases[0].id,
project_id: req.params.projectId,
});
res.json(sync);
}
export async function syncDelete(req: Request, res: Response<any>) {
T.emit('evt', { evt_type: 'webhooks:deleted' });
res.json(await SyncSource.delete(req.params.syncId));
}
export async function syncUpdate(req: Request, res: Response) {
T.emit('evt', { evt_type: 'webhooks:updated' });
res.json(await SyncSource.update(req.params.syncId, req.body));
}
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/projects/:projectId/syncs',
ncMetaAclMw(syncSourceList, 'syncSourceList')
);
router.post(
'/api/v1/db/meta/projects/:projectId/syncs',
ncMetaAclMw(syncCreate, 'syncSourceCreate')
);
router.get(
'/api/v1/db/meta/projects/:projectId/syncs/:baseId',
ncMetaAclMw(syncSourceList, 'syncSourceList')
);
router.post(
'/api/v1/db/meta/projects/:projectId/syncs/:baseId',
ncMetaAclMw(syncCreate, 'syncSourceCreate')
);
router.delete(
'/api/v1/db/meta/syncs/:syncId',
ncMetaAclMw(syncDelete, 'syncSourceDelete')
);
router.patch(
'/api/v1/db/meta/syncs/:syncId',
ncMetaAclMw(syncUpdate, 'syncSourceUpdate')
);
export default router;

12
packages/nocodb/src/lib/controllers/tableController.ts

@ -1,7 +1,6 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import { TableListType, TableReqType, TableType } from 'nocodb-sdk'; import { TableListType, TableReqType, TableType } from 'nocodb-sdk';
import { getAjvValidatorMw } from '../meta/api/helpers'; import { getAjvValidatorMw } from '../meta/api/helpers';
import { tableUpdate } from '../meta/api/tableApis';
import { metaApiMetrics } from '../meta/helpers/apiMetrics'; import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; import { PagedResponseImpl } from '../meta/helpers/PagedResponse';
@ -59,6 +58,17 @@ export async function tableReorder(req: Request, res: Response) {
); );
} }
export async function tableUpdate(req: Request<any, any>, res) {
tableService.tableUpdate({
tableId: req.params.tableId,
table: req.body,
})
res.json({ msg: 'success' })
}
const router = Router({ mergeParams: true }); const router = Router({ mergeParams: true });
router.get( router.get(
'/api/v1/db/meta/projects/:projectId/tables', '/api/v1/db/meta/projects/:projectId/tables',

23
packages/nocodb/src/lib/controllers/userApi/helpers.ts

@ -1,23 +0,0 @@
import * as jwt from 'jsonwebtoken';
import crypto from 'crypto';
import User from '../../models/User';
import { NcConfig } from '../../../interface/config';
export function genJwt(user: User, config: NcConfig) {
return jwt.sign(
{
email: user.email,
firstname: user.firstname,
lastname: user.lastname,
id: user.id,
roles: user.roles,
token_version: user.token_version,
},
config.auth.jwt.secret,
config.auth.jwt.options
);
}
export function randomTokenString(): string {
return crypto.randomBytes(40).toString('hex');
}

1
packages/nocodb/src/lib/controllers/userApi/index.ts

@ -1 +0,0 @@
export * from './userApis';

283
packages/nocodb/src/lib/controllers/userApi/initAdminFromEnv.ts

@ -1,283 +0,0 @@
import User from '../../../models/User';
import { v4 as uuidv4 } from 'uuid';
import { promisify } from 'util';
import bcrypt from 'bcryptjs';
import Noco from '../../../Noco';
import { CacheScope, MetaTable } from '../../../utils/globals';
import ProjectUser from '../../../models/ProjectUser';
import { validatePassword } from 'nocodb-sdk';
import boxen from 'boxen';
import NocoCache from '../../../cache/NocoCache';
import { T } from 'nc-help';
const { isEmail } = require('validator');
const rolesLevel = { owner: 0, creator: 1, editor: 2, commenter: 3, viewer: 4 };
export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
if (process.env.NC_ADMIN_EMAIL && process.env.NC_ADMIN_PASSWORD) {
if (!isEmail(process.env.NC_ADMIN_EMAIL?.trim())) {
console.log(
'\n',
boxen(
`Provided admin email '${process.env.NC_ADMIN_EMAIL}' is not valid`,
{
title: 'Invalid admin email',
padding: 1,
borderStyle: 'double',
titleAlignment: 'center',
borderColor: 'red',
}
),
'\n'
);
process.exit(1);
}
const { valid, error, hint } = validatePassword(
process.env.NC_ADMIN_PASSWORD
);
if (!valid) {
console.log(
'\n',
boxen(`${error}${hint ? `\n\n${hint}` : ''}`, {
title: 'Invalid admin password',
padding: 1,
borderStyle: 'double',
titleAlignment: 'center',
borderColor: 'red',
}),
'\n'
);
process.exit(1);
}
let ncMeta;
try {
ncMeta = await _ncMeta.startTransaction();
const email = process.env.NC_ADMIN_EMAIL.toLowerCase().trim();
const salt = await promisify(bcrypt.genSalt)(10);
const password = await promisify(bcrypt.hash)(
process.env.NC_ADMIN_PASSWORD,
salt
);
const email_verification_token = uuidv4();
const roles = 'user,super';
// if super admin not present
if (await User.isFirst(ncMeta)) {
// roles = 'owner,creator,editor'
T.emit('evt', {
evt_type: 'project:invite',
count: 1,
});
await User.insert(
{
firstname: '',
lastname: '',
email,
salt,
password,
email_verification_token,
roles,
},
ncMeta
);
} else {
const salt = await promisify(bcrypt.genSalt)(10);
const password = await promisify(bcrypt.hash)(
process.env.NC_ADMIN_PASSWORD,
salt
);
const email_verification_token = uuidv4();
const superUser = await ncMeta.metaGet2(null, null, MetaTable.USERS, {
roles: 'user,super',
});
if (!superUser?.id) {
const existingUserWithNewEmail = await User.getByEmail(email, ncMeta);
if (existingUserWithNewEmail?.id) {
// clear cache
await NocoCache.delAll(
CacheScope.USER,
`${existingUserWithNewEmail.email}___*`
);
await NocoCache.del(
`${CacheScope.USER}:${existingUserWithNewEmail.id}`
);
await NocoCache.del(
`${CacheScope.USER}:${existingUserWithNewEmail.email}`
);
// Update email and password of super admin account
await User.update(
existingUserWithNewEmail.id,
{
salt,
email,
password,
email_verification_token,
token_version: null,
refresh_token: null,
roles,
},
ncMeta
);
} else {
T.emit('evt', {
evt_type: 'project:invite',
count: 1,
});
await User.insert(
{
firstname: '',
lastname: '',
email,
salt,
password,
email_verification_token,
roles,
},
ncMeta
);
}
} else if (email !== superUser.email) {
// update admin email and password and migrate projects
// if user already present and associated with some project
// check user account already present with the new admin email
const existingUserWithNewEmail = await User.getByEmail(email, ncMeta);
if (existingUserWithNewEmail?.id) {
// get all project access belongs to the existing account
// and migrate to the admin account
const existingUserProjects = await ncMeta.metaList2(
null,
null,
MetaTable.PROJECT_USERS,
{
condition: { fk_user_id: existingUserWithNewEmail.id },
}
);
for (const existingUserProject of existingUserProjects) {
const userProject = await ProjectUser.get(
existingUserProject.project_id,
superUser.id,
ncMeta
);
// if admin user already have access to the project
// then update role based on the highest access level
if (userProject) {
if (
rolesLevel[userProject.roles] >
rolesLevel[existingUserProject.roles]
) {
await ProjectUser.update(
userProject.project_id,
superUser.id,
existingUserProject.roles,
ncMeta
);
}
} else {
// if super doesn't have access then add the access
await ProjectUser.insert(
{
...existingUserProject,
fk_user_id: superUser.id,
},
ncMeta
);
}
// delete the old project access entry from DB
await ProjectUser.delete(
existingUserProject.project_id,
existingUserProject.fk_user_id,
ncMeta
);
}
// delete existing user
await ncMeta.metaDelete(
null,
null,
MetaTable.USERS,
existingUserWithNewEmail.id
);
// clear cache
await NocoCache.delAll(
CacheScope.USER,
`${existingUserWithNewEmail.email}___*`
);
await NocoCache.del(
`${CacheScope.USER}:${existingUserWithNewEmail.id}`
);
await NocoCache.del(
`${CacheScope.USER}:${existingUserWithNewEmail.email}`
);
// Update email and password of super admin account
await User.update(
superUser.id,
{
salt,
email,
password,
email_verification_token,
token_version: null,
refresh_token: null,
},
ncMeta
);
} else {
// if email's are not different update the password and hash
await User.update(
superUser.id,
{
salt,
email,
password,
email_verification_token,
token_version: null,
refresh_token: null,
},
ncMeta
);
}
} else {
const newPasswordHash = await promisify(bcrypt.hash)(
process.env.NC_ADMIN_PASSWORD,
superUser.salt
);
if (newPasswordHash !== superUser.password) {
// if email's are same and passwords are different
// then update the password and token version
await User.update(
superUser.id,
{
salt,
password,
email_verification_token,
token_version: null,
refresh_token: null,
},
ncMeta
);
}
}
}
await ncMeta.commit();
} catch (e) {
console.log('Error occurred while updating/creating admin user');
console.log(e);
await ncMeta.rollback(e);
}
}
}

336
packages/nocodb/src/lib/controllers/userApi/initStrategies.ts

@ -1,336 +0,0 @@
import { OrgUserRoles } from 'nocodb-sdk';
import User from '../../models/User';
import ProjectUser from '../../models/ProjectUser';
import { promisify } from 'util';
import { Strategy as CustomStrategy } from 'passport-custom';
import passport from 'passport';
import passportJWT from 'passport-jwt';
import { Strategy as AuthTokenStrategy } from 'passport-auth-token';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
const PassportLocalStrategy = require('passport-local').Strategy;
const ExtractJwt = passportJWT.ExtractJwt;
const JwtStrategy = passportJWT.Strategy;
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromHeader('xc-auth'),
};
import bcrypt from 'bcryptjs';
import Project from '../../models/Project';
import NocoCache from '../../cache/NocoCache';
import { CacheGetType, CacheScope } from '../../utils/globals';
import ApiToken from '../../models/ApiToken';
import Noco from '../../Noco';
import Plugin from '../../models/Plugin';
import { registerNewUserIfAllowed } from './userApis';
export function initStrategies(router): void {
passport.use(
'authtoken',
new AuthTokenStrategy(
{ headerFields: ['xc-token'], passReqToCallback: true },
(req, token, done) => {
ApiToken.getByToken(token)
.then((apiToken) => {
if (!apiToken) {
return done({ msg: 'Invalid token' });
}
if (!apiToken.fk_user_id) return done(null, { roles: 'editor' });
User.get(apiToken.fk_user_id)
.then((user) => {
user['is_api_token'] = true;
if (req.ncProjectId) {
ProjectUser.get(req.ncProjectId, user.id)
.then(async (projectUser) => {
user.roles = projectUser?.roles || user.roles;
user.roles =
user.roles === 'owner' ? 'owner,creator' : user.roles;
// + (user.roles ? `,${user.roles}` : '');
// todo : cache
// await NocoCache.set(`${CacheScope.USER}:${key}`, user);
done(null, user);
})
.catch((e) => done(e));
} else {
return done(null, user);
}
})
.catch((e) => {
console.log(e);
done({ msg: 'User not found' });
});
})
.catch((e) => {
console.log(e);
done({ msg: 'Invalid token' });
});
}
)
);
passport.serializeUser(function (
{
id,
email,
email_verified,
roles: _roles,
provider,
firstname,
lastname,
isAuthorized,
isPublicBase,
token_version,
},
done
) {
const roles = (_roles || '')
.split(',')
.reduce((obj, role) => Object.assign(obj, { [role]: true }), {});
if (roles.owner) {
roles.creator = true;
}
done(null, {
isAuthorized,
isPublicBase,
id,
email,
email_verified,
provider,
firstname,
lastname,
roles,
token_version,
});
});
passport.deserializeUser(function (user, done) {
done(null, user);
});
passport.use(
new JwtStrategy(
{
secretOrKey: Noco.getConfig().auth.jwt.secret,
...jwtOptions,
passReqToCallback: true,
...Noco.getConfig().auth.jwt.options,
},
async (req, jwtPayload, done) => {
// todo: improve this
if (
req.ncProjectId &&
jwtPayload.roles?.split(',').includes(OrgUserRoles.SUPER_ADMIN)
) {
return User.getByEmail(jwtPayload?.email).then(async (user) => {
return done(null, {
...user,
roles: `owner,creator,${OrgUserRoles.SUPER_ADMIN}`,
});
});
}
const keyVals = [jwtPayload?.email];
if (req.ncProjectId) {
keyVals.push(req.ncProjectId);
}
const key = keyVals.join('___');
const cachedVal = await NocoCache.get(
`${CacheScope.USER}:${key}`,
CacheGetType.TYPE_OBJECT
);
if (cachedVal) {
if (
!cachedVal.token_version ||
!jwtPayload.token_version ||
cachedVal.token_version !== jwtPayload.token_version
) {
return done(new Error('Token Expired. Please login again.'));
}
return done(null, cachedVal);
}
User.getByEmail(jwtPayload?.email)
.then(async (user) => {
if (
!user.token_version ||
!jwtPayload.token_version ||
user.token_version !== jwtPayload.token_version
) {
return done(new Error('Token Expired. Please login again.'));
}
if (req.ncProjectId) {
// this.xcMeta
// .metaGet(req.ncProjectId, null, 'nc_projects_users', {
// user_id: user?.id
// })
ProjectUser.get(req.ncProjectId, user.id)
.then(async (projectUser) => {
user.roles = projectUser?.roles || user.roles;
user.roles =
user.roles === 'owner' ? 'owner,creator' : user.roles;
// + (user.roles ? `,${user.roles}` : '');
await NocoCache.set(`${CacheScope.USER}:${key}`, user);
done(null, user);
})
.catch((e) => done(e));
} else {
// const roles = projectUser?.roles ? JSON.parse(projectUser.roles) : {guest: true};
if (user) {
await NocoCache.set(`${CacheScope.USER}:${key}`, user);
return done(null, user);
} else {
return done(new Error('User not found'));
}
}
})
.catch((err) => {
return done(err);
});
}
)
);
passport.use(
new PassportLocalStrategy(
{
usernameField: 'email',
session: false,
},
async (email, password, done) => {
try {
const user = await User.getByEmail(email);
if (!user) {
return done({ msg: `Email ${email} is not registered!` });
}
if (!user.salt) {
return done({
msg: `Please sign up with the invite token first or reset the password by clicking Forgot your password.`,
});
}
const hashedPassword = await promisify(bcrypt.hash)(
password,
user.salt
);
if (user.password !== hashedPassword) {
return done({ msg: `Password not valid!` });
} else {
return done(null, user);
}
} catch (e) {
done(e);
}
}
)
);
passport.use(
'baseView',
new CustomStrategy(async (req: any, callback) => {
let user;
if (req.headers['xc-shared-base-id']) {
// const cacheKey = `nc_shared_bases||${req.headers['xc-shared-base-id']}`;
let sharedProject = null;
if (!sharedProject) {
sharedProject = await Project.getByUuid(
req.headers['xc-shared-base-id']
);
}
user = {
roles: sharedProject?.roles,
};
}
callback(null, user);
})
);
// mostly copied from older code
Plugin.getPluginByTitle('Google').then((googlePlugin) => {
if (googlePlugin && googlePlugin.input) {
const settings = JSON.parse(googlePlugin.input);
process.env.NC_GOOGLE_CLIENT_ID = settings.client_id;
process.env.NC_GOOGLE_CLIENT_SECRET = settings.client_secret;
}
if (
process.env.NC_GOOGLE_CLIENT_ID &&
process.env.NC_GOOGLE_CLIENT_SECRET
) {
const googleAuthParamsOrig = GoogleStrategy.prototype.authorizationParams;
GoogleStrategy.prototype.authorizationParams = (options: any) => {
const params = googleAuthParamsOrig.call(this, options);
if (options.state) {
params.state = options.state;
}
return params;
};
const clientConfig = {
clientID: process.env.NC_GOOGLE_CLIENT_ID,
clientSecret: process.env.NC_GOOGLE_CLIENT_SECRET,
// todo: update url
callbackURL: 'http://localhost:3000',
passReqToCallback: true,
};
const googleStrategy = new GoogleStrategy(
clientConfig,
async (req, _accessToken, _refreshToken, profile, done) => {
const email = profile.emails[0].value;
User.getByEmail(email)
.then(async (user) => {
if (user) {
// if project id defined extract project level roles
if (req.ncProjectId) {
ProjectUser.get(req.ncProjectId, user.id)
.then(async (projectUser) => {
user.roles = projectUser?.roles || user.roles;
user.roles =
user.roles === 'owner' ? 'owner,creator' : user.roles;
// + (user.roles ? `,${user.roles}` : '');
done(null, user);
})
.catch((e) => done(e));
} else {
return done(null, user);
}
// if user not found create new user if allowed
// or return error
} else {
const salt = await promisify(bcrypt.genSalt)(10);
const user = await registerNewUserIfAllowed({
firstname: null,
lastname: null,
email_verification_token: null,
email: profile.emails[0].value,
password: '',
salt,
});
return done(null, user);
}
})
.catch((err) => {
return done(err);
});
}
);
passport.use(googleStrategy);
}
});
router.use(passport.initialize());
}

70
packages/nocodb/src/lib/controllers/userApi/ui/auth/emailVerify.ts

@ -1,70 +0,0 @@
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/src/lib/controllers/userApi/ui/auth/resetPassword.ts

@ -1,108 +0,0 @@
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/src/lib/controllers/userApi/ui/emailTemplates/forgotPassword.ts

@ -1,171 +0,0 @@
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/src/lib/controllers/userApi/ui/emailTemplates/invite.ts

@ -1,208 +0,0 @@
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/src/lib/controllers/userApi/ui/emailTemplates/verify.ts

@ -1,207 +0,0 @@
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>
`;

12
packages/nocodb/src/lib/meta/api/index.ts

@ -22,12 +22,12 @@ import hookApis from '../../controllers/hookController';
import pluginApis from '../../controllers/pluginController'; import pluginApis from '../../controllers/pluginController';
import gridViewColumnApis from '../../controllers/gridViewColumnController'; import gridViewColumnApis from '../../controllers/gridViewColumnController';
import kanbanViewApis from '../../controllers/kanbanViewController'; import kanbanViewApis from '../../controllers/kanbanViewController';
import { userApis } from '../../controllers/userApi'; import { userController } from '../../controllers/userController';
// import extractProjectIdAndAuthenticate from './helpers/extractProjectIdAndAuthenticate'; // import extractProjectIdAndAuthenticate from './helpers/extractProjectIdAndAuthenticate';
import utilApis from '../../controllers/utilController'; import utilApis from '../../controllers/utilController';
import projectUserApis from '../../controllers/projectUserController'; import projectUserApis from '../../controllers/projectUserController';
import sharedBaseApis from '../../controllers/sharedBaseController'; import sharedBaseApis from '../../controllers/sharedBaseController';
import { initStrategies } from '../../controllers/userApi/initStrategies'; import { initStrategies } from '../../controllers/userController/initStrategies';
import modelVisibilityApis from '../../controllers/modelVisibilityController'; import modelVisibilityApis from '../../controllers/modelVisibilityController';
import metaDiffApis from '../../controllers/metaDiffController'; import metaDiffApis from '../../controllers/metaDiffController';
import cacheApis from '../../controllers/cacheController'; import cacheApis from '../../controllers/cacheController';
@ -51,9 +51,9 @@ import { Server, Socket } from 'socket.io';
import passport from 'passport'; import passport from 'passport';
import crypto from 'crypto'; import crypto from 'crypto';
import swaggerApis from '../../controllers/swagger/swaggerApis'; import swaggerApis from '../../controllers/swaggerController';
import importApis from '../../controllers/sync/importApis'; import importApis from '../../controllers/syncController/importApis';
import syncSourceApis from '../../controllers/sync/syncSourceApis'; import syncSourceApis from '../../controllers/syncController';
import mapViewApis from '../../controllers/mapViewController'; import mapViewApis from '../../controllers/mapViewController';
const clients: { [id: string]: Socket } = {}; const clients: { [id: string]: Socket } = {};
@ -108,7 +108,7 @@ export default function (router: Router, server) {
router.use(kanbanViewApis); router.use(kanbanViewApis);
router.use(mapViewApis); router.use(mapViewApis);
userApis(router); userController(router);
const io = new Server(server, { const io = new Server(server, {
cors: { cors: {

3
packages/nocodb/src/lib/services/attachmentService.ts

@ -1,3 +1,6 @@
// @ts-ignore // @ts-ignore
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';

2
packages/nocodb/src/lib/services/projectUserService.ts

@ -282,7 +282,7 @@ export async function sendInviteEmail(
): Promise<any> { ): Promise<any> {
try { try {
const template = ( const template = (
await import('../meta/api/userApi/ui/emailTemplates/invite') await import('./userService/ui/emailTemplates/invite')
).default; ).default;
const emailAdapter = await NcPluginMgrv2.emailAdapter(); const emailAdapter = await NcPluginMgrv2.emailAdapter();

109
packages/nocodb/src/lib/services/tableService.ts

@ -1,4 +1,3 @@
// @ts-ignore
import DOMPurify from 'isomorphic-dompurify'; import DOMPurify from 'isomorphic-dompurify';
import { import {
AuditOperationSubTypes, AuditOperationSubTypes,
@ -28,6 +27,112 @@ import View from '../models/View';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import { T } from 'nc-help'; import { T } from 'nc-help';
export async function tableUpdate(param: {
tableId: any;
table: TableReqType & { project_id?: string };
projectId?: string;
}) {
const model = await Model.get(param.tableId);
const project = await Project.getWithInfo(
param.table.project_id || param.projectId
);
const base = project.bases.find((b) => b.id === model.base_id);
if (model.project_id !== project.id) {
NcError.badRequest('Model does not belong to project');
}
// if meta present update meta and return
// todo: allow user to update meta and other prop in single api call
if ('meta' in param.table) {
await Model.updateMeta(param.tableId, param.table.meta);
return { msg: 'success' }
}
if (!param.table.table_name) {
NcError.badRequest(
'Missing table name `table_name` property in request body'
);
}
if (base.is_meta && project.prefix) {
if (!param.table.table_name.startsWith(project.prefix)) {
param.table.table_name = `${project.prefix}${param.table.table_name}`;
}
}
param.table.table_name = DOMPurify.sanitize(param.table.table_name);
// validate table name
if (/^\s+|\s+$/.test(param.table.table_name)) {
NcError.badRequest(
'Leading or trailing whitespace not allowed in table names'
);
}
if (
!(await Model.checkTitleAvailable({
table_name: param.table.table_name,
project_id: project.id,
base_id: base.id,
}))
) {
NcError.badRequest('Duplicate table name');
}
if (!param.table.title) {
param.table.title = getTableNameAlias(
param.table.table_name,
project.prefix,
base
);
}
if (
!(await Model.checkAliasAvailable({
title: param.table.title,
project_id: project.id,
base_id: base.id,
}))
) {
NcError.badRequest('Duplicate table alias');
}
const sqlMgr = await ProjectMgrv2.getSqlMgr(project);
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
let tableNameLengthLimit = 255;
const sqlClientType = sqlClient.knex.clientType();
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
tableNameLengthLimit = 64;
} else if (sqlClientType === 'pg') {
tableNameLengthLimit = 63;
} else if (sqlClientType === 'mssql') {
tableNameLengthLimit = 128;
}
if (param.table.table_name.length > tableNameLengthLimit) {
NcError.badRequest(`Table name exceeds ${tableNameLengthLimit} characters`);
}
await Model.updateAliasAndTableName(
param.tableId,
param.table.title,
param.table.table_name
);
await sqlMgr.sqlOpPlus(base, 'tableRename', {
...param.table,
tn: param.table.table_name,
tn_old: model.table_name,
});
T.emit('evt', { evt_type: 'table:updated' });
return true;
}
export function reorderTable(param: { tableId: string; order: any }) { export function reorderTable(param: { tableId: string; order: any }) {
return Model.updateOrder(param.tableId, param.order); return Model.updateOrder(param.tableId, param.order);
} }
@ -100,7 +205,7 @@ export async function getTableWithAccessibleViews(param: {
// todo: optimise // todo: optimise
const viewList = <View[]>await xcVisibilityMetaGet(table.project_id, [table]); const viewList = <View[]>await xcVisibilityMetaGet(table.project_id, [table]);
//await View.list(req.params.tableId) //await View.list(param.tableId)
table.views = viewList.filter((table: any) => { table.views = viewList.filter((table: any) => {
return Object.keys(param.user?.roles).some( return Object.keys(param.user?.roles).some(
(role) => param.user?.roles[role] && !table.disabled[role] (role) => param.user?.roles[role] && !table.disabled[role]

2
packages/nocodb/src/lib/services/userService/helpers.ts

@ -1,7 +1,7 @@
import * as jwt from 'jsonwebtoken'; import * as jwt from 'jsonwebtoken';
import crypto from 'crypto'; import crypto from 'crypto';
import User from '../../models/User';
import { NcConfig } from '../../../interface/config'; import { NcConfig } from '../../../interface/config';
import { User } from '../../models';
export function genJwt(user: User, config: NcConfig) { export function genJwt(user: User, config: NcConfig) {
return jwt.sign( return jwt.sign(

Loading…
Cancel
Save