Compare commits
22 Commits
83c9d090e1
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
dc65b49219
|
|||
|
9c45fb11ee
|
|||
|
45dfc22c60
|
|||
|
a38fb20e5a
|
|||
|
cb85e81d67
|
|||
|
59fee19a76
|
|||
|
a79e2186a2
|
|||
|
8f93e1ab7d
|
|||
|
34a4a6664f
|
|||
|
76e6bbdbbf
|
|||
|
5e84eaea70
|
|||
|
b8a68c2480
|
|||
|
95983021ed
|
|||
|
02f7424b56
|
|||
|
93c70b0e1d
|
|||
|
d85f9a992b
|
|||
|
fc071096d8
|
|||
|
a34a5df5a3
|
|||
|
65a5e91ad1
|
|||
|
6cb7f0d59b
|
|||
|
ccfa28877c
|
|||
|
a8f7189cb3
|
@@ -67,6 +67,7 @@ const options = {
|
||||
swaggerDefinition,
|
||||
// Paths to files containing OpenAPI definitions
|
||||
apis: [
|
||||
'./src/models/**/*.interface.ts',
|
||||
'./src/models/**/*.router.ts'
|
||||
]
|
||||
};
|
||||
|
||||
Generated
+37
-3
@@ -15,9 +15,10 @@
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.18.2",
|
||||
"guid-typescript": "^1.0.9",
|
||||
"mariadb": "^3.0.2",
|
||||
"nodemailer": "^6.9.8",
|
||||
"random-words": "^1.1.1",
|
||||
"swagger-jsdoc": "^6.1.0",
|
||||
"swagger-ui-express": "^4.3.0",
|
||||
@@ -27,8 +28,10 @@
|
||||
"@types/app-root-path": "^1.2.4",
|
||||
"@types/bcrypt": "^3.0.1",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/express": "^4.17.15",
|
||||
"@types/jest": "^28.1.3",
|
||||
"@types/node": "^18.11.17",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/random-words": "^1.1.2",
|
||||
"@types/swagger-jsdoc": "^6.0.1",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
@@ -39,7 +42,7 @@
|
||||
"source-map-support": "^0.5.19",
|
||||
"ts-jest": "^28.0.5",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^4.1.5"
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@@ -1272,6 +1275,15 @@
|
||||
"integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==",
|
||||
"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": {
|
||||
"version": "2.7.2",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
@@ -6592,6 +6612,15 @@
|
||||
"integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==",
|
||||
"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": {
|
||||
"version": "2.7.2",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"express": "^4.18.2",
|
||||
"guid-typescript": "^1.0.9",
|
||||
"mariadb": "^3.0.2",
|
||||
"nodemailer": "^6.9.8",
|
||||
"random-words": "^1.1.1",
|
||||
"swagger-jsdoc": "^6.1.0",
|
||||
"swagger-ui-express": "^4.3.0",
|
||||
@@ -34,6 +35,7 @@
|
||||
"@types/express": "^4.17.15",
|
||||
"@types/jest": "^28.1.3",
|
||||
"@types/node": "^18.11.17",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/random-words": "^1.1.2",
|
||||
"@types/swagger-jsdoc": "^6.0.1",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
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
|
||||
},
|
||||
tls: {rejectUnauthorized: false}
|
||||
});
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import express, {Request, Response} from 'express';
|
||||
import {Guid} from 'guid-typescript';
|
||||
import logger from '../../middleware/logger';
|
||||
import {eventsRouter} from './events/events.router';
|
||||
import {usersRouter} from './users/users.router';
|
||||
|
||||
/**
|
||||
* Router Definition
|
||||
@@ -12,8 +13,42 @@ import {eventsRouter} from './events/events.router';
|
||||
export const calendarRouter = express.Router();
|
||||
|
||||
calendarRouter.use('/events', eventsRouter);
|
||||
calendarRouter.use('/users', usersRouter);
|
||||
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /calendar:
|
||||
* get:
|
||||
* summary: Calendar API root endpoint
|
||||
* description: Returns a welcome message for the Nachklang e.V. Calendar API.
|
||||
* tags:
|
||||
* - calendar
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
* content:
|
||||
* text/plain:
|
||||
* schema:
|
||||
* type: string
|
||||
* example: Nachklang e.V. Calendar API Endpoint
|
||||
* 500:
|
||||
* description: Server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* example: PROCESSING_ERROR
|
||||
* message:
|
||||
* type: string
|
||||
* example: Internal Server Error. Try again later.
|
||||
* reference:
|
||||
* type: string
|
||||
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||
*/
|
||||
calendarRouter.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
res.status(200).send('Nachklang e.V. Calendar API Endpoint');
|
||||
|
||||
@@ -1,28 +1,72 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as UserService from '../users/users.service';
|
||||
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const checkAdminPrivileges = (password: string) => {
|
||||
return password == process.env.ADMIN_CREDENTIAL;
|
||||
/**
|
||||
* Checks if the password gives admin privileges (view / create / edit / delete)
|
||||
* @param password
|
||||
*/
|
||||
export const checkAdminPrivileges = async (sessionId: string, sessionKey: string, ip: string) => {
|
||||
if(sessionId) {
|
||||
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||
return user.isActive;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the password gives member view privileges
|
||||
* @param password
|
||||
*/
|
||||
export const checkMemberPrivileges = async (sessionId: string, sessionKey: string, password: string, ip: string) => {
|
||||
if(sessionId) {
|
||||
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||
return user.isActive;
|
||||
}
|
||||
|
||||
export const checkMemberPrivileges = (password: string) => {
|
||||
return password == process.env.MEMBER_CREDENTIAL;
|
||||
}
|
||||
|
||||
export const checkManagementPrivileges = (password: string) => {
|
||||
/**
|
||||
* Checks if the password gives choir view privileges
|
||||
* @param password
|
||||
*/
|
||||
export const checkChoirPrivileges = async (sessionId: string, sessionKey: string, password: string, ip: string) => {
|
||||
if(sessionId) {
|
||||
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||
return user.isActive;
|
||||
}
|
||||
|
||||
return password == process.env.CHOIR_CREDENTIAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the password gives management view privileges
|
||||
* @param password
|
||||
*/
|
||||
export const checkManagementPrivileges = async (sessionId: string, sessionKey: string, password: string, ip: string) => {
|
||||
if(sessionId) {
|
||||
let user = await UserService.checkSession(sessionId, sessionKey, ip);
|
||||
return user.isActive;
|
||||
}
|
||||
|
||||
return password == process.env.MANAGEMENT_CREDENTIAL;
|
||||
}
|
||||
|
||||
export const hasAccess = (calendarName: string, password: string) => {
|
||||
export const hasAccess = async (calendarName: string, sessionId: string, sessionKey: string, password: string, ip: string) => {
|
||||
switch (calendarName) {
|
||||
case 'public':
|
||||
return true;
|
||||
case 'members':
|
||||
return checkMemberPrivileges(password);
|
||||
return await checkMemberPrivileges(sessionId, sessionKey, password, ip);
|
||||
case 'choir':
|
||||
return await checkChoirPrivileges(sessionId, sessionKey, password, ip);
|
||||
case 'management':
|
||||
return checkManagementPrivileges(password);
|
||||
return await checkManagementPrivileges(sessionId, sessionKey, password, ip);
|
||||
case 'birthdays':
|
||||
return await checkChoirPrivileges(sessionId, sessionKey, password, ip);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,113 @@
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Event:
|
||||
* type: object
|
||||
* required:
|
||||
* - eventId
|
||||
* - calendarId
|
||||
* - uuid
|
||||
* - name
|
||||
* - description
|
||||
* - startDateTime
|
||||
* - endDateTime
|
||||
* - createdDate
|
||||
* - location
|
||||
* - createdById
|
||||
* - url
|
||||
* - wholeDay
|
||||
* properties:
|
||||
* eventId:
|
||||
* type: integer
|
||||
* description: The unique identifier for the event
|
||||
* example: 123
|
||||
* calendarId:
|
||||
* type: integer
|
||||
* description: The ID of the calendar this event belongs to
|
||||
* example: 1
|
||||
* uuid:
|
||||
* type: string
|
||||
* description: A unique UUID for the event
|
||||
* example: "550e8400-e29b-41d4-a716-446655440000"
|
||||
* name:
|
||||
* type: string
|
||||
* description: The name/title of the event
|
||||
* example: "Concert at Musikhochschule"
|
||||
* description:
|
||||
* type: string
|
||||
* description: A detailed description of the event
|
||||
* example: "Annual concert at the Musikhochschule"
|
||||
* startDateTime:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: The start date and time of the event
|
||||
* example: "2023-06-15T19:00:00.000Z"
|
||||
* endDateTime:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: The end date and time of the event
|
||||
* example: "2023-06-15T21:00:00.000Z"
|
||||
* createdDate:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: The date and time when the event was created
|
||||
* example: "2023-05-01T10:00:00.000Z"
|
||||
* lastModifiedDate:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* example: "2023-05-01T10:00:00.000Z"
|
||||
* location:
|
||||
* type: string
|
||||
* description: The location of the event
|
||||
* example: "Musikhochschule, Karlsruhe"
|
||||
* createdBy:
|
||||
* type: string
|
||||
* description: The name of the user who created the event
|
||||
* example: "John Doe"
|
||||
* createdById:
|
||||
* type: integer
|
||||
* description: The ID of the user who created the event
|
||||
* example: 456
|
||||
* lastModifiedBy:
|
||||
* type: string
|
||||
* description: The name of the user who last modified the event
|
||||
* example: "John Doe"
|
||||
* lastModifiedById:
|
||||
* type: integer
|
||||
* description: The ID of the user who last modified the event
|
||||
* example: 456
|
||||
* url:
|
||||
* type: string
|
||||
* description: A URL with more information about the event
|
||||
* example: "https://www.nachklang.art/events/concert"
|
||||
* wholeDay:
|
||||
* type: boolean
|
||||
* description: Whether the event lasts the whole day
|
||||
* example: false
|
||||
* status:
|
||||
* type: string
|
||||
* description: The status of the event
|
||||
* enum: [PUBLIC, PRIVATE, DRAFT, DELETED]
|
||||
* example: "PUBLIC"
|
||||
*/
|
||||
export interface Event {
|
||||
event_id: number;
|
||||
calendar_id: number;
|
||||
eventId: number;
|
||||
calendarId: number;
|
||||
uuid: string;
|
||||
name: string;
|
||||
description: string;
|
||||
start_datetime: Date;
|
||||
end_datetime: Date;
|
||||
created_date: Date;
|
||||
startDateTime: Date;
|
||||
endDateTime: Date;
|
||||
createdDate: Date;
|
||||
lastModifiedDate?: Date;
|
||||
location: string;
|
||||
created_by: string;
|
||||
createdBy?: string;
|
||||
createdById: number;
|
||||
lastModifiedBy?: string;
|
||||
lastModifiedById?: number;
|
||||
url: string;
|
||||
wholeDay: boolean;
|
||||
repeatFrequency: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,11 +14,107 @@ export const getAllEvents = async (calendarId: number): Promise<Event[]> => {
|
||||
let conn = await NachklangCalendarDB.getConnection();
|
||||
let eventRows: Event[] = [];
|
||||
try {
|
||||
const eventsQuery = 'SELECT * FROM events WHERE calendar_id = ?';
|
||||
const calendarQuery = 'SELECT calendar_id, includes_calendars FROM calendars WHERE calendar_id = ?';
|
||||
const calendarRes = await conn.query(calendarQuery, calendarId);
|
||||
let calendarsToFetch: number[] = [calendarId];
|
||||
for(let row of calendarRes) {
|
||||
let includes: number[] = JSON.parse(row.includes_calendars);
|
||||
calendarsToFetch = [...calendarsToFetch, ...includes];
|
||||
}
|
||||
|
||||
const eventsQuery = `
|
||||
SELECT e.calendar_id, e.uuid, e.created_date, e.created_by_id, u.full_name as created_by_name, u2.full_name as last_modified_by_name, v.* FROM events e
|
||||
INNER JOIN (
|
||||
SELECT event_id, MAX(event_version_id) AS latest_version
|
||||
FROM event_versions
|
||||
GROUP BY event_id
|
||||
) latest_versions
|
||||
ON e.event_id = latest_versions.event_id
|
||||
INNER JOIN event_versions v
|
||||
ON v.event_id = latest_versions.event_id AND v.event_version_id = latest_versions.latest_version
|
||||
LEFT OUTER JOIN users u ON u.user_id = e.created_by_id
|
||||
LEFT OUTER JOIN users u2 ON u2.user_id = v.version_created_by_id
|
||||
WHERE e.calendar_id IN (?) AND v.status = 'PUBLIC'
|
||||
ORDER BY e.event_id`;
|
||||
const eventsRes = await conn.query(eventsQuery, [calendarsToFetch]);
|
||||
|
||||
for (let row of eventsRes) {
|
||||
eventRows.push({
|
||||
eventId: row.event_id,
|
||||
calendarId: row.calendar_id,
|
||||
uuid: row.uuid,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
startDateTime: row.start_datetime,
|
||||
endDateTime: row.end_datetime,
|
||||
createdDate: row.created_date,
|
||||
lastModifiedDate: row.version_created_at,
|
||||
location: row.location,
|
||||
createdBy: row.created_by_name,
|
||||
createdById: row.created_by_id,
|
||||
lastModifiedBy: row.last_modified_by_name,
|
||||
lastModifiedById: row.version_created_by_id,
|
||||
url: row.url,
|
||||
wholeDay: row.whole_day,
|
||||
repeatFrequency: row.repeat_frequency
|
||||
});
|
||||
}
|
||||
|
||||
return eventRows;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
// Return connection
|
||||
await conn.end();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all events for the given calendar for the admin UI (therefore includes admin relevant information and
|
||||
* ignores the calendar includes
|
||||
* @param calendarId
|
||||
*/
|
||||
export const getAllEventsAdmin = async (calendarId: number): Promise<Event[]> => {
|
||||
let conn = await NachklangCalendarDB.getConnection();
|
||||
let eventRows: Event[] = [];
|
||||
try {
|
||||
const eventsQuery = `
|
||||
SELECT e.calendar_id, e.uuid, e.created_date, e.created_by_id, u.full_name as created_by_name, u2.full_name as last_modified_by_name, v.* FROM events e
|
||||
INNER JOIN (
|
||||
SELECT event_id, MAX(event_version_id) AS latest_version
|
||||
FROM event_versions
|
||||
GROUP BY event_id
|
||||
) latest_versions
|
||||
ON e.event_id = latest_versions.event_id
|
||||
INNER JOIN event_versions v
|
||||
ON v.event_id = latest_versions.event_id AND v.event_version_id = latest_versions.latest_version
|
||||
LEFT OUTER JOIN users u ON u.user_id = e.created_by_id
|
||||
LEFT OUTER JOIN users u2 ON u2.user_id = v.version_created_by_id
|
||||
WHERE e.calendar_id = ?
|
||||
ORDER BY e.event_id`;
|
||||
const eventsRes = await conn.query(eventsQuery, calendarId);
|
||||
|
||||
for (let row of eventsRes) {
|
||||
eventRows.push(row);
|
||||
eventRows.push({
|
||||
eventId: row.event_id,
|
||||
calendarId: row.calendar_id,
|
||||
uuid: row.uuid,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
startDateTime: row.start_datetime,
|
||||
endDateTime: row.end_datetime,
|
||||
createdDate: row.created_date,
|
||||
lastModifiedDate: row.version_created_at,
|
||||
location: row.location,
|
||||
createdBy: row.created_by_name,
|
||||
createdById: row.created_by_id,
|
||||
lastModifiedBy: row.last_modified_by_name,
|
||||
lastModifiedById: row.version_created_by_id,
|
||||
url: row.url,
|
||||
wholeDay: row.whole_day,
|
||||
repeatFrequency: row.repeat_frequency,
|
||||
status: row.status
|
||||
});
|
||||
}
|
||||
|
||||
return eventRows;
|
||||
@@ -38,14 +134,21 @@ export const createEvent = async (event: Event): Promise<number> => {
|
||||
let conn = await NachklangCalendarDB.getConnection();
|
||||
try {
|
||||
let eventUUID = Guid.create().toString();
|
||||
const eventsQuery = 'INSERT INTO events (calendar_id, uuid, name, description, start_datetime, end_datetime, location, created_by, url) VALUES (?,?,?,?,?,?,?,?,?) RETURNING event_id';
|
||||
const eventsRes = await conn.query(eventsQuery, [event.calendar_id, eventUUID, event.name, event.description, event.start_datetime, event.end_datetime, event.location, event.created_by, event.url]);
|
||||
const eventsQuery = 'INSERT INTO events (calendar_id, uuid, created_by_id) VALUES (?,?,?) RETURNING event_id';
|
||||
const eventsRes = await conn.execute(eventsQuery, [event.calendarId, eventUUID, event.createdById]);
|
||||
|
||||
const versionQuery = 'INSERT INTO event_versions (event_id, name, description, start_datetime, end_datetime, whole_day, repeat_frequency, location, url, status, version_created_by_id) VALUES (?,?,?,?,?,?,?,?,?,?,?);'
|
||||
const versionRes = await conn.execute(versionQuery, [eventsRes[0].event_id, event.name, event.description, event.startDateTime, event.endDateTime, event.wholeDay, event.repeatFrequency, event.location, event.url, event.status, event.createdById]);
|
||||
|
||||
await conn.commit();
|
||||
|
||||
return eventsRes[0].event_id;
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
// Return connection
|
||||
await conn.commit();
|
||||
await conn.end();
|
||||
}
|
||||
};
|
||||
@@ -54,20 +157,132 @@ export const createEvent = async (event: Event): Promise<number> => {
|
||||
* Update the given event in the database
|
||||
* @param event The event to update
|
||||
*/
|
||||
export const updateEvent = async (event: Event): Promise<boolean> => {
|
||||
export const updateEvent = async (event: Event): Promise<number> => {
|
||||
let conn = await NachklangCalendarDB.getConnection();
|
||||
try {
|
||||
let eventUUID = Guid.create().toString();
|
||||
const eventsQuery = 'UPDATE events SET name = ?, description = ?, start_datetime = ?, end_datetime = ?, location = ?, created_by = ?, url = ? WHERE event_id = ?';
|
||||
const eventsRes = await conn.query(eventsQuery, [event.name, event.description, event.start_datetime, event.end_datetime, event.location, event.created_by, event.url, event.event_id]);
|
||||
const versionQuery = 'INSERT INTO event_versions (event_id, name, description, start_datetime, end_datetime, whole_day, repeat_frequency, location, url, status, version_created_by_id) VALUES (?,?,?,?,?,?,?,?,?,?,?);'
|
||||
const versionRes = await conn.execute(versionQuery, [event.eventId, event.name, event.description, event.startDateTime, event.endDateTime, event.wholeDay, event.repeatFrequency, event.location, event.url, event.status, event.createdById]);
|
||||
|
||||
console.log(eventsRes);
|
||||
await conn.commit();
|
||||
|
||||
return eventsRes.affectedRows === 1;
|
||||
return versionRes.affectedRows;
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
// Return connection
|
||||
await conn.commit();
|
||||
await conn.end();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the given event from the database
|
||||
* @param event The event to delete
|
||||
*/
|
||||
export const deleteEvent = async (event: Event): Promise<boolean> => {
|
||||
let conn = await NachklangCalendarDB.getConnection();
|
||||
try {
|
||||
const versionQuery = 'INSERT INTO event_versions (event_id, status, version_created_by_id) VALUES (?,?,?);'
|
||||
const versionRes = await conn.execute(versionQuery, [event.eventId, 'DELETED', event.createdById]);
|
||||
|
||||
await conn.commit();
|
||||
|
||||
return versionRes.affectedRows === 1;
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
// Return connection
|
||||
await conn.commit();
|
||||
await conn.end();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Moves an event to the specified calendar
|
||||
* @param event The event to move. Has to have the target calendar set already.
|
||||
*/
|
||||
export const moveEvent = async (event: Event): Promise<boolean> => {
|
||||
let conn = await NachklangCalendarDB.getConnection();
|
||||
try {
|
||||
const eventQuery = 'UPDATE events SET calendar_id = ? WHERE event_id = ?';
|
||||
const eventRes = await conn.execute(eventQuery, [event.calendarId, event.eventId]);
|
||||
|
||||
await conn.commit();
|
||||
|
||||
return eventRes.affectedRows === 1;
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
// Return connection
|
||||
await conn.commit();
|
||||
await conn.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next upcoming event for the given calendar
|
||||
* @param calendarId The calendar Id
|
||||
*/
|
||||
export const getNextUpcomingEvent = async (calendarId: number): Promise<Event | null> => {
|
||||
let conn = await NachklangCalendarDB.getConnection();
|
||||
try {
|
||||
const calendarQuery = 'SELECT calendar_id, includes_calendars FROM calendars WHERE calendar_id = ?';
|
||||
const calendarRes = await conn.query(calendarQuery, calendarId);
|
||||
let calendarsToFetch: number[] = [calendarId];
|
||||
for(let row of calendarRes) {
|
||||
let includes: number[] = JSON.parse(row.includes_calendars);
|
||||
calendarsToFetch = [...calendarsToFetch, ...includes];
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const eventsQuery = `
|
||||
SELECT e.calendar_id, e.uuid, e.created_date, e.created_by_id, u.full_name as created_by_name, u2.full_name as last_modified_by_name, v.* FROM events e
|
||||
INNER JOIN (
|
||||
SELECT event_id, MAX(event_version_id) AS latest_version
|
||||
FROM event_versions
|
||||
GROUP BY event_id
|
||||
) latest_versions
|
||||
ON e.event_id = latest_versions.event_id
|
||||
INNER JOIN event_versions v
|
||||
ON v.event_id = latest_versions.event_id AND v.event_version_id = latest_versions.latest_version
|
||||
LEFT OUTER JOIN users u ON u.user_id = e.created_by_id
|
||||
LEFT OUTER JOIN users u2 ON u2.user_id = v.version_created_by_id
|
||||
WHERE e.calendar_id IN (?) AND v.status = 'PUBLIC' AND v.start_datetime > ?
|
||||
ORDER BY v.start_datetime ASC
|
||||
LIMIT 1`;
|
||||
const eventsRes = await conn.query(eventsQuery, [calendarsToFetch, now]);
|
||||
|
||||
if (eventsRes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const row = eventsRes[0];
|
||||
return {
|
||||
eventId: row.event_id,
|
||||
calendarId: row.calendar_id,
|
||||
uuid: row.uuid,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
startDateTime: row.start_datetime,
|
||||
endDateTime: row.end_datetime,
|
||||
createdDate: row.created_date,
|
||||
lastModifiedDate: row.version_created_at,
|
||||
location: row.location,
|
||||
createdBy: row.created_by_name,
|
||||
createdById: row.created_by_id,
|
||||
lastModifiedBy: row.last_modified_by_name,
|
||||
lastModifiedById: row.version_created_by_id,
|
||||
url: row.url,
|
||||
wholeDay: row.whole_day,
|
||||
repeatFrequency: row.repeat_frequency
|
||||
} as Event;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
// Return connection
|
||||
await conn.end();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import {Event} from './event.interface';
|
||||
|
||||
/**
|
||||
* Interface to external classes - Turns the given events into an ical string
|
||||
* @param events
|
||||
*/
|
||||
export const convertToIcal = async (events: Event[]): Promise<string> => {
|
||||
try {
|
||||
let ical: iCalFile = {body: []};
|
||||
@@ -15,6 +19,10 @@ export const convertToIcal = async (events: Event[]): Promise<string> => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to serialize an iCalFile object into an ical string
|
||||
* @param ical
|
||||
*/
|
||||
const serializeIcalFile = (ical: iCalFile): string => {
|
||||
let returnString = '';
|
||||
|
||||
@@ -27,6 +35,10 @@ const serializeIcalFile = (ical: iCalFile): string => {
|
||||
return returnString;
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to serialize a single ical event into an ical event string
|
||||
* @param icalevent
|
||||
*/
|
||||
const serializeIcalEvent = (icalevent: iCalEvent): string => {
|
||||
let returnString = '';
|
||||
|
||||
@@ -34,18 +46,27 @@ const serializeIcalEvent = (icalevent: iCalEvent): string => {
|
||||
returnString += 'UID:' + icalevent.uid;
|
||||
returnString += 'DTSTAMP:' + icalevent.created;
|
||||
returnString += 'ORGANIZER:' + icalevent.organizer;
|
||||
if(icalevent.wholeDay) {
|
||||
returnString += 'DTSTART;VALUE=DATE:' + icalevent.start;
|
||||
returnString += 'DTEND;VALUE=DATE:' + icalevent.end;
|
||||
} else {
|
||||
returnString += 'DTSTART;TZID=Europe/Berlin:' + icalevent.start;
|
||||
returnString += 'DTEND;TZID=Europe/Berlin:' + icalevent.end;
|
||||
}
|
||||
if(!isNullOrBlank(icalevent.repeatFrequency)) returnString += 'RRULE:FREQ=' + icalevent.repeatFrequency;
|
||||
returnString += 'SUMMARY:' + icalevent.summary;
|
||||
returnString += 'DESCRIPTION:' + icalevent.description;
|
||||
returnString += 'LOCATION:' + icalevent.location;
|
||||
returnString += 'URL:' + icalevent.url;
|
||||
if(!isNullOrBlank(icalevent.description)) returnString += 'DESCRIPTION:' + icalevent.description;
|
||||
if(!isNullOrBlank(icalevent.location)) returnString += 'LOCATION:' + icalevent.location;
|
||||
if(!isNullOrBlank(icalevent.url)) returnString += 'URL:' + icalevent.url;
|
||||
returnString += icalevent.footer;
|
||||
|
||||
return returnString;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Method to generate the ical header string
|
||||
* @param ical
|
||||
*/
|
||||
const generateHeaderInfo = (ical: iCalFile) => {
|
||||
ical.header = 'BEGIN:VCALENDAR\n' +
|
||||
'VERSION:2.0\n' +
|
||||
@@ -73,40 +94,70 @@ const generateHeaderInfo = (ical: iCalFile) => {
|
||||
'END:VTIMEZONE\n';
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to generate the ical footer info
|
||||
* @param ical
|
||||
*/
|
||||
const generateFooterInfo = (ical: iCalFile) => {
|
||||
ical.footer = 'END:VCALENDAR';
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to add events to the iCalFile object
|
||||
* @param ical
|
||||
* @param event
|
||||
*/
|
||||
const addEventToFile = (ical: iCalFile, event: Event) => {
|
||||
ical.body.push(createIcalEvent(event));
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to turn an event object into an iCalEvent object
|
||||
* @param event
|
||||
*/
|
||||
const createIcalEvent = (event: Event): iCalEvent => {
|
||||
let description = event.description ? event.description + '\n' : '';
|
||||
let location = event.location ? event.location + '\n' : '';
|
||||
let url = event.url ? event.url + '\n' : '';
|
||||
|
||||
return {
|
||||
header: 'BEGIN:VEVENT\n',
|
||||
uid: event.uuid + '\n',
|
||||
created: formatDate(event.created_date) + 'Z\n',
|
||||
organizer: event.created_by + '\n',
|
||||
start: formatDate(event.start_datetime) + '\n',
|
||||
end: formatDate(event.end_datetime) + '\n',
|
||||
created: formatDate(event.createdDate) + 'Z\n',
|
||||
organizer: event.createdBy + '\n',
|
||||
start: formatDate(event.startDateTime, event.wholeDay) + '\n',
|
||||
end: formatDate(event.endDateTime, event.wholeDay, true) + '\n',
|
||||
repeatFrequency: event.repeatFrequency ? event.repeatFrequency + '\n' : '',
|
||||
summary: event.name + '\n',
|
||||
description: event.description + '\n',
|
||||
location: event.location + '\n',
|
||||
url: event.url + '\n',
|
||||
description: description,
|
||||
location: location,
|
||||
url: url,
|
||||
wholeDay: event.wholeDay,
|
||||
footer: 'END:VEVENT\n'
|
||||
};
|
||||
};
|
||||
|
||||
const formatDate = (date: Date): string => {
|
||||
/**
|
||||
* Helper method to format dates in a valid iCal format
|
||||
* @param date
|
||||
* @param wholeDayFormat
|
||||
* @param isEndDate
|
||||
*/
|
||||
const formatDate = (date: Date, wholeDayFormat: boolean = false, isEndDate: boolean = false): string => {
|
||||
let returnString = '';
|
||||
|
||||
// We need to do this for whole day events as otherwise the event ends one day too early
|
||||
if(wholeDayFormat && isEndDate) date.setDate(date.getDate() + 1)
|
||||
|
||||
returnString += date.getFullYear();
|
||||
returnString += (date.getMonth() + 1).toString().padStart(2, '0'); // +1 Because JS sucks
|
||||
returnString += date.getDate().toString().padStart(2, '0');
|
||||
if(!wholeDayFormat) {
|
||||
returnString += 'T';
|
||||
returnString += date.getHours().toString().padStart(2, '0');
|
||||
returnString += date.getMinutes().toString().padStart(2, '0');
|
||||
returnString += date.getSeconds().toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
return returnString;
|
||||
};
|
||||
@@ -128,5 +179,15 @@ export interface iCalEvent {
|
||||
description: string;
|
||||
location: string;
|
||||
url: string;
|
||||
wholeDay: boolean;
|
||||
repeatFrequency: string;
|
||||
footer: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given string is null, undefined or blank
|
||||
* @param str The string to check
|
||||
*/
|
||||
function isNullOrBlank(str: string | null): boolean {
|
||||
return str === null || str === undefined || str.trim() === '';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Session:
|
||||
* type: object
|
||||
* required:
|
||||
* - sessionId
|
||||
* - userId
|
||||
* - sessionKey
|
||||
* - sessionKeyHash
|
||||
* - lastIP
|
||||
* properties:
|
||||
* sessionId:
|
||||
* type: integer
|
||||
* description: The unique identifier for the session
|
||||
* example: 789
|
||||
* userId:
|
||||
* type: integer
|
||||
* description: The ID of the user this session belongs to
|
||||
* example: 456
|
||||
* sessionKey:
|
||||
* type: string
|
||||
* description: The session key used for authentication
|
||||
* example: "abc123def456"
|
||||
* sessionKeyHash:
|
||||
* type: string
|
||||
* description: The hashed session key (not returned in API responses)
|
||||
* example: "$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG"
|
||||
* createdDate:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: The date and time when the session was created
|
||||
* example: "2023-05-01T10:00:00.000Z"
|
||||
* validUntil:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: The date and time until when the session is valid
|
||||
* example: "2023-05-08T10:00:00.000Z"
|
||||
* lastIP:
|
||||
* type: string
|
||||
* description: The last IP address used with this session
|
||||
* example: "192.168.1.1"
|
||||
*/
|
||||
export interface Session {
|
||||
sessionId: number;
|
||||
userId: number;
|
||||
sessionKey: string;
|
||||
sessionKeyHash: string;
|
||||
createdDate?: Date;
|
||||
validUntil?: Date;
|
||||
lastIP: string;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* User:
|
||||
* type: object
|
||||
* required:
|
||||
* - userId
|
||||
* - fullName
|
||||
* - passwordHash
|
||||
* - email
|
||||
* - isActive
|
||||
* properties:
|
||||
* userId:
|
||||
* type: integer
|
||||
* description: The unique identifier for the user
|
||||
* example: 456
|
||||
* fullName:
|
||||
* type: string
|
||||
* description: The full name of the user
|
||||
* example: "John Doe"
|
||||
* passwordHash:
|
||||
* type: string
|
||||
* description: The hashed password of the user (not returned in API responses)
|
||||
* example: "$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG"
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* description: The email address of the user
|
||||
* example: "john.doe@nachklang.art"
|
||||
* isActive:
|
||||
* type: boolean
|
||||
* description: Whether the user account is active
|
||||
* example: true
|
||||
*/
|
||||
export interface User {
|
||||
userId: number;
|
||||
fullName: string;
|
||||
passwordHash: string;
|
||||
email: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
@@ -0,0 +1,671 @@
|
||||
/**
|
||||
* Required External Modules and Interfaces
|
||||
*/
|
||||
|
||||
import express, {Request, Response} from 'express';
|
||||
import * as UserService from './users.service';
|
||||
import {Session} from './session.interface';
|
||||
import {User} from './user.interface';
|
||||
import {Guid} from 'guid-typescript';
|
||||
import logger from '../../../middleware/logger';
|
||||
|
||||
/**
|
||||
* Router Definition
|
||||
*/
|
||||
|
||||
export const usersRouter = express.Router();
|
||||
|
||||
|
||||
/**
|
||||
* Controller Definitions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /calendar/users/register:
|
||||
* post:
|
||||
* summary: Register a new user
|
||||
* description: Creates a new user account with the provided email, password, and full name. Only accepts official Nachklang email addresses.
|
||||
* tags:
|
||||
* - calendar
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - email
|
||||
* - password
|
||||
* - fullName
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* example: john.doe@nachklang.art
|
||||
* description: Must be an official Nachklang email address
|
||||
* password:
|
||||
* type: string
|
||||
* format: password
|
||||
* example: securePassword123
|
||||
* fullName:
|
||||
* type: string
|
||||
* example: John Doe
|
||||
* responses:
|
||||
* 201:
|
||||
* description: User registered successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* sessionId:
|
||||
* type: integer
|
||||
* example: 123
|
||||
* sessionKey:
|
||||
* type: string
|
||||
* example: abc123def456
|
||||
* 400:
|
||||
* description: Bad request - missing or invalid parameters
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* message:
|
||||
* type: string
|
||||
* example: Missing parameters
|
||||
* 500:
|
||||
* description: Server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* example: PROCESSING_ERROR
|
||||
* message:
|
||||
* type: string
|
||||
* example: Internal Server Error. Try again later.
|
||||
* reference:
|
||||
* type: string
|
||||
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||
*/
|
||||
// POST users/register
|
||||
usersRouter.post('/register', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const password: string = req.body.password;
|
||||
const email: string = req.body.email;
|
||||
const fullName: string = req.body.fullName;
|
||||
const ip: string = req.socket.remoteAddress ?? '';
|
||||
|
||||
if (!password || !email || !fullName) {
|
||||
// Missing
|
||||
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
|
||||
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
|
||||
const session: Session = await UserService.createUser(email, password, fullName, ip);
|
||||
|
||||
// Send the session details back to the user
|
||||
res.status(201).send({
|
||||
sessionId: session.sessionId,
|
||||
sessionKey: session.sessionKey
|
||||
});
|
||||
} 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/activate:
|
||||
* get:
|
||||
* summary: Activate a user account
|
||||
* description: Activates a user account using the provided user ID and activation token.
|
||||
* tags:
|
||||
* - calendar
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: The ID of the user to activate
|
||||
* - in: query
|
||||
* name: token
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: The activation token sent to the user's email
|
||||
* responses:
|
||||
* 200:
|
||||
* description: User activated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* example: OK
|
||||
* message:
|
||||
* type: string
|
||||
* example: User activated
|
||||
* 400:
|
||||
* description: Bad request - missing parameters or activation failed
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* example: PROCESSING_ERROR
|
||||
* message:
|
||||
* type: string
|
||||
* example: Error activating user. Please contact your administrator.
|
||||
* 500:
|
||||
* description: Server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* example: PROCESSING_ERROR
|
||||
* message:
|
||||
* type: string
|
||||
* example: Internal Server Error. Try again later.
|
||||
* reference:
|
||||
* type: string
|
||||
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||
*/
|
||||
// GET /users/activate
|
||||
usersRouter.get('/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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /calendar/users/login:
|
||||
* post:
|
||||
* summary: Login a user
|
||||
* description: Authenticates a user with the provided email and password and returns a session.
|
||||
* tags:
|
||||
* - calendar
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - email
|
||||
* - password
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* example: john.doe@nachklang.art
|
||||
* password:
|
||||
* type: string
|
||||
* format: password
|
||||
* example: securePassword123
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Login successful
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* sessionId:
|
||||
* type: integer
|
||||
* example: 123
|
||||
* sessionKey:
|
||||
* type: string
|
||||
* example: abc123def456
|
||||
* 400:
|
||||
* description: Bad request - missing parameters
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* message:
|
||||
* type: string
|
||||
* example: Missing parameters
|
||||
* 401:
|
||||
* description: Unauthorized - invalid credentials
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* message:
|
||||
* type: string
|
||||
* example: Wrong username and / or password
|
||||
* sessionId:
|
||||
* type: integer
|
||||
* example: -1
|
||||
* sessionKey:
|
||||
* type: string
|
||||
* example: ""
|
||||
* 500:
|
||||
* description: Server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* example: PROCESSING_ERROR
|
||||
* message:
|
||||
* type: string
|
||||
* example: Internal Server Error. Try again later.
|
||||
* reference:
|
||||
* type: string
|
||||
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||
*/
|
||||
// POST users/login
|
||||
usersRouter.post('/login', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const password: string = req.body.password;
|
||||
const email: string = req.body.email;
|
||||
const ip: string = req.socket.remoteAddress ?? '';
|
||||
|
||||
if (!password || !email) {
|
||||
// Missing
|
||||
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a session
|
||||
const session: Session = await UserService.login(email, password, ip);
|
||||
|
||||
if (!session.sessionId) {
|
||||
// Error logging in, probably wrong username / password
|
||||
res.status(401).send(JSON.stringify({message: 'Wrong username and / or password', sessionId: -1, sessionKey: ''}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the session details back to the user
|
||||
res.status(200).send({
|
||||
sessionId: session.sessionId,
|
||||
sessionKey: session.sessionKey
|
||||
});
|
||||
} 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/checkSessionValid:
|
||||
* post:
|
||||
* summary: Check if a session is valid
|
||||
* description: Checks if the provided session is valid and returns the user information if it is.
|
||||
* tags:
|
||||
* - calendar
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - sessionId
|
||||
* - sessionKey
|
||||
* properties:
|
||||
* sessionId:
|
||||
* type: integer
|
||||
* example: 123
|
||||
* sessionKey:
|
||||
* type: string
|
||||
* example: abc123def456
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Session is valid
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* 401:
|
||||
* description: Unauthorized - invalid session
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* messages:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* example: ["Invalid session"]
|
||||
* 500:
|
||||
* description: Server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* example: PROCESSING_ERROR
|
||||
* message:
|
||||
* type: string
|
||||
* example: Internal Server Error. Try again later.
|
||||
* reference:
|
||||
* type: string
|
||||
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
||||
*/
|
||||
// POST users/checkSessionValid
|
||||
usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const ip: string = req.socket.remoteAddress ?? '';
|
||||
const session_id = req.body.sessionId;
|
||||
const session_key = req.body.sessionKey;
|
||||
|
||||
if (!session_id || !session_key) {
|
||||
// Error logging in, probably wrong username / password
|
||||
res.status(401).send(JSON.stringify({messages: ['No session detected']}));
|
||||
return;
|
||||
}
|
||||
|
||||
const user: User = await UserService.checkSession(session_id, session_key, ip);
|
||||
|
||||
if (!user.userId) {
|
||||
// Error logging in, probably wrong username / password
|
||||
res.status(401).send(JSON.stringify({messages: ['Invalid session']}));
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).send(user);
|
||||
} 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/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
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,284 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import {Guid} from 'guid-typescript';
|
||||
import {User} from './user.interface';
|
||||
import {Session} from './session.interface';
|
||||
import {NachklangCalendarDB} from '../Calendar.db';
|
||||
import {MailService} from "../../../common/common.mail.nodemailer";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
/**
|
||||
* Data Model Interfaces
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Service Methods
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a user record in the database, also creates a session. Returns the session if successful.
|
||||
*/
|
||||
export const createUser = async (email: string, password: string, fullName: string, ip: string): Promise<Session> => {
|
||||
let conn = await NachklangCalendarDB.getConnection();
|
||||
try {
|
||||
// Hash password and generate + hash session key
|
||||
const pwHash = bcrypt.hashSync(password, 10);
|
||||
const sessionKey = Guid.create().toString();
|
||||
const sessionKeyHash = bcrypt.hashSync(sessionKey, 10);
|
||||
|
||||
const activationToken = Guid.create().toString();
|
||||
|
||||
// Create user entry in SQL
|
||||
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, activationToken]);
|
||||
|
||||
// Get user id of the created user
|
||||
let userId: number = -1;
|
||||
for (const row of userIdRes) {
|
||||
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
|
||||
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]);
|
||||
await conn.commit();
|
||||
|
||||
// Get session id of the created session
|
||||
let sessionId: number = -1;
|
||||
for (const row of sessionIdRes) {
|
||||
sessionId = row.session_id;
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: sessionId,
|
||||
userId: userId,
|
||||
sessionKey: sessionKey,
|
||||
sessionKeyHash: 'HIDDEN',
|
||||
lastIP: ip
|
||||
};
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
// Return connection
|
||||
await conn.end();
|
||||
}
|
||||
};
|
||||
|
||||
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.
|
||||
* Returns the session information in case of a successful login
|
||||
*/
|
||||
export const login = async (email: string, password: string, ip: string): Promise<Session> => {
|
||||
let conn = await NachklangCalendarDB.getConnection();
|
||||
try {
|
||||
// Get saved password hash
|
||||
const query = 'SELECT user_id, password_hash FROM users WHERE email = ?';
|
||||
const userRows = await conn.query(query, email);
|
||||
let savedHash = '';
|
||||
let userId = -1;
|
||||
for (const row of userRows) {
|
||||
savedHash = row.password_hash;
|
||||
userId = row.user_id;
|
||||
}
|
||||
|
||||
// Check for correct password
|
||||
if (!bcrypt.compareSync(password, savedHash)) {
|
||||
// Wrong password, return invalid
|
||||
return {} as Session;
|
||||
}
|
||||
|
||||
// Generate + hash session key
|
||||
const sessionKey = Guid.create().toString();
|
||||
const sessionKeyHash = bcrypt.hashSync(sessionKey, 10);
|
||||
|
||||
// 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 sessionIdRes = await conn.query(sessionQuery, [userId, sessionKeyHash, ip]);
|
||||
await conn.commit();
|
||||
|
||||
// Get session id of the created session
|
||||
let sessionId: number = -1;
|
||||
for (const row of sessionIdRes) {
|
||||
sessionId = row.session_id;
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: sessionId,
|
||||
userId: userId,
|
||||
sessionKey: sessionKey,
|
||||
sessionKeyHash: 'HIDDEN',
|
||||
lastIP: ip
|
||||
};
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
// Return connection
|
||||
await conn.end();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given session information are valid and returns the user information if they are
|
||||
*/
|
||||
export const checkSession = async (sessionId: string, sessionKey: string, ip: string): Promise<User> => {
|
||||
let conn = await NachklangCalendarDB.getConnection();
|
||||
try {
|
||||
// Get saved session key hash
|
||||
const query = 'SELECT user_id, session_key_hash, valid_until FROM sessions WHERE session_id = ?';
|
||||
const sessionRows = await conn.query(query, sessionId);
|
||||
let savedHash = '';
|
||||
let userId = -1;
|
||||
let validUntil = new Date();
|
||||
for (const row of sessionRows) {
|
||||
savedHash = row.session_key_hash;
|
||||
userId = row.user_id;
|
||||
validUntil = row.valid_until;
|
||||
}
|
||||
|
||||
// Check for correct key
|
||||
if (!bcrypt.compareSync(sessionKey, savedHash)) {
|
||||
// Wrong key, return invalid
|
||||
return {} as User;
|
||||
}
|
||||
|
||||
// Check if the session is still valid
|
||||
if (validUntil <= new Date()) {
|
||||
// Session expired, return invalid
|
||||
return {} as User;
|
||||
}
|
||||
|
||||
// Update session entry in SQL
|
||||
const updateSessionsQuery = 'UPDATE sessions SET last_IP = ? WHERE session_id = ?';
|
||||
const userIdRes = await conn.query(updateSessionsQuery, [ip, sessionId]);
|
||||
await conn.commit();
|
||||
|
||||
// Get the other required user information
|
||||
const userQuery = 'SELECT user_id, email, full_name, is_active FROM users WHERE user_id = ?';
|
||||
const userRows = await conn.query(userQuery, userId);
|
||||
let username = '';
|
||||
let email = '';
|
||||
let fullName = '';
|
||||
let is_active = false;
|
||||
for (const row of userRows) {
|
||||
username = row.username;
|
||||
email = row.email;
|
||||
fullName = row.full_name;
|
||||
is_active = row.is_active;
|
||||
}
|
||||
|
||||
// Everything is fine, return user information
|
||||
return {
|
||||
userId: userId,
|
||||
email: email,
|
||||
passwordHash: 'HIDDEN',
|
||||
fullName: fullName,
|
||||
isActive: is_active
|
||||
};
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
// Return connection
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user