mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
9 changed files with 1524 additions and 12 deletions
@ -0,0 +1,33 @@ |
|||||||
|
import crypto from 'crypto'; |
||||||
|
import * as jwt from 'jsonwebtoken'; |
||||||
|
import type User from '../../models/User'; |
||||||
|
import type { NcConfig } from '../../../interface/config'; |
||||||
|
import type { Response } from 'express'; |
||||||
|
|
||||||
|
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'); |
||||||
|
} |
||||||
|
|
||||||
|
export 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); |
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
export default `<!DOCTYPE html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>NocoDB - Verify Email</title> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/fonts.roboto.css" rel="stylesheet"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/materialdesignicons.5.x.min.css" rel="stylesheet"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/vuetify.2.x.min.css" rel="stylesheet"> |
||||||
|
<script src="<%- ncPublicUrl %>/js/vue.2.6.14.min.js"></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="app"> |
||||||
|
<v-app> |
||||||
|
<v-container> |
||||||
|
<v-row class="justify-center"> |
||||||
|
<v-col class="col-12 col-md-6"> |
||||||
|
<v-alert v-if="valid" type="success"> |
||||||
|
Email verified successfully! |
||||||
|
</v-alert> |
||||||
|
<v-alert v-else-if="errMsg" type="error"> |
||||||
|
{{errMsg}} |
||||||
|
</v-alert> |
||||||
|
|
||||||
|
<template v-else> |
||||||
|
|
||||||
|
<v-skeleton-loader type="heading"></v-skeleton-loader> |
||||||
|
|
||||||
|
</template> |
||||||
|
</v-col> |
||||||
|
</v-row> |
||||||
|
</v-container> |
||||||
|
</v-app> |
||||||
|
</div> |
||||||
|
<script src="<%- ncPublicUrl %>/js/vuetify.2.x.min.js"></script> |
||||||
|
<script src="<%- ncPublicUrl %>/js/axios.0.19.2.min.js"></script> |
||||||
|
|
||||||
|
<script> |
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
vuetify: new Vuetify(), |
||||||
|
data: { |
||||||
|
valid: null, |
||||||
|
errMsg: null, |
||||||
|
validForm: false, |
||||||
|
token: <%- token %>, |
||||||
|
greeting: 'Password Reset', |
||||||
|
formdata: { |
||||||
|
password: '', |
||||||
|
newPassword: '' |
||||||
|
}, |
||||||
|
success: false |
||||||
|
}, |
||||||
|
methods: {}, |
||||||
|
async created() { |
||||||
|
try { |
||||||
|
const valid = (await axios.post('<%- baseUrl %>/api/v1/auth/email/validate/' + this.token)).data; |
||||||
|
this.valid = !!valid; |
||||||
|
} catch (e) { |
||||||
|
this.valid = false; |
||||||
|
if(e.response && e.response.data && e.response.data.msg){ |
||||||
|
this.errMsg = e.response.data.msg; |
||||||
|
}else{ |
||||||
|
this.errMsg = 'Some error occurred'; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html>`;
|
@ -0,0 +1,108 @@ |
|||||||
|
export default `<!DOCTYPE html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>NocoDB - Reset Password</title> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/fonts.roboto.css" rel="stylesheet"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/materialdesignicons.5.x.min.css" rel="stylesheet"> |
||||||
|
<link href="<%- ncPublicUrl %>/css/vuetify.2.x.min.css" rel="stylesheet"> |
||||||
|
<script src="<%- ncPublicUrl %>/js/vue.2.6.14.min.js"></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="app"> |
||||||
|
<v-app> |
||||||
|
<v-container> |
||||||
|
<v-row class="justify-center"> |
||||||
|
<v-col class="col-12 col-md-6"> |
||||||
|
<v-alert v-if="success" type="success"> |
||||||
|
Password reset successful! |
||||||
|
</v-alert> |
||||||
|
<template v-else> |
||||||
|
|
||||||
|
<v-form ref="form" v-model="validForm" v-if="valid === true" ref="formType" class="ma-auto" |
||||||
|
lazy-validation> |
||||||
|
|
||||||
|
|
||||||
|
<v-text-field |
||||||
|
name="input-10-2" |
||||||
|
label="New password" |
||||||
|
type="password" |
||||||
|
v-model="formdata.password" |
||||||
|
:rules="[v => !!v || 'Password is required']" |
||||||
|
></v-text-field> |
||||||
|
|
||||||
|
<v-text-field |
||||||
|
name="input-10-2" |
||||||
|
type="password" |
||||||
|
label="Confirm new password" |
||||||
|
v-model="formdata.newPassword" |
||||||
|
:rules="[v => !!v || 'Password is required', v => v === formdata.password || 'Password mismatch']" |
||||||
|
></v-text-field> |
||||||
|
|
||||||
|
<v-btn |
||||||
|
:disabled="!validForm" |
||||||
|
large |
||||||
|
@click="resetPassword" |
||||||
|
> |
||||||
|
RESET PASSWORD |
||||||
|
</v-btn> |
||||||
|
|
||||||
|
</v-form> |
||||||
|
<div v-else-if="valid === false">Not a valid url</div> |
||||||
|
<div v-else> |
||||||
|
<v-skeleton-loader type="actions"></v-skeleton-loader> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</v-col> |
||||||
|
</v-row> |
||||||
|
</v-container> |
||||||
|
</v-app> |
||||||
|
</div> |
||||||
|
<script src="<%- ncPublicUrl %>/js/vuetify.2.x.min.js"></script> |
||||||
|
<script src="<%- ncPublicUrl %>/js/axios.0.19.2.min.js"></script> |
||||||
|
|
||||||
|
<script> |
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
vuetify: new Vuetify(), |
||||||
|
data: { |
||||||
|
valid: null, |
||||||
|
validForm: false, |
||||||
|
token: <%- token %>, |
||||||
|
greeting: 'Password Reset', |
||||||
|
formdata: { |
||||||
|
password: '', |
||||||
|
newPassword: '' |
||||||
|
}, |
||||||
|
success: false |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
async resetPassword() { |
||||||
|
if (this.$refs.form.validate()) { |
||||||
|
try { |
||||||
|
const res = await axios.post('<%- baseUrl %>api/v1/db/auth/password/reset/' + this.token, { |
||||||
|
...this.formdata |
||||||
|
}); |
||||||
|
this.success = true; |
||||||
|
} catch (e) { |
||||||
|
if (e.response && e.response.data && e.response.data.msg) { |
||||||
|
alert('Failed to reset password: ' + e.response.data.msg) |
||||||
|
} else { |
||||||
|
alert('Some error occurred') |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
async created() { |
||||||
|
try { |
||||||
|
const valid = (await axios.post('<%- baseUrl %>api/v1/db/auth/token/validate/' + this.token)).data; |
||||||
|
this.valid = !!valid; |
||||||
|
} catch (e) { |
||||||
|
this.valid = false; |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html>`;
|
@ -0,0 +1,171 @@ |
|||||||
|
export default `<!doctype html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta name="viewport" content="width=device-width"> |
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
||||||
|
<title>Simple Transactional Email</title> |
||||||
|
<style> |
||||||
|
@media only screen and (max-width: 620px) { |
||||||
|
table[class=body] h1 { |
||||||
|
font-size: 28px !important; |
||||||
|
margin-bottom: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] p, |
||||||
|
table[class=body] ul, |
||||||
|
table[class=body] ol, |
||||||
|
table[class=body] td, |
||||||
|
table[class=body] span, |
||||||
|
table[class=body] a { |
||||||
|
font-size: 16px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .wrapper, |
||||||
|
table[class=body] .article { |
||||||
|
padding: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .content { |
||||||
|
padding: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .container { |
||||||
|
padding: 0 !important; |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .main { |
||||||
|
border-left-width: 0 !important; |
||||||
|
border-radius: 0 !important; |
||||||
|
border-right-width: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn table { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn a { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .img-responsive { |
||||||
|
height: auto !important; |
||||||
|
max-width: 100% !important; |
||||||
|
width: auto !important; |
||||||
|
} |
||||||
|
} |
||||||
|
@media all { |
||||||
|
.ExternalClass { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.ExternalClass, |
||||||
|
.ExternalClass p, |
||||||
|
.ExternalClass span, |
||||||
|
.ExternalClass font, |
||||||
|
.ExternalClass td, |
||||||
|
.ExternalClass div { |
||||||
|
line-height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.apple-link a { |
||||||
|
color: inherit !important; |
||||||
|
font-family: inherit !important; |
||||||
|
font-size: inherit !important; |
||||||
|
font-weight: inherit !important; |
||||||
|
line-height: inherit !important; |
||||||
|
text-decoration: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
#MessageViewBody a { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
font-size: inherit; |
||||||
|
font-family: inherit; |
||||||
|
font-weight: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary table td:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary a:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
border-color: #34495e !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"> |
||||||
|
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;" width="100%" bgcolor="#f6f6f6"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" width="580" valign="top"> |
||||||
|
<div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;"> |
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER --> |
||||||
|
<table role="presentation" class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" width="100%"> |
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA --> |
||||||
|
<tr> |
||||||
|
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">Hi,</p> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">To change your NocoDB account password click the following link.</p> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" width="100%"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" valign="top" align="center" bgcolor="#1088ff"> <a href="<%- resetLink %>" target="_blank" style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Reset Password</a> </td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">Thanks regards NocoDB.</p> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA --> |
||||||
|
</table> |
||||||
|
<!-- END CENTERED WHITE CONTAINER --> |
||||||
|
|
||||||
|
<!-- START FOOTER --> |
||||||
|
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%"> |
||||||
|
<tr> |
||||||
|
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" valign="top" align="center"> |
||||||
|
<span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;"></span> |
||||||
|
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" valign="top" align="center"> |
||||||
|
<a href="http://nocodb.com/">NocoDB</a> |
||||||
|
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<!-- END FOOTER --> |
||||||
|
|
||||||
|
</div> |
||||||
|
</td> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
`;
|
@ -0,0 +1,208 @@ |
|||||||
|
export default `<!doctype html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta name="viewport" content="width=device-width"> |
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
||||||
|
<title>Simple Transactional Email</title> |
||||||
|
<style> |
||||||
|
@media only screen and (max-width: 620px) { |
||||||
|
table[class=body] h1 { |
||||||
|
font-size: 28px !important; |
||||||
|
margin-bottom: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] p, |
||||||
|
table[class=body] ul, |
||||||
|
table[class=body] ol, |
||||||
|
table[class=body] td, |
||||||
|
table[class=body] span, |
||||||
|
table[class=body] a { |
||||||
|
font-size: 16px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .wrapper, |
||||||
|
table[class=body] .article { |
||||||
|
padding: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .content { |
||||||
|
padding: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .container { |
||||||
|
padding: 0 !important; |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .main { |
||||||
|
border-left-width: 0 !important; |
||||||
|
border-radius: 0 !important; |
||||||
|
border-right-width: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn table { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn a { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .img-responsive { |
||||||
|
height: auto !important; |
||||||
|
max-width: 100% !important; |
||||||
|
width: auto !important; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@media all { |
||||||
|
.ExternalClass { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.ExternalClass, |
||||||
|
.ExternalClass p, |
||||||
|
.ExternalClass span, |
||||||
|
.ExternalClass font, |
||||||
|
.ExternalClass td, |
||||||
|
.ExternalClass div { |
||||||
|
line-height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.apple-link a { |
||||||
|
color: inherit !important; |
||||||
|
font-family: inherit !important; |
||||||
|
font-size: inherit !important; |
||||||
|
font-weight: inherit !important; |
||||||
|
line-height: inherit !important; |
||||||
|
text-decoration: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
#MessageViewBody a { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
font-size: inherit; |
||||||
|
font-family: inherit; |
||||||
|
font-weight: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary table td:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary a:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
border-color: #34495e !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body class="" |
||||||
|
style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"> |
||||||
|
<span class="preheader" |
||||||
|
style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;" |
||||||
|
width="100%" bgcolor="#f6f6f6"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
<td class="container" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" |
||||||
|
width="580" valign="top"> |
||||||
|
<div class="content" |
||||||
|
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;"> |
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER --> |
||||||
|
<table role="presentation" class="main" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA --> |
||||||
|
<tr> |
||||||
|
<td class="wrapper" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" |
||||||
|
valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" |
||||||
|
valign="top"> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Hi,</p> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
I invited you to be "<%- roles -%>" of the NocoDB project "<%- projectName %>". |
||||||
|
Click the button below to to accept my invitation.</p> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
class="btn btn-primary" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td align="left" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" |
||||||
|
valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" |
||||||
|
cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" |
||||||
|
valign="top" align="center" bgcolor="#1088ff"><a |
||||||
|
href="<%- signupLink %>" target="_blank" |
||||||
|
style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Signup</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Thanks regards <%- adminEmail %>.</p> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA --> |
||||||
|
</table> |
||||||
|
<!-- END CENTERED WHITE CONTAINER --> |
||||||
|
|
||||||
|
<!-- START FOOTER --> |
||||||
|
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tr> |
||||||
|
<td class="content-block" |
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
||||||
|
valign="top" align="center"> |
||||||
|
<span class="apple-link" |
||||||
|
style="color: #999999; font-size: 12px; text-align: center;"></span> |
||||||
|
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<td class="content-block powered-by" |
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
||||||
|
valign="top" align="center"> |
||||||
|
<a href="http://nocodb.com/">NocoDB</a> |
||||||
|
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<!-- END FOOTER --> |
||||||
|
|
||||||
|
</div> |
||||||
|
</td> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
`;
|
@ -0,0 +1,207 @@ |
|||||||
|
export default `<!doctype html>
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta name="viewport" content="width=device-width"> |
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
||||||
|
<title>Simple Transactional Email</title> |
||||||
|
<style> |
||||||
|
@media only screen and (max-width: 620px) { |
||||||
|
table[class=body] h1 { |
||||||
|
font-size: 28px !important; |
||||||
|
margin-bottom: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] p, |
||||||
|
table[class=body] ul, |
||||||
|
table[class=body] ol, |
||||||
|
table[class=body] td, |
||||||
|
table[class=body] span, |
||||||
|
table[class=body] a { |
||||||
|
font-size: 16px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .wrapper, |
||||||
|
table[class=body] .article { |
||||||
|
padding: 10px !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .content { |
||||||
|
padding: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .container { |
||||||
|
padding: 0 !important; |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .main { |
||||||
|
border-left-width: 0 !important; |
||||||
|
border-radius: 0 !important; |
||||||
|
border-right-width: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn table { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .btn a { |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
table[class=body] .img-responsive { |
||||||
|
height: auto !important; |
||||||
|
max-width: 100% !important; |
||||||
|
width: auto !important; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@media all { |
||||||
|
.ExternalClass { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.ExternalClass, |
||||||
|
.ExternalClass p, |
||||||
|
.ExternalClass span, |
||||||
|
.ExternalClass font, |
||||||
|
.ExternalClass td, |
||||||
|
.ExternalClass div { |
||||||
|
line-height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.apple-link a { |
||||||
|
color: inherit !important; |
||||||
|
font-family: inherit !important; |
||||||
|
font-size: inherit !important; |
||||||
|
font-weight: inherit !important; |
||||||
|
line-height: inherit !important; |
||||||
|
text-decoration: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
#MessageViewBody a { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
font-size: inherit; |
||||||
|
font-family: inherit; |
||||||
|
font-weight: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary table td:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary a:hover { |
||||||
|
background-color: #34495e !important; |
||||||
|
border-color: #34495e !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body class="" |
||||||
|
style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"> |
||||||
|
<span class="preheader" |
||||||
|
style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;" |
||||||
|
width="100%" bgcolor="#f6f6f6"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
<td class="container" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;" |
||||||
|
width="580" valign="top"> |
||||||
|
<div class="content" |
||||||
|
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;"> |
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER --> |
||||||
|
<table role="presentation" class="main" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA --> |
||||||
|
<tr> |
||||||
|
<td class="wrapper" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" |
||||||
|
valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" |
||||||
|
valign="top"> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Hi,</p> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Please verify your email address by clicking the following button.</p> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
class="btn btn-primary" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td align="left" |
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;" |
||||||
|
valign="top"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" |
||||||
|
cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;" |
||||||
|
valign="top" align="center" bgcolor="#1088ff"><a |
||||||
|
href="<%- verifyLink %>" target="_blank" |
||||||
|
style="border: solid 1px rgb(23, 139, 255); border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: rgb(23, 139, 255); border-color: #3498db; color: #ffffff;">Verify</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;"> |
||||||
|
Thanks regards NocoDB.</p> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA --> |
||||||
|
</table> |
||||||
|
<!-- END CENTERED WHITE CONTAINER --> |
||||||
|
|
||||||
|
<!-- START FOOTER --> |
||||||
|
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;"> |
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" |
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" |
||||||
|
width="100%"> |
||||||
|
<tr> |
||||||
|
<td class="content-block" |
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
||||||
|
valign="top" align="center"> |
||||||
|
<span class="apple-link" |
||||||
|
style="color: #999999; font-size: 12px; text-align: center;"></span> |
||||||
|
<!-- <br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<td class="content-block powered-by" |
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;" |
||||||
|
valign="top" align="center"> |
||||||
|
<a href="http://nocodb.com/">NocoDB</a> |
||||||
|
<!-- Powered by <a href="http://htmlemail.io">HTMLemail</a>.--> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<!-- END FOOTER --> |
||||||
|
|
||||||
|
</div> |
||||||
|
</td> |
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
`;
|
@ -1,10 +1,249 @@ |
|||||||
import { Controller, Get, Request, UseGuards } from '@nestjs/common' |
import { |
||||||
import { AuthGuard } from '@nestjs/passport' |
Body, |
||||||
import { UsersService } from './users.service' |
Controller, |
||||||
|
Get, |
||||||
|
Param, |
||||||
|
Post, |
||||||
|
Request, |
||||||
|
Response, |
||||||
|
UseGuards, |
||||||
|
} from '@nestjs/common'; |
||||||
|
import { promisify } from 'util'; |
||||||
|
import { NcError } from '../../helpers/catchError'; |
||||||
|
import { Acl } from '../../middlewares/extract-project-id/extract-project-id.middleware'; |
||||||
|
import Noco from '../../Noco'; |
||||||
|
import extractRolesObj from '../../utils/extractRolesObj'; |
||||||
|
import { genJwt, randomTokenString, setTokenCookie } from './helpers'; |
||||||
|
import { UsersService } from './users.service'; |
||||||
|
|
||||||
|
import * as ejs from 'ejs'; |
||||||
|
import { Audit, User } from 'src/models'; |
||||||
|
import { AuthGuard } from '@nestjs/passport'; |
||||||
@Controller() |
@Controller() |
||||||
export class UsersController { |
export class UsersController { |
||||||
constructor(private readonly usersService: UsersService) { |
constructor(private readonly usersService: UsersService) {} |
||||||
|
|
||||||
|
@Post([ |
||||||
|
'/auth/user/signup', |
||||||
|
'/api/v1/db/auth/user/signup', |
||||||
|
'/api/v1/auth/user/signup', |
||||||
|
]) |
||||||
|
async signup(@Request() req: any, @Request() res: any): Promise<any> { |
||||||
|
return await this.usersService.signup({ |
||||||
|
body: req.body, |
||||||
|
req, |
||||||
|
res, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Post([ |
||||||
|
'/auth/token/refresh', |
||||||
|
'/api/v1/db/auth/token/refresh', |
||||||
|
'/api/v1/auth/token/refresh', |
||||||
|
]) |
||||||
|
async refreshToken(@Request() req: any, @Request() res: any): Promise<any> { |
||||||
|
return await this.usersService.refreshToken({ |
||||||
|
body: req.body, |
||||||
|
req, |
||||||
|
res, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async 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 = randomTokenString(); |
||||||
|
|
||||||
|
if (!user.token_version) { |
||||||
|
user.token_version = 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; |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
|
@Post([ |
||||||
|
'/auth/user/signin', |
||||||
|
'/api/v1/db/auth/user/signin', |
||||||
|
'/api/v1/auth/user/signin', |
||||||
|
]) |
||||||
|
@UseGuards(AuthGuard('local')) |
||||||
|
async signin(@Request() req) { |
||||||
|
return this.usersService.login(req.user); |
||||||
|
} |
||||||
|
|
||||||
|
@Post(`/auth/google/genTokenByCode`) |
||||||
|
async googleSignin(req, res, next) { |
||||||
|
// todo
|
||||||
|
/* 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);*/ |
||||||
|
} |
||||||
|
|
||||||
|
@Get('/auth/google') |
||||||
|
googleAuthenticate() { |
||||||
|
/* passport.authenticate('google', { |
||||||
|
scope: ['profile', 'email'], |
||||||
|
state: req.query.state, |
||||||
|
callbackURL: req.ncSiteUrl + Noco.getConfig().dashboardPath, |
||||||
|
})(req, res, next)*/ |
||||||
|
} |
||||||
|
|
||||||
|
@Get(['/auth/user/me', '/api/v1/db/auth/user/me', '/api/v1/auth/user/me']) |
||||||
|
@UseGuards(AuthGuard('jwt')) |
||||||
|
async me(@Request() req) { |
||||||
|
const user = { |
||||||
|
...req.user, |
||||||
|
roles: extractRolesObj(req.user.roles), |
||||||
|
}; |
||||||
|
return user; |
||||||
|
} |
||||||
|
|
||||||
|
@Post([ |
||||||
|
'/user/password/change', |
||||||
|
'/api/v1/db/auth/password/change', |
||||||
|
'/api/v1/auth/password/change', |
||||||
|
]) |
||||||
|
@Acl('passwordChange') |
||||||
|
async passwordChange(@Request() req: any, @Body() body: any): Promise<any> { |
||||||
|
if (!(req as any).isAuthenticated()) { |
||||||
|
NcError.forbidden('Not allowed'); |
||||||
|
} |
||||||
|
|
||||||
|
await this.usersService.passwordChange({ |
||||||
|
user: req['user'], |
||||||
|
req, |
||||||
|
body: req.body, |
||||||
|
}); |
||||||
|
|
||||||
|
return { msg: 'Password has been updated successfully' }; |
||||||
|
} |
||||||
|
|
||||||
|
@Post([ |
||||||
|
'/auth/password/forgot', |
||||||
|
'/api/v1/db/auth/password/forgot', |
||||||
|
'/api/v1/auth/password/forgot', |
||||||
|
]) |
||||||
|
async passwordForgot(@Request() req: any, @Body() body: any): Promise<any> { |
||||||
|
await this.usersService.passwordForgot({ |
||||||
|
siteUrl: (req as any).ncSiteUrl, |
||||||
|
body: req.body, |
||||||
|
req, |
||||||
|
}); |
||||||
|
|
||||||
|
return { msg: 'Please check your email to reset the password' }; |
||||||
|
} |
||||||
|
|
||||||
|
@Post([ |
||||||
|
'/auth/token/validate/:tokenId', |
||||||
|
'/api/v1/db/auth/token/validate/:tokenId', |
||||||
|
'/api/v1/auth/token/validate/:tokenId', |
||||||
|
]) |
||||||
|
async tokenValidate(@Param('tokenId') tokenId: string): Promise<any> { |
||||||
|
await this.usersService.tokenValidate({ |
||||||
|
token: tokenId, |
||||||
|
}); |
||||||
|
return { msg: 'Token has been validated successfully' }; |
||||||
|
} |
||||||
|
|
||||||
|
@Post([ |
||||||
|
'/auth/password/reset/:tokenId', |
||||||
|
'/api/v1/db/auth/password/reset/:tokenId', |
||||||
|
'/api/v1/auth/password/reset/:tokenId', |
||||||
|
]) |
||||||
|
async passwordReset( |
||||||
|
@Request() req: any, |
||||||
|
@Param('tokenId') tokenId: string, |
||||||
|
@Body() body: any, |
||||||
|
): Promise<any> { |
||||||
|
await this.usersService.passwordReset({ |
||||||
|
token: tokenId, |
||||||
|
body: body, |
||||||
|
req, |
||||||
|
}); |
||||||
|
|
||||||
|
return { msg: 'Password has been reset successfully' }; |
||||||
|
} |
||||||
|
|
||||||
|
@Post([ |
||||||
|
'/api/v1/db/auth/email/validate/:tokenId', |
||||||
|
'/api/v1/auth/email/validate/:tokenId', |
||||||
|
]) |
||||||
|
async emailVerification( |
||||||
|
@Request() req: any, |
||||||
|
@Param('tokenId') tokenId: string, |
||||||
|
): Promise<any> { |
||||||
|
await this.usersService.emailVerification({ |
||||||
|
token: tokenId, |
||||||
|
req, |
||||||
|
}); |
||||||
|
|
||||||
|
return { msg: 'Email has been verified successfully' }; |
||||||
|
} |
||||||
|
|
||||||
|
@Get([ |
||||||
|
'/api/v1/db/auth/password/reset/:tokenId', |
||||||
|
'/auth/password/reset/:tokenId', |
||||||
|
]) |
||||||
|
async renderPasswordReset( |
||||||
|
@Request() req: any, |
||||||
|
@Response() res: any, |
||||||
|
@Param('tokenId') tokenId: string, |
||||||
|
): Promise<any> { |
||||||
|
try { |
||||||
|
res.send( |
||||||
|
ejs.render((await import('./ui/auth/resetPassword')).default, { |
||||||
|
ncPublicUrl: process.env.NC_PUBLIC_URL || '', |
||||||
|
token: JSON.stringify(tokenId), |
||||||
|
baseUrl: `/`, |
||||||
|
}), |
||||||
|
); |
||||||
|
} catch (e) { |
||||||
|
return res.status(400).json({ msg: e.message }); |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
||||||
|
@ -1,21 +1,497 @@ |
|||||||
import { Injectable } from '@nestjs/common'; |
import { Injectable } from '@nestjs/common'; |
||||||
import { MetaService, MetaTable } from '../../meta/meta.service' |
import { JwtService } from '@nestjs/jwt'; |
||||||
|
import { |
||||||
|
OrgUserRoles, |
||||||
|
PasswordChangeReqType, |
||||||
|
PasswordForgotReqType, |
||||||
|
PasswordResetReqType, |
||||||
|
SignUpReqType, |
||||||
|
UserType, |
||||||
|
validatePassword, |
||||||
|
} from 'nocodb-sdk'; |
||||||
|
import { promisify } from 'util'; |
||||||
|
import { NC_APP_SETTINGS } from '../../constants'; |
||||||
|
import { validatePayload } from '../../helpers'; |
||||||
|
import { NcError } from '../../helpers/catchError'; |
||||||
|
import NcPluginMgrv2 from '../../helpers/NcPluginMgrv2'; |
||||||
|
import { randomTokenString } from '../../helpers/stringHelpers'; |
||||||
|
import { MetaService, MetaTable } from '../../meta/meta.service'; |
||||||
|
import { Audit, Store, User } from '../../models'; |
||||||
|
import { v4 as uuidv4 } from 'uuid'; |
||||||
|
import { isEmail } from 'validator'; |
||||||
|
import { T } from 'nc-help'; |
||||||
|
import * as ejs from 'ejs'; |
||||||
|
import bcrypt from 'bcryptjs'; |
||||||
|
import Noco from '../../Noco'; |
||||||
|
import { genJwt, setTokenCookie } from './helpers'; |
||||||
|
|
||||||
@Injectable() |
@Injectable() |
||||||
export class UsersService { |
export class UsersService { |
||||||
|
constructor( |
||||||
|
private metaService: MetaService, |
||||||
|
private jwtService: JwtService, |
||||||
|
) {} |
||||||
|
|
||||||
constructor(private metaService: MetaService) { |
async findOne(email: string) { |
||||||
|
const user = await this.metaService.metaGet(null, null, MetaTable.USERS, { |
||||||
|
email, |
||||||
|
}); |
||||||
|
|
||||||
|
return user; |
||||||
} |
} |
||||||
|
|
||||||
async findOne(email: string) { |
async insert(param: { |
||||||
const user = await this.metaService.metaGet(null, null, MetaTable.USERS, { email }); |
token_version: string; |
||||||
|
firstname: any; |
||||||
|
password: any; |
||||||
|
salt: any; |
||||||
|
email_verification_token: any; |
||||||
|
roles: string; |
||||||
|
email: string; |
||||||
|
lastname: any; |
||||||
|
}) { |
||||||
|
return this.metaService.metaInsert2(null, null, MetaTable.USERS, param); |
||||||
|
} |
||||||
|
|
||||||
|
async registerNewUserIfAllowed({ |
||||||
|
firstname, |
||||||
|
lastname, |
||||||
|
email, |
||||||
|
salt, |
||||||
|
password, |
||||||
|
email_verification_token, |
||||||
|
}: { |
||||||
|
firstname; |
||||||
|
lastname; |
||||||
|
email: string; |
||||||
|
salt: any; |
||||||
|
password; |
||||||
|
email_verification_token; |
||||||
|
}) { |
||||||
|
let roles: string = OrgUserRoles.CREATOR; |
||||||
|
|
||||||
return user; |
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, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async passwordChange(param: { |
||||||
|
body: PasswordChangeReqType; |
||||||
|
user: UserType; |
||||||
|
req: any; |
||||||
|
}): Promise<any> { |
||||||
|
validatePayload( |
||||||
|
'swagger.json#/components/schemas/PasswordChangeReq', |
||||||
|
param.body, |
||||||
|
); |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
async passwordForgot(param: { |
||||||
|
body: PasswordForgotReqType; |
||||||
|
siteUrl: string; |
||||||
|
req: any; |
||||||
|
}): Promise<any> { |
||||||
|
validatePayload( |
||||||
|
'swagger.json#/components/schemas/PasswordForgotReq', |
||||||
|
param.body, |
||||||
|
); |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
async 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; |
||||||
|
} |
||||||
|
|
||||||
|
async passwordReset(param: { |
||||||
|
body: PasswordResetReqType; |
||||||
|
token: string; |
||||||
|
// todo: exclude
|
||||||
|
req: any; |
||||||
|
}): Promise<any> { |
||||||
|
validatePayload( |
||||||
|
'swagger.json#/components/schemas/PasswordResetReq', |
||||||
|
param.body, |
||||||
|
); |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
async 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; |
||||||
|
} |
||||||
|
|
||||||
|
async refreshToken(param: { |
||||||
|
body: SignUpReqType; |
||||||
|
req: any; |
||||||
|
res: any; |
||||||
|
}): Promise<any> { |
||||||
|
try { |
||||||
|
if (!param.req?.cookies?.refresh_token) { |
||||||
|
NcError.badRequest(`Missing refresh token`); |
||||||
|
} |
||||||
|
|
||||||
|
const user = await User.getByRefreshToken( |
||||||
|
param.req.cookies.refresh_token, |
||||||
|
); |
||||||
|
|
||||||
|
if (!user) { |
||||||
|
NcError.badRequest(`Invalid refresh token`); |
||||||
|
} |
||||||
|
|
||||||
|
const refreshToken = randomTokenString(); |
||||||
|
|
||||||
|
await User.update(user.id, { |
||||||
|
email: user.email, |
||||||
|
refresh_token: refreshToken, |
||||||
|
}); |
||||||
|
|
||||||
|
setTokenCookie(param.res, refreshToken); |
||||||
|
|
||||||
|
return { |
||||||
|
token: genJwt(user, Noco.getConfig()), |
||||||
|
} as any; |
||||||
|
} catch (e) { |
||||||
|
NcError.badRequest(e.message); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async signup(param: { |
||||||
|
body: SignUpReqType; |
||||||
|
req: any; |
||||||
|
res: any; |
||||||
|
}): Promise<any> { |
||||||
|
validatePayload('swagger.json#/components/schemas/SignUpReq', param.body); |
||||||
|
|
||||||
|
const { |
||||||
|
email: _email, |
||||||
|
firstname, |
||||||
|
lastname, |
||||||
|
token, |
||||||
|
ignore_subscribe, |
||||||
|
} = param.req.body; |
||||||
|
|
||||||
|
let { password } = param.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) { |
||||||
|
T.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 this.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: |
||||||
|
(param.req as any).ncSiteUrl + |
||||||
|
`/email/verify/${user.email_verification_token}`, |
||||||
|
}), |
||||||
|
}); |
||||||
|
} catch (e) { |
||||||
|
console.log( |
||||||
|
'Warning : `mailSend` failed, Please configure emailClient configuration.', |
||||||
|
); |
||||||
|
} |
||||||
|
await promisify((param.req as any).login.bind(param.req))(user); |
||||||
|
|
||||||
|
const refreshToken = randomTokenString(); |
||||||
|
|
||||||
|
await User.update(user.id, { |
||||||
|
refresh_token: refreshToken, |
||||||
|
email: user.email, |
||||||
|
}); |
||||||
|
|
||||||
|
setTokenCookie(param.res, refreshToken); |
||||||
|
|
||||||
|
user = (param.req as any).user; |
||||||
|
|
||||||
|
await Audit.insert({ |
||||||
|
op_type: 'AUTHENTICATION', |
||||||
|
op_sub_type: 'SIGNUP', |
||||||
|
user: user.email, |
||||||
|
description: `signed up `, |
||||||
|
ip: (param.req as any).clientIp, |
||||||
|
}); |
||||||
|
|
||||||
|
return { |
||||||
|
token: genJwt(user, Noco.getConfig()), |
||||||
|
} as any; |
||||||
} |
} |
||||||
|
|
||||||
async insert(param: { token_version: string; firstname: any; password: any; salt: any; email_verification_token: any; roles: string; email: string; lastname: any }) { |
async login(user: any) { |
||||||
return this.metaService.metaInsert2(null, null, MetaTable.USERS, param) |
delete user.password; |
||||||
|
delete user.salt; |
||||||
|
const payload = user; |
||||||
|
return { |
||||||
|
token: this.jwtService.sign(payload), |
||||||
|
}; |
||||||
} |
} |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue