diff --git a/packages/nocodb/src/lib/controllers/userController/index.ts b/packages/nocodb/src/lib/controllers/userController/index.ts
new file mode 100644
index 0000000000..c4a5003430
--- /dev/null
+++ b/packages/nocodb/src/lib/controllers/userController/index.ts
@@ -0,0 +1 @@
+export * from './userApis';
diff --git a/packages/nocodb/src/lib/controllers/userController/initStrategies.ts b/packages/nocodb/src/lib/controllers/userController/initStrategies.ts
new file mode 100644
index 0000000000..3b6b99c9aa
--- /dev/null
+++ b/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());
+}
diff --git a/packages/nocodb/src/lib/controllers/userController/ui/auth/emailVerify.ts b/packages/nocodb/src/lib/controllers/userController/ui/auth/emailVerify.ts
new file mode 100644
index 0000000000..f7412b12ba
--- /dev/null
+++ b/packages/nocodb/src/lib/controllers/userController/ui/auth/emailVerify.ts
@@ -0,0 +1,70 @@
+export default `
+
+
+ NocoDB - Verify Email
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Email verified successfully!
+
+
+ {{errMsg}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/nocodb/src/lib/controllers/userController/ui/auth/resetPassword.ts b/packages/nocodb/src/lib/controllers/userController/ui/auth/resetPassword.ts
new file mode 100644
index 0000000000..514fc6d739
--- /dev/null
+++ b/packages/nocodb/src/lib/controllers/userController/ui/auth/resetPassword.ts
@@ -0,0 +1,108 @@
+export default `
+
+
+ NocoDB - Reset Password
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Password reset successful!
+
+
+
+
+
+
+
+
+
+
+
+ RESET PASSWORD
+
+
+
+ Not a valid url
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/forgotPassword.ts b/packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/forgotPassword.ts
new file mode 100644
index 0000000000..afb2f5849a
--- /dev/null
+++ b/packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/forgotPassword.ts
@@ -0,0 +1,171 @@
+export default `
+
+
+
+
+ Simple Transactional Email
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hi,
+ To change your NocoDB account password click the following link.
+
+ Thanks regards NocoDB.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/invite.ts b/packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/invite.ts
new file mode 100644
index 0000000000..fc81f9409e
--- /dev/null
+++ b/packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/invite.ts
@@ -0,0 +1,208 @@
+export default `
+
+
+
+
+ Simple Transactional Email
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hi,
+
+ I invited you to be "<%- roles -%>" of the NocoDB project "<%- projectName %>".
+ Click the button below to to accept my invitation.
+
+
+ Thanks regards <%- adminEmail %>.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/verify.ts b/packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/verify.ts
new file mode 100644
index 0000000000..11702cc659
--- /dev/null
+++ b/packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/verify.ts
@@ -0,0 +1,207 @@
+export default `
+
+
+
+
+ Simple Transactional Email
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hi,
+
+ Please verify your email address by clicking the following button.
+
+
+ Thanks regards NocoDB.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/nocodb/src/lib/controllers/userController/userApis.ts b/packages/nocodb/src/lib/controllers/userController/userApis.ts
new file mode 100644
index 0000000000..367eb4591b
--- /dev/null
+++ b/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) {
+ 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 =>
+ 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 =>
+ 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 {
+ res.json(req?.session?.passport?.user ?? {});
+}
+
+async function passwordChange(req: Request, res): Promise {
+ 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, res): Promise {
+ 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 {
+ await userService.tokenValidate({
+ token: req.params.tokenId,
+ });
+ res.json(true);
+}
+
+async function passwordReset(req, res): Promise {
+ await userService.passwordReset({
+ token: req.params.tokenId,
+ body: req.body,
+ req,
+ });
+
+ res.json({ msg: 'Password reset successful' });
+}
+
+async function emailVerification(req, res): Promise {
+ await userService.emailVerification({
+ token: req.params.tokenId,
+ req,
+ });
+
+ res.json({ msg: 'Email verified successfully' });
+}
+
+async function refreshToken(req, res): Promise {
+ 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 {
+ 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 };
diff --git a/packages/nocodb/src/lib/services/index.ts b/packages/nocodb/src/lib/services/index.ts
index dbbbdacc65..d0fe29bdcf 100644
--- a/packages/nocodb/src/lib/services/index.ts
+++ b/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';
diff --git a/packages/nocodb/src/lib/services/userService/helpers.ts b/packages/nocodb/src/lib/services/userService/helpers.ts
new file mode 100644
index 0000000000..4cfe0bd687
--- /dev/null
+++ b/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');
+}
diff --git a/packages/nocodb/src/lib/services/userService/index.ts b/packages/nocodb/src/lib/services/userService/index.ts
new file mode 100644
index 0000000000..73b9a9948c
--- /dev/null
+++ b/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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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'
+
diff --git a/packages/nocodb/src/lib/services/userService/initAdminFromEnv.ts b/packages/nocodb/src/lib/services/userService/initAdminFromEnv.ts
new file mode 100644
index 0000000000..8f9bfdce74
--- /dev/null
+++ b/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);
+ }
+ }
+}