mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
12 changed files with 2167 additions and 0 deletions
@ -0,0 +1 @@ |
|||||||
|
export * from './userApis'; |
@ -0,0 +1,332 @@ |
|||||||
|
import { OrgUserRoles } from 'nocodb-sdk'; |
||||||
|
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 NocoCache from '../../cache/NocoCache'; |
||||||
|
import { ApiToken, Plugin, Project, ProjectUser, User } from '../../models'; |
||||||
|
import Noco from '../../Noco'; |
||||||
|
import { CacheGetType, CacheScope } from '../../utils/globals'; |
||||||
|
import { userService } from '../../services'; |
||||||
|
|
||||||
|
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 userService.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()); |
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
export default `<!DOCTYPE html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>NocoDB - Verify Email</title> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/fonts.roboto.css" rel="stylesheet"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/materialdesignicons.5.x.min.css" rel="stylesheet"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/vuetify.2.x.min.css" rel="stylesheet"> |
||||||
|
<script src="<%- ncPublicUrl %>/js/vue.2.6.14.min.js"></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="app"> |
||||||
|
<v-app> |
||||||
|
<v-container> |
||||||
|
<v-row class="justify-center"> |
||||||
|
<v-col class="col-12 col-md-6"> |
||||||
|
<v-alert v-if="valid" type="success"> |
||||||
|
Email verified successfully! |
||||||
|
</v-alert> |
||||||
|
<v-alert v-else-if="errMsg" type="error"> |
||||||
|
{{errMsg}} |
||||||
|
</v-alert> |
||||||
|
|
||||||
|
<template v-else> |
||||||
|
|
||||||
|
<v-skeleton-loader type="heading"></v-skeleton-loader> |
||||||
|
|
||||||
|
</template> |
||||||
|
</v-col> |
||||||
|
</v-row> |
||||||
|
</v-container> |
||||||
|
</v-app> |
||||||
|
</div> |
||||||
|
<script src="<%- ncPublicUrl %>/js/vuetify.2.x.min.js"></script> |
||||||
|
<script src="<%- ncPublicUrl %>/js/axios.0.19.2.min.js"></script> |
||||||
|
|
||||||
|
<script> |
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
vuetify: new Vuetify(), |
||||||
|
data: { |
||||||
|
valid: null, |
||||||
|
errMsg: null, |
||||||
|
validForm: false, |
||||||
|
token: <%- token %>, |
||||||
|
greeting: 'Password Reset', |
||||||
|
formdata: { |
||||||
|
password: '', |
||||||
|
newPassword: '' |
||||||
|
}, |
||||||
|
success: false |
||||||
|
}, |
||||||
|
methods: {}, |
||||||
|
async created() { |
||||||
|
try { |
||||||
|
const valid = (await axios.post('<%- baseUrl %>/api/v1/auth/email/validate/' + this.token)).data; |
||||||
|
this.valid = !!valid; |
||||||
|
} catch (e) { |
||||||
|
this.valid = false; |
||||||
|
if(e.response && e.response.data && e.response.data.msg){ |
||||||
|
this.errMsg = e.response.data.msg; |
||||||
|
}else{ |
||||||
|
this.errMsg = 'Some error occurred'; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html>`;
|
@ -0,0 +1,108 @@ |
|||||||
|
export default `<!DOCTYPE html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>NocoDB - Reset Password</title> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/fonts.roboto.css" rel="stylesheet"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/materialdesignicons.5.x.min.css" rel="stylesheet"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/vuetify.2.x.min.css" rel="stylesheet"> |
||||||
|
<script src="<%- ncPublicUrl %>/js/vue.2.6.14.min.js"></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="app"> |
||||||
|
<v-app> |
||||||
|
<v-container> |
||||||
|
<v-row class="justify-center"> |
||||||
|
<v-col class="col-12 col-md-6"> |
||||||
|
<v-alert v-if="success" type="success"> |
||||||
|
Password reset successful! |
||||||
|
</v-alert> |
||||||
|
<template v-else> |
||||||
|
|
||||||
|
<v-form ref="form" v-model="validForm" v-if="valid === true" ref="formType" class="ma-auto" |
||||||
|
lazy-validation> |
||||||
|
|
||||||
|
|
||||||
|
<v-text-field |
||||||
|
name="input-10-2" |
||||||
|
label="New password" |
||||||
|
type="password" |
||||||
|
v-model="formdata.password" |
||||||
|
:rules="[v => !!v || 'Password is required']" |
||||||
|
></v-text-field> |
||||||
|
|
||||||
|
<v-text-field |
||||||
|
name="input-10-2" |
||||||
|
type="password" |
||||||
|
label="Confirm new password" |
||||||
|
v-model="formdata.newPassword" |
||||||
|
:rules="[v => !!v || 'Password is required', v => v === formdata.password || 'Password mismatch']" |
||||||
|
></v-text-field> |
||||||
|
|
||||||
|
<v-btn |
||||||
|
:disabled="!validForm" |
||||||
|
large |
||||||
|
@click="resetPassword" |
||||||
|
> |
||||||
|
RESET PASSWORD |
||||||
|
</v-btn> |
||||||
|
|
||||||
|
</v-form> |
||||||
|
<div v-else-if="valid === false">Not a valid url</div> |
||||||
|
<div v-else> |
||||||
|
<v-skeleton-loader type="actions"></v-skeleton-loader> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</v-col> |
||||||
|
</v-row> |
||||||
|
</v-container> |
||||||
|
</v-app> |
||||||
|
</div> |
||||||
|
<script src="<%- ncPublicUrl %>/js/vuetify.2.x.min.js"></script> |
||||||
|
<script src="<%- ncPublicUrl %>/js/axios.0.19.2.min.js"></script> |
||||||
|
|
||||||
|
<script> |
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
vuetify: new Vuetify(), |
||||||
|
data: { |
||||||
|
valid: null, |
||||||
|
validForm: false, |
||||||
|
token: <%- token %>, |
||||||
|
greeting: 'Password Reset', |
||||||
|
formdata: { |
||||||
|
password: '', |
||||||
|
newPassword: '' |
||||||
|
}, |
||||||
|
success: false |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
async resetPassword() { |
||||||
|
if (this.$refs.form.validate()) { |
||||||
|
try { |
||||||
|
const res = await axios.post('<%- baseUrl %>api/v1/db/auth/password/reset/' + this.token, { |
||||||
|
...this.formdata |
||||||
|
}); |
||||||
|
this.success = true; |
||||||
|
} catch (e) { |
||||||
|
if (e.response && e.response.data && e.response.data.msg) { |
||||||
|
alert('Failed to reset password: ' + e.response.data.msg) |
||||||
|
} else { |
||||||
|
alert('Some error occurred') |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
async created() { |
||||||
|
try { |
||||||
|
const valid = (await axios.post('<%- baseUrl %>api/v1/db/auth/token/validate/' + this.token)).data; |
||||||
|
this.valid = !!valid; |
||||||
|
} catch (e) { |
||||||
|
this.valid = false; |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html>`;
|
@ -0,0 +1,171 @@ |
|||||||
|
export default `<!doctype html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta name="viewport" content="width=device-width"> |
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
||||||
|
<title>Simple Transactional Email</title> |
||||||
|
<style> |
||||||
|
@media only screen and (max-width: 620px) { |
||||||
|
table[class=body] h1 { |
||||||
|
font-size: 28px !important; |
||||||
|
margin-bottom: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] p, |
||||||
|
table[class=body] ul, |
||||||
|
table[class=body] ol, |
||||||
|
table[class=body] td, |
||||||
|
table[class=body] span, |
||||||
|
table[class=body] a { |
||||||
|
font-size: 16px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .wrapper, |
||||||
|
table[class=body] .article { |
||||||
|
padding: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .content { |
||||||
|
padding: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .container { |
||||||
|
padding: 0 !important; |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .main { |
||||||
|
border-left-width: 0 !important; |
||||||
|
border-radius: 0 !important; |
||||||
|
border-right-width: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn table { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn a { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .img-responsive { |
||||||
|
height: auto !important; |
||||||
|
max-width: 100% !important; |
||||||
|
width: auto !important; |
||||||
|
} |
||||||
|
} |
||||||
|
@media all { |
||||||
|
.ExternalClass { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.ExternalClass, |
||||||
|
.ExternalClass p, |
||||||
|
.ExternalClass span, |
||||||
|
.ExternalClass font, |
||||||
|
.ExternalClass td, |
||||||
|
.ExternalClass div { |
||||||
|
line-height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.apple-link a { |
||||||
|
color: inherit !important; |
||||||
|
font-family: inherit !important; |
||||||
|
font-size: inherit !important; |
||||||
|
font-weight: inherit !important; |
||||||
|
line-height: inherit !important; |
||||||
|
text-decoration: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
#MessageViewBody a { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
font-size: inherit; |
||||||
|
font-family: inherit; |
||||||
|
font-weight: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary table td:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary a:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
border-color: #34495e !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"> |
||||||
|
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;" width="100%" bgcolor="#f6f6f6"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" width="580" valign="top"> |
||||||
|
<div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;"> |
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER --> |
||||||
|
<table role="presentation" class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" width="100%"> |
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA --> |
||||||
|
<tr> |
||||||
|
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">Hi,</p> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">To change your NocoDB account password click the following link.</p> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" width="100%"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" valign="top" align="center" bgcolor="#1088ff"> <a href="<%- resetLink %>" target="_blank" style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Reset Password</a> </td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">Thanks regards NocoDB.</p> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA --> |
||||||
|
</table> |
||||||
|
<!-- END CENTERED WHITE CONTAINER --> |
||||||
|
|
||||||
|
<!-- START FOOTER --> |
||||||
|
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%"> |
||||||
|
<tr> |
||||||
|
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" valign="top" align="center"> |
||||||
|
<span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;"></span> |
||||||
|
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" valign="top" align="center"> |
||||||
|
<a href="http://nocodb.com/">NocoDB</a> |
||||||
|
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<!-- END FOOTER --> |
||||||
|
|
||||||
|
</div> |
||||||
|
</td> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
`;
|
@ -0,0 +1,208 @@ |
|||||||
|
export default `<!doctype html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta name="viewport" content="width=device-width"> |
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
||||||
|
<title>Simple Transactional Email</title> |
||||||
|
<style> |
||||||
|
@media only screen and (max-width: 620px) { |
||||||
|
table[class=body] h1 { |
||||||
|
font-size: 28px !important; |
||||||
|
margin-bottom: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] p, |
||||||
|
table[class=body] ul, |
||||||
|
table[class=body] ol, |
||||||
|
table[class=body] td, |
||||||
|
table[class=body] span, |
||||||
|
table[class=body] a { |
||||||
|
font-size: 16px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .wrapper, |
||||||
|
table[class=body] .article { |
||||||
|
padding: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .content { |
||||||
|
padding: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .container { |
||||||
|
padding: 0 !important; |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .main { |
||||||
|
border-left-width: 0 !important; |
||||||
|
border-radius: 0 !important; |
||||||
|
border-right-width: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn table { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn a { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .img-responsive { |
||||||
|
height: auto !important; |
||||||
|
max-width: 100% !important; |
||||||
|
width: auto !important; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@media all { |
||||||
|
.ExternalClass { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.ExternalClass, |
||||||
|
.ExternalClass p, |
||||||
|
.ExternalClass span, |
||||||
|
.ExternalClass font, |
||||||
|
.ExternalClass td, |
||||||
|
.ExternalClass div { |
||||||
|
line-height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.apple-link a { |
||||||
|
color: inherit !important; |
||||||
|
font-family: inherit !important; |
||||||
|
font-size: inherit !important; |
||||||
|
font-weight: inherit !important; |
||||||
|
line-height: inherit !important; |
||||||
|
text-decoration: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
#MessageViewBody a { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
font-size: inherit; |
||||||
|
font-family: inherit; |
||||||
|
font-weight: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary table td:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary a:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
border-color: #34495e !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body class="" |
||||||
|
style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"> |
||||||
|
<span class="preheader" |
||||||
|
style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;" |
||||||
|
width="100%" bgcolor="#f6f6f6"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
<td class="container" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" |
||||||
|
width="580" valign="top"> |
||||||
|
<div class="content" |
||||||
|
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;"> |
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER --> |
||||||
|
<table role="presentation" class="main" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA --> |
||||||
|
<tr> |
||||||
|
<td class="wrapper" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" |
||||||
|
valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" |
||||||
|
valign="top"> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Hi,</p> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
I invited you to be "<%- roles -%>" of the NocoDB project "<%- projectName %>". |
||||||
|
Click the button below to to accept my invitation.</p> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
class="btn btn-primary" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td align="left" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" |
||||||
|
valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" |
||||||
|
cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" |
||||||
|
valign="top" align="center" bgcolor="#1088ff"><a |
||||||
|
href="<%- signupLink %>" target="_blank" |
||||||
|
style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Signup</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Thanks regards <%- adminEmail %>.</p> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA --> |
||||||
|
</table> |
||||||
|
<!-- END CENTERED WHITE CONTAINER --> |
||||||
|
|
||||||
|
<!-- START FOOTER --> |
||||||
|
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tr> |
||||||
|
<td class="content-block" |
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
||||||
|
valign="top" align="center"> |
||||||
|
<span class="apple-link" |
||||||
|
style="color: #999999; font-size: 12px; text-align: center;"></span> |
||||||
|
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<td class="content-block powered-by" |
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
||||||
|
valign="top" align="center"> |
||||||
|
<a href="http://nocodb.com/">NocoDB</a> |
||||||
|
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<!-- END FOOTER --> |
||||||
|
|
||||||
|
</div> |
||||||
|
</td> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
`;
|
@ -0,0 +1,207 @@ |
|||||||
|
export default `<!doctype html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta name="viewport" content="width=device-width"> |
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
||||||
|
<title>Simple Transactional Email</title> |
||||||
|
<style> |
||||||
|
@media only screen and (max-width: 620px) { |
||||||
|
table[class=body] h1 { |
||||||
|
font-size: 28px !important; |
||||||
|
margin-bottom: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] p, |
||||||
|
table[class=body] ul, |
||||||
|
table[class=body] ol, |
||||||
|
table[class=body] td, |
||||||
|
table[class=body] span, |
||||||
|
table[class=body] a { |
||||||
|
font-size: 16px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .wrapper, |
||||||
|
table[class=body] .article { |
||||||
|
padding: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .content { |
||||||
|
padding: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .container { |
||||||
|
padding: 0 !important; |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .main { |
||||||
|
border-left-width: 0 !important; |
||||||
|
border-radius: 0 !important; |
||||||
|
border-right-width: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn table { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn a { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .img-responsive { |
||||||
|
height: auto !important; |
||||||
|
max-width: 100% !important; |
||||||
|
width: auto !important; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@media all { |
||||||
|
.ExternalClass { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.ExternalClass, |
||||||
|
.ExternalClass p, |
||||||
|
.ExternalClass span, |
||||||
|
.ExternalClass font, |
||||||
|
.ExternalClass td, |
||||||
|
.ExternalClass div { |
||||||
|
line-height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.apple-link a { |
||||||
|
color: inherit !important; |
||||||
|
font-family: inherit !important; |
||||||
|
font-size: inherit !important; |
||||||
|
font-weight: inherit !important; |
||||||
|
line-height: inherit !important; |
||||||
|
text-decoration: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
#MessageViewBody a { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
font-size: inherit; |
||||||
|
font-family: inherit; |
||||||
|
font-weight: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary table td:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary a:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
border-color: #34495e !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body class="" |
||||||
|
style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"> |
||||||
|
<span class="preheader" |
||||||
|
style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;" |
||||||
|
width="100%" bgcolor="#f6f6f6"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
<td class="container" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" |
||||||
|
width="580" valign="top"> |
||||||
|
<div class="content" |
||||||
|
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;"> |
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER --> |
||||||
|
<table role="presentation" class="main" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA --> |
||||||
|
<tr> |
||||||
|
<td class="wrapper" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" |
||||||
|
valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" |
||||||
|
valign="top"> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Hi,</p> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Please verify your email address by clicking the following button.</p> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
class="btn btn-primary" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td align="left" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" |
||||||
|
valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" |
||||||
|
cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" |
||||||
|
valign="top" align="center" bgcolor="#1088ff"><a |
||||||
|
href="<%- verifyLink %>" target="_blank" |
||||||
|
style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Verify</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Thanks regards NocoDB.</p> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA --> |
||||||
|
</table> |
||||||
|
<!-- END CENTERED WHITE CONTAINER --> |
||||||
|
|
||||||
|
<!-- START FOOTER --> |
||||||
|
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tr> |
||||||
|
<td class="content-block" |
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
||||||
|
valign="top" align="center"> |
||||||
|
<span class="apple-link" |
||||||
|
style="color: #999999; font-size: 12px; text-align: center;"></span> |
||||||
|
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<td class="content-block powered-by" |
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
||||||
|
valign="top" align="center"> |
||||||
|
<a href="http://nocodb.com/">NocoDB</a> |
||||||
|
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<!-- END FOOTER --> |
||||||
|
|
||||||
|
</div> |
||||||
|
</td> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
`;
|
@ -0,0 +1,462 @@ |
|||||||
|
import { Request, Response } from 'express'; |
||||||
|
import { TableType, validatePassword } from 'nocodb-sdk'; |
||||||
|
import { Tele } from 'nc-help'; |
||||||
|
|
||||||
|
const { isEmail } = require('validator'); |
||||||
|
import * as ejs from 'ejs'; |
||||||
|
|
||||||
|
import bcrypt from 'bcryptjs'; |
||||||
|
import { promisify } from 'util'; |
||||||
|
|
||||||
|
const { v4: uuidv4 } = require('uuid'); |
||||||
|
|
||||||
|
import passport from 'passport'; |
||||||
|
import { getAjvValidatorMw } from '../../meta/api/helpers'; |
||||||
|
import catchError, { NcError } from '../../meta/helpers/catchError'; |
||||||
|
import extractProjectIdAndAuthenticate from '../../meta/helpers/extractProjectIdAndAuthenticate'; |
||||||
|
import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw'; |
||||||
|
import NcPluginMgrv2 from '../../meta/helpers/NcPluginMgrv2'; |
||||||
|
import { Audit, User } from '../../models'; |
||||||
|
import Noco from '../../Noco'; |
||||||
|
import { genJwt } from './helpers'; |
||||||
|
import { userService } from '../../services'; |
||||||
|
|
||||||
|
export async function signup(req: Request, res: Response<TableType>) { |
||||||
|
const { |
||||||
|
email: _email, |
||||||
|
firstname, |
||||||
|
lastname, |
||||||
|
token, |
||||||
|
ignore_subscribe, |
||||||
|
} = req.body; |
||||||
|
let { password } = req.body; |
||||||
|
|
||||||
|
// validate password and throw error if password is satisfying the conditions
|
||||||
|
const { valid, error } = validatePassword(password); |
||||||
|
if (!valid) { |
||||||
|
NcError.badRequest(`Password : ${error}`); |
||||||
|
} |
||||||
|
|
||||||
|
if (!isEmail(_email)) { |
||||||
|
NcError.badRequest(`Invalid email`); |
||||||
|
} |
||||||
|
|
||||||
|
const email = _email.toLowerCase(); |
||||||
|
|
||||||
|
let user = await User.getByEmail(email); |
||||||
|
|
||||||
|
if (user) { |
||||||
|
if (token) { |
||||||
|
if (token !== user.invite_token) { |
||||||
|
NcError.badRequest(`Invalid invite url`); |
||||||
|
} else if (user.invite_token_expires < new Date()) { |
||||||
|
NcError.badRequest( |
||||||
|
'Expired invite url, Please contact super admin to get a new invite url' |
||||||
|
); |
||||||
|
} |
||||||
|
} else { |
||||||
|
// todo : opening up signup for timebeing
|
||||||
|
// return next(new Error(`Email '${email}' already registered`));
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const salt = await promisify(bcrypt.genSalt)(10); |
||||||
|
password = await promisify(bcrypt.hash)(password, salt); |
||||||
|
const email_verification_token = uuidv4(); |
||||||
|
|
||||||
|
if (!ignore_subscribe) { |
||||||
|
Tele.emit('evt_subscribe', email); |
||||||
|
} |
||||||
|
|
||||||
|
if (user) { |
||||||
|
if (token) { |
||||||
|
await User.update(user.id, { |
||||||
|
firstname, |
||||||
|
lastname, |
||||||
|
salt, |
||||||
|
password, |
||||||
|
email_verification_token, |
||||||
|
invite_token: null, |
||||||
|
invite_token_expires: null, |
||||||
|
email: user.email, |
||||||
|
}); |
||||||
|
} else { |
||||||
|
NcError.badRequest('User already exist'); |
||||||
|
} |
||||||
|
} else { |
||||||
|
await userService.registerNewUserIfAllowed({ |
||||||
|
firstname, |
||||||
|
lastname, |
||||||
|
email, |
||||||
|
salt, |
||||||
|
password, |
||||||
|
email_verification_token, |
||||||
|
}); |
||||||
|
} |
||||||
|
user = await User.getByEmail(email); |
||||||
|
|
||||||
|
try { |
||||||
|
const template = (await import('./ui/emailTemplates/verify')).default; |
||||||
|
await ( |
||||||
|
await NcPluginMgrv2.emailAdapter() |
||||||
|
).mailSend({ |
||||||
|
to: email, |
||||||
|
subject: 'Verify email', |
||||||
|
html: ejs.render(template, { |
||||||
|
verifyLink: |
||||||
|
(req as any).ncSiteUrl + |
||||||
|
`/email/verify/${user.email_verification_token}`, |
||||||
|
}), |
||||||
|
}); |
||||||
|
} catch (e) { |
||||||
|
console.log( |
||||||
|
'Warning : `mailSend` failed, Please configure emailClient configuration.' |
||||||
|
); |
||||||
|
} |
||||||
|
await promisify((req as any).login.bind(req))(user); |
||||||
|
const refreshToken = userService.randomTokenString(); |
||||||
|
await User.update(user.id, { |
||||||
|
refresh_token: refreshToken, |
||||||
|
email: user.email, |
||||||
|
}); |
||||||
|
|
||||||
|
setTokenCookie(res, refreshToken); |
||||||
|
|
||||||
|
user = (req as any).user; |
||||||
|
|
||||||
|
await Audit.insert({ |
||||||
|
op_type: 'AUTHENTICATION', |
||||||
|
op_sub_type: 'SIGNUP', |
||||||
|
user: user.email, |
||||||
|
description: `signed up `, |
||||||
|
ip: (req as any).clientIp, |
||||||
|
}); |
||||||
|
|
||||||
|
res.json({ |
||||||
|
token: genJwt(user, Noco.getConfig()), |
||||||
|
} as any); |
||||||
|
} |
||||||
|
|
||||||
|
async function successfulSignIn({ |
||||||
|
user, |
||||||
|
err, |
||||||
|
info, |
||||||
|
req, |
||||||
|
res, |
||||||
|
auditDescription, |
||||||
|
}) { |
||||||
|
try { |
||||||
|
if (!user || !user.email) { |
||||||
|
if (err) { |
||||||
|
return res.status(400).send(err); |
||||||
|
} |
||||||
|
if (info) { |
||||||
|
return res.status(400).send(info); |
||||||
|
} |
||||||
|
return res.status(400).send({ msg: 'Your signin has failed' }); |
||||||
|
} |
||||||
|
|
||||||
|
await promisify((req as any).login.bind(req))(user); |
||||||
|
const refreshToken = userService.randomTokenString(); |
||||||
|
|
||||||
|
if (!user.token_version) { |
||||||
|
user.token_version = userService.randomTokenString(); |
||||||
|
} |
||||||
|
|
||||||
|
await User.update(user.id, { |
||||||
|
refresh_token: refreshToken, |
||||||
|
email: user.email, |
||||||
|
token_version: user.token_version, |
||||||
|
}); |
||||||
|
setTokenCookie(res, refreshToken); |
||||||
|
|
||||||
|
await Audit.insert({ |
||||||
|
op_type: 'AUTHENTICATION', |
||||||
|
op_sub_type: 'SIGNIN', |
||||||
|
user: user.email, |
||||||
|
ip: req.clientIp, |
||||||
|
description: auditDescription, |
||||||
|
}); |
||||||
|
|
||||||
|
res.json({ |
||||||
|
token: genJwt(user, Noco.getConfig()), |
||||||
|
} as any); |
||||||
|
} catch (e) { |
||||||
|
console.log(e); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function signin(req, res, next) { |
||||||
|
passport.authenticate( |
||||||
|
'local', |
||||||
|
{ session: false }, |
||||||
|
async (err, user, info): Promise<any> => |
||||||
|
await successfulSignIn({ |
||||||
|
user, |
||||||
|
err, |
||||||
|
info, |
||||||
|
req, |
||||||
|
res, |
||||||
|
auditDescription: 'signed in', |
||||||
|
}) |
||||||
|
)(req, res, next); |
||||||
|
} |
||||||
|
|
||||||
|
async function googleSignin(req, res, next) { |
||||||
|
passport.authenticate( |
||||||
|
'google', |
||||||
|
{ |
||||||
|
session: false, |
||||||
|
callbackURL: req.ncSiteUrl + Noco.getConfig().dashboardPath, |
||||||
|
}, |
||||||
|
async (err, user, info): Promise<any> => |
||||||
|
await successfulSignIn({ |
||||||
|
user, |
||||||
|
err, |
||||||
|
info, |
||||||
|
req, |
||||||
|
res, |
||||||
|
auditDescription: 'signed in using Google Auth', |
||||||
|
}) |
||||||
|
)(req, res, next); |
||||||
|
} |
||||||
|
|
||||||
|
function setTokenCookie(res: Response, token): void { |
||||||
|
// create http only cookie with refresh token that expires in 7 days
|
||||||
|
const cookieOptions = { |
||||||
|
httpOnly: true, |
||||||
|
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), |
||||||
|
}; |
||||||
|
res.cookie('refresh_token', token, cookieOptions); |
||||||
|
} |
||||||
|
|
||||||
|
async function me(req, res): Promise<any> { |
||||||
|
res.json(req?.session?.passport?.user ?? {}); |
||||||
|
} |
||||||
|
|
||||||
|
async function passwordChange(req: Request<any, any>, res): Promise<any> { |
||||||
|
if (!(req as any).isAuthenticated()) { |
||||||
|
NcError.forbidden('Not allowed'); |
||||||
|
} |
||||||
|
|
||||||
|
await userService.passwordChange({ |
||||||
|
user: req['user'], |
||||||
|
req, |
||||||
|
body: req.body, |
||||||
|
}); |
||||||
|
|
||||||
|
res.json({ msg: 'Password updated successfully' }); |
||||||
|
} |
||||||
|
|
||||||
|
async function passwordForgot(req: Request<any, any>, res): Promise<any> { |
||||||
|
await userService.passwordForgot({ |
||||||
|
siteUrl: (req as any).ncSiteUrl, |
||||||
|
body: req.body, |
||||||
|
req, |
||||||
|
}); |
||||||
|
|
||||||
|
res.json({ msg: 'Please check your email to reset the password' }); |
||||||
|
} |
||||||
|
|
||||||
|
async function tokenValidate(req, res): Promise<any> { |
||||||
|
await userService.tokenValidate({ |
||||||
|
token: req.params.tokenId, |
||||||
|
}); |
||||||
|
res.json(true); |
||||||
|
} |
||||||
|
|
||||||
|
async function passwordReset(req, res): Promise<any> { |
||||||
|
await userService.passwordReset({ |
||||||
|
token: req.params.tokenId, |
||||||
|
body: req.body, |
||||||
|
req, |
||||||
|
}); |
||||||
|
|
||||||
|
res.json({ msg: 'Password reset successful' }); |
||||||
|
} |
||||||
|
|
||||||
|
async function emailVerification(req, res): Promise<any> { |
||||||
|
await userService.emailVerification({ |
||||||
|
token: req.params.tokenId, |
||||||
|
req, |
||||||
|
}); |
||||||
|
|
||||||
|
res.json({ msg: 'Email verified successfully' }); |
||||||
|
} |
||||||
|
|
||||||
|
async function refreshToken(req, res): Promise<any> { |
||||||
|
try { |
||||||
|
if (!req?.cookies?.refresh_token) { |
||||||
|
return res.status(400).json({ msg: 'Missing refresh token' }); |
||||||
|
} |
||||||
|
|
||||||
|
const user = await User.getByRefreshToken(req.cookies.refresh_token); |
||||||
|
|
||||||
|
if (!user) { |
||||||
|
return res.status(400).json({ msg: 'Invalid refresh token' }); |
||||||
|
} |
||||||
|
|
||||||
|
const refreshToken = userService.randomTokenString(); |
||||||
|
|
||||||
|
await User.update(user.id, { |
||||||
|
email: user.email, |
||||||
|
refresh_token: refreshToken, |
||||||
|
}); |
||||||
|
|
||||||
|
setTokenCookie(res, refreshToken); |
||||||
|
|
||||||
|
res.json({ |
||||||
|
token: genJwt(user, Noco.getConfig()), |
||||||
|
} as any); |
||||||
|
} catch (e) { |
||||||
|
return res.status(400).json({ msg: e.message }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function renderPasswordReset(req, res): Promise<any> { |
||||||
|
try { |
||||||
|
res.send( |
||||||
|
ejs.render((await import('./ui/auth/resetPassword')).default, { |
||||||
|
ncPublicUrl: process.env.NC_PUBLIC_URL || '', |
||||||
|
token: JSON.stringify(req.params.tokenId), |
||||||
|
baseUrl: `/`, |
||||||
|
}) |
||||||
|
); |
||||||
|
} catch (e) { |
||||||
|
return res.status(400).json({ msg: e.message }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const mapRoutes = (router) => { |
||||||
|
// todo: old api - /auth/signup?tool=1
|
||||||
|
router.post( |
||||||
|
'/auth/user/signup', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/SignUpReq'), |
||||||
|
catchError(signup) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/auth/user/signin', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'), |
||||||
|
catchError(signin) |
||||||
|
); |
||||||
|
router.get('/auth/user/me', extractProjectIdAndAuthenticate, catchError(me)); |
||||||
|
router.post( |
||||||
|
'/auth/password/forgot', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/ForgotPasswordReq'), |
||||||
|
catchError(passwordForgot) |
||||||
|
); |
||||||
|
router.post('/auth/token/validate/:tokenId', catchError(tokenValidate)); |
||||||
|
router.post( |
||||||
|
'/auth/password/reset/:tokenId', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/PasswordResetReq'), |
||||||
|
catchError(passwordReset) |
||||||
|
); |
||||||
|
router.post('/auth/email/validate/:tokenId', catchError(emailVerification)); |
||||||
|
router.post( |
||||||
|
'/user/password/change', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/PasswordChangeReq'), |
||||||
|
ncMetaAclMw(passwordChange, 'passwordChange') |
||||||
|
); |
||||||
|
router.post('/auth/token/refresh', catchError(refreshToken)); |
||||||
|
|
||||||
|
/* Google auth apis */ |
||||||
|
|
||||||
|
router.post(`/auth/google/genTokenByCode`, catchError(googleSignin)); |
||||||
|
|
||||||
|
router.get('/auth/google', (req: any, res, next) => |
||||||
|
passport.authenticate('google', { |
||||||
|
scope: ['profile', 'email'], |
||||||
|
state: req.query.state, |
||||||
|
callbackURL: req.ncSiteUrl + Noco.getConfig().dashboardPath, |
||||||
|
})(req, res, next) |
||||||
|
); |
||||||
|
|
||||||
|
// deprecated APIs
|
||||||
|
router.post( |
||||||
|
'/api/v1/db/auth/user/signup', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/SignUpReq'), |
||||||
|
catchError(signup) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/db/auth/user/signin', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'), |
||||||
|
catchError(signin) |
||||||
|
); |
||||||
|
router.get( |
||||||
|
'/api/v1/db/auth/user/me', |
||||||
|
extractProjectIdAndAuthenticate, |
||||||
|
catchError(me) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/db/auth/password/forgot', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/ForgotPasswordReq'), |
||||||
|
catchError(passwordForgot) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/db/auth/token/validate/:tokenId', |
||||||
|
catchError(tokenValidate) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/db/auth/password/reset/:tokenId', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/PasswordResetReq'), |
||||||
|
catchError(passwordReset) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/db/auth/email/validate/:tokenId', |
||||||
|
catchError(emailVerification) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/db/auth/password/change', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/PasswordChangeReq'), |
||||||
|
ncMetaAclMw(passwordChange, 'passwordChange') |
||||||
|
); |
||||||
|
router.post('/api/v1/db/auth/token/refresh', catchError(refreshToken)); |
||||||
|
router.get( |
||||||
|
'/api/v1/db/auth/password/reset/:tokenId', |
||||||
|
catchError(renderPasswordReset) |
||||||
|
); |
||||||
|
|
||||||
|
// new API
|
||||||
|
router.post( |
||||||
|
'/api/v1/auth/user/signup', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/SignUpReq'), |
||||||
|
catchError(signup) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/auth/user/signin', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'), |
||||||
|
catchError(signin) |
||||||
|
); |
||||||
|
router.get( |
||||||
|
'/api/v1/auth/user/me', |
||||||
|
extractProjectIdAndAuthenticate, |
||||||
|
catchError(me) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/auth/password/forgot', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/ForgotPasswordReq'), |
||||||
|
catchError(passwordForgot) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/auth/token/validate/:tokenId', |
||||||
|
catchError(tokenValidate) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/auth/password/reset/:tokenId', |
||||||
|
catchError(passwordReset) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/auth/email/validate/:tokenId', |
||||||
|
catchError(emailVerification) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/auth/password/change', |
||||||
|
getAjvValidatorMw('swagger.json#/components/schemas/PasswordChangeReq'), |
||||||
|
ncMetaAclMw(passwordChange, 'passwordChange') |
||||||
|
); |
||||||
|
router.post('/api/v1/auth/token/refresh', catchError(refreshToken)); |
||||||
|
// respond with password reset page
|
||||||
|
router.get('/auth/password/reset/:tokenId', catchError(renderPasswordReset)); |
||||||
|
}; |
||||||
|
export { mapRoutes as userApis }; |
@ -0,0 +1,23 @@ |
|||||||
|
import * as jwt from 'jsonwebtoken'; |
||||||
|
import crypto from 'crypto'; |
||||||
|
import { NcConfig } from '../../../interface/config' |
||||||
|
import { User } from '../../models' |
||||||
|
|
||||||
|
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'); |
||||||
|
} |
@ -0,0 +1,301 @@ |
|||||||
|
import { Request, Response } from 'express'; |
||||||
|
import { |
||||||
|
PasswordChangeReqType, |
||||||
|
PasswordForgotReqType, PasswordResetReqType, |
||||||
|
SignUpReqType, |
||||||
|
TableType, |
||||||
|
UserType, |
||||||
|
validatePassword, |
||||||
|
} from 'nocodb-sdk' |
||||||
|
import { OrgUserRoles } from 'nocodb-sdk'; |
||||||
|
import { NC_APP_SETTINGS } from '../../../constants'; |
||||||
|
import Store from '../../../models/Store'; |
||||||
|
import { Tele } from 'nc-help'; |
||||||
|
import catchError, { NcError } from '../../helpers/catchError'; |
||||||
|
|
||||||
|
const { isEmail } = require('validator'); |
||||||
|
import * as ejs from 'ejs'; |
||||||
|
|
||||||
|
import bcrypt from 'bcryptjs'; |
||||||
|
import { promisify } from 'util'; |
||||||
|
import User from '../../../models/User'; |
||||||
|
|
||||||
|
const { v4: uuidv4 } = require('uuid'); |
||||||
|
import Audit from '../../../models/Audit'; |
||||||
|
import NcPluginMgrv2 from '../../helpers/NcPluginMgrv2'; |
||||||
|
|
||||||
|
import passport from 'passport'; |
||||||
|
import extractProjectIdAndAuthenticate from '../../helpers/extractProjectIdAndAuthenticate'; |
||||||
|
import ncMetaAclMw from '../../helpers/ncMetaAclMw'; |
||||||
|
import { MetaTable } from '../../../utils/globals'; |
||||||
|
import Noco from '../../../Noco'; |
||||||
|
import { getAjvValidatorMw } from '../helpers'; |
||||||
|
import { genJwt } from './helpers'; |
||||||
|
import { randomTokenString } from '../../helpers/stringHelpers'; |
||||||
|
|
||||||
|
export async function registerNewUserIfAllowed({ |
||||||
|
firstname, |
||||||
|
lastname, |
||||||
|
email, |
||||||
|
salt, |
||||||
|
password, |
||||||
|
email_verification_token, |
||||||
|
}: { |
||||||
|
firstname; |
||||||
|
lastname; |
||||||
|
email: string; |
||||||
|
salt: any; |
||||||
|
password; |
||||||
|
email_verification_token; |
||||||
|
}) { |
||||||
|
let roles: string = OrgUserRoles.CREATOR; |
||||||
|
|
||||||
|
if (await User.isFirst()) { |
||||||
|
roles = `${OrgUserRoles.CREATOR},${OrgUserRoles.SUPER_ADMIN}`; |
||||||
|
// todo: update in nc_store
|
||||||
|
// roles = 'owner,creator,editor'
|
||||||
|
Tele.emit('evt', { |
||||||
|
evt_type: 'project:invite', |
||||||
|
count: 1, |
||||||
|
}); |
||||||
|
} else { |
||||||
|
let settings: { invite_only_signup?: boolean } = {}; |
||||||
|
try { |
||||||
|
settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value); |
||||||
|
} catch {} |
||||||
|
|
||||||
|
if (settings?.invite_only_signup) { |
||||||
|
NcError.badRequest('Not allowed to signup, contact super admin.'); |
||||||
|
} else { |
||||||
|
roles = OrgUserRoles.VIEWER; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const token_version = randomTokenString(); |
||||||
|
|
||||||
|
return await User.insert({ |
||||||
|
firstname, |
||||||
|
lastname, |
||||||
|
email, |
||||||
|
salt, |
||||||
|
password, |
||||||
|
email_verification_token, |
||||||
|
roles, |
||||||
|
token_version, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
export async function passwordChange(param: { |
||||||
|
body: PasswordChangeReqType |
||||||
|
user: UserType |
||||||
|
req:any |
||||||
|
}): Promise<any> { |
||||||
|
const { currentPassword, newPassword } = param.body; |
||||||
|
|
||||||
|
if (!currentPassword || !newPassword) { |
||||||
|
return NcError.badRequest('Missing new/old password'); |
||||||
|
} |
||||||
|
|
||||||
|
// validate password and throw error if password is satisfying the conditions
|
||||||
|
const { valid, error } = validatePassword(newPassword); |
||||||
|
|
||||||
|
if (!valid) { |
||||||
|
NcError.badRequest(`Password : ${error}`); |
||||||
|
} |
||||||
|
|
||||||
|
const user = await User.getByEmail(param.user.email); |
||||||
|
|
||||||
|
const hashedPassword = await promisify(bcrypt.hash)( |
||||||
|
currentPassword, |
||||||
|
user.salt |
||||||
|
); |
||||||
|
|
||||||
|
if (hashedPassword !== user.password) { |
||||||
|
return NcError.badRequest('Current password is wrong'); |
||||||
|
} |
||||||
|
|
||||||
|
const salt = await promisify(bcrypt.genSalt)(10); |
||||||
|
const password = await promisify(bcrypt.hash)(newPassword, salt); |
||||||
|
|
||||||
|
await User.update(user.id, { |
||||||
|
salt, |
||||||
|
password, |
||||||
|
email: user.email, |
||||||
|
token_version: null, |
||||||
|
}); |
||||||
|
|
||||||
|
await Audit.insert({ |
||||||
|
op_type: 'AUTHENTICATION', |
||||||
|
op_sub_type: 'PASSWORD_CHANGE', |
||||||
|
user: user.email, |
||||||
|
description: `changed password `, |
||||||
|
ip: param.req?.clientIp, |
||||||
|
}); |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
export async function passwordForgot(param:{ |
||||||
|
body: PasswordForgotReqType; |
||||||
|
siteUrl: string; |
||||||
|
req:any |
||||||
|
}): Promise<any> { |
||||||
|
const _email = param.body.email; |
||||||
|
|
||||||
|
if (!_email) { |
||||||
|
NcError.badRequest('Please enter your email address.'); |
||||||
|
} |
||||||
|
|
||||||
|
const email = _email.toLowerCase(); |
||||||
|
const user = await User.getByEmail(email); |
||||||
|
|
||||||
|
if (user) { |
||||||
|
const token = uuidv4(); |
||||||
|
await User.update(user.id, { |
||||||
|
email: user.email, |
||||||
|
reset_password_token: token, |
||||||
|
reset_password_expires: new Date(Date.now() + 60 * 60 * 1000), |
||||||
|
token_version: null, |
||||||
|
}); |
||||||
|
try { |
||||||
|
const template = (await import('./ui/emailTemplates/forgotPassword')) |
||||||
|
.default; |
||||||
|
await NcPluginMgrv2.emailAdapter().then((adapter) => |
||||||
|
adapter.mailSend({ |
||||||
|
to: user.email, |
||||||
|
subject: 'Password Reset Link', |
||||||
|
text: `Visit following link to update your password : ${ |
||||||
|
param.siteUrl |
||||||
|
}/auth/password/reset/${token}.`,
|
||||||
|
html: ejs.render(template, { |
||||||
|
resetLink: param.siteUrl + `/auth/password/reset/${token}`, |
||||||
|
}), |
||||||
|
}) |
||||||
|
); |
||||||
|
} catch (e) { |
||||||
|
console.log(e); |
||||||
|
return NcError.badRequest( |
||||||
|
'Email Plugin is not found. Please contact administrators to configure it in App Store first.' |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
await Audit.insert({ |
||||||
|
op_type: 'AUTHENTICATION', |
||||||
|
op_sub_type: 'PASSWORD_FORGOT', |
||||||
|
user: user.email, |
||||||
|
description: `requested for password reset `, |
||||||
|
ip: param.req?.clientIp, |
||||||
|
}); |
||||||
|
} else { |
||||||
|
return NcError.badRequest('Your email has not been registered.'); |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
export async function tokenValidate(param:{ |
||||||
|
token: string; |
||||||
|
}): Promise<any> { |
||||||
|
const token = param.token; |
||||||
|
|
||||||
|
const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, { |
||||||
|
reset_password_token: token, |
||||||
|
}); |
||||||
|
|
||||||
|
if (!user || !user.email) { |
||||||
|
NcError.badRequest('Invalid reset url'); |
||||||
|
} |
||||||
|
if (new Date(user.reset_password_expires) < new Date()) { |
||||||
|
NcError.badRequest('Password reset url expired'); |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
export async function passwordReset(param:{ |
||||||
|
body: PasswordResetReqType; |
||||||
|
token: string; |
||||||
|
// todo: exclude
|
||||||
|
req:any; |
||||||
|
}): Promise<any> { |
||||||
|
const { token, body, req } = param; |
||||||
|
|
||||||
|
const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, { |
||||||
|
reset_password_token: token, |
||||||
|
}); |
||||||
|
|
||||||
|
if (!user) { |
||||||
|
NcError.badRequest('Invalid reset url'); |
||||||
|
} |
||||||
|
if (user.reset_password_expires < new Date()) { |
||||||
|
NcError.badRequest('Password reset url expired'); |
||||||
|
} |
||||||
|
if (user.provider && user.provider !== 'local') { |
||||||
|
NcError.badRequest('Email registered via social account'); |
||||||
|
} |
||||||
|
|
||||||
|
// validate password and throw error if password is satisfying the conditions
|
||||||
|
const { valid, error } = validatePassword(body.password); |
||||||
|
if (!valid) { |
||||||
|
NcError.badRequest(`Password : ${error}`); |
||||||
|
} |
||||||
|
|
||||||
|
const salt = await promisify(bcrypt.genSalt)(10); |
||||||
|
const password = await promisify(bcrypt.hash)(body.password, salt); |
||||||
|
|
||||||
|
await User.update(user.id, { |
||||||
|
salt, |
||||||
|
password, |
||||||
|
email: user.email, |
||||||
|
reset_password_expires: null, |
||||||
|
reset_password_token: '', |
||||||
|
token_version: null, |
||||||
|
}); |
||||||
|
|
||||||
|
await Audit.insert({ |
||||||
|
op_type: 'AUTHENTICATION', |
||||||
|
op_sub_type: 'PASSWORD_RESET', |
||||||
|
user: user.email, |
||||||
|
description: `did reset password `, |
||||||
|
ip: req.clientIp, |
||||||
|
}); |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
export async function emailVerification(param: { |
||||||
|
token: string; |
||||||
|
// todo: exclude
|
||||||
|
req: any; |
||||||
|
}): Promise<any> { |
||||||
|
const { token, req } = param; |
||||||
|
|
||||||
|
const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, { |
||||||
|
email_verification_token: token, |
||||||
|
}); |
||||||
|
|
||||||
|
if (!user) { |
||||||
|
NcError.badRequest('Invalid verification url'); |
||||||
|
} |
||||||
|
|
||||||
|
await User.update(user.id, { |
||||||
|
email: user.email, |
||||||
|
email_verification_token: '', |
||||||
|
email_verified: true, |
||||||
|
}); |
||||||
|
|
||||||
|
await Audit.insert({ |
||||||
|
op_type: 'AUTHENTICATION', |
||||||
|
op_sub_type: 'EMAIL_VERIFICATION', |
||||||
|
user: user.email, |
||||||
|
description: `verified email `, |
||||||
|
ip: req.clientIp, |
||||||
|
}); |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
export * from './helpers' |
||||||
|
export * from './initAdminFromEnv' |
||||||
|
|
@ -0,0 +1,283 @@ |
|||||||
|
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 { Tele } 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'
|
||||||
|
Tele.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 { |
||||||
|
Tele.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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue