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 { AuthGuard } from '@nestjs/passport' |
||||
import { UsersService } from './users.service' |
||||
import { |
||||
Body, |
||||
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() |
||||
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 { 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() |
||||
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) { |
||||
const user = await this.metaService.metaGet(null, null, MetaTable.USERS, { email }); |
||||
async insert(param: { |
||||
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 }) { |
||||
return this.metaService.metaInsert2(null, null, MetaTable.USERS, param) |
||||
async login(user: any) { |
||||
delete user.password; |
||||
delete user.salt; |
||||
const payload = user; |
||||
return { |
||||
token: this.jwtService.sign(payload), |
||||
}; |
||||
} |
||||
} |
||||
|
Loading…
Reference in new issue