Add password reset endpoints and mail service for user activation
All checks were successful
Jenkins Production Deployment
All checks were successful
Jenkins Production Deployment
This commit is contained in:
parent
34a4a6664f
commit
8f93e1ab7d
40
package-lock.json
generated
40
package-lock.json
generated
|
@ -15,9 +15,10 @@
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.18.2",
|
||||||
"guid-typescript": "^1.0.9",
|
"guid-typescript": "^1.0.9",
|
||||||
"mariadb": "^3.0.2",
|
"mariadb": "^3.0.2",
|
||||||
|
"nodemailer": "^6.9.8",
|
||||||
"random-words": "^1.1.1",
|
"random-words": "^1.1.1",
|
||||||
"swagger-jsdoc": "^6.1.0",
|
"swagger-jsdoc": "^6.1.0",
|
||||||
"swagger-ui-express": "^4.3.0",
|
"swagger-ui-express": "^4.3.0",
|
||||||
|
@ -27,8 +28,10 @@
|
||||||
"@types/app-root-path": "^1.2.4",
|
"@types/app-root-path": "^1.2.4",
|
||||||
"@types/bcrypt": "^3.0.1",
|
"@types/bcrypt": "^3.0.1",
|
||||||
"@types/debug": "^4.1.5",
|
"@types/debug": "^4.1.5",
|
||||||
"@types/express": "^4.17.11",
|
"@types/express": "^4.17.15",
|
||||||
"@types/jest": "^28.1.3",
|
"@types/jest": "^28.1.3",
|
||||||
|
"@types/node": "^18.11.17",
|
||||||
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/random-words": "^1.1.2",
|
"@types/random-words": "^1.1.2",
|
||||||
"@types/swagger-jsdoc": "^6.0.1",
|
"@types/swagger-jsdoc": "^6.0.1",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
|
@ -39,7 +42,7 @@
|
||||||
"source-map-support": "^0.5.19",
|
"source-map-support": "^0.5.19",
|
||||||
"ts-jest": "^28.0.5",
|
"ts-jest": "^28.0.5",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"typescript": "^4.1.5"
|
"typescript": "^4.9.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
|
@ -1272,6 +1275,15 @@
|
||||||
"integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==",
|
"integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/nodemailer": {
|
||||||
|
"version": "6.4.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz",
|
||||||
|
"integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/prettier": {
|
"node_modules/@types/prettier": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
||||||
|
@ -4020,6 +4032,14 @@
|
||||||
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==",
|
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "6.9.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.8.tgz",
|
||||||
|
"integrity": "sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nopt": {
|
"node_modules/nopt": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||||
|
@ -6592,6 +6612,15 @@
|
||||||
"integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==",
|
"integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/nodemailer": {
|
||||||
|
"version": "6.4.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz",
|
||||||
|
"integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/prettier": {
|
"@types/prettier": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
||||||
|
@ -8691,6 +8720,11 @@
|
||||||
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==",
|
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"nodemailer": {
|
||||||
|
"version": "6.9.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.8.tgz",
|
||||||
|
"integrity": "sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ=="
|
||||||
|
},
|
||||||
"nopt": {
|
"nopt": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"guid-typescript": "^1.0.9",
|
"guid-typescript": "^1.0.9",
|
||||||
"mariadb": "^3.0.2",
|
"mariadb": "^3.0.2",
|
||||||
|
"nodemailer": "^6.9.8",
|
||||||
"random-words": "^1.1.1",
|
"random-words": "^1.1.1",
|
||||||
"swagger-jsdoc": "^6.1.0",
|
"swagger-jsdoc": "^6.1.0",
|
||||||
"swagger-ui-express": "^4.3.0",
|
"swagger-ui-express": "^4.3.0",
|
||||||
|
@ -34,6 +35,7 @@
|
||||||
"@types/express": "^4.17.15",
|
"@types/express": "^4.17.15",
|
||||||
"@types/jest": "^28.1.3",
|
"@types/jest": "^28.1.3",
|
||||||
"@types/node": "^18.11.17",
|
"@types/node": "^18.11.17",
|
||||||
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/random-words": "^1.1.2",
|
"@types/random-words": "^1.1.2",
|
||||||
"@types/swagger-jsdoc": "^6.0.1",
|
"@types/swagger-jsdoc": "^6.0.1",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
|
|
37
src/common/common.mail.nodemailer.ts
Normal file
37
src/common/common.mail.nodemailer.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import * as nodemailer from 'nodemailer';
|
||||||
|
|
||||||
|
export namespace MailService {
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.EMAIL_HOST,
|
||||||
|
pool: true,
|
||||||
|
port: 465,
|
||||||
|
secure: true,
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_USERNAME,
|
||||||
|
pass: process.env.EMAIL_PASSWORD
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mailConfigurations = {
|
||||||
|
|
||||||
|
// It should be a string of sender email
|
||||||
|
from: 'noreply@nachklang.art',
|
||||||
|
|
||||||
|
// Comma Separated list of mails
|
||||||
|
to: 'mail@pmueller.me',
|
||||||
|
|
||||||
|
// Subject of Email
|
||||||
|
subject: '',
|
||||||
|
|
||||||
|
// This would be the text of email body
|
||||||
|
text: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendMail = async (recipientAddress: string, subject: string, body: string) => {
|
||||||
|
mailConfigurations.to = recipientAddress;
|
||||||
|
mailConfigurations.subject = subject;
|
||||||
|
mailConfigurations.text = body;
|
||||||
|
|
||||||
|
await transporter.sendMail(mailConfigurations);
|
||||||
|
};
|
||||||
|
}
|
|
@ -28,12 +28,19 @@ usersRouter.post('/register', async (req: Request, res: Response) => {
|
||||||
const fullName: string = req.body.fullName;
|
const fullName: string = req.body.fullName;
|
||||||
const ip: string = req.socket.remoteAddress ?? '';
|
const ip: string = req.socket.remoteAddress ?? '';
|
||||||
|
|
||||||
if (!password || !email) {
|
if (!password || !email || !fullName) {
|
||||||
// Missing
|
// Missing
|
||||||
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
|
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emailRegex = /^[a-zA-Z0-9\_\-\.]+@nachklang\.art$/;
|
||||||
|
|
||||||
|
if(!emailRegex.test(email)) {
|
||||||
|
res.status(400).send(JSON.stringify({message: 'Must use an official Nachklang email address'}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create the user and a session
|
// Create the user and a session
|
||||||
const session: Session = await UserService.createUser(email, password, fullName, ip);
|
const session: Session = await UserService.createUser(email, password, fullName, ip);
|
||||||
|
|
||||||
|
@ -53,6 +60,41 @@ usersRouter.post('/register', async (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /users/activate
|
||||||
|
usersRouter.post('/activate', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const userId: number = parseInt(req.query.id as string ?? '-1', 10);
|
||||||
|
const token: string = req.query.token as string ?? '';
|
||||||
|
|
||||||
|
if (!userId || !token) {
|
||||||
|
// Missing
|
||||||
|
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the user and a session
|
||||||
|
const success: boolean = await UserService.activateUser(userId, token);
|
||||||
|
|
||||||
|
// Send the session details back to the user
|
||||||
|
if(success) {
|
||||||
|
res.status(200).send({
|
||||||
|
'status': 'OK',
|
||||||
|
'message': 'User activated'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(400).send({'status': 'PROCESSING_ERROR','message': 'Error activating user. Please contact your administrator.'});
|
||||||
|
} catch (e: any) {
|
||||||
|
let errorGuid = Guid.create().toString();
|
||||||
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
||||||
|
res.status(500).send({
|
||||||
|
'status': 'PROCESSING_ERROR',
|
||||||
|
'message': 'Internal Server Error. Try again later.',
|
||||||
|
'reference': errorGuid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// POST users/login
|
// POST users/login
|
||||||
usersRouter.post('/login', async (req: Request, res: Response) => {
|
usersRouter.post('/login', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
@ -123,3 +165,227 @@ usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/users/initiatePasswordReset:
|
||||||
|
* post:
|
||||||
|
* summary: Initiates a password reset
|
||||||
|
* description: Checks if the user exists and if so, initiates a password reset by sending an email to the user.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Success
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: Success
|
||||||
|
* description: A list of status messages
|
||||||
|
* 400:
|
||||||
|
* description: Problem with the request. Please consider the returned detailed error.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: Missing parameters
|
||||||
|
* description: A list of error messages
|
||||||
|
* 401:
|
||||||
|
* description: Problem with authorizing the user. Please check the provided credentials.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: Invalid session
|
||||||
|
* description: A list of error messages
|
||||||
|
* 500:
|
||||||
|
* description: A server error occurred. Please try again. If this issue persists, contact the admin.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* description: The response status
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* description: The detailed error message
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* description: An error reference for getting support concerning this error.
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* example: patrick@nachklang.art
|
||||||
|
*/
|
||||||
|
usersRouter.post('/initiatePasswordReset', async(req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const username = req.body.username;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
// Error logging in, probably wrong username / password
|
||||||
|
res.status(400).send(JSON.stringify({messages: ['No username given']}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success: boolean = await UserService.initiatePasswordReset(username);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
// Error logging in, probably wrong username / password
|
||||||
|
res.status(401).send(JSON.stringify({messages: ['Error']}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send(JSON.stringify({messages: ['Success']}));
|
||||||
|
} catch (e: any) {
|
||||||
|
let errorGuid = Guid.create().toString();
|
||||||
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
||||||
|
res.status(500).send({
|
||||||
|
'status': 'PROCESSING_ERROR',
|
||||||
|
'message': 'Internal Server Error. Try again later.',
|
||||||
|
'reference': errorGuid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /calendar/users/finalizePasswordReset:
|
||||||
|
* post:
|
||||||
|
* summary: Finalizes the password reset
|
||||||
|
* description: Checks if the given token is valid and if so, finalizes the password reset by setting the new password.
|
||||||
|
* tags:
|
||||||
|
* - calendar
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Success
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: Success
|
||||||
|
* description: A list of status messages
|
||||||
|
* 400:
|
||||||
|
* description: Problem with the request. Please consider the returned detailed error.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: Missing parameters
|
||||||
|
* description: A list of error messages
|
||||||
|
* 401:
|
||||||
|
* description: Problem with authorizing the user. Please check the provided credentials.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: Invalid session
|
||||||
|
* description: A list of error messages
|
||||||
|
* 500:
|
||||||
|
* description: A server error occurred. Please try again. If this issue persists, contact the admin.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* description: The response status
|
||||||
|
* example: PROCESSING_ERROR
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* description: The detailed error message
|
||||||
|
* example: Internal Server Error. Try again later.
|
||||||
|
* reference:
|
||||||
|
* type: string
|
||||||
|
* description: An error reference for getting support concerning this error.
|
||||||
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* example: patrick@nachklang.art
|
||||||
|
* token:
|
||||||
|
* type: string
|
||||||
|
* example: 3ccd147f-720b-4e29-a8b7-46b63de31555
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* example: ExtremelyBadPassword
|
||||||
|
*/
|
||||||
|
usersRouter.post('/finalizePasswordReset', async(req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const username = req.body.username;
|
||||||
|
const token = req.body.token;
|
||||||
|
const newPassword = req.body.password;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
// Error logging in, probably wrong username / password
|
||||||
|
res.status(400).send(JSON.stringify({messages: ['No username, token or password given']}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success: boolean = await UserService.finalizePasswordReset(username, token, newPassword);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
// Error logging in, probably wrong username / password
|
||||||
|
res.status(401).send(JSON.stringify({messages: ['Error']}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send(JSON.stringify({messages: ['Success']}));
|
||||||
|
} catch (e: any) {
|
||||||
|
let errorGuid = Guid.create().toString();
|
||||||
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
||||||
|
res.status(500).send({
|
||||||
|
'status': 'PROCESSING_ERROR',
|
||||||
|
'message': 'Internal Server Error. Try again later.',
|
||||||
|
'reference': errorGuid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {Guid} from 'guid-typescript';
|
||||||
import {User} from './user.interface';
|
import {User} from './user.interface';
|
||||||
import {Session} from './session.interface';
|
import {Session} from './session.interface';
|
||||||
import {NachklangCalendarDB} from '../Calendar.db';
|
import {NachklangCalendarDB} from '../Calendar.db';
|
||||||
|
import {MailService} from "../../../common/common.mail.nodemailer";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
@ -27,9 +28,11 @@ export const createUser = async (email: string, password: string, fullName: stri
|
||||||
const sessionKey = Guid.create().toString();
|
const sessionKey = Guid.create().toString();
|
||||||
const sessionKeyHash = bcrypt.hashSync(sessionKey, 10);
|
const sessionKeyHash = bcrypt.hashSync(sessionKey, 10);
|
||||||
|
|
||||||
|
const activationToken = Guid.create().toString();
|
||||||
|
|
||||||
// Create user entry in SQL
|
// Create user entry in SQL
|
||||||
const userQuery = 'INSERT INTO users (email, password_hash, full_name) VALUES (?, ?, ?) RETURNING user_id';
|
const userQuery = 'INSERT INTO users (email, password_hash, full_name, activation_token) VALUES (?, ?, ?, ?) RETURNING user_id';
|
||||||
const userIdRes = await conn.query(userQuery, [email, pwHash, fullName]);
|
const userIdRes = await conn.query(userQuery, [email, pwHash, fullName, activationToken]);
|
||||||
|
|
||||||
// Get user id of the created user
|
// Get user id of the created user
|
||||||
let userId: number = -1;
|
let userId: number = -1;
|
||||||
|
@ -37,6 +40,9 @@ export const createUser = async (email: string, password: string, fullName: stri
|
||||||
userId = row.user_id;
|
userId = row.user_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send email with activation link
|
||||||
|
await MailService.sendMail(email, 'Activate your Nachklang account', `Hi ${fullName},\n\nPlease click on the following link to activate your account:\n\nhttps://api.nachklang.art/calendar/users/activate?id=${userId}&token=${activationToken}`);
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
const sessionQuery = 'INSERT INTO sessions (user_id, session_key_hash, created_date, valid_until, last_ip) VALUES (?,?,NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY),?) RETURNING session_id';
|
const sessionQuery = 'INSERT INTO sessions (user_id, session_key_hash, created_date, valid_until, last_ip) VALUES (?,?,NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY),?) RETURNING session_id';
|
||||||
const sessionIdRes = await conn.query(sessionQuery, [userId, sessionKeyHash, ip]);
|
const sessionIdRes = await conn.query(sessionQuery, [userId, sessionKeyHash, ip]);
|
||||||
|
@ -63,6 +69,29 @@ export const createUser = async (email: string, password: string, fullName: stri
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const activateUser = async (userId: number, token: string): Promise<boolean> => {
|
||||||
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
|
try {
|
||||||
|
const checkTokenQuery = 'SELECT user_id, activation_token FROM users WHERE user_id = ? AND is_active = 0';
|
||||||
|
const userNameRes = await conn.query(checkTokenQuery, [userId]);
|
||||||
|
let storedToken = '';
|
||||||
|
for (const row of userNameRes) {
|
||||||
|
storedToken = row.activation_token;
|
||||||
|
}
|
||||||
|
if (storedToken!== token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const activateQuery = 'UPDATE users SET is_active = 1, activation_token = null WHERE user_id =?';
|
||||||
|
const activateRes = await conn.execute(activateQuery, [userId]);
|
||||||
|
return activateRes.affectedRows !== 0;
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
// Return connection
|
||||||
|
await conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the given credentials are valid and creates a new session if they are.
|
* Checks if the given credentials are valid and creates a new session if they are.
|
||||||
* Returns the session information in case of a successful login
|
* Returns the session information in case of a successful login
|
||||||
|
@ -180,3 +209,76 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
|
||||||
await conn.end();
|
await conn.end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initiatePasswordReset = async (email: string): Promise<boolean> => {
|
||||||
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
|
try {
|
||||||
|
const checkUsernameQuery = 'SELECT user_id, full_Name FROM users WHERE email = ?';
|
||||||
|
const userNameRes = await conn.query(checkUsernameQuery, [email]);
|
||||||
|
if (userNameRes.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let userId: number = -1;
|
||||||
|
let fullName: string = '';
|
||||||
|
for(let row of userNameRes) {
|
||||||
|
userId = row.user_id;
|
||||||
|
fullName = row.full_Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resetToken = Guid.create().toString();
|
||||||
|
let resetTokenHash = bcrypt.hashSync(resetToken, 10);
|
||||||
|
|
||||||
|
const updateQuery = 'UPDATE users SET pw_reset_token_hash = ? WHERE user_id = ?';
|
||||||
|
const updateRes = await conn.execute(updateQuery, [resetTokenHash, userId]);
|
||||||
|
|
||||||
|
if(updateRes.affectedRows === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await conn.commit();
|
||||||
|
|
||||||
|
await MailService.sendMail(email, 'Password Reset', `Hello ${fullName},\n\nYou requested a password reset for your BonkApp account. If you did not request this, please ignore this email.\n\nTo reset your password, please use the following reset token:\n\n${resetToken}`);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
// Return connection
|
||||||
|
await conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const finalizePasswordReset = async (email: string, token: string, newPassword: string): Promise<boolean> => {
|
||||||
|
let conn = await NachklangCalendarDB.getConnection();
|
||||||
|
try {
|
||||||
|
const checkTokenQuery = 'SELECT user_id, pw_reset_token_hash FROM users WHERE email = ?';
|
||||||
|
const userNameRes = await conn.query(checkTokenQuery, [email]);
|
||||||
|
if (userNameRes.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let userId: string = '';
|
||||||
|
let tokenHash: string = '';
|
||||||
|
for(let row of userNameRes) {
|
||||||
|
userId = row.user_id;
|
||||||
|
tokenHash = row.pw_reset_token_hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!bcrypt.compareSync(token, tokenHash)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pwHash = bcrypt.hashSync(newPassword, 10);
|
||||||
|
const updatePasswordQuery = 'UPDATE users SET password_hash = ?, pw_reset_token_hash = NULL WHERE user_id = ?';
|
||||||
|
const updateRes = await conn.execute(updatePasswordQuery, [pwHash, userId]);
|
||||||
|
|
||||||
|
if(updateRes.affectedRows > 0) {
|
||||||
|
await conn.commit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
// Return connection
|
||||||
|
await conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user