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