mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
39 changed files with 144 additions and 5158 deletions
@ -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>`;
|
|
@ -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; |
|
@ -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> |
|
||||||
`;
|
|
@ -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; |
|
@ -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>; |
|
||||||
} |
|
@ -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>; |
|
||||||
} |
|
@ -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, |
|
||||||
}; |
|
File diff suppressed because it is too large
Load Diff
@ -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; |
|
||||||
} |
|
@ -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; |
|
||||||
}; |
|
@ -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({}); |
|
||||||
}) |
|
||||||
); |
|
||||||
}; |
|
@ -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; |
|
@ -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 +0,0 @@ |
|||||||
export * from './userApis'; |
|
@ -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); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -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()); |
|
||||||
} |
|
@ -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>`;
|
|
@ -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>`;
|
|
@ -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"> </td> |
|
||||||
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" width="580" valign="top"> |
|
||||||
<div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;"> |
|
||||||
|
|
||||||
<!-- START CENTERED WHITE CONTAINER --> |
|
||||||
<table role="presentation" class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" width="100%"> |
|
||||||
|
|
||||||
<!-- START MAIN CONTENT AREA --> |
|
||||||
<tr> |
|
||||||
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" valign="top"> |
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%"> |
|
||||||
<tr> |
|
||||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> |
|
||||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">Hi,</p> |
|
||||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">To change your NocoDB account password click the following link.</p> |
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" width="100%"> |
|
||||||
<tbody> |
|
||||||
<tr> |
|
||||||
<td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" valign="top"> |
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> |
|
||||||
<tbody> |
|
||||||
<tr> |
|
||||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" valign="top" align="center" bgcolor="#1088ff"> <a href="<%- resetLink %>" target="_blank" style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Reset Password</a> </td> |
|
||||||
</tr> |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">Thanks regards NocoDB.</p> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
</table> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
|
|
||||||
<!-- END MAIN CONTENT AREA --> |
|
||||||
</table> |
|
||||||
<!-- END CENTERED WHITE CONTAINER --> |
|
||||||
|
|
||||||
<!-- START FOOTER --> |
|
||||||
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;"> |
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%"> |
|
||||||
<tr> |
|
||||||
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" valign="top" align="center"> |
|
||||||
<span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;"></span> |
|
||||||
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.--> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
<tr> |
|
||||||
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" valign="top" align="center"> |
|
||||||
<a href="http://nocodb.com/">NocoDB</a> |
|
||||||
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.--> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
</table> |
|
||||||
</div> |
|
||||||
<!-- END FOOTER --> |
|
||||||
|
|
||||||
</div> |
|
||||||
</td> |
|
||||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
|
||||||
</tr> |
|
||||||
</table> |
|
||||||
</body> |
|
||||||
</html> |
|
||||||
`;
|
|
@ -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"> </td> |
|
||||||
<td class="container" |
|
||||||
style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" |
|
||||||
width="580" valign="top"> |
|
||||||
<div class="content" |
|
||||||
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;"> |
|
||||||
|
|
||||||
<!-- START CENTERED WHITE CONTAINER --> |
|
||||||
<table role="presentation" class="main" |
|
||||||
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" |
|
||||||
width="100%"> |
|
||||||
|
|
||||||
<!-- START MAIN CONTENT AREA --> |
|
||||||
<tr> |
|
||||||
<td class="wrapper" |
|
||||||
style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" |
|
||||||
valign="top"> |
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
|
||||||
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
|
||||||
width="100%"> |
|
||||||
<tr> |
|
||||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" |
|
||||||
valign="top"> |
|
||||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
|
||||||
Hi,</p> |
|
||||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
|
||||||
I invited you to be "<%- roles -%>" of the NocoDB project "<%- projectName %>". |
|
||||||
Click the button below to to accept my invitation.</p> |
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
|
||||||
class="btn btn-primary" |
|
||||||
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" |
|
||||||
width="100%"> |
|
||||||
<tbody> |
|
||||||
<tr> |
|
||||||
<td align="left" |
|
||||||
style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" |
|
||||||
valign="top"> |
|
||||||
<table role="presentation" border="0" cellpadding="0" |
|
||||||
cellspacing="0" |
|
||||||
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> |
|
||||||
<tbody> |
|
||||||
<tr> |
|
||||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" |
|
||||||
valign="top" align="center" bgcolor="#1088ff"><a |
|
||||||
href="<%- signupLink %>" target="_blank" |
|
||||||
style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Signup</a> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
|
||||||
Thanks regards <%- adminEmail %>.</p> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
</table> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
|
|
||||||
<!-- END MAIN CONTENT AREA --> |
|
||||||
</table> |
|
||||||
<!-- END CENTERED WHITE CONTAINER --> |
|
||||||
|
|
||||||
<!-- START FOOTER --> |
|
||||||
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;"> |
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
|
||||||
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
|
||||||
width="100%"> |
|
||||||
<tr> |
|
||||||
<td class="content-block" |
|
||||||
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
|
||||||
valign="top" align="center"> |
|
||||||
<span class="apple-link" |
|
||||||
style="color: #999999; font-size: 12px; text-align: center;"></span> |
|
||||||
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.--> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
<tr> |
|
||||||
<td class="content-block powered-by" |
|
||||||
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
|
||||||
valign="top" align="center"> |
|
||||||
<a href="http://nocodb.com/">NocoDB</a> |
|
||||||
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.--> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
</table> |
|
||||||
</div> |
|
||||||
<!-- END FOOTER --> |
|
||||||
|
|
||||||
</div> |
|
||||||
</td> |
|
||||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
|
||||||
</tr> |
|
||||||
</table> |
|
||||||
</body> |
|
||||||
</html> |
|
||||||
`;
|
|
@ -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"> </td> |
|
||||||
<td class="container" |
|
||||||
style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" |
|
||||||
width="580" valign="top"> |
|
||||||
<div class="content" |
|
||||||
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;"> |
|
||||||
|
|
||||||
<!-- START CENTERED WHITE CONTAINER --> |
|
||||||
<table role="presentation" class="main" |
|
||||||
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" |
|
||||||
width="100%"> |
|
||||||
|
|
||||||
<!-- START MAIN CONTENT AREA --> |
|
||||||
<tr> |
|
||||||
<td class="wrapper" |
|
||||||
style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" |
|
||||||
valign="top"> |
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
|
||||||
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
|
||||||
width="100%"> |
|
||||||
<tr> |
|
||||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" |
|
||||||
valign="top"> |
|
||||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
|
||||||
Hi,</p> |
|
||||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
|
||||||
Please verify your email address by clicking the following button.</p> |
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
|
||||||
class="btn btn-primary" |
|
||||||
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" |
|
||||||
width="100%"> |
|
||||||
<tbody> |
|
||||||
<tr> |
|
||||||
<td align="left" |
|
||||||
style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" |
|
||||||
valign="top"> |
|
||||||
<table role="presentation" border="0" cellpadding="0" |
|
||||||
cellspacing="0" |
|
||||||
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> |
|
||||||
<tbody> |
|
||||||
<tr> |
|
||||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" |
|
||||||
valign="top" align="center" bgcolor="#1088ff"><a |
|
||||||
href="<%- verifyLink %>" target="_blank" |
|
||||||
style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Verify</a> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
|
||||||
Thanks regards NocoDB.</p> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
</table> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
|
|
||||||
<!-- END MAIN CONTENT AREA --> |
|
||||||
</table> |
|
||||||
<!-- END CENTERED WHITE CONTAINER --> |
|
||||||
|
|
||||||
<!-- START FOOTER --> |
|
||||||
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;"> |
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
|
||||||
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
|
||||||
width="100%"> |
|
||||||
<tr> |
|
||||||
<td class="content-block" |
|
||||||
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
|
||||||
valign="top" align="center"> |
|
||||||
<span class="apple-link" |
|
||||||
style="color: #999999; font-size: 12px; text-align: center;"></span> |
|
||||||
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.--> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
<tr> |
|
||||||
<td class="content-block powered-by" |
|
||||||
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
|
||||||
valign="top" align="center"> |
|
||||||
<a href="http://nocodb.com/">NocoDB</a> |
|
||||||
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.--> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
</table> |
|
||||||
</div> |
|
||||||
<!-- END FOOTER --> |
|
||||||
|
|
||||||
</div> |
|
||||||
</td> |
|
||||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
|
||||||
</tr> |
|
||||||
</table> |
|
||||||
</body> |
|
||||||
</html> |
|
||||||
`;
|
|
Loading…
Reference in new issue