Browse Source

refactor: user api

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/5239/head
Pranav C 2 years ago
parent
commit
b3a5b4802d
  1. 1
      packages/nocodb/src/lib/controllers/userController/index.ts
  2. 332
      packages/nocodb/src/lib/controllers/userController/initStrategies.ts
  3. 70
      packages/nocodb/src/lib/controllers/userController/ui/auth/emailVerify.ts
  4. 108
      packages/nocodb/src/lib/controllers/userController/ui/auth/resetPassword.ts
  5. 171
      packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/forgotPassword.ts
  6. 208
      packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/invite.ts
  7. 207
      packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/verify.ts
  8. 462
      packages/nocodb/src/lib/controllers/userController/userApis.ts
  9. 1
      packages/nocodb/src/lib/services/index.ts
  10. 23
      packages/nocodb/src/lib/services/userService/helpers.ts
  11. 301
      packages/nocodb/src/lib/services/userService/index.ts
  12. 283
      packages/nocodb/src/lib/services/userService/initAdminFromEnv.ts

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

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

332
packages/nocodb/src/lib/controllers/userController/initStrategies.ts

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

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

@ -0,0 +1,70 @@
export default `<!DOCTYPE html>
<html>
<head>
<title>NocoDB - Verify Email</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<link href="<%- ncPublicUrl %>/css/fonts.roboto.css" rel="stylesheet">
<link href="<%- ncPublicUrl %>/css/materialdesignicons.5.x.min.css" rel="stylesheet">
<link href="<%- ncPublicUrl %>/css/vuetify.2.x.min.css" rel="stylesheet">
<script src="<%- ncPublicUrl %>/js/vue.2.6.14.min.js"></script>
</head>
<body>
<div id="app">
<v-app>
<v-container>
<v-row class="justify-center">
<v-col class="col-12 col-md-6">
<v-alert v-if="valid" type="success">
Email verified successfully!
</v-alert>
<v-alert v-else-if="errMsg" type="error">
{{errMsg}}
</v-alert>
<template v-else>
<v-skeleton-loader type="heading"></v-skeleton-loader>
</template>
</v-col>
</v-row>
</v-container>
</v-app>
</div>
<script src="<%- ncPublicUrl %>/js/vuetify.2.x.min.js"></script>
<script src="<%- ncPublicUrl %>/js/axios.0.19.2.min.js"></script>
<script>
var app = new Vue({
el: '#app',
vuetify: new Vuetify(),
data: {
valid: null,
errMsg: null,
validForm: false,
token: <%- token %>,
greeting: 'Password Reset',
formdata: {
password: '',
newPassword: ''
},
success: false
},
methods: {},
async created() {
try {
const valid = (await axios.post('<%- baseUrl %>/api/v1/auth/email/validate/' + this.token)).data;
this.valid = !!valid;
} catch (e) {
this.valid = false;
if(e.response && e.response.data && e.response.data.msg){
this.errMsg = e.response.data.msg;
}else{
this.errMsg = 'Some error occurred';
}
}
}
})
</script>
</body>
</html>`;

108
packages/nocodb/src/lib/controllers/userController/ui/auth/resetPassword.ts

@ -0,0 +1,108 @@
export default `<!DOCTYPE html>
<html>
<head>
<title>NocoDB - Reset Password</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<link href="<%- ncPublicUrl %>/css/fonts.roboto.css" rel="stylesheet">
<link href="<%- ncPublicUrl %>/css/materialdesignicons.5.x.min.css" rel="stylesheet">
<link href="<%- ncPublicUrl %>/css/vuetify.2.x.min.css" rel="stylesheet">
<script src="<%- ncPublicUrl %>/js/vue.2.6.14.min.js"></script>
</head>
<body>
<div id="app">
<v-app>
<v-container>
<v-row class="justify-center">
<v-col class="col-12 col-md-6">
<v-alert v-if="success" type="success">
Password reset successful!
</v-alert>
<template v-else>
<v-form ref="form" v-model="validForm" v-if="valid === true" ref="formType" class="ma-auto"
lazy-validation>
<v-text-field
name="input-10-2"
label="New password"
type="password"
v-model="formdata.password"
:rules="[v => !!v || 'Password is required']"
></v-text-field>
<v-text-field
name="input-10-2"
type="password"
label="Confirm new password"
v-model="formdata.newPassword"
:rules="[v => !!v || 'Password is required', v => v === formdata.password || 'Password mismatch']"
></v-text-field>
<v-btn
:disabled="!validForm"
large
@click="resetPassword"
>
RESET PASSWORD
</v-btn>
</v-form>
<div v-else-if="valid === false">Not a valid url</div>
<div v-else>
<v-skeleton-loader type="actions"></v-skeleton-loader>
</div>
</template>
</v-col>
</v-row>
</v-container>
</v-app>
</div>
<script src="<%- ncPublicUrl %>/js/vuetify.2.x.min.js"></script>
<script src="<%- ncPublicUrl %>/js/axios.0.19.2.min.js"></script>
<script>
var app = new Vue({
el: '#app',
vuetify: new Vuetify(),
data: {
valid: null,
validForm: false,
token: <%- token %>,
greeting: 'Password Reset',
formdata: {
password: '',
newPassword: ''
},
success: false
},
methods: {
async resetPassword() {
if (this.$refs.form.validate()) {
try {
const res = await axios.post('<%- baseUrl %>api/v1/db/auth/password/reset/' + this.token, {
...this.formdata
});
this.success = true;
} catch (e) {
if (e.response && e.response.data && e.response.data.msg) {
alert('Failed to reset password: ' + e.response.data.msg)
} else {
alert('Some error occurred')
}
}
}
}
},
async created() {
try {
const valid = (await axios.post('<%- baseUrl %>api/v1/db/auth/token/validate/' + this.token)).data;
this.valid = !!valid;
} catch (e) {
this.valid = false;
}
}
})
</script>
</body>
</html>`;

171
packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/forgotPassword.ts

@ -0,0 +1,171 @@
export default `<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Simple Transactional Email</title>
<style>
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
</head>
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;" width="100%" bgcolor="#f6f6f6">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" width="580" valign="top">
<div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<table role="presentation" class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" width="100%">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" valign="top">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">Hi,</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">To change your NocoDB account password click the following link.</p>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" width="100%">
<tbody>
<tr>
<td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" valign="top">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" valign="top" align="center" bgcolor="#1088ff"> <a href="<%- resetLink %>" target="_blank" style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Reset Password</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">Thanks regards NocoDB.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- END CENTERED WHITE CONTAINER -->
<!-- START FOOTER -->
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
<tr>
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" valign="top" align="center">
<span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;"></span>
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.-->
</td>
</tr>
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" valign="top" align="center">
<a href="http://nocodb.com/">NocoDB</a>
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.-->
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
</tr>
</table>
</body>
</html>
`;

208
packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/invite.ts

@ -0,0 +1,208 @@
export default `<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Simple Transactional Email</title>
<style>
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
</head>
<body class=""
style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<span class="preheader"
style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body"
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;"
width="100%" bgcolor="#f6f6f6">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
<td class="container"
style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;"
width="580" valign="top">
<div class="content"
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<table role="presentation" class="main"
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;"
width="100%">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper"
style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;"
valign="top">
<table role="presentation" border="0" cellpadding="0" cellspacing="0"
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;"
width="100%">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"
valign="top">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">
Hi,</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">
I invited you to be "<%- roles -%>" of the NocoDB project "<%- projectName %>".
Click the button below to to accept my invitation.</p>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"
class="btn btn-primary"
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;"
width="100%">
<tbody>
<tr>
<td align="left"
style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;"
valign="top">
<table role="presentation" border="0" cellpadding="0"
cellspacing="0"
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;"
valign="top" align="center" bgcolor="#1088ff"><a
href="<%- signupLink %>" target="_blank"
style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Signup</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">
Thanks regards <%- adminEmail %>.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- END CENTERED WHITE CONTAINER -->
<!-- START FOOTER -->
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0"
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;"
width="100%">
<tr>
<td class="content-block"
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;"
valign="top" align="center">
<span class="apple-link"
style="color: #999999; font-size: 12px; text-align: center;"></span>
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.-->
</td>
</tr>
<tr>
<td class="content-block powered-by"
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;"
valign="top" align="center">
<a href="http://nocodb.com/">NocoDB</a>
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.-->
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
</tr>
</table>
</body>
</html>
`;

207
packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/verify.ts

@ -0,0 +1,207 @@
export default `<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Simple Transactional Email</title>
<style>
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
</head>
<body class=""
style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<span class="preheader"
style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body"
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;"
width="100%" bgcolor="#f6f6f6">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
<td class="container"
style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;"
width="580" valign="top">
<div class="content"
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<table role="presentation" class="main"
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;"
width="100%">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper"
style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;"
valign="top">
<table role="presentation" border="0" cellpadding="0" cellspacing="0"
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;"
width="100%">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"
valign="top">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">
Hi,</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">
Please verify your email address by clicking the following button.</p>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"
class="btn btn-primary"
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;"
width="100%">
<tbody>
<tr>
<td align="left"
style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;"
valign="top">
<table role="presentation" border="0" cellpadding="0"
cellspacing="0"
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;"
valign="top" align="center" bgcolor="#1088ff"><a
href="<%- verifyLink %>" target="_blank"
style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Verify</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">
Thanks regards NocoDB.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- END CENTERED WHITE CONTAINER -->
<!-- START FOOTER -->
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0"
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;"
width="100%">
<tr>
<td class="content-block"
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;"
valign="top" align="center">
<span class="apple-link"
style="color: #999999; font-size: 12px; text-align: center;"></span>
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.-->
</td>
</tr>
<tr>
<td class="content-block powered-by"
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;"
valign="top" align="center">
<a href="http://nocodb.com/">NocoDB</a>
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.-->
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
</tr>
</table>
</body>
</html>
`;

462
packages/nocodb/src/lib/controllers/userController/userApis.ts

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

1
packages/nocodb/src/lib/services/index.ts

@ -31,3 +31,4 @@ export * as bulkDataService from './dataService/bulkData';
export * as cacheService from './cacheService';
export * as auditService from './auditService';
export * as swaggerService from './swaggerService';
export * as userService from './userService';

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

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

301
packages/nocodb/src/lib/services/userService/index.ts

@ -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'

283
packages/nocodb/src/lib/services/userService/initAdminFromEnv.ts

@ -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…
Cancel
Save