|
|
|
@ -1,82 +1,24 @@
|
|
|
|
|
import { Request, Response } from 'express'; |
|
|
|
|
import { TableType, validatePassword } from 'nocodb-sdk'; |
|
|
|
|
import { OrgUserRoles } from 'nocodb-sdk'; |
|
|
|
|
import { NC_APP_SETTINGS } from '../../../constants'; |
|
|
|
|
import Store from '../../../models/Store'; |
|
|
|
|
import { T } 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'
|
|
|
|
|
T.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, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
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 { userService } from '../../services'; |
|
|
|
|
|
|
|
|
|
export async function signup(req: Request, res: Response<TableType>) { |
|
|
|
|
const { |
|
|
|
@ -141,7 +83,7 @@ export async function signup(req: Request, res: Response<TableType>) {
|
|
|
|
|
NcError.badRequest('User already exist'); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
await registerNewUserIfAllowed({ |
|
|
|
|
await userService.registerNewUserIfAllowed({ |
|
|
|
|
firstname, |
|
|
|
|
lastname, |
|
|
|
|
email, |
|
|
|
@ -171,7 +113,7 @@ export async function signup(req: Request, res: Response<TableType>) {
|
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
await promisify((req as any).login.bind(req))(user); |
|
|
|
|
const refreshToken = randomTokenString(); |
|
|
|
|
const refreshToken = userService.randomTokenString(); |
|
|
|
|
await User.update(user.id, { |
|
|
|
|
refresh_token: refreshToken, |
|
|
|
|
email: user.email, |
|
|
|
@ -190,7 +132,7 @@ export async function signup(req: Request, res: Response<TableType>) {
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
res.json({ |
|
|
|
|
token: genJwt(user, Noco.getConfig()), |
|
|
|
|
token: userService.genJwt(user, Noco.getConfig()), |
|
|
|
|
} as any); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -214,10 +156,10 @@ async function successfulSignIn({
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
await promisify((req as any).login.bind(req))(user); |
|
|
|
|
const refreshToken = randomTokenString(); |
|
|
|
|
const refreshToken = userService.randomTokenString(); |
|
|
|
|
|
|
|
|
|
if (!user.token_version) { |
|
|
|
|
user.token_version = randomTokenString(); |
|
|
|
|
user.token_version = userService.randomTokenString(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
await User.update(user.id, { |
|
|
|
@ -236,7 +178,7 @@ async function successfulSignIn({
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
res.json({ |
|
|
|
|
token: genJwt(user, Noco.getConfig()), |
|
|
|
|
token: userService.genJwt(user, Noco.getConfig()), |
|
|
|
|
} as any); |
|
|
|
|
} catch (e) { |
|
|
|
|
console.log(e); |
|
|
|
@ -279,15 +221,13 @@ async function googleSignin(req, res, next) {
|
|
|
|
|
)(req, res, next); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const REFRESH_TOKEN_COOKIE_KEY = 'refresh_token'; |
|
|
|
|
|
|
|
|
|
function setTokenCookie(res, token): void { |
|
|
|
|
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_COOKIE_KEY, token, cookieOptions); |
|
|
|
|
res.cookie('refresh_token', token, cookieOptions); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function me(req, res): Promise<any> { |
|
|
|
@ -298,184 +238,47 @@ async function passwordChange(req: Request<any, any>, res): Promise<any> {
|
|
|
|
|
if (!(req as any).isAuthenticated()) { |
|
|
|
|
NcError.forbidden('Not allowed'); |
|
|
|
|
} |
|
|
|
|
const { currentPassword, newPassword } = req.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((req as any).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: (req as any).clientIp, |
|
|
|
|
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> { |
|
|
|
|
const _email = req.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, |
|
|
|
|
await userService.passwordForgot({ |
|
|
|
|
siteUrl: (req as any).ncSiteUrl, |
|
|
|
|
body: req.body, |
|
|
|
|
req, |
|
|
|
|
}); |
|
|
|
|
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 : ${ |
|
|
|
|
(req as any).ncSiteUrl |
|
|
|
|
}/auth/password/reset/${token}.`,
|
|
|
|
|
html: ejs.render(template, { |
|
|
|
|
resetLink: (req as any).ncSiteUrl + `/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: (req as any).clientIp, |
|
|
|
|
}); |
|
|
|
|
} else { |
|
|
|
|
return NcError.badRequest('Your email has not been registered.'); |
|
|
|
|
} |
|
|
|
|
res.json({ msg: 'Please check your email to reset the password' }); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function tokenValidate(req, res): Promise<any> { |
|
|
|
|
const token = req.params.tokenId; |
|
|
|
|
|
|
|
|
|
const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, { |
|
|
|
|
reset_password_token: token, |
|
|
|
|
await userService.tokenValidate({ |
|
|
|
|
token: req.params.tokenId, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
if (!user || !user.email) { |
|
|
|
|
NcError.badRequest('Invalid reset url'); |
|
|
|
|
} |
|
|
|
|
if (new Date(user.reset_password_expires) < new Date()) { |
|
|
|
|
NcError.badRequest('Password reset url expired'); |
|
|
|
|
} |
|
|
|
|
res.json(true); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function passwordReset(req, res): Promise<any> { |
|
|
|
|
const token = req.params.tokenId; |
|
|
|
|
|
|
|
|
|
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(req.body.password); |
|
|
|
|
if (!valid) { |
|
|
|
|
NcError.badRequest(`Password : ${error}`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const salt = await promisify(bcrypt.genSalt)(10); |
|
|
|
|
const password = await promisify(bcrypt.hash)(req.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, |
|
|
|
|
await userService.passwordReset({ |
|
|
|
|
token: req.params.tokenId, |
|
|
|
|
body: req.body, |
|
|
|
|
req, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
res.json({ msg: 'Password reset successful' }); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function emailVerification(req, res): Promise<any> { |
|
|
|
|
const token = req.params.tokenId; |
|
|
|
|
|
|
|
|
|
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, |
|
|
|
|
await userService.emailVerification({ |
|
|
|
|
token: req.params.tokenId, |
|
|
|
|
req, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
res.json({ msg: 'Email verified successfully' }); |
|
|
|
@ -487,15 +290,13 @@ async function refreshToken(req, res): Promise<any> {
|
|
|
|
|
return res.status(400).json({ msg: 'Missing refresh token' }); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const user = await User.getByRefreshToken( |
|
|
|
|
req.cookies[REFRESH_TOKEN_COOKIE_KEY] |
|
|
|
|
); |
|
|
|
|
const user = await User.getByRefreshToken(req.cookies.refresh_token); |
|
|
|
|
|
|
|
|
|
if (!user) { |
|
|
|
|
return res.status(400).json({ msg: 'Invalid refresh token' }); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const refreshToken = randomTokenString(); |
|
|
|
|
const refreshToken = userService.randomTokenString(); |
|
|
|
|
|
|
|
|
|
await User.update(user.id, { |
|
|
|
|
email: user.email, |
|
|
|
@ -505,7 +306,7 @@ async function refreshToken(req, res): Promise<any> {
|
|
|
|
|
setTokenCookie(res, refreshToken); |
|
|
|
|
|
|
|
|
|
res.json({ |
|
|
|
|
token: genJwt(user, Noco.getConfig()), |
|
|
|
|
token: userService.genJwt(user, Noco.getConfig()), |
|
|
|
|
} as any); |
|
|
|
|
} catch (e) { |
|
|
|
|
return res.status(400).json({ msg: e.message }); |
|
|
|
@ -526,30 +327,6 @@ async function renderPasswordReset(req, res): Promise<any> {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// clear refresh token cookie and update user refresh token to null
|
|
|
|
|
const signout = async (req, res): Promise<any> => { |
|
|
|
|
const resBody = { msg: 'Success' }; |
|
|
|
|
if (!req.cookies[REFRESH_TOKEN_COOKIE_KEY]) { |
|
|
|
|
return res.json(resBody); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const user = await User.getByRefreshToken( |
|
|
|
|
req.cookies[REFRESH_TOKEN_COOKIE_KEY] |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if (!user) { |
|
|
|
|
return res.json(resBody); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
res.clearCookie(REFRESH_TOKEN_COOKIE_KEY); |
|
|
|
|
|
|
|
|
|
await User.update(user.id, { |
|
|
|
|
refresh_token: null, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
res.json(resBody); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const mapRoutes = (router) => { |
|
|
|
|
// todo: old api - /auth/signup?tool=1
|
|
|
|
|
router.post( |
|
|
|
@ -562,7 +339,6 @@ const mapRoutes = (router) => {
|
|
|
|
|
getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'), |
|
|
|
|
catchError(signin) |
|
|
|
|
); |
|
|
|
|
router.post('/auth/user/signout', catchError(signout)); |
|
|
|
|
router.get('/auth/user/me', extractProjectIdAndAuthenticate, catchError(me)); |
|
|
|
|
router.post( |
|
|
|
|
'/auth/password/forgot', |
|
|
|
@ -651,7 +427,6 @@ const mapRoutes = (router) => {
|
|
|
|
|
getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'), |
|
|
|
|
catchError(signin) |
|
|
|
|
); |
|
|
|
|
router.post('/api/v1/auth/user/signout', catchError(signout)); |
|
|
|
|
router.get( |
|
|
|
|
'/api/v1/auth/user/me', |
|
|
|
|
extractProjectIdAndAuthenticate, |
|
|
|
@ -683,4 +458,4 @@ const mapRoutes = (router) => {
|
|
|
|
|
// respond with password reset page
|
|
|
|
|
router.get('/auth/password/reset/:tokenId', catchError(renderPasswordReset)); |
|
|
|
|
}; |
|
|
|
|
export { mapRoutes as userApis }; |
|
|
|
|
export { mapRoutes as userController }; |
|
|
|
|