Initial commit
commit
d01175bc30
|
@ -0,0 +1 @@
|
||||||
|
.env
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
|
@ -0,0 +1,15 @@
|
||||||
|
FROM node:20
|
||||||
|
|
||||||
|
ENV TZ=Europe/Berlin
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npx tsc
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "dist/main.js"]
|
|
@ -0,0 +1,22 @@
|
||||||
|
DOCKER_REGISTRY_URL="dockreg.ex.umbach.dev"
|
||||||
|
DOCKER_IMAGE_NAME="zeitadler-terminplaner-backend"
|
||||||
|
|
||||||
|
# only allow to run this script as root
|
||||||
|
if [ "$EUID" -ne 0 ]
|
||||||
|
then echo "Please run as root"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# rm images
|
||||||
|
docker image rm $DOCKER_IMAGE_NAME
|
||||||
|
|
||||||
|
# build images
|
||||||
|
docker build -t $DOCKER_IMAGE_NAME .
|
||||||
|
|
||||||
|
# tag images
|
||||||
|
docker image tag $DOCKER_IMAGE_NAME:latest $DOCKER_REGISTRY_URL/$DOCKER_IMAGE_NAME:latest
|
||||||
|
|
||||||
|
# push to self-hosted docker registry
|
||||||
|
docker push $DOCKER_REGISTRY_URL/$DOCKER_IMAGE_NAME
|
||||||
|
|
||||||
|
echo "Uploaded $DOCKER_IMAGE_NAME to $DOCKER_REGISTRY_URL"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@types/amqplib": "^0.10.4",
|
||||||
|
"amqplib": "^0.10.3",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"email-validator": "^2.0.4",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"googleapis": "^130.0.0",
|
||||||
|
"mariadb": "^3.2.3",
|
||||||
|
"redis": "^4.6.12",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"winston": "^3.11.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cookie-parser": "^1.4.6",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/node": "^20.10.7",
|
||||||
|
"nodemon": "^3.0.2",
|
||||||
|
"ts-node": "^10.9.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/main.ts"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Join Button</title>
|
||||||
|
<style>
|
||||||
|
.joinButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
.joinButton p {
|
||||||
|
background-color: #f32409;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
padding: 15px 32px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
background-color: #f32409;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
background-color: #f54f3d;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
background-color: #f32409;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="joinButton">
|
||||||
|
<p>404</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/*// Function to extract URL parameters
|
||||||
|
function getUrlParameters() {
|
||||||
|
var queryParams = window.location.search.substring(1);
|
||||||
|
var params = queryParams.split('&');
|
||||||
|
var paramsObject = {};
|
||||||
|
|
||||||
|
for (var i = 0; i < params.length; i++) {
|
||||||
|
var pair = params[i].split('=');
|
||||||
|
var key = decodeURIComponent(pair[0]);
|
||||||
|
var value = decodeURIComponent(pair[1]);
|
||||||
|
paramsObject[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return paramsObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display URL parameters on the page
|
||||||
|
function displayUrlParameters() {
|
||||||
|
var params = getUrlParameters();
|
||||||
|
var paramsHtml = '<pre>' + JSON.stringify(params, null, 2) + '</pre>';
|
||||||
|
|
||||||
|
document.body.innerHTML += paramsHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the function to display URL parameters
|
||||||
|
displayUrlParameters();*/
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
|
@ -0,0 +1,86 @@
|
||||||
|
let KKInnovation = {
|
||||||
|
openPopup: async function ({ url }) {
|
||||||
|
if (this.isPopupOpen) return;
|
||||||
|
|
||||||
|
this.isPopupOpen = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.loadStyles('%PUBLIC_URL%/embedPopup/style.css');
|
||||||
|
|
||||||
|
let popup = document.createElement('div');
|
||||||
|
popup.id = 'KKInnovation-embed-popup';
|
||||||
|
|
||||||
|
popup.innerHTML = `
|
||||||
|
<div id="KKInnovation-embed-popup-content">
|
||||||
|
<div id="KKInnovation-embed-popup-loading"></div>
|
||||||
|
<iframe src="${url}" frameborder="0"></iframe>
|
||||||
|
</div>
|
||||||
|
<div id="KKInnovation-embed-popup-close">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 10.586l-3.293-3.293-1.414 1.414 3.293 3.293-3.293 3.293 1.414 1.414 3.293-3.293 3.293 3.293 1.414-1.414-3.293-3.293 3.293-3.293-1.414-1.414z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(popup);
|
||||||
|
setTimeout(() => {
|
||||||
|
popup.classList.add('KKInnovation-embed-popup-open');
|
||||||
|
|
||||||
|
// disable scrolling
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Add click event listener to the background div
|
||||||
|
document.getElementById('KKInnovation-embed-popup').addEventListener('click', (event) => {
|
||||||
|
// Check if the clicked element is the background div
|
||||||
|
if (event.target.id === 'KKInnovation-embed-popup') {
|
||||||
|
|
||||||
|
// enable scrolling
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
|
||||||
|
popup.classList.remove('KKInnovation-embed-popup-open');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isPopupOpen = false;
|
||||||
|
popup.remove();
|
||||||
|
|
||||||
|
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.isPopupOpen = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadStyles: async function (url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.stylesLoaded) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load stylesheet
|
||||||
|
let link = document.createElement('link');
|
||||||
|
link.type = 'text/css';
|
||||||
|
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
document.getElementsByTagName('head')[0].appendChild(link);
|
||||||
|
|
||||||
|
// wait for stylesheet to load
|
||||||
|
link.onload = function () {
|
||||||
|
stylesLoaded = true;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
link.onerror = function () {
|
||||||
|
reject();
|
||||||
|
};
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
isPopupOpen: false,
|
||||||
|
stylesLoaded: false,
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
@keyframes KKInnovatopn-embed-popup-spin {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -50%) rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#KKInnovation-embed-popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
background-color: rgb(0, 0, 0, 0.5);
|
||||||
|
opacity: 0;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
|
||||||
|
transition: opacity 300ms cubic-bezier(0.55, 0.085, 0.68, 0.53);
|
||||||
|
}
|
||||||
|
|
||||||
|
.KKInnovation-embed-popup-open {
|
||||||
|
opacity: 1 !important;
|
||||||
|
pointer-events: all !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#KKInnovation-embed-popup-content {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% + 15px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
height: calc(100% - 30px);
|
||||||
|
max-width: 1200px;
|
||||||
|
max-height: 80vh;
|
||||||
|
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: show;
|
||||||
|
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.5);
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#KKInnovation-embed-popup-loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 5px solid #0003;
|
||||||
|
border-top-color: #006eff;
|
||||||
|
animation: KKInnovatopn-embed-popup-spin 1s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#KKInnovation-embed-popup-content iframe {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
width: calc(100% + 2px);
|
||||||
|
height: calc(100% + 2px);
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#KKInnovation-embed-popup-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
fill: #fff;
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Join Button</title>
|
||||||
|
<style>
|
||||||
|
.joinButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
.joinButton p {
|
||||||
|
background-color: #f32409;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
padding: 15px 32px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
background-color: #f32409;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
background-color: #f54f3d;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
background-color: #f32409;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="joinButton">
|
||||||
|
<p>Error :(</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/*// Function to extract URL parameters
|
||||||
|
function getUrlParameters() {
|
||||||
|
var queryParams = window.location.search.substring(1);
|
||||||
|
var params = queryParams.split('&');
|
||||||
|
var paramsObject = {};
|
||||||
|
|
||||||
|
for (var i = 0; i < params.length; i++) {
|
||||||
|
var pair = params[i].split('=');
|
||||||
|
var key = decodeURIComponent(pair[0]);
|
||||||
|
var value = decodeURIComponent(pair[1]);
|
||||||
|
paramsObject[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return paramsObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display URL parameters on the page
|
||||||
|
function displayUrlParameters() {
|
||||||
|
var params = getUrlParameters();
|
||||||
|
var paramsHtml = '<pre>' + JSON.stringify(params, null, 2) + '</pre>';
|
||||||
|
|
||||||
|
document.body.innerHTML += paramsHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the function to display URL parameters
|
||||||
|
displayUrlParameters();*/
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,75 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Join Button</title>
|
||||||
|
<style>
|
||||||
|
.joinButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
.joinButton p {
|
||||||
|
background-color: #72f309;
|
||||||
|
border: none;
|
||||||
|
color: #000;
|
||||||
|
padding: 15px 32px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
background-color: #72f309;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
background-color: #9fd50c;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
background-color: #72f309;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="joinButton">
|
||||||
|
<p>Thank you for joining! :)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/*// Function to extract URL parameters
|
||||||
|
function getUrlParameters() {
|
||||||
|
var queryParams = window.location.search.substring(1);
|
||||||
|
var params = queryParams.split('&');
|
||||||
|
var paramsObject = {};
|
||||||
|
|
||||||
|
for (var i = 0; i < params.length; i++) {
|
||||||
|
var pair = params[i].split('=');
|
||||||
|
var key = decodeURIComponent(pair[0]);
|
||||||
|
var value = decodeURIComponent(pair[1]);
|
||||||
|
paramsObject[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return paramsObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display URL parameters on the page
|
||||||
|
function displayUrlParameters() {
|
||||||
|
var params = getUrlParameters();
|
||||||
|
var paramsHtml = '<pre>' + JSON.stringify(params, null, 2) + '</pre>';
|
||||||
|
|
||||||
|
document.body.innerHTML += paramsHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the function to display URL parameters
|
||||||
|
displayUrlParameters();*/
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,67 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>OAuth Umach</title>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||||
|
<style>
|
||||||
|
.joinButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
.joinButton button {
|
||||||
|
background-color: #09b1f3;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 15px 32px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.joinButton button:hover {
|
||||||
|
background-color: #0c7cd5;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genToken {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f33;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
transition: all 200ms;
|
||||||
|
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genToken:hover {
|
||||||
|
background-color: #d00;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-image: url('bild.jpg');
|
||||||
|
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script src="/hello.all.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="joinButton">
|
||||||
|
<button onclick="KKInnovation.openPopup({url: 'https://calendar.ex.umbach.dev/embed/?id=643573ed-988b-4dac-8084-6958efcc56d4'});">Termin Buchen</button>
|
||||||
|
</div>
|
||||||
|
<script src="https://calendar.ex.umbach.dev/embedPopup/script.js" type="text/javascript" async></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,459 @@
|
||||||
|
import { calendar_v3, google } from 'googleapis';
|
||||||
|
import { getAccessToken, getOAuthClient } from './oauthAccess';
|
||||||
|
import calendar_ids from '../database/calendar_ids';
|
||||||
|
import user_google_tokens from '../database/user_google_tokens';
|
||||||
|
import { getUser } from '../database/user';
|
||||||
|
|
||||||
|
import { cacheEventsTime, defaultUserSettings } from '../config';
|
||||||
|
|
||||||
|
import { OpenHoursItem, getClosedHours, getEndOfDay, getOpenHours, getStartOfDayOfUser } from './openHoursList';
|
||||||
|
import { MasterUser, User, UserID } from '../user/types';
|
||||||
|
import { Store, StoreID, getStore } from '../database/store';
|
||||||
|
import { getAllBusyEventsFromDatabase } from '../calendarSync/getAllEvents';
|
||||||
|
import redisClient from '../redis/init';
|
||||||
|
import MyRedisKeys from '../redis/keys';
|
||||||
|
import { storeLogger, userLogger } from '../logger/logger';
|
||||||
|
|
||||||
|
type CalendarID = string;
|
||||||
|
type timestamp = number;
|
||||||
|
|
||||||
|
type EventType = calendar_v3.Schema$Event;
|
||||||
|
type SimpleEventType = {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SimpleUserEventType = {
|
||||||
|
userId: string;
|
||||||
|
events: SimpleEventType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getEvents(calendarID: string, earliestBookingTime: Date, endDate: Date): Promise<EventType[]> {
|
||||||
|
try {
|
||||||
|
const _myEvents = await getAllBusyEventsFromDatabase(calendarID, earliestBookingTime, endDate);
|
||||||
|
|
||||||
|
// convert _myEvents to google calendar events
|
||||||
|
let myEvents: EventType[] = [];
|
||||||
|
for (const event of _myEvents) {
|
||||||
|
myEvents.push({
|
||||||
|
id: event.event_id,
|
||||||
|
start: event.start_time ? { dateTime: event.start_time.toISOString() } : undefined,
|
||||||
|
end: event.end_time ? { dateTime: event.end_time.toISOString() } : undefined,
|
||||||
|
status: event.status_val,
|
||||||
|
updated: event.updated_time ? event.updated_time.toISOString() : undefined,
|
||||||
|
transparency: event.transparency,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return myEvents;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBookedTimeFromOneUser(store: Store, user: User, day: Date, time?: Date): Promise<{ start: Date; end: Date }[]> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let openHours: OpenHoursItem[];
|
||||||
|
let endDate: Date;
|
||||||
|
let earliestBookingTime: Date;
|
||||||
|
|
||||||
|
try {
|
||||||
|
openHours = await getOpenHours(store.store_id); /* eg [
|
||||||
|
{ start: 2024-01-10T04:30:00.000Z, end: 2024-01-10T14:00:00.000Z },
|
||||||
|
{ start: 2024-01-11T04:30:00.000Z, end: 2024-01-11T14:00:00.000Z },
|
||||||
|
{ start: 2024-01-12T04:30:00.000Z, end: 2024-01-12T14:00:00.000Z },
|
||||||
|
]*/
|
||||||
|
|
||||||
|
// day is the day to check
|
||||||
|
|
||||||
|
endDate = await getEndOfDay(openHours, day);
|
||||||
|
earliestBookingTime = await getStartOfDayOfUser(user, openHours, day, time !== undefined);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// check if time is set
|
||||||
|
if (time !== undefined) {
|
||||||
|
if (time.getTime() < earliestBookingTime.getTime()) {
|
||||||
|
reject('no free time found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate < earliestBookingTime) {
|
||||||
|
reject('no free time found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let masterCalendarIDs: string[] = [];
|
||||||
|
let myCalendarIDs: string[] = [];
|
||||||
|
if (user.settings.calendar_using_primary_calendar === true) {
|
||||||
|
try {
|
||||||
|
const primaryCalendarID = (await calendar_ids.getPrimaryCalendar(user.user_id)).calendar_id;
|
||||||
|
|
||||||
|
myCalendarIDs.push(primaryCalendarID);
|
||||||
|
userLogger.debug(user.user_id, 'using primary calendar for user', user.username);
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
masterCalendarIDs.push(await calendar_ids.getEventCalendarID(user));
|
||||||
|
|
||||||
|
async function getAllEvents(calendarIDs: string[]) {
|
||||||
|
let events: EventType[] = [];
|
||||||
|
|
||||||
|
for (const calendarID of calendarIDs) {
|
||||||
|
const calRes = await getEvents(calendarID, earliestBookingTime, endDate);
|
||||||
|
events.push(...calRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
let masterEvents = await getAllEvents(masterCalendarIDs);
|
||||||
|
let myEvents = myCalendarIDs.length >= 1 ? await getAllEvents(myCalendarIDs) : [];
|
||||||
|
|
||||||
|
// invert openHours and only keep closed hours that are between earliestBookingTime and endTime
|
||||||
|
const closedHours = getClosedHours(openHours, earliestBookingTime, endDate);
|
||||||
|
|
||||||
|
// merge myEvents and masterEvents into one array and merge overlapping events into one
|
||||||
|
let _events: EventType[] = [...masterEvents, ...myEvents];
|
||||||
|
|
||||||
|
// change events to only contain start and end time
|
||||||
|
let __events: (SimpleEventType | undefined)[] = _events.map((event) => {
|
||||||
|
const start = event.start?.dateTime || event.start?.date;
|
||||||
|
const end = event.end?.dateTime || event.end?.date;
|
||||||
|
|
||||||
|
if (!start || !end) return;
|
||||||
|
|
||||||
|
return { start: new Date(start), end: new Date(end) };
|
||||||
|
});
|
||||||
|
|
||||||
|
__events = [...__events, ...closedHours];
|
||||||
|
|
||||||
|
// remove undefined
|
||||||
|
let events: SimpleEventType[] = __events.filter((event) => event !== undefined) as { start: Date; end: Date }[];
|
||||||
|
|
||||||
|
// sort events by start time
|
||||||
|
events.sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||||
|
|
||||||
|
// merge overlapping events
|
||||||
|
let mergedEvents: SimpleEventType[] = [];
|
||||||
|
for (let i = 0; i < events.length; i++) {
|
||||||
|
const event = events[i];
|
||||||
|
|
||||||
|
let found = false;
|
||||||
|
for (let j = 0; j < mergedEvents.length; j++) {
|
||||||
|
const mergedEvent = mergedEvents[j];
|
||||||
|
|
||||||
|
if (event.start >= mergedEvent.start && event.start <= mergedEvent.end) {
|
||||||
|
if (event.end > mergedEvent.end) {
|
||||||
|
mergedEvent.end = event.end;
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
} else if (event.end >= mergedEvent.start && event.end <= mergedEvent.end) {
|
||||||
|
if (event.start < mergedEvent.start) {
|
||||||
|
mergedEvent.start = event.start;
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
} else if (event.start <= mergedEvent.start && event.end >= mergedEvent.end) {
|
||||||
|
mergedEvent.start = event.start;
|
||||||
|
mergedEvent.end = event.end;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
mergedEvents.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('mergedEvents: ', mergedEvents);
|
||||||
|
userLogger.debug(user.user_id, 'booked time: ', JSON.stringify(mergedEvents, null, 2));
|
||||||
|
|
||||||
|
resolve(mergedEvents); // return booked time
|
||||||
|
} catch (error) {
|
||||||
|
userLogger.error(user.user_id, (error as Error).toString());
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFreeTimeFromOneUser(user: User, bookTimeLength: number, day: Date): Promise<SimpleEventType[]> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
//check if user is in redis
|
||||||
|
|
||||||
|
let redisFreeTime = await redisClient.get(MyRedisKeys.freeTimeFromOneUserCache(user.user_id, bookTimeLength, day));
|
||||||
|
if (redisFreeTime) {
|
||||||
|
let freeTime: SimpleEventType[] = JSON.parse(redisFreeTime);
|
||||||
|
|
||||||
|
// change start and end to Date
|
||||||
|
for (const time of freeTime) {
|
||||||
|
time.start = new Date(time.start);
|
||||||
|
time.end = new Date(time.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(freeTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
userLogger.error(user.user_id, (error as Error).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
userLogger.debug(user.user_id, 'calc freetime: ', day.toLocaleDateString());
|
||||||
|
|
||||||
|
const store = await getStore(user.store_id);
|
||||||
|
const openHours = await getOpenHours(store.store_id);
|
||||||
|
|
||||||
|
// check if "day" is in user.settings.calendar_max_future_booking_days range
|
||||||
|
const maxFutureBookingDays = user.settings.calendar_max_future_booking_days;
|
||||||
|
const maxFutureBookingDaysDate = new Date(new Date().getTime() + maxFutureBookingDays * 24 * 60 * 60 * 1000);
|
||||||
|
if (day.getTime() > maxFutureBookingDaysDate.getTime()) {
|
||||||
|
reject('day is out of range');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let earliestBookingTime = await getStartOfDayOfUser(user, openHours, day);
|
||||||
|
let bookedTime = await getBookedTimeFromOneUser(store, user, day);
|
||||||
|
|
||||||
|
let endTime = await getEndOfDay(openHours, day);
|
||||||
|
|
||||||
|
// create freeTime from bookedTime
|
||||||
|
let freeTime: SimpleEventType[] = [];
|
||||||
|
for (let i = 0; i < bookedTime.length; i++) {
|
||||||
|
const time = bookedTime[i];
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
if (earliestBookingTime < time.start) {
|
||||||
|
freeTime.push({ start: earliestBookingTime, end: time.start });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
freeTime.push({ start: bookedTime[i - 1].end, end: time.start });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bookedTime.length == 0) {
|
||||||
|
freeTime.push({ start: earliestBookingTime, end: endTime });
|
||||||
|
} else {
|
||||||
|
freeTime.push({ start: bookedTime[bookedTime.length - 1].end, end: endTime });
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove freeTime that is too short
|
||||||
|
let _freeTime: SimpleEventType[] = [];
|
||||||
|
for (const time of freeTime) {
|
||||||
|
if (time.end.getTime() - time.start.getTime() + 1000 /*1000ms: just for better fitting*/ >= bookTimeLength * 60 * 1000) {
|
||||||
|
// set time.end to time.end - bookTimeLength
|
||||||
|
time.end = new Date(time.end.getTime() - bookTimeLength * 60 * 1000);
|
||||||
|
|
||||||
|
// check if time.end is before time.start
|
||||||
|
if (time.end.getTime() < time.start.getTime()) {
|
||||||
|
// if so, set time.end to time.start
|
||||||
|
time.end = new Date(time.start.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
_freeTime.push(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
freeTime = _freeTime;
|
||||||
|
|
||||||
|
// to redis
|
||||||
|
try {
|
||||||
|
await redisClient.set(MyRedisKeys.freeTimeFromOneUserCache(user.user_id, bookTimeLength, day), JSON.stringify(freeTime), { EX: 24 * 60 * 60 });
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
resolve(freeTime);
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
await redisClient.set(MyRedisKeys.freeTimeFromOneUserCache(user.user_id, bookTimeLength, day), JSON.stringify([]), { EX: 24 * 60 * 60 });
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFreeTimeOfDay(storeID: StoreID, userIDs: string[], bookTimeLength: number, day: Date): Promise<SimpleUserEventType[]> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const redisKey = MyRedisKeys.freeTimeOfDayCache(storeID, userIDs, bookTimeLength, day);
|
||||||
|
|
||||||
|
try {
|
||||||
|
//check if user is in redis
|
||||||
|
let redisFreeTime = await redisClient.get(redisKey);
|
||||||
|
|
||||||
|
if (redisFreeTime) {
|
||||||
|
let freeTime: SimpleUserEventType[] = JSON.parse(redisFreeTime);
|
||||||
|
|
||||||
|
// change start and end to Date
|
||||||
|
for (const user of freeTime) {
|
||||||
|
for (const time of user.events) {
|
||||||
|
time.start = new Date(time.start);
|
||||||
|
time.end = new Date(time.end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(freeTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const store = await getStore(storeID);
|
||||||
|
if (store === undefined) {
|
||||||
|
reject('store not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let users: User[] = [];
|
||||||
|
for (const userID of userIDs) {
|
||||||
|
try {
|
||||||
|
const user = await getUser(userID);
|
||||||
|
if (user === undefined) {
|
||||||
|
reject('user not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user.store_id !== storeID) {
|
||||||
|
//reject('user.store_id !== storeID');
|
||||||
|
//return;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
users.push(user);
|
||||||
|
} catch (error) {
|
||||||
|
userLogger.error(userID, (error as Error).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openHours = await getOpenHours(storeID);
|
||||||
|
|
||||||
|
// day is the day to check
|
||||||
|
|
||||||
|
let freeTime: SimpleUserEventType[] = [];
|
||||||
|
|
||||||
|
let endTime = await getEndOfDay(openHours, day); // this also checks if day is in openHours
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
try {
|
||||||
|
const _freeTime = await getFreeTimeFromOneUser(user, bookTimeLength, day);
|
||||||
|
|
||||||
|
freeTime.push({ userId: user.user_id, events: _freeTime });
|
||||||
|
} catch (error) {
|
||||||
|
userLogger.debug(user.user_id, '@reject', (error as Error).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache to redis
|
||||||
|
try {
|
||||||
|
await redisClient.set(redisKey, JSON.stringify(freeTime), { EX: 24 * 60 * 60 });
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
resolve(freeTime);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFreeNextDays(store_id: StoreID, _users: UserID[], serviceDuration: number, _date: Date): Promise<Date[]> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// get next days until end of month
|
||||||
|
const nextDays = [];
|
||||||
|
|
||||||
|
const date = new Date(_date); // 11.01.2024 11:17:19
|
||||||
|
let currentDate = date.getDate(); // 1-31
|
||||||
|
date.setHours(0, 0, 0, 0); // 11.01.2024 00:00:00
|
||||||
|
date.setDate(1); // 01.01.2024 00:00:00
|
||||||
|
date.setMonth(date.getMonth() + 1); // 01.02.2024 00:00:00
|
||||||
|
date.setDate(0); // 31.01.2024 00:00:00
|
||||||
|
const lastDayOfMonth = date.getDate();
|
||||||
|
|
||||||
|
// set currentDate to new Date() when month of _date is the current month
|
||||||
|
|
||||||
|
if (new Date().getMonth() === _date.getMonth() && new Date().getFullYear() === _date.getFullYear()) {
|
||||||
|
_date = new Date();
|
||||||
|
currentDate = _date.getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = currentDate - 1; i < lastDayOfMonth; i++) {
|
||||||
|
const day = new Date(_date);
|
||||||
|
|
||||||
|
if (currentDate !== i + 1) {
|
||||||
|
day.setHours(0, 0, 0, 0);
|
||||||
|
day.setDate(i + 1);
|
||||||
|
}
|
||||||
|
nextDays.push(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
let freeDays: Date[] = [];
|
||||||
|
let users: UserID[] = [];
|
||||||
|
|
||||||
|
for (const user_id of _users) {
|
||||||
|
try {
|
||||||
|
const user = await getUser(user_id);
|
||||||
|
|
||||||
|
await calendar_ids.getEventCalendarID(user);
|
||||||
|
|
||||||
|
users.push(user.user_id);
|
||||||
|
userLogger.debug(user.user_id, 'calc getFreeNextDays:');
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (users.length > 0) {
|
||||||
|
for (let i = 0; i < nextDays.length; i++) {
|
||||||
|
const day = nextDays[i];
|
||||||
|
try {
|
||||||
|
//const measureTime = new Date().getTime();
|
||||||
|
const freeTime = await getFreeTimeOfDay(store_id, users, serviceDuration, day);
|
||||||
|
|
||||||
|
let isDayFree = false;
|
||||||
|
for (const key in freeTime) {
|
||||||
|
const time = freeTime[key].events;
|
||||||
|
if (time.length > 0) {
|
||||||
|
isDayFree = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isDayFree) {
|
||||||
|
freeDays.push(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('getFreeNextDays took ', new Date().getTime() - measureTime, 'ms');
|
||||||
|
} catch (error) {
|
||||||
|
//console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(freeDays);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
storeLogger.error(store_id, (error as Error).toString());
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function printDate(str: string, date: Date) {
|
||||||
|
console.log(
|
||||||
|
str,
|
||||||
|
date.toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getBookedTimeFromOneUser, getFreeTimeOfDay, getFreeNextDays, EventType };
|
|
@ -0,0 +1,239 @@
|
||||||
|
import { OAuth2Client } from 'google-auth-library';
|
||||||
|
import { google } from 'googleapis';
|
||||||
|
import { clientId, clientSecret, defaultPrimaryCalendarId } from '../config';
|
||||||
|
import { getAccessToken, getOAuthClient } from './oauthAccess';
|
||||||
|
|
||||||
|
import lang from '../lang/default';
|
||||||
|
import calendar_ids, { CalendarType } from '../database/calendar_ids';
|
||||||
|
import { getAllUsersIDsByStoreID, getUser } from '../database/user';
|
||||||
|
import { Store, getStoreByUser } from '../database/store';
|
||||||
|
import { User } from '../user/types';
|
||||||
|
import { enableCalendarSync } from '../calendarSync/refreshSync';
|
||||||
|
import { userLogger } from '../logger/logger';
|
||||||
|
|
||||||
|
async function getPrefix(store: Store) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
resolve(store.name);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initCalendar(userId: string) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
function noPermission() {
|
||||||
|
userLogger.error(userId, 'No permission to access calendar');
|
||||||
|
reject('403');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// wait 1 second
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
userLogger.info(userId, 'initCalendar...');
|
||||||
|
|
||||||
|
const user = await getUser(userId);
|
||||||
|
|
||||||
|
const store = await getStoreByUser(user);
|
||||||
|
const ownerUser = await getUser(store.owner_user_id);
|
||||||
|
const accessToken = await getAccessToken(ownerUser.user_id);
|
||||||
|
|
||||||
|
const calendar = google.calendar({ version: 'v3', auth: await getOAuthClient(accessToken) });
|
||||||
|
|
||||||
|
let calendars = await calendar_ids.getCalendars(ownerUser.user_id);
|
||||||
|
|
||||||
|
console.log('calendars', calendars);
|
||||||
|
userLogger.debug(userId, 'calendars', JSON.stringify(calendars, null, 2));
|
||||||
|
|
||||||
|
// delete calendar if it not exists in google
|
||||||
|
for (let i = 0; i < calendars.length; i++) {
|
||||||
|
const cal = calendars[i];
|
||||||
|
|
||||||
|
if (cal.type === 'primary') {
|
||||||
|
if (user.isOwner !== true) continue;
|
||||||
|
|
||||||
|
await enableCalendarSync(cal.calendar_id);
|
||||||
|
userLogger.info(userId, 'primary calendar enabled', cal.calendar_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (user.isOwner !== true && cal.worker_user_id !== user.user_id) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
/*const googleCalendar = await calendar.calendarList.get({
|
||||||
|
calendarId: cal.calendar_id,
|
||||||
|
});*/
|
||||||
|
const googleCalendar = await calendar.calendars.get({
|
||||||
|
calendarId: cal.calendar_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await enableCalendarSync(cal.calendar_id);
|
||||||
|
|
||||||
|
// check if i own the googleCalendar
|
||||||
|
//console.log(googleCalendar);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('initCalendar:', (error as any).response?.status);
|
||||||
|
|
||||||
|
if ((error as any).response?.status == 404) {
|
||||||
|
delete calendars[i];
|
||||||
|
await calendar_ids.deleteCalendar(user, cal.calendar_id);
|
||||||
|
|
||||||
|
userLogger.warn(userId, 'deleted calendar ' + cal.calendar_id);
|
||||||
|
} else if ((error as any).response?.status == 403) {
|
||||||
|
noPermission();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
userLogger.error(userId, 'initCalendar:', JSON.stringify(error, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete all undefined values in calendars
|
||||||
|
calendars = calendars.filter((value) => value);
|
||||||
|
|
||||||
|
const requiredCalendars: CalendarType[] = [];
|
||||||
|
|
||||||
|
if (user.isOwner === true) {
|
||||||
|
requiredCalendars.push('myEvents');
|
||||||
|
requiredCalendars.push('openHours');
|
||||||
|
} else {
|
||||||
|
requiredCalendars.push('workerEvents');
|
||||||
|
}
|
||||||
|
requiredCalendars.push('primary');
|
||||||
|
|
||||||
|
for (let i = 0; i < requiredCalendars.length; i++) {
|
||||||
|
const requiredCalendar = requiredCalendars[i];
|
||||||
|
|
||||||
|
let found = false;
|
||||||
|
let foundId = '';
|
||||||
|
|
||||||
|
for (let j = 0; j < calendars.length; j++) {
|
||||||
|
const cal = calendars[j];
|
||||||
|
|
||||||
|
if (cal.type == requiredCalendar) {
|
||||||
|
if (requiredCalendar === 'workerEvents' && cal.worker_user_id !== user.user_id) continue;
|
||||||
|
|
||||||
|
found = true;
|
||||||
|
foundId = cal.calendar_id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertCalendar() {
|
||||||
|
console.log('inserting calendar ' + requiredCalendar);
|
||||||
|
userLogger.info(userId, 'inserting calendar ' + requiredCalendar);
|
||||||
|
|
||||||
|
let prefix = user.username;
|
||||||
|
|
||||||
|
if (requiredCalendar === 'primary') {
|
||||||
|
await calendar_ids.addCalendar(userId, defaultPrimaryCalendarId + '#' + userId, requiredCalendar);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredCalendar === 'myEvents' || requiredCalendar === 'openHours') prefix = lang.calendar[requiredCalendar];
|
||||||
|
|
||||||
|
const cal = await calendar.calendars.insert({
|
||||||
|
requestBody: {
|
||||||
|
summary: prefix + ' - ' + (await getPrefix(store)),
|
||||||
|
description: prefix,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cal.data.id) {
|
||||||
|
console.error('cal.data.id is undefined');
|
||||||
|
userLogger.error(userId, 'cal.data.id is undefined');
|
||||||
|
reject('cal.data.id is undefined');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await calendar_ids.addCalendar(ownerUser.user_id, cal.data.id, requiredCalendar, user.isOwner !== true ? user.user_id : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found || (requiredCalendar === 'primary' && user.isOwner !== true)) {
|
||||||
|
await insertCalendar();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (requiredCalendar === 'primary') continue;
|
||||||
|
|
||||||
|
const caltest = await calendar.calendars.get({
|
||||||
|
calendarId: foundId,
|
||||||
|
});
|
||||||
|
//console.log('found: ', caltest.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
console.log('calendar not found in init');
|
||||||
|
userLogger.warn(userId, 'initCalendar: calendar not found, creating...');
|
||||||
|
|
||||||
|
await insertCalendar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isOwner === true) {
|
||||||
|
//also initCalendar for all workers
|
||||||
|
try {
|
||||||
|
const workersIDs = await getAllUsersIDsByStoreID(store.store_id);
|
||||||
|
|
||||||
|
for (let i = 0; i < workersIDs.length; i++) {
|
||||||
|
if (workersIDs[i] === user.user_id) continue;
|
||||||
|
const workerID = workersIDs[i];
|
||||||
|
|
||||||
|
await initCalendar(workerID);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
if ((error as any).response?.status == 403) {
|
||||||
|
noPermission();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeCalendar(user: User) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const store = await getStoreByUser(user);
|
||||||
|
const ownerUser = await getUser(store.owner_user_id);
|
||||||
|
const accessToken = await getAccessToken(ownerUser.user_id);
|
||||||
|
|
||||||
|
const calendar = google.calendar({ version: 'v3', auth: await getOAuthClient(accessToken) });
|
||||||
|
|
||||||
|
const calendars = await calendar_ids.getCalendars(ownerUser.user_id);
|
||||||
|
|
||||||
|
for (let i = 0; i < calendars.length; i++) {
|
||||||
|
const cal = calendars[i];
|
||||||
|
|
||||||
|
if ((cal.type == 'workerEvents' && cal.worker_user_id === user.user_id) || user.isOwner === true) {
|
||||||
|
console.log('deleting calendar ' + cal.calendar_id);
|
||||||
|
console.log('From User:', user.user_id, ' (Owner:', ownerUser.user_id, ')');
|
||||||
|
userLogger.warn(user.user_id, 'deleting calendar ' + cal.calendar_id, 'From User:', user.user_id, ' (Owner:', ownerUser.user_id, ')');
|
||||||
|
|
||||||
|
/*await calendar.calendars.delete({
|
||||||
|
calendarId: cal.calendar_id,
|
||||||
|
});*/ // no permission to delete calendar
|
||||||
|
|
||||||
|
await calendar_ids.deleteCalendar(user, cal.calendar_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default initCalendar;
|
||||||
|
export { removeCalendar };
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { google } from 'googleapis';
|
||||||
|
import { CalendarEvent } from '../database/reservedCustomer';
|
||||||
|
import { Store, getStoreByUser } from '../database/store';
|
||||||
|
import { getUser } from '../database/user';
|
||||||
|
import { User } from '../user/types';
|
||||||
|
import { EventType, getBookedTimeFromOneUser } from './getFreeTime';
|
||||||
|
import { getAccessToken, getOAuthClient } from './oauthAccess';
|
||||||
|
import { get } from 'http';
|
||||||
|
import { userInfo } from 'os';
|
||||||
|
import calendar_ids from '../database/calendar_ids';
|
||||||
|
import { getActivity } from '../database/services';
|
||||||
|
import { getConnection } from '../database/initDatabase';
|
||||||
|
|
||||||
|
import { insertEvent as insertEventGoogleMariadb, deleteEvent as deleteEventGoogleMariadb } from '../calendarSync/misc';
|
||||||
|
import clearRedisCache from '../redis/clearCache';
|
||||||
|
import { userLogger } from '../logger/logger';
|
||||||
|
|
||||||
|
async function checkIfTimeIsAvailable(store: Store, user: User, start: Date, end: Date): Promise<void> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// check if start is before end
|
||||||
|
if (start >= end) {
|
||||||
|
reject('Start time is after end time');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if start and end is in the same day, month and year
|
||||||
|
if (start.getDate() !== end.getDate() || start.getMonth() !== end.getMonth() || start.getFullYear() !== end.getFullYear()) {
|
||||||
|
reject('Start and end is not in the same day, month and year');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await clearRedisCache(store.store_id);
|
||||||
|
|
||||||
|
const bookedTime = await getBookedTimeFromOneUser(store, user, start, start /* time is important */);
|
||||||
|
console.log('bookedTime', bookedTime);
|
||||||
|
|
||||||
|
// check if start and end is not overlapping with other events in bookedTime
|
||||||
|
for (let i = 0; i < bookedTime.length; i++) {
|
||||||
|
if (start >= bookedTime[i].start && start < bookedTime[i].end) {
|
||||||
|
reject('Start time is overlapping with another event');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (end > bookedTime[i].start && end <= bookedTime[i].end) {
|
||||||
|
reject('End time is overlapping with another event');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start <= bookedTime[i].start && end >= bookedTime[i].end) {
|
||||||
|
reject('New event overlaps an existing event');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertEvent(event: CalendarEvent): Promise<{ event: CalendarEvent; googleEvent: EventType }> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
// insert event into google calendar
|
||||||
|
let calendarUser = await getUser(event.user_id);
|
||||||
|
if (calendarUser === undefined) {
|
||||||
|
reject('User not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const store = await getStoreByUser(calendarUser);
|
||||||
|
|
||||||
|
const accessToken = await getAccessToken(store.owner_user_id);
|
||||||
|
const calendar = google.calendar({ version: 'v3', auth: await getOAuthClient(accessToken) });
|
||||||
|
|
||||||
|
let activity = await getActivity(event.store_id, event.activity_id, true);
|
||||||
|
let description = activity.name + '\n';
|
||||||
|
description += 'E-Mail: ' + event.customer_email + '\n';
|
||||||
|
description += '\n';
|
||||||
|
description += '----\n';
|
||||||
|
description += event.customer_message;
|
||||||
|
|
||||||
|
const calendarID = await calendar_ids.getEventCalendarID(calendarUser);
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
summary: event.customer_name,
|
||||||
|
description: description,
|
||||||
|
start: {
|
||||||
|
dateTime: event.time_start.toISOString(),
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: event.time_end.toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const calRes = await calendar.events.insert({
|
||||||
|
calendarId: calendarID,
|
||||||
|
requestBody,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (calRes.data.id !== null) {
|
||||||
|
await insertEventGoogleMariadb(calRes.data, calendarID, conn);
|
||||||
|
event.calendar_event_id = calRes.data.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
userLogger.info(event.user_id, 'Event inserted', JSON.stringify(event, null, 2));
|
||||||
|
|
||||||
|
resolve({ event: event, googleEvent: calRes.data });
|
||||||
|
} catch (error) {
|
||||||
|
console.log('insertEvent error', error);
|
||||||
|
|
||||||
|
reject(error);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.release();
|
||||||
|
await clearRedisCache(event.store_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteEvent(event: CalendarEvent, ignoreIfNotExist?: boolean): Promise<void> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
// delete event from google calendar
|
||||||
|
let calendarUser = await getUser(event.user_id);
|
||||||
|
if (calendarUser === undefined) {
|
||||||
|
reject('User not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const store = await getStoreByUser(calendarUser);
|
||||||
|
|
||||||
|
const calendarID = await calendar_ids.getEventCalendarID(calendarUser);
|
||||||
|
|
||||||
|
if (event.calendar_event_id) await deleteEventGoogleMariadb(event.calendar_event_id, calendarID, conn);
|
||||||
|
|
||||||
|
const accessToken = await getAccessToken(store.owner_user_id);
|
||||||
|
const calendar = google.calendar({ version: 'v3', auth: await getOAuthClient(accessToken) });
|
||||||
|
|
||||||
|
await calendar.events.delete({
|
||||||
|
calendarId: calendarID,
|
||||||
|
eventId: event.calendar_event_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
userLogger.info(event.user_id, 'Event deleted', JSON.stringify(event, null, 2));
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
} catch (_error) {
|
||||||
|
const error = _error as any;
|
||||||
|
if (error.response && error.response.status === 410) {
|
||||||
|
// 410 means event is already deleted
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ignoreIfNotExist === true && error.response && error.response.status === 404) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(error);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.release();
|
||||||
|
await clearRedisCache(event.store_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { checkIfTimeIsAvailable, insertEvent, deleteEvent };
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { clientId, clientSecret } from '../config';
|
||||||
|
|
||||||
|
import GoogleTokensDatabase from '../database/user_google_tokens';
|
||||||
|
|
||||||
|
import { OAuth2Client } from 'google-auth-library';
|
||||||
|
import initCalendar from './initCalendar';
|
||||||
|
import { userLogger } from '../logger/logger';
|
||||||
|
|
||||||
|
function initOauthAccess(userId: string, refreshToken: string, accessToken: string, expiryDate: Date, googleUuid: string) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
// get access token
|
||||||
|
|
||||||
|
const oauth2Client = new OAuth2Client({
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
});
|
||||||
|
|
||||||
|
oauth2Client.setCredentials({
|
||||||
|
access_token: accessToken,
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
//expiry_date: tokens.expiry_date.getTime(),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await GoogleTokensDatabase.initGoogleAccount(userId, accessToken, refreshToken, expiryDate, googleUuid);
|
||||||
|
userLogger.info(userId, 'Google account initialized');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await initCalendar(userId);
|
||||||
|
|
||||||
|
await GoogleTokensDatabase.updateGoogleAccountStatus(userId, 'OK');
|
||||||
|
resolve(accessToken);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
if (err === '403') {
|
||||||
|
await GoogleTokensDatabase.updateGoogleAccountStatus(userId, 'NOPERM');
|
||||||
|
} else {
|
||||||
|
await GoogleTokensDatabase.updateGoogleAccountStatus(userId, 'ERROR');
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
|
await GoogleTokensDatabase.updateGoogleAccountStatus(userId, 'ERROR');
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAccessToken(userId: string): Promise<string> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
GoogleTokensDatabase.getGoogleTokens(userId)
|
||||||
|
.then((tokens) => {
|
||||||
|
// check if token is expired
|
||||||
|
const expiryDate = tokens.expiry_date;
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
//console.log('token expires in ' + (expiryDate.getTime() - now.getTime()) / 1000 / 60 + ' minutes');
|
||||||
|
|
||||||
|
// refresh token if it expires in less than 1 minute
|
||||||
|
if (expiryDate.getTime() - now.getTime() < 1 * 60 * 1000) {
|
||||||
|
//console.log('token expired, refreshing...');
|
||||||
|
|
||||||
|
const oauth2Client = new OAuth2Client({
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
//redirectUri,
|
||||||
|
});
|
||||||
|
|
||||||
|
oauth2Client.setCredentials({
|
||||||
|
access_token: tokens.access_token,
|
||||||
|
refresh_token: tokens.refresh_token,
|
||||||
|
expiry_date: tokens.expiry_date.getTime(),
|
||||||
|
});
|
||||||
|
|
||||||
|
oauth2Client
|
||||||
|
.refreshAccessToken()
|
||||||
|
.then((refreshedTokens: any) => {
|
||||||
|
const accessToken = refreshedTokens.credentials.access_token;
|
||||||
|
const expireDate = new Date(refreshedTokens.credentials.expiry_date);
|
||||||
|
|
||||||
|
// save to database
|
||||||
|
GoogleTokensDatabase.updateAccessToken(userId, accessToken, expireDate)
|
||||||
|
.then(() => {
|
||||||
|
userLogger.info(userId, 'Google access token refreshed');
|
||||||
|
resolve(accessToken);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('updateAccessToken()', err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
if (err?.response?.data?.error === 'invalid_grant') {
|
||||||
|
GoogleTokensDatabase.unlinkGoogleAccount(userId);
|
||||||
|
} else {
|
||||||
|
userLogger.error(userId, 'Failed to refresh Google access token', JSON.stringify(err, null, 2));
|
||||||
|
}
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(tokens.access_token);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('GetAccessToken', err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getOAuthClient(accessToken: string) {
|
||||||
|
const auth = new OAuth2Client({
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
});
|
||||||
|
|
||||||
|
auth.setCredentials({ access_token: accessToken });
|
||||||
|
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initOauthAccess, getAccessToken, getOAuthClient };
|
|
@ -0,0 +1,215 @@
|
||||||
|
import { google } from 'googleapis';
|
||||||
|
import { getAccessToken, getOAuthClient } from './oauthAccess';
|
||||||
|
import calendar_ids from '../database/calendar_ids';
|
||||||
|
import user_google_tokens from '../database/user_google_tokens';
|
||||||
|
import { getUser } from '../database/user';
|
||||||
|
|
||||||
|
import { User, UserID } from '../user/types';
|
||||||
|
import { StoreID, getAllStoreIDs, getStore } from '../database/store';
|
||||||
|
import { GLOBAL_calendar_min_earliest_booking_time } from '../config';
|
||||||
|
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { refreshOpenHours } from '../calendarSync/openHours';
|
||||||
|
import MyRedisKeys from '../redis/keys';
|
||||||
|
import redisClient from '../redis/init';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
interface OpenHoursItem {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getOpenHours(storeID: string): Promise<OpenHoursItem[]> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let cachedOpenHours = await redisClient.get(MyRedisKeys.calendarOpenHours(storeID));
|
||||||
|
|
||||||
|
if (cachedOpenHours) {
|
||||||
|
let openHours = JSON.parse(cachedOpenHours) as OpenHoursItem[];
|
||||||
|
|
||||||
|
// change the date strings to Date objects
|
||||||
|
openHours = openHours.map((openHour) => {
|
||||||
|
return {
|
||||||
|
start: new Date(openHour.start),
|
||||||
|
end: new Date(openHour.end),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
resolve(openHours);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openHours = await forceGetFreshOpenHours(storeID);
|
||||||
|
|
||||||
|
resolve(openHours);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
reject([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forceGetFreshOpenHours(storeID: string): Promise<OpenHoursItem[]> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
console.log('forceGetFreshOpenHours', storeID);
|
||||||
|
const resp = await refreshOpenHours(storeID);
|
||||||
|
resolve(resp || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
reject([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClosedHours(openHours: OpenHoursItem[], earliestBookingTime: Date, endDate: Date) {
|
||||||
|
let closedHours: OpenHoursItem[] = [];
|
||||||
|
for (let i = 0; i < openHours.length; i++) {
|
||||||
|
const openHour = openHours[i];
|
||||||
|
|
||||||
|
// Skip the openHour if it's before the earliestBookingTime or after the endTime
|
||||||
|
if (openHour.end < earliestBookingTime || openHour.start > endDate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
if (earliestBookingTime < openHour.start) {
|
||||||
|
closedHours.push({ start: earliestBookingTime, end: openHour.start });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const previousEnd = openHours[i - 1].end;
|
||||||
|
// Only add to closedHours if the previous end time is within the booking window
|
||||||
|
if (previousEnd >= earliestBookingTime && previousEnd <= endDate) {
|
||||||
|
closedHours.push({ start: previousEnd, end: openHour.start });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closedHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStartOfDay(openHours: OpenHoursItem[], day: Date): Promise<Date> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let startDate = null;
|
||||||
|
let startOfDay = new Date(day);
|
||||||
|
startOfDay.setHours(0, 0, 0, 0);
|
||||||
|
const currentTime = new Date();
|
||||||
|
|
||||||
|
for (let i = openHours.length - 1; i >= 0; i--) {
|
||||||
|
const openHour = openHours[i];
|
||||||
|
|
||||||
|
if (openHour.start <= startOfDay && openHour.end >= startOfDay) {
|
||||||
|
startDate = startOfDay;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the first date in openHours that is after startOfDay
|
||||||
|
if (openHour.start >= startOfDay && openHour.start <= (startDate || Infinity)) {
|
||||||
|
startDate = openHour.start;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startDate) {
|
||||||
|
reject('The office is closed on this day');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if startDate is before currentTime, set startDate to currentTime
|
||||||
|
if (startDate < currentTime) {
|
||||||
|
startDate = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(startDate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStartOfDayOfUser(user: User, openHours: OpenHoursItem[], day: Date, doNotAddOffset?: boolean): Promise<Date> {
|
||||||
|
const timeShiftOffset = doNotAddOffset ? 0 : GLOBAL_calendar_min_earliest_booking_time;
|
||||||
|
let timeShift = (user.settings.calendar_min_earliest_booking_time + timeShiftOffset) * 60 * 1000;
|
||||||
|
let endOfDay = await getEndOfDay(openHours, day);
|
||||||
|
|
||||||
|
if (new Date().getTime() + timeShift > endOfDay.getTime()) {
|
||||||
|
console.error('no earliestBookingTime found');
|
||||||
|
throw new Error('The office is closed on this day');
|
||||||
|
}
|
||||||
|
|
||||||
|
const startOfDay = await getStartOfDay(openHours, day);
|
||||||
|
|
||||||
|
if (new Date().getTime() + timeShift < startOfDay.getTime()) {
|
||||||
|
timeShift = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const earliestBookingTime = new Date(startOfDay.getTime() + timeShift);
|
||||||
|
|
||||||
|
if (earliestBookingTime > endOfDay) {
|
||||||
|
console.error('no earliestBookingTime found');
|
||||||
|
throw new Error('The office is closed on this day');
|
||||||
|
}
|
||||||
|
return earliestBookingTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEndOfDay(openHours: OpenHoursItem[], day: Date): Promise<Date> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let endDate = null;
|
||||||
|
let endOfDay = new Date(day);
|
||||||
|
endOfDay.setHours(23, 59, 59, 999);
|
||||||
|
const currentTime = new Date();
|
||||||
|
|
||||||
|
for (let i = 0; i < openHours.length; i++) {
|
||||||
|
const openHour = openHours[i];
|
||||||
|
|
||||||
|
if (openHour.start <= endOfDay && openHour.end >= endOfDay) {
|
||||||
|
endDate = endOfDay;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the last date in openHours that is before endOfDay
|
||||||
|
if (openHour.end <= endOfDay && openHour.end >= (endDate || 0)) {
|
||||||
|
if (openHour.end >= currentTime) {
|
||||||
|
endDate = openHour.end;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!endDate) {
|
||||||
|
console.error('no endDate found');
|
||||||
|
reject('The office is closed on this day');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*if (!endDate) {
|
||||||
|
// find the last date in openHours
|
||||||
|
for (let i = openHours.length - 1; i >= 0; i--) {
|
||||||
|
const openHour = openHours[i];
|
||||||
|
|
||||||
|
if (openHour.end >= (endDate || 0)) {
|
||||||
|
endDate = openHour.end;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!endDate) {
|
||||||
|
console.error('no endDate found');
|
||||||
|
reject('The office is closed on this day');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if currentTime is before endDate, set endDate to endOfDay
|
||||||
|
if (currentTime < endDate) {
|
||||||
|
endDate = endOfDay;
|
||||||
|
} else {
|
||||||
|
console.error('no endDate found');
|
||||||
|
reject('The office is closed on this day');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
resolve(endDate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { forceGetFreshOpenHours, getOpenHours, getEndOfDay, getStartOfDayOfUser, getClosedHours, OpenHoursItem };
|
|
@ -0,0 +1,70 @@
|
||||||
|
import calendar_ids from '../database/calendar_ids';
|
||||||
|
import { getConnection } from '../database/initDatabase';
|
||||||
|
|
||||||
|
interface CalendarEvent {
|
||||||
|
event_id: string;
|
||||||
|
calendar_id: string;
|
||||||
|
start_time: Date;
|
||||||
|
end_time: Date;
|
||||||
|
status_val: string;
|
||||||
|
updated_time: Date;
|
||||||
|
transparency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllEventsFromDatabase(calendarId: string, start: Date, end: Date) {
|
||||||
|
return new Promise<CalendarEvent[]>(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
if ((await calendar_ids.checkIfCalendarExists(calendarId)) === false) {
|
||||||
|
reject('Calendar does not exist ' + calendarId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const query =
|
||||||
|
'SELECT * FROM `calendar_events` WHERE `calendar_id` = ? AND ((`start_time` >= ? AND `start_time` <= ?) OR (`end_time` >= ? AND `end_time` <= ?) OR (`start_time` <= ? AND `end_time` >= ?)) ORDER BY `start_time`';
|
||||||
|
const params = [calendarId, start, end, start, end, start, end];
|
||||||
|
|
||||||
|
const rows = await conn.query(query, params);
|
||||||
|
|
||||||
|
resolve(rows as CalendarEvent[]);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('getAllEventsFromDatabase error', error);
|
||||||
|
reject(error);
|
||||||
|
} finally {
|
||||||
|
if (conn) {
|
||||||
|
conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllBusyEventsFromDatabase(calendarId: string, start: Date, end: Date) {
|
||||||
|
return new Promise<CalendarEvent[]>(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
if ((await calendar_ids.checkIfCalendarExists(calendarId)) === false) {
|
||||||
|
reject('Calendar does not exist ' + calendarId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const query =
|
||||||
|
'SELECT * FROM `calendar_events` WHERE `calendar_id` = ? AND `status_val` = "confirmed" AND `transparency` = "opaque" AND ((`start_time` >= ? AND `start_time` <= ?) OR (`end_time` >= ? AND `end_time` <= ?) OR (`start_time` <= ? AND `end_time` >= ?)) ORDER BY `start_time`';
|
||||||
|
const params = [calendarId, start, end, start, end, start, end];
|
||||||
|
|
||||||
|
const rows = await conn.query(query, params);
|
||||||
|
|
||||||
|
resolve(rows as CalendarEvent[]);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('getAllEventsFromDatabase error', error);
|
||||||
|
reject(error);
|
||||||
|
} finally {
|
||||||
|
if (conn) {
|
||||||
|
conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getAllEventsFromDatabase, getAllBusyEventsFromDatabase };
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { log } from 'console';
|
||||||
|
import { calendar_v3 } from 'googleapis';
|
||||||
|
import { PoolConnection } from 'mariadb';
|
||||||
|
import logger from '../logger/logger';
|
||||||
|
|
||||||
|
async function deleteEvent(eventId: string, calendarId: string, conn: PoolConnection) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
await conn.query('DELETE FROM `calendar_events` WHERE `event_id` = ? AND `calendar_id` = ?', [eventId, calendarId]);
|
||||||
|
|
||||||
|
resolve(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('deleteEvent error', error);
|
||||||
|
logger.error('deleteEvent error', JSON.stringify(error, null, 2));
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// true if event is inserted, false if event is deleted
|
||||||
|
async function insertEvent(event: calendar_v3.Schema$Event, calendarId: string, conn: PoolConnection): Promise<boolean> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (event.status === 'cancelled') {
|
||||||
|
try {
|
||||||
|
if (event.id) {
|
||||||
|
await deleteEvent(event.id, calendarId, conn);
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let eventStart, eventEnd, eventUpdated, eventTransparency;
|
||||||
|
|
||||||
|
if (event.start && event.start.date) {
|
||||||
|
eventStart = new Date(event.start.date) || undefined;
|
||||||
|
eventStart.setHours(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
if (event.start && event.start.dateTime) eventStart = new Date(event.start.dateTime) || undefined;
|
||||||
|
|
||||||
|
if (event.end && event.end.date) {
|
||||||
|
eventEnd = new Date(event.end.date) || undefined;
|
||||||
|
eventEnd.setHours(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
if (event.end && event.end.dateTime) eventEnd = new Date(event.end.dateTime) || undefined;
|
||||||
|
|
||||||
|
if (!event.transparency) eventTransparency = 'opaque';
|
||||||
|
else eventTransparency = event.transparency;
|
||||||
|
|
||||||
|
//if (event.updated) eventUpdated = new Date(event.updated);
|
||||||
|
// use own updated time instead of google's
|
||||||
|
eventUpdated = new Date();
|
||||||
|
|
||||||
|
// insert event into database
|
||||||
|
let query = 'INSERT INTO `calendar_events` (`event_id`, `calendar_id`, `start_time`, `end_time`, `status_val`, `updated_time`, `transparency`) VALUES (?, ?, ?, ?, ?, ?, ?)';
|
||||||
|
let params = [event.id, calendarId, eventStart, eventEnd, event.status, eventUpdated, eventTransparency];
|
||||||
|
|
||||||
|
let updates = [];
|
||||||
|
if (eventStart !== null && eventStart !== undefined) {
|
||||||
|
updates.push('`start_time` = ?');
|
||||||
|
params.push(eventStart);
|
||||||
|
}
|
||||||
|
if (eventEnd !== null && eventEnd !== undefined) {
|
||||||
|
updates.push('`end_time` = ?');
|
||||||
|
params.push(eventEnd);
|
||||||
|
}
|
||||||
|
if (event.status !== null && event.status !== undefined) {
|
||||||
|
updates.push('`status_val` = ?');
|
||||||
|
params.push(event.status);
|
||||||
|
}
|
||||||
|
if (eventUpdated !== null && eventUpdated !== undefined) {
|
||||||
|
updates.push('`updated_time` = ?');
|
||||||
|
params.push(eventUpdated);
|
||||||
|
}
|
||||||
|
if (eventTransparency !== null && eventTransparency !== undefined) {
|
||||||
|
updates.push('`transparency` = ?');
|
||||||
|
params.push(eventTransparency);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.length > 0) {
|
||||||
|
query += ' ON DUPLICATE KEY UPDATE ' + updates.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.query(query, params);
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('insertEvent error', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { insertEvent, deleteEvent };
|
|
@ -0,0 +1,131 @@
|
||||||
|
// External libraries
|
||||||
|
import { google } from 'googleapis';
|
||||||
|
|
||||||
|
// Calendar related imports
|
||||||
|
import { getAccessToken, getOAuthClient } from '../calendar/oauthAccess';
|
||||||
|
|
||||||
|
// Database related imports
|
||||||
|
import calendar_ids from '../database/calendar_ids';
|
||||||
|
import user_google_tokens from '../database/user_google_tokens';
|
||||||
|
import { getAllUsersIDsByStoreID, getUser } from '../database/user';
|
||||||
|
import { StoreID, getAllStoreIDs, getStore } from '../database/store';
|
||||||
|
|
||||||
|
// User related imports
|
||||||
|
import { User, UserID } from '../user/types';
|
||||||
|
|
||||||
|
// Event related imports
|
||||||
|
import { getAllEventsFromDatabase } from './getAllEvents';
|
||||||
|
|
||||||
|
// Redis related imports
|
||||||
|
import MyRedisKeys from '../redis/keys';
|
||||||
|
import redisClient from '../redis/init';
|
||||||
|
|
||||||
|
interface OpenHoursItem {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getHighestCalendarMaxFutureBookingDays(storeID: StoreID) {
|
||||||
|
return new Promise<number>(async (resolve, reject) => {
|
||||||
|
// get all users of this store
|
||||||
|
const users = await getAllUsersIDsByStoreID(storeID);
|
||||||
|
|
||||||
|
let highest_calendar_max_future_booking_days = 0;
|
||||||
|
for (const user_id of users) {
|
||||||
|
const user = await getUser(user_id);
|
||||||
|
if (user.settings.calendar_max_future_booking_days > highest_calendar_max_future_booking_days) {
|
||||||
|
highest_calendar_max_future_booking_days = user.settings.calendar_max_future_booking_days;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(highest_calendar_max_future_booking_days);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshOpenHours(storeID: StoreID) {
|
||||||
|
try {
|
||||||
|
let openHoursList: OpenHoursItem[] = [];
|
||||||
|
|
||||||
|
const store = await getStore(storeID);
|
||||||
|
const user_id = store.owner_user_id;
|
||||||
|
|
||||||
|
const highest_calendar_max_future_booking_days = await getHighestCalendarMaxFutureBookingDays(storeID);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const calRes = await getAllEventsFromDatabase(
|
||||||
|
await calendar_ids.getOpenHoursCalendar(user_id),
|
||||||
|
new Date(),
|
||||||
|
new Date(new Date().getTime() + highest_calendar_max_future_booking_days * 24 * 60 * 60 * 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (calRes) {
|
||||||
|
const events = calRes;
|
||||||
|
if (events.length) {
|
||||||
|
let eventList: OpenHoursItem[] = [];
|
||||||
|
for (let i = 0; i < events.length; i++) {
|
||||||
|
const event = events[i];
|
||||||
|
const start = event.start_time;
|
||||||
|
const end = event.end_time;
|
||||||
|
|
||||||
|
if (!start || !end) continue;
|
||||||
|
|
||||||
|
eventList.push({ start: new Date(start), end: new Date(end) });
|
||||||
|
}
|
||||||
|
|
||||||
|
let filteredEventList = [];
|
||||||
|
for (let i = 0; i < eventList.length; i++) {
|
||||||
|
const event = eventList[i];
|
||||||
|
|
||||||
|
let found = false;
|
||||||
|
for (let j = 0; j < filteredEventList.length; j++) {
|
||||||
|
const filteredEvent = filteredEventList[j];
|
||||||
|
|
||||||
|
if (event.start >= filteredEvent.start && event.start <= filteredEvent.end) {
|
||||||
|
if (event.end > filteredEvent.end) {
|
||||||
|
filteredEvent.end = event.end;
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
} else if (event.end >= filteredEvent.start && event.end <= filteredEvent.end) {
|
||||||
|
if (event.start < filteredEvent.start) {
|
||||||
|
filteredEvent.start = event.start;
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
} else if (event.start <= filteredEvent.start && event.end >= filteredEvent.end) {
|
||||||
|
filteredEvent.start = event.start;
|
||||||
|
filteredEvent.end = event.end;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
filteredEventList.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openHoursList = filteredEventList;
|
||||||
|
|
||||||
|
// save open hours to redis
|
||||||
|
const redisKey = MyRedisKeys.calendarOpenHours(storeID);
|
||||||
|
await redisClient.set(redisKey, JSON.stringify(openHoursList), { EX: 24 * 60 * 60 }); // 24 hours
|
||||||
|
|
||||||
|
return openHoursList;
|
||||||
|
} else {
|
||||||
|
console.log('No upcoming events found.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('No calendar response found.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('The API returned an error: ' + err);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { refreshOpenHours, getHighestCalendarMaxFutureBookingDays };
|
|
@ -0,0 +1,622 @@
|
||||||
|
// External libraries
|
||||||
|
import { calendar_v3, google } from 'googleapis';
|
||||||
|
|
||||||
|
// Database related imports
|
||||||
|
import { getConnection } from '../database/initDatabase';
|
||||||
|
import { getUser } from '../database/user';
|
||||||
|
|
||||||
|
// Calendar related imports
|
||||||
|
import { getAccessToken, getOAuthClient } from '../calendar/oauthAccess';
|
||||||
|
|
||||||
|
// Redis related imports
|
||||||
|
import redisClient from '../redis/init';
|
||||||
|
import MyRedisKeys from '../redis/keys';
|
||||||
|
|
||||||
|
// Config and utilities
|
||||||
|
import { GLOBAL_google_webhook_time_delay, GLOBAL_sync_calendar_every_minutes, googleSyncMaxDays, publicURL } from '../config';
|
||||||
|
import countUp from '../requestCounter';
|
||||||
|
import countGoogleRequestStatUp from '../requestCounter';
|
||||||
|
|
||||||
|
// Other internal modules
|
||||||
|
import { insertEvent } from './misc';
|
||||||
|
import { refreshOpenHours } from './openHours';
|
||||||
|
import generateRandomString from '../randomString';
|
||||||
|
import { PoolConnection } from 'mariadb';
|
||||||
|
import { User } from '../user/types';
|
||||||
|
import { forceGetFreshOpenHours } from '../calendar/openHoursList';
|
||||||
|
import clearRedisCache from '../redis/clearCache';
|
||||||
|
import fillCache from '../redis/fillCache';
|
||||||
|
import calendar_ids from '../database/calendar_ids';
|
||||||
|
import { storeLogger, userLogger } from '../logger/logger';
|
||||||
|
import GoogleTokensDatabase from '../database/user_google_tokens';
|
||||||
|
|
||||||
|
interface CalendarIDRow {
|
||||||
|
user_id: string;
|
||||||
|
calendar_id: string;
|
||||||
|
type: string;
|
||||||
|
worker_user_id?: string;
|
||||||
|
sync_token?: string;
|
||||||
|
next_sync?: Date;
|
||||||
|
sync_token_expires_at?: Date;
|
||||||
|
webhook_channel_id?: string;
|
||||||
|
webhook_resource_id?: string;
|
||||||
|
webhook_expires_at?: Date;
|
||||||
|
isChanged?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshSyncToken() {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const currentTime = new Date(new Date().getTime() + 1000 * 60 * 30); // when it expires in 30 minutes
|
||||||
|
|
||||||
|
const rows: CalendarIDRow[] = await conn.query('SELECT * FROM `calendar_ids` WHERE `is_disabled` IS NULL AND (`sync_token` IS NULL OR `sync_token_expires_at` < ?) ORDER BY RAND() LIMIT 1', [
|
||||||
|
currentTime,
|
||||||
|
]);
|
||||||
|
|
||||||
|
conn.end();
|
||||||
|
//console.log('Found ' + rows.length + ' calendars without sync token');
|
||||||
|
|
||||||
|
if (rows.length >= 1) {
|
||||||
|
for (const row of rows) {
|
||||||
|
try {
|
||||||
|
await syncCalendar(row, false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
if (conn) {
|
||||||
|
conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _updateSyncToken(_myCalendar: CalendarIDRow, user: User, calendarId: string, fullCalendarId: string, conn: PoolConnection, refreshLock: Function): Promise<CalendarIDRow> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let myCalendar: CalendarIDRow = _myCalendar;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isPrimary = myCalendar.type === 'primary';
|
||||||
|
if (isPrimary && user.settings.calendar_using_primary_calendar !== true) {
|
||||||
|
resolve(myCalendar);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (myCalendar.sync_token && myCalendar.sync_token_expires_at && myCalendar.sync_token_expires_at > new Date()) {
|
||||||
|
resolve(myCalendar);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!myCalendar.sync_token || !myCalendar.sync_token_expires_at || myCalendar.sync_token_expires_at < new Date()) {
|
||||||
|
await calendar_ids.clearCalendarDatabase(myCalendar.calendar_id);
|
||||||
|
console.log('Cleared calendar database for calendar ' + myCalendar.calendar_id);
|
||||||
|
userLogger.info(user.user_id, '_updateSyncToken()', 'Cleared calendar database for calendar ' + myCalendar.calendar_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const next_sync = new Date(new Date().getTime() + 1000 * 60 * 15); // first time sync again in 15 minutes
|
||||||
|
|
||||||
|
const accessToken = await getAccessToken(user.user_id);
|
||||||
|
const calendar = google.calendar({ version: 'v3', auth: await getOAuthClient(accessToken) });
|
||||||
|
|
||||||
|
// create calendar sync token for row.calendar_id
|
||||||
|
const timeMax = new Date(new Date().getTime() + googleSyncMaxDays * 24 * 60 * 60 * 1000);
|
||||||
|
const expireTime = new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000); // make new whole sync every 14 days
|
||||||
|
let getListSettings: calendar_v3.Params$Resource$Events$List = {
|
||||||
|
calendarId: calendarId,
|
||||||
|
timeMin: new Date().toISOString(),
|
||||||
|
timeMax: timeMax.toISOString(),
|
||||||
|
maxResults: 2500, // warning: this is a hard limit
|
||||||
|
singleEvents: true,
|
||||||
|
showDeleted: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
userLogger.info(user.user_id, '_updateSyncToken()', "running full sync for calendar '" + fullCalendarId + "'");
|
||||||
|
|
||||||
|
// get calendar events
|
||||||
|
countGoogleRequestStatUp('refreshSyncToken');
|
||||||
|
let events = await calendar.events.list(getListSettings);
|
||||||
|
|
||||||
|
const totalEvents = [events];
|
||||||
|
|
||||||
|
let pageCounter = 0;
|
||||||
|
|
||||||
|
// get all events
|
||||||
|
while (events.data.nextPageToken) {
|
||||||
|
pageCounter++;
|
||||||
|
// refresh lock
|
||||||
|
await refreshLock();
|
||||||
|
|
||||||
|
// wait random time between 0 and 10 seconds
|
||||||
|
conn.end();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, Math.random() * 10000));
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
getListSettings.pageToken = events.data.nextPageToken;
|
||||||
|
countGoogleRequestStatUp('refreshSyncTokenPage');
|
||||||
|
events = await calendar.events.list(getListSettings);
|
||||||
|
|
||||||
|
totalEvents.push(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get sync token
|
||||||
|
const newSyncToken = events.data.nextSyncToken;
|
||||||
|
|
||||||
|
userLogger.info(user.user_id, '_updateSyncToken()', "calendar '" + fullCalendarId + "' took " + (pageCounter + 1) + ' pages to sync');
|
||||||
|
|
||||||
|
if (newSyncToken) {
|
||||||
|
let insertedEvents = 0;
|
||||||
|
let deletedEvents = 0;
|
||||||
|
|
||||||
|
for (const _events of totalEvents) {
|
||||||
|
if (!_events.data.items) continue;
|
||||||
|
|
||||||
|
for (const event of _events.data.items) {
|
||||||
|
// insert event into database
|
||||||
|
|
||||||
|
const isInsered = await insertEvent(event, fullCalendarId, conn);
|
||||||
|
if (isInsered) {
|
||||||
|
insertedEvents++;
|
||||||
|
} else {
|
||||||
|
// deleted
|
||||||
|
deletedEvents++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userLogger.info(
|
||||||
|
user.user_id,
|
||||||
|
'_updateSyncToken()',
|
||||||
|
"calendar '" + fullCalendarId + "' took " + (pageCounter + 1) + ' pages to sync and inserted ' + insertedEvents + ' events and deleted ' + deletedEvents + ' events'
|
||||||
|
);
|
||||||
|
|
||||||
|
// update sync token
|
||||||
|
await conn.query('UPDATE `calendar_ids` SET `sync_token` = ?, `sync_token_expires_at` = ?, `next_sync` = ? WHERE `calendar_id` = ?', [
|
||||||
|
newSyncToken,
|
||||||
|
expireTime /*also min. sync_token_expires_at*/,
|
||||||
|
next_sync,
|
||||||
|
fullCalendarId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
myCalendar.sync_token = newSyncToken;
|
||||||
|
myCalendar.sync_token_expires_at = expireTime;
|
||||||
|
myCalendar.next_sync = next_sync;
|
||||||
|
} else {
|
||||||
|
console.error('newSyncToken is null');
|
||||||
|
myCalendar.sync_token = undefined;
|
||||||
|
myCalendar.sync_token_expires_at = undefined;
|
||||||
|
myCalendar.next_sync = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
myCalendar.isChanged = true;
|
||||||
|
|
||||||
|
resolve(myCalendar);
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as any).code === 403 || (error as any).code === 404) {
|
||||||
|
console.error('403 error, setting is_disabled to 1');
|
||||||
|
userLogger.error(user.user_id, '_updateSyncToken()', '403 error, setting is_disabled to 1');
|
||||||
|
await conn.query('UPDATE `calendar_ids` SET `is_disabled` = 1 WHERE `calendar_id` = ?', [fullCalendarId]);
|
||||||
|
myCalendar.isDisabled = true;
|
||||||
|
myCalendar.isChanged = true;
|
||||||
|
|
||||||
|
resolve(myCalendar);
|
||||||
|
} else {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _updateEvents(_myCalendar: CalendarIDRow, user: User, calendarId: string, fullCalendarId: string, conn: PoolConnection, refreshLock: Function): Promise<CalendarIDRow> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let myCalendar: CalendarIDRow = _myCalendar;
|
||||||
|
const isPrimary = myCalendar.type === 'primary';
|
||||||
|
if (isPrimary && user.settings.calendar_using_primary_calendar !== true) {
|
||||||
|
resolve(myCalendar);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!myCalendar.sync_token || !myCalendar.sync_token_expires_at || myCalendar.sync_token_expires_at < new Date()) {
|
||||||
|
resolve(myCalendar);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (myCalendar.next_sync && myCalendar.next_sync > new Date()) {
|
||||||
|
resolve(myCalendar);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = await getAccessToken(user.user_id);
|
||||||
|
const calendar = google.calendar({ version: 'v3', auth: await getOAuthClient(accessToken) });
|
||||||
|
|
||||||
|
const getListSettings: calendar_v3.Params$Resource$Events$List = {
|
||||||
|
calendarId: calendarId,
|
||||||
|
syncToken: myCalendar.sync_token,
|
||||||
|
maxResults: 2500, // warning: this is a hard limit
|
||||||
|
singleEvents: true,
|
||||||
|
showDeleted: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// get calendar events
|
||||||
|
countGoogleRequestStatUp('updateEvents');
|
||||||
|
let events = await calendar.events.list({
|
||||||
|
...getListSettings,
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalEvents = [events];
|
||||||
|
|
||||||
|
let pageCounter = 0;
|
||||||
|
|
||||||
|
// get all events
|
||||||
|
while (events.data.nextPageToken) {
|
||||||
|
pageCounter++;
|
||||||
|
|
||||||
|
// refresh lock
|
||||||
|
await refreshLock();
|
||||||
|
|
||||||
|
// wait random time between 0 and 10 seconds
|
||||||
|
conn.end();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, Math.random() * 10000));
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
countGoogleRequestStatUp('updateEventsPage');
|
||||||
|
events = await calendar.events.list({
|
||||||
|
pageToken: events.data.nextPageToken,
|
||||||
|
...getListSettings,
|
||||||
|
});
|
||||||
|
|
||||||
|
totalEvents.push(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update sync token
|
||||||
|
const syncToken = events.data.nextSyncToken;
|
||||||
|
const next_sync = new Date(new Date().getTime() + 1000 * 60 * GLOBAL_sync_calendar_every_minutes);
|
||||||
|
|
||||||
|
if (syncToken && syncToken !== myCalendar.sync_token) {
|
||||||
|
// update sync token in database
|
||||||
|
await conn.query('UPDATE `calendar_ids` SET `sync_token` = ? WHERE `calendar_id` = ?', [syncToken, fullCalendarId]);
|
||||||
|
|
||||||
|
console.log('Updated sync token for calendar ' + fullCalendarId, 'from', myCalendar.sync_token, 'to', syncToken);
|
||||||
|
myCalendar.sync_token = syncToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
let insertedEvents = 0;
|
||||||
|
let deletedEvents = 0;
|
||||||
|
|
||||||
|
// update events
|
||||||
|
for (const _events of totalEvents) {
|
||||||
|
if (!_events.data.items) continue;
|
||||||
|
|
||||||
|
for (const event of _events.data.items) {
|
||||||
|
// insert event into database
|
||||||
|
console.log('inserting event', event);
|
||||||
|
const isInsered = await insertEvent(event, fullCalendarId, conn);
|
||||||
|
|
||||||
|
if (isInsered) {
|
||||||
|
insertedEvents++;
|
||||||
|
} else {
|
||||||
|
// deleted
|
||||||
|
deletedEvents++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userLogger.info(
|
||||||
|
user.user_id,
|
||||||
|
'_updateEvents()',
|
||||||
|
"calendar '" + fullCalendarId + "' took " + (pageCounter + 1) + ' pages to sync and inserted ' + insertedEvents + ' events and deleted ' + deletedEvents + ' events'
|
||||||
|
);
|
||||||
|
|
||||||
|
// update next_sync
|
||||||
|
await conn.query('UPDATE `calendar_ids` SET `next_sync` = ? WHERE `calendar_id` = ?', [next_sync, fullCalendarId]);
|
||||||
|
console.log('Updated next_sync for calendar ' + fullCalendarId, 'to', next_sync);
|
||||||
|
|
||||||
|
myCalendar.next_sync = next_sync;
|
||||||
|
myCalendar.isChanged = true;
|
||||||
|
|
||||||
|
resolve(myCalendar);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _subscribeToWebhook(_myCalendar: CalendarIDRow, user: User, calendarId: string, fullCalendarId: string, conn: PoolConnection, refreshLock: Function): Promise<CalendarIDRow> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let myCalendar: CalendarIDRow = _myCalendar;
|
||||||
|
const isPrimary = myCalendar.type === 'primary';
|
||||||
|
if (isPrimary && user.settings.calendar_using_primary_calendar !== true) {
|
||||||
|
resolve(myCalendar);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!myCalendar.sync_token || !myCalendar.sync_token_expires_at || myCalendar.sync_token_expires_at < new Date()) {
|
||||||
|
resolve(myCalendar);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (myCalendar.webhook_channel_id && myCalendar.webhook_expires_at && myCalendar.webhook_expires_at > new Date()) {
|
||||||
|
resolve(myCalendar);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = await getAccessToken(user.user_id);
|
||||||
|
const calendar = google.calendar({ version: 'v3', auth: await getOAuthClient(accessToken) });
|
||||||
|
|
||||||
|
const webhookId = await generateRandomString(32);
|
||||||
|
|
||||||
|
const webhook = await calendar.events.watch({
|
||||||
|
calendarId: calendarId,
|
||||||
|
requestBody: {
|
||||||
|
id: webhookId,
|
||||||
|
type: 'web_hook',
|
||||||
|
address: publicURL + '/api/v1/notification/' + encodeURIComponent(fullCalendarId),
|
||||||
|
token: process.env.GOOGLE_WEBHOOK_SECRET,
|
||||||
|
//expiration: 86400, // 24 hours
|
||||||
|
},
|
||||||
|
singleEvents: true,
|
||||||
|
showDeleted: true,
|
||||||
|
timeMin: new Date().toISOString(),
|
||||||
|
timeMax: myCalendar.sync_token_expires_at.toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('webhook', webhook);
|
||||||
|
console.log('webhook', webhook.data);
|
||||||
|
|
||||||
|
const webhookResourceId = webhook.data.resourceId;
|
||||||
|
let webhookExpiration;
|
||||||
|
if (Number(webhook.data.expiration) > 0) {
|
||||||
|
webhookExpiration = new Date(Number(webhook.data.expiration));
|
||||||
|
} else {
|
||||||
|
webhookExpiration = new Date(new Date().getTime() + 86400 * 1000); // 24 hours
|
||||||
|
}
|
||||||
|
|
||||||
|
// update webhook channel id and expiration
|
||||||
|
await conn.query('UPDATE `calendar_ids` SET `webhook_channel_id` = ?, `webhook_resource_id` = ?, `webhook_expires_at` = ? WHERE `calendar_id` = ?', [
|
||||||
|
webhookId,
|
||||||
|
webhookResourceId,
|
||||||
|
webhookExpiration,
|
||||||
|
fullCalendarId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
myCalendar.webhook_channel_id = webhookId;
|
||||||
|
myCalendar.webhook_resource_id = webhookResourceId || undefined;
|
||||||
|
myCalendar.webhook_expires_at = webhookExpiration;
|
||||||
|
|
||||||
|
userLogger.info(user.user_id, '_subscribeToWebhook()', 'Subscribed to webhook for calendar ' + fullCalendarId, 'with webhook id', webhookId);
|
||||||
|
|
||||||
|
resolve(myCalendar);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _checkifAccessTokenIsValid(_myCalendar: CalendarIDRow, user: User, calendarId: string, fullCalendarId: string, conn: PoolConnection): Promise<CalendarIDRow> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let myCalendar: CalendarIDRow = _myCalendar;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// this can be used because when user.settings.calendar_using_primary_calendar is changed, the backend won't recognize it until the next sync
|
||||||
|
|
||||||
|
/*if (myCalendar.type === 'primary' && user.settings.calendar_using_primary_calendar !== true) {
|
||||||
|
throw new Error('Primary calendar is not used');
|
||||||
|
}*/
|
||||||
|
|
||||||
|
await getAccessToken(user.user_id);
|
||||||
|
resolve(myCalendar);
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
// set `is_disabled` to 1 in `calendar_ids` table
|
||||||
|
await conn.query('UPDATE `calendar_ids` SET `is_disabled` = 1 WHERE `calendar_id` = ?', [fullCalendarId]);
|
||||||
|
myCalendar.isDisabled = true;
|
||||||
|
myCalendar.isChanged = true;
|
||||||
|
|
||||||
|
resolve(myCalendar);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enableCalendarSync(calendarId: string) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const rows = await conn.query('UPDATE `calendar_ids` SET `is_disabled` = NULL WHERE `calendar_id` = ?', [calendarId]);
|
||||||
|
|
||||||
|
resolve(rows);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
} finally {
|
||||||
|
if (conn) {
|
||||||
|
conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncCalendar(_myCalendar: CalendarIDRow, waitForLock: boolean = true) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let myCalendar = { ..._myCalendar };
|
||||||
|
|
||||||
|
const redisLockKey = MyRedisKeys.calendarSyncLock(myCalendar.calendar_id);
|
||||||
|
const redisLockKeyTTL = 2 * 60; // 2 minutes
|
||||||
|
|
||||||
|
let isRedisLocked = null;
|
||||||
|
|
||||||
|
function lock() {
|
||||||
|
return redisClient.set(redisLockKey, 'true', { EX: redisLockKeyTTL, NX: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshLock() {
|
||||||
|
return redisClient.expire(redisLockKey, redisLockKeyTTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isRedisLocked = await lock();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if lock is not set
|
||||||
|
if (isRedisLocked === null) {
|
||||||
|
if (waitForLock) {
|
||||||
|
while (isRedisLocked === null) {
|
||||||
|
// Wait for a short period before trying again
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for 1 second
|
||||||
|
|
||||||
|
// Try to acquire the lock again
|
||||||
|
isRedisLocked = await lock();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject('Lock is already set');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let conn;
|
||||||
|
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
let calendarId = myCalendar.calendar_id;
|
||||||
|
let fullCalendarId = myCalendar.calendar_id;
|
||||||
|
let userId = myCalendar.user_id;
|
||||||
|
|
||||||
|
const user = await getUser(myCalendar.user_id);
|
||||||
|
|
||||||
|
const isPrimary = myCalendar.type === 'primary';
|
||||||
|
if (isPrimary) {
|
||||||
|
calendarId = fullCalendarId.split('#')[0];
|
||||||
|
userId = fullCalendarId.split('#')[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
myCalendar = await _checkifAccessTokenIsValid(myCalendar, user, calendarId, fullCalendarId, conn);
|
||||||
|
|
||||||
|
if (myCalendar.isDisabled !== true) {
|
||||||
|
myCalendar = await _updateSyncToken(myCalendar, user, calendarId, fullCalendarId, conn, refreshLock);
|
||||||
|
myCalendar = await _updateEvents(myCalendar, user, calendarId, fullCalendarId, conn, refreshLock);
|
||||||
|
myCalendar = await _subscribeToWebhook(myCalendar, user, calendarId, fullCalendarId, conn, refreshLock);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
if ((error as any).code === 410) {
|
||||||
|
// 410 is the code for the sync token being invalid
|
||||||
|
myCalendar.sync_token = undefined;
|
||||||
|
myCalendar.sync_token_expires_at = undefined;
|
||||||
|
myCalendar.next_sync = undefined;
|
||||||
|
myCalendar.isChanged = true;
|
||||||
|
|
||||||
|
await conn.query('UPDATE `calendar_ids` SET `sync_token` = NULL, `sync_token_expires_at` = NULL, `next_sync` = NULL WHERE `calendar_id` = ?', [fullCalendarId]);
|
||||||
|
|
||||||
|
userLogger.error(user.user_id, 'syncCalendar() error 410', 'Sync token is invalid');
|
||||||
|
} else if ((error as any).code === 401) {
|
||||||
|
// 401 is the code for the access token being invalid
|
||||||
|
await conn.query('UPDATE `calendar_ids` SET `is_disabled` = 1 WHERE `calendar_id` = ?', [fullCalendarId]);
|
||||||
|
myCalendar.isDisabled = true;
|
||||||
|
myCalendar.isChanged = true;
|
||||||
|
|
||||||
|
userLogger.error(user.user_id, 'syncCalendar() error 401', 'Access token is invalid');
|
||||||
|
|
||||||
|
// set `next_sync` = NULL for all calendars of this user
|
||||||
|
await conn.query('UPDATE `calendar_ids` SET `next_sync` = NULL WHERE `user_id` = ?', [user.user_id]);
|
||||||
|
|
||||||
|
if (user.isOwner) {
|
||||||
|
if (myCalendar.type === 'openHours') {
|
||||||
|
await GoogleTokensDatabase.updateGoogleAccountStatus(user.user_id, 'NOPERM');
|
||||||
|
storeLogger.error(user.store_id, 'syncCalendar() error 401', 'Access token is invalid', "set store status to 'NOPERM'");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await GoogleTokensDatabase.updateGoogleAccountStatus(user.user_id, 'NOPERM');
|
||||||
|
storeLogger.error(user.store_id, 'syncCalendar() error 401', 'Access token is invalid', "set store status to 'NOPERM'");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let errorCode = (error as any).code !== undefined && (error as any).code !== null ? (error as any).code : 'unknown';
|
||||||
|
userLogger.error(user.user_id, 'syncCalendar() error', errorCode, (error as Error).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (myCalendar.isChanged === true) {
|
||||||
|
// clear whole redis cache of this store
|
||||||
|
await clearRedisCache(user.store_id);
|
||||||
|
|
||||||
|
// update open hours
|
||||||
|
if (myCalendar.type === 'openHours') {
|
||||||
|
await forceGetFreshOpenHours(user.store_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fillCache(user.store_id);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(myCalendar);
|
||||||
|
} catch (error) {
|
||||||
|
userLogger.error(myCalendar.user_id, 'syncCalendar()', 'Error', JSON.stringify(error, null, 2));
|
||||||
|
console.log('syncCalendar()');
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
reject(error);
|
||||||
|
} finally {
|
||||||
|
if (conn) {
|
||||||
|
conn.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove lock
|
||||||
|
await redisClient.del(redisLockKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scheduleCalendarSync(fullcalendarId: string, channel_id: string) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
let rows = await conn.query('SELECT * FROM `calendar_ids` WHERE `calendar_id` = ?', [fullcalendarId]);
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
reject('Calendar not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = rows[0];
|
||||||
|
|
||||||
|
if (row.webhook_channel_id !== channel_id) {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next_sync = new Date(new Date().getTime() + 1000 * GLOBAL_google_webhook_time_delay);
|
||||||
|
|
||||||
|
await conn.query('UPDATE `calendar_ids` SET `next_sync` = ? WHERE `calendar_id` = ?', [next_sync, fullcalendarId]);
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
} finally {
|
||||||
|
if (conn) {
|
||||||
|
conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default syncCalendar;
|
||||||
|
export { refreshSyncToken, scheduleCalendarSync, enableCalendarSync };
|
|
@ -0,0 +1,50 @@
|
||||||
|
// External libraries
|
||||||
|
import { calendar_v3, google } from 'googleapis';
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
import { googleSyncMaxDays } from '../config';
|
||||||
|
|
||||||
|
// Redis related imports
|
||||||
|
import redisClient from '../redis/init';
|
||||||
|
import MyRedisKeys from '../redis/keys';
|
||||||
|
|
||||||
|
// Database related imports
|
||||||
|
import { getConnection } from '../database/initDatabase';
|
||||||
|
import { getUser } from '../database/user';
|
||||||
|
|
||||||
|
// Calendar related imports
|
||||||
|
import { getAccessToken, getOAuthClient } from '../calendar/oauthAccess';
|
||||||
|
|
||||||
|
// Event related imports
|
||||||
|
import { insertEvent } from './misc';
|
||||||
|
import { refreshOpenHours } from './openHours';
|
||||||
|
|
||||||
|
// Request counter
|
||||||
|
import countGoogleRequestStatUp from '../requestCounter';
|
||||||
|
import syncCalendar from './refreshSync';
|
||||||
|
|
||||||
|
async function updateEvents() {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const currentTime = new Date();
|
||||||
|
|
||||||
|
const rows = await conn.query('SELECT * FROM `calendar_ids` WHERE `is_disabled` IS NULL AND `sync_token` IS NOT NULL AND (`next_sync` < ? OR `next_sync` IS NULL) ORDER BY RAND() LIMIT 1', [
|
||||||
|
currentTime,
|
||||||
|
]);
|
||||||
|
conn.end();
|
||||||
|
|
||||||
|
if (rows.length >= 1) {
|
||||||
|
const row = rows[0];
|
||||||
|
|
||||||
|
await syncCalendar(row, false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default updateEvents;
|
|
@ -0,0 +1,56 @@
|
||||||
|
// External libraries
|
||||||
|
import { calendar_v3, google } from 'googleapis';
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
import { googleSyncMaxDays } from '../config';
|
||||||
|
|
||||||
|
// Redis related imports
|
||||||
|
import redisClient from '../redis/init';
|
||||||
|
import MyRedisKeys from '../redis/keys';
|
||||||
|
|
||||||
|
// Database related imports
|
||||||
|
import { getConnection } from '../database/initDatabase';
|
||||||
|
import { getUser } from '../database/user';
|
||||||
|
|
||||||
|
// Calendar related imports
|
||||||
|
import { getAccessToken, getOAuthClient } from '../calendar/oauthAccess';
|
||||||
|
|
||||||
|
// Event related imports
|
||||||
|
import { insertEvent } from './misc';
|
||||||
|
import { refreshOpenHours } from './openHours';
|
||||||
|
|
||||||
|
// Request counter
|
||||||
|
import countGoogleRequestStatUp from '../requestCounter';
|
||||||
|
import syncCalendar from './refreshSync';
|
||||||
|
|
||||||
|
async function updateWebhooks() {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const currentTime = new Date(new Date().getTime() + 1000 * 60 * 30); // when it expires in 30 minutes
|
||||||
|
|
||||||
|
// sync_token !== null AND (webhook_channel_id === NULL OR webhook_expires_at < new Date())
|
||||||
|
const rows = await conn.query(
|
||||||
|
'SELECT * FROM `calendar_ids` WHERE `is_disabled` IS NULL AND `sync_token` IS NOT NULL AND (`webhook_channel_id` IS NULL OR `webhook_expires_at` < ?) ORDER BY RAND() LIMIT 1',
|
||||||
|
[currentTime]
|
||||||
|
);
|
||||||
|
conn.end();
|
||||||
|
|
||||||
|
if (rows.length >= 1) {
|
||||||
|
const row = rows[0];
|
||||||
|
|
||||||
|
await syncCalendar(row, false);
|
||||||
|
|
||||||
|
// wait 2 seconds
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
await updateWebhooks();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default updateWebhooks;
|
|
@ -0,0 +1,47 @@
|
||||||
|
import e from 'express';
|
||||||
|
|
||||||
|
// crash if no recaptcha keys are set
|
||||||
|
if (!process.env.RECAPTCHA_SITE_KEY || !process.env.RECAPTCHA_SECRET_KEY) {
|
||||||
|
throw new Error('no recaptcha keys found!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedSecretKey = encodeURIComponent(process.env.RECAPTCHA_SECRET_KEY);
|
||||||
|
|
||||||
|
async function verifyCaptcha(token: string, ip?: string): Promise<boolean> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
console.log('verifying captcha token from ip:', ip);
|
||||||
|
|
||||||
|
if (typeof token !== 'string' || token.trim() === '') {
|
||||||
|
reject('Invalid token');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!encodedSecretKey) {
|
||||||
|
reject('captcha secret not set');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url;
|
||||||
|
if (!ip) {
|
||||||
|
url = 'https://www.google.com/recaptcha/api/siteverify?secret=' + encodedSecretKey + '&response=' + encodeURIComponent(token);
|
||||||
|
} else {
|
||||||
|
url = 'https://www.google.com/recaptcha/api/siteverify?secret=' + encodedSecretKey + '&response=' + encodeURIComponent(token) + '&remoteip=' + ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { method: 'POST' });
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.success) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
reject('captcha failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject('Error verifying captcha');
|
||||||
|
console.error('Error verifying captcha:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default verifyCaptcha;
|
|
@ -0,0 +1,69 @@
|
||||||
|
import path from 'path';
|
||||||
|
import { StoreSettings } from './database/store';
|
||||||
|
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const webPort = process.env.BACKEND_PORT;
|
||||||
|
const webHost = process.env.BACKEND_HOST;
|
||||||
|
|
||||||
|
const publicFolder = path.resolve(__dirname + '/../public/');
|
||||||
|
|
||||||
|
const clientId = process.env.GOOGLE_CLIENT_ID; // Google API client ID
|
||||||
|
const clientSecret = process.env.GOOGLE_CLIENT_SECRET; // Google API client secret
|
||||||
|
|
||||||
|
const domain = process.env.DOMAIN;
|
||||||
|
|
||||||
|
const cacheEventsTime = 1000 * 60 * 10; // 10 minutes
|
||||||
|
|
||||||
|
const googleSyncMaxDays = Number(process.env.GOOGLE_SYNC_MAX_DAYS);
|
||||||
|
|
||||||
|
const defaultPrimaryCalendarId = 'primary';
|
||||||
|
|
||||||
|
const databaseCredentials = {
|
||||||
|
host: process.env.MARIADB_HOST,
|
||||||
|
port: Number(process.env.MARIADB_PORT),
|
||||||
|
user: process.env.MARIADB_USERNAME,
|
||||||
|
database: process.env.MARIADB_DATABASE,
|
||||||
|
password: process.env.MARIADB_PASSWORD,
|
||||||
|
};
|
||||||
|
|
||||||
|
const GLOBAL_calendar_min_earliest_booking_time = 15; // because of email verification
|
||||||
|
|
||||||
|
const defaultUserSettings: StoreSettings = {
|
||||||
|
calendar_max_future_booking_days: 7,
|
||||||
|
calendar_min_earliest_booking_time: 0, // in minutes (+ GLOBAL_calendar_min_earliest_booking_time)
|
||||||
|
calendar_using_primary_calendar: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const REDIS_URL = 'redis://' + process.env.REDIS_HOST + ':' + process.env.REDIS_PORT;
|
||||||
|
|
||||||
|
const publicURL = process.env.PUBLIC_URL;
|
||||||
|
|
||||||
|
const GLOBAL_sync_calendar_every_minutes = Number(process.env.SYNC_CALENDAR_EVERY_MINUTES) || 360; // 6 hours
|
||||||
|
const GLOBAL_google_webhook_time_delay = Number(process.env.GOOGLE_WEBHOOK_TIME_DELAY_SECONDS) || 5; // 5 seconds
|
||||||
|
|
||||||
|
let notifyBeforeAppointment: number[] = [1440, 60]; // in minutes // the order is important! -> index 0 is the first notification, last index is the last notification
|
||||||
|
|
||||||
|
//sort notifyBeforeAppointment
|
||||||
|
notifyBeforeAppointment.sort((a, b) => b - a);
|
||||||
|
|
||||||
|
export {
|
||||||
|
webPort,
|
||||||
|
webHost,
|
||||||
|
defaultPrimaryCalendarId,
|
||||||
|
publicFolder,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
databaseCredentials,
|
||||||
|
defaultUserSettings,
|
||||||
|
cacheEventsTime,
|
||||||
|
GLOBAL_calendar_min_earliest_booking_time,
|
||||||
|
GLOBAL_sync_calendar_every_minutes,
|
||||||
|
GLOBAL_google_webhook_time_delay,
|
||||||
|
domain,
|
||||||
|
googleSyncMaxDays,
|
||||||
|
REDIS_URL,
|
||||||
|
publicURL,
|
||||||
|
notifyBeforeAppointment,
|
||||||
|
};
|
|
@ -0,0 +1,252 @@
|
||||||
|
import { User } from '../user/types';
|
||||||
|
import { getConnection } from './initDatabase';
|
||||||
|
import { getStoreByUser } from './store';
|
||||||
|
|
||||||
|
type CalendarType = 'openHours' | 'myEvents' | 'workerEvents' | 'primary';
|
||||||
|
|
||||||
|
async function addCalendar(user_id: string, calendar_id: string, type: CalendarType, worker_user_id?: string) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const _worker_user_id = worker_user_id ? worker_user_id : null;
|
||||||
|
if (type == 'workerEvents' && _worker_user_id == null) {
|
||||||
|
reject('worker_user_id must be set for type workerEvents');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
const rows = await conn.query(
|
||||||
|
`
|
||||||
|
INSERT INTO calendar_ids (user_id, calendar_id, type, worker_user_id)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
user_id = VALUES(user_id),
|
||||||
|
calendar_id = VALUES(calendar_id),
|
||||||
|
type = VALUES(type),
|
||||||
|
worker_user_id = VALUES(worker_user_id),
|
||||||
|
sync_token = NULL,
|
||||||
|
is_disabled = NULL
|
||||||
|
`,
|
||||||
|
[user_id, calendar_id, type, _worker_user_id]
|
||||||
|
);
|
||||||
|
console.log(rows);
|
||||||
|
|
||||||
|
resolve(rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type Calendar = {
|
||||||
|
user_id: string;
|
||||||
|
calendar_id: string;
|
||||||
|
type: CalendarType;
|
||||||
|
worker_user_id?: string; // only for workerEvents
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getCalendars(user_id: string): Promise<Calendar[]> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
const rows = await conn.query('SELECT * FROM calendar_ids WHERE user_id=?', [user_id]);
|
||||||
|
|
||||||
|
let calendars: Calendar[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const calendar: Calendar = rows[i];
|
||||||
|
|
||||||
|
if (calendar.user_id != user_id) {
|
||||||
|
console.error('user_id does not match');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = calendar.type;
|
||||||
|
|
||||||
|
if (type === 'workerEvents' && calendar.worker_user_id === null) {
|
||||||
|
console.error('worker_user_id must be set for type workerEvents. calendar.calendar_id:', calendar.calendar_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type != 'workerEvents' && type != 'openHours' && type != 'myEvents' && type != 'primary') {
|
||||||
|
console.error('invalid type "' + type + '" calendar.calendar_id:', calendar.calendar_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
calendars.push(calendar);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(calendars);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPrimaryCalendar(user_id: string): Promise<Calendar> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
const rows = await conn.query('SELECT calendar_id FROM calendar_ids WHERE user_id=? AND type="primary" AND is_disabled IS NULL', [user_id]);
|
||||||
|
|
||||||
|
if (rows.length >= 1) {
|
||||||
|
let calendar: Calendar = rows[0];
|
||||||
|
|
||||||
|
//calendar.calendar_id = calendar.calendar_id.split('#')[0];
|
||||||
|
|
||||||
|
resolve(calendar);
|
||||||
|
} else {
|
||||||
|
reject('no calendar found');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteCalendar(user: User, calendar_id: string) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
let rows;
|
||||||
|
|
||||||
|
if (user.isOwner === true) {
|
||||||
|
await clearCalendarDatabase(calendar_id);
|
||||||
|
rows = await conn.query('DELETE FROM calendar_ids WHERE user_id=? AND calendar_id=?', [user.user_id, calendar_id]);
|
||||||
|
} else {
|
||||||
|
const store = await getStoreByUser(user);
|
||||||
|
const owner_user_id = store.owner_user_id;
|
||||||
|
await clearCalendarDatabase(calendar_id);
|
||||||
|
rows = await conn.query('DELETE FROM calendar_ids WHERE user_id=? AND calendar_id=? AND worker_user_id=?', [owner_user_id, calendar_id, user.user_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearCalendarDatabase(calendar_id: string) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
await conn.query('DELETE FROM calendar_events WHERE calendar_id=?', [calendar_id]);
|
||||||
|
resolve(undefined);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getOpenHoursCalendar(user_id: string): Promise<string> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
const rows = await conn.query('SELECT calendar_id FROM calendar_ids WHERE user_id=? AND type="openHours"', [user_id]);
|
||||||
|
|
||||||
|
if (rows.length >= 1) {
|
||||||
|
resolve(rows[0].calendar_id);
|
||||||
|
} else {
|
||||||
|
reject('no calendar found');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEventCalendarID(user: User): Promise<string> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
let rows;
|
||||||
|
if (user.isOwner === true) rows = await conn.query('SELECT calendar_id FROM calendar_ids WHERE user_id=? AND type="myEvents" AND is_disabled IS NULL', [user.user_id]);
|
||||||
|
else {
|
||||||
|
let store = await getStoreByUser(user);
|
||||||
|
if (!store) {
|
||||||
|
reject('no store found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let owner_user_id = store.owner_user_id;
|
||||||
|
|
||||||
|
rows = await conn.query('SELECT calendar_id FROM calendar_ids WHERE user_id=? AND type="workerEvents" AND worker_user_id=? AND is_disabled IS NULL', [owner_user_id, user.user_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length >= 1) {
|
||||||
|
resolve(rows[0].calendar_id);
|
||||||
|
} else {
|
||||||
|
reject('no calendar found');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkIfCalendarExists(calendarId: string): Promise<boolean> {
|
||||||
|
return new Promise<boolean>(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const rows = await conn.query('SELECT * FROM `calendar_ids` WHERE `calendar_id` = ? AND `is_disabled` IS NULL', [calendarId]);
|
||||||
|
|
||||||
|
if (rows.length >= 1) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('checkIfCalendarExists error', error);
|
||||||
|
reject(error);
|
||||||
|
} finally {
|
||||||
|
if (conn) {
|
||||||
|
conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendar_ids = {
|
||||||
|
addCalendar,
|
||||||
|
getCalendars,
|
||||||
|
deleteCalendar,
|
||||||
|
getOpenHoursCalendar,
|
||||||
|
getEventCalendarID,
|
||||||
|
getPrimaryCalendar,
|
||||||
|
checkIfCalendarExists,
|
||||||
|
clearCalendarDatabase,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default calendar_ids;
|
||||||
|
export type { CalendarType };
|
|
@ -0,0 +1,28 @@
|
||||||
|
//mariadb
|
||||||
|
import mariadb from 'mariadb';
|
||||||
|
// config
|
||||||
|
import { databaseCredentials } from '../config';
|
||||||
|
// init
|
||||||
|
const pool = mariadb.createPool({
|
||||||
|
host: databaseCredentials.host,
|
||||||
|
port: databaseCredentials.port,
|
||||||
|
user: databaseCredentials.user,
|
||||||
|
database: databaseCredentials.database,
|
||||||
|
password: databaseCredentials.password,
|
||||||
|
connectionLimit: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getConnection() {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await pool.getConnection();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default pool;
|
||||||
|
|
||||||
|
export { getConnection };
|
|
@ -0,0 +1,372 @@
|
||||||
|
import { User } from '../user/types';
|
||||||
|
import { getConnection } from './initDatabase';
|
||||||
|
import { Store } from './store';
|
||||||
|
|
||||||
|
import { GLOBAL_calendar_min_earliest_booking_time, domain } from '../config';
|
||||||
|
|
||||||
|
import { deleteEvent, insertEvent } from '../calendar/insertEvent';
|
||||||
|
import calendar_ids from './calendar_ids';
|
||||||
|
import { getUser } from './user';
|
||||||
|
import sendAppointmentCanceledMail from '../mail/verifyFail';
|
||||||
|
import { sendMailIsVerifiedMail, sendNewAppointmentForEmployeeMail } from '../mail/verify';
|
||||||
|
import generateRandomString from '../randomString';
|
||||||
|
import clearRedisCache from '../redis/clearCache';
|
||||||
|
import { getActivity } from './services';
|
||||||
|
|
||||||
|
type CustomerID = string;
|
||||||
|
|
||||||
|
interface CalendarEvent {
|
||||||
|
store_id: string;
|
||||||
|
user_id: string;
|
||||||
|
activity_id: string;
|
||||||
|
calendar_event_id?: string;
|
||||||
|
time_start: Date;
|
||||||
|
time_end: Date;
|
||||||
|
customer_email: string;
|
||||||
|
customer_name: string;
|
||||||
|
customer_message?: string;
|
||||||
|
customer_id: CustomerID;
|
||||||
|
customer_verified_time?: Date;
|
||||||
|
send_next_notification_at?: Date;
|
||||||
|
created_at: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PublicCalendarEvent {
|
||||||
|
username: string;
|
||||||
|
activity_name: string;
|
||||||
|
time_start: Date;
|
||||||
|
time_end: Date;
|
||||||
|
isVerified: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createEvent(
|
||||||
|
store: Store,
|
||||||
|
user: User,
|
||||||
|
activity_id: string,
|
||||||
|
time_start: Date,
|
||||||
|
time_end: Date,
|
||||||
|
customer_email: string,
|
||||||
|
customer_name: string,
|
||||||
|
customer_message?: string
|
||||||
|
): Promise<CalendarEvent> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const created_at = new Date();
|
||||||
|
|
||||||
|
// create a uuid for the customer email
|
||||||
|
const customer_uuid = await generateRandomString(128);
|
||||||
|
|
||||||
|
let calendarEvent: CalendarEvent = {
|
||||||
|
store_id: store.store_id,
|
||||||
|
user_id: user.user_id,
|
||||||
|
activity_id: activity_id,
|
||||||
|
time_start: time_start,
|
||||||
|
time_end: time_end,
|
||||||
|
customer_email: customer_email,
|
||||||
|
customer_name: customer_name,
|
||||||
|
customer_message: customer_message,
|
||||||
|
customer_id: customer_uuid,
|
||||||
|
|
||||||
|
created_at: created_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertedEvent = await insertEvent(calendarEvent);
|
||||||
|
|
||||||
|
calendarEvent = insertedEvent.event;
|
||||||
|
|
||||||
|
const rows = await conn.query(
|
||||||
|
`INSERT INTO calendar_customer
|
||||||
|
(store_id, name, email, message, customer_id, calendar_event_id, user_id, activity_id, time_start, time_end, created_at)
|
||||||
|
VALUES
|
||||||
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
[store.store_id, customer_name, customer_email, customer_message, customer_uuid, calendarEvent.calendar_event_id, user.user_id, activity_id, time_start, time_end, created_at]
|
||||||
|
);
|
||||||
|
|
||||||
|
resolve(calendarEvent);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEvent(customer_id: CustomerID): Promise<CalendarEvent | undefined> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const rows = await conn.query(`SELECT * FROM calendar_customer WHERE customer_id=?`, [customer_id]);
|
||||||
|
|
||||||
|
if (rows.length < 1) {
|
||||||
|
resolve(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = rows[0];
|
||||||
|
|
||||||
|
const calendarEvent: CalendarEvent = {
|
||||||
|
store_id: row.store_id,
|
||||||
|
user_id: row.user_id,
|
||||||
|
activity_id: row.activity_id,
|
||||||
|
time_start: row.time_start,
|
||||||
|
time_end: row.time_end,
|
||||||
|
customer_email: row.email,
|
||||||
|
calendar_event_id: row.calendar_event_id,
|
||||||
|
customer_name: row.name,
|
||||||
|
customer_message: row.message,
|
||||||
|
customer_id: row.customer_id,
|
||||||
|
customer_verified_time: row.verified_time,
|
||||||
|
created_at: row.created_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve(calendarEvent);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPublicEvent(customer_id: CustomerID): Promise<PublicCalendarEvent | undefined> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let calendarEvent = await getEvent(customer_id);
|
||||||
|
|
||||||
|
if (calendarEvent === undefined) {
|
||||||
|
resolve(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let username = 'unknown';
|
||||||
|
let activity_name = 'unknown';
|
||||||
|
|
||||||
|
try {
|
||||||
|
let user = await getUser(calendarEvent.user_id);
|
||||||
|
username = user.username;
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let activity = await getActivity(calendarEvent.store_id, calendarEvent.activity_id, false);
|
||||||
|
activity_name = activity.name;
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
let publicCalendarEvent: PublicCalendarEvent = {
|
||||||
|
username: username,
|
||||||
|
activity_name: activity_name,
|
||||||
|
time_start: calendarEvent.time_start,
|
||||||
|
time_end: calendarEvent.time_end,
|
||||||
|
isVerified: calendarEvent.customer_verified_time !== undefined && calendarEvent.customer_verified_time !== null,
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve(publicCalendarEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyEvent(customer_id: CustomerID): Promise<CalendarEvent | undefined> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const verified_time = new Date();
|
||||||
|
let isNewVerify = false;
|
||||||
|
|
||||||
|
// also select the event
|
||||||
|
const calendarEventRows = await conn.query(`SELECT * FROM calendar_customer WHERE customer_id=?`, [customer_id]);
|
||||||
|
|
||||||
|
if (calendarEventRows.length != 1) {
|
||||||
|
resolve(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const calendarEventRow = calendarEventRows[0];
|
||||||
|
|
||||||
|
if (calendarEventRow.verified_time === null || calendarEventRow.verified_time === undefined) {
|
||||||
|
const rows = await conn.query(`UPDATE calendar_customer SET verified_time=? WHERE customer_id=?`, [verified_time, customer_id]);
|
||||||
|
isNewVerify = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarEvent: CalendarEvent = {
|
||||||
|
store_id: calendarEventRow.store_id,
|
||||||
|
user_id: calendarEventRow.user_id,
|
||||||
|
activity_id: calendarEventRow.activity_id,
|
||||||
|
time_start: calendarEventRow.time_start,
|
||||||
|
time_end: calendarEventRow.time_end,
|
||||||
|
customer_email: calendarEventRow.email,
|
||||||
|
calendar_event_id: calendarEventRow.calendar_event_id,
|
||||||
|
customer_name: calendarEventRow.name,
|
||||||
|
customer_message: calendarEventRow.message,
|
||||||
|
customer_id: calendarEventRow.customer_id,
|
||||||
|
customer_verified_time: calendarEventRow.verified_time,
|
||||||
|
created_at: calendarEventRow.created_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve(calendarEvent);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isNewVerify === true) {
|
||||||
|
await sendMailIsVerifiedMail(calendarEvent);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error sending sendMailIsVerifiedMail');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelEvent(customer_id: CustomerID): Promise<CalendarEvent | undefined> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
// also select the event
|
||||||
|
const calendarEventRows = await conn.query(`SELECT * FROM calendar_customer WHERE customer_id=?`, [customer_id]);
|
||||||
|
|
||||||
|
console.log('calendarEventRows', calendarEventRows);
|
||||||
|
|
||||||
|
if (calendarEventRows.length != 1) {
|
||||||
|
resolve(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarEventRow = calendarEventRows[0];
|
||||||
|
|
||||||
|
const calendarEvent: CalendarEvent = {
|
||||||
|
store_id: calendarEventRow.store_id,
|
||||||
|
user_id: calendarEventRow.user_id,
|
||||||
|
activity_id: calendarEventRow.activity_id,
|
||||||
|
time_start: calendarEventRow.time_start,
|
||||||
|
time_end: calendarEventRow.time_end,
|
||||||
|
customer_email: calendarEventRow.email,
|
||||||
|
calendar_event_id: calendarEventRow.calendar_event_id,
|
||||||
|
customer_name: calendarEventRow.name,
|
||||||
|
customer_message: calendarEventRow.message,
|
||||||
|
customer_id: calendarEventRow.customer_id,
|
||||||
|
customer_verified_time: calendarEventRow.verified_time,
|
||||||
|
created_at: calendarEventRow.created_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
await deleteEvent(calendarEvent, true);
|
||||||
|
|
||||||
|
await sendAppointmentCanceledMail(calendarEvent);
|
||||||
|
await conn.query(`DELETE FROM calendar_customer WHERE customer_id=?`, [customer_id]);
|
||||||
|
|
||||||
|
resolve(calendarEvent);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAllThatIsUnverified(): Promise<void> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const dateTimeout = new Date().getTime() - GLOBAL_calendar_min_earliest_booking_time * 60 * 1000;
|
||||||
|
|
||||||
|
const rows = await conn.query(`SELECT * FROM calendar_customer WHERE verified_time IS NULL AND created_at < ?`, [new Date(dateTimeout)]);
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const row = rows[i];
|
||||||
|
|
||||||
|
const calendarEvent: CalendarEvent = {
|
||||||
|
store_id: row.store_id,
|
||||||
|
user_id: row.user_id,
|
||||||
|
activity_id: row.activity_id,
|
||||||
|
time_start: row.time_start,
|
||||||
|
time_end: row.time_end,
|
||||||
|
customer_email: row.email,
|
||||||
|
calendar_event_id: row.calendar_event_id,
|
||||||
|
customer_name: row.name,
|
||||||
|
customer_message: row.message,
|
||||||
|
customer_id: row.customer_id,
|
||||||
|
customer_verified_time: row.verified_time,
|
||||||
|
created_at: row.created_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (calendarEvent.calendar_event_id !== undefined) {
|
||||||
|
await deleteEvent(calendarEvent, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendAppointmentCanceledMail(calendarEvent);
|
||||||
|
await conn.query(`DELETE FROM calendar_customer WHERE customer_id=?`, [row.customer_id]);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error deleting unverified event');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateEMailVerificationLink(customer_id: CustomerID): string {
|
||||||
|
return process.env.REACT_STATUS_URL + '/?id=' + encodeURIComponent(customer_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateEMailCancelLink(customer_id: CustomerID): string {
|
||||||
|
return process.env.REACT_STATUS_URL + '/?id=' + encodeURIComponent(customer_id) + '&cancel=true';
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateEMailCancelNotificationLink(customer_id: CustomerID): string {
|
||||||
|
return process.env.REACT_STATUS_URL + '/?id=' + encodeURIComponent(customer_id) + '¬ify=false';
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(deleteAllThatIsUnverified, 1000 * 60); // every minute
|
||||||
|
deleteAllThatIsUnverified();
|
||||||
|
|
||||||
|
const MyEvents = {
|
||||||
|
createEvent: createEvent,
|
||||||
|
verifyEvent: verifyEvent,
|
||||||
|
cancelEvent: cancelEvent,
|
||||||
|
generateEMailVerificationLink: generateEMailVerificationLink,
|
||||||
|
generateEMailCancelLink: generateEMailCancelLink,
|
||||||
|
generateEMailCancelNotificationLink: generateEMailCancelNotificationLink,
|
||||||
|
getEvent: getEvent,
|
||||||
|
getPublicEvent: getPublicEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CalendarEvent };
|
||||||
|
export default MyEvents;
|
||||||
|
|
||||||
|
/*
|
||||||
|
CREATE TABLE `calendar_customer` (
|
||||||
|
`store_id` varchar(255) NOT NULL,
|
||||||
|
`name` varchar(64) NOT NULL,
|
||||||
|
`email` varchar(255) NOT NULL,
|
||||||
|
`message` varchar(1000) DEFAULT NULL,
|
||||||
|
`customer_id` varchar(64) NOT NULL PRIMARY KEY,
|
||||||
|
`calendar_event_id` varchar(255) NOT NULL,
|
||||||
|
`user_id` varchar(255) NOT NULL,
|
||||||
|
`activity_id` varchar(255) NOT NULL,
|
||||||
|
`time_start` datetime NOT NULL,
|
||||||
|
`time_end` datetime NOT NULL,
|
||||||
|
`verified_time` datetime DEFAULT NULL,
|
||||||
|
`created_at` datetime NOT NULL
|
||||||
|
)
|
||||||
|
*/
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { defaultUserSettings } from '../config';
|
||||||
|
import { User, UserID } from '../user/types';
|
||||||
|
import { getConnection } from './initDatabase';
|
||||||
|
import { StoreID } from './store';
|
||||||
|
import { getAllUsersIDsAndNamesByStoreID, getAllUsersIDsByStoreID } from './user';
|
||||||
|
|
||||||
|
type ServicesTypeId = string;
|
||||||
|
type ActivityTypeId = string;
|
||||||
|
|
||||||
|
interface ServicesType {
|
||||||
|
id: ServicesTypeId;
|
||||||
|
name: string;
|
||||||
|
activities: ActivityType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityUsersType = UserID[];
|
||||||
|
|
||||||
|
interface ActivityType {
|
||||||
|
id: ActivityTypeId;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
duration: number;
|
||||||
|
users: ActivityUsersType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// displayMinimal: if all employees are assinged to the activity, the users object will be empty
|
||||||
|
async function getUserIDsByActivityID(store_id: StoreID, activity_id: ActivityTypeId, displayMinimal: boolean): Promise<ActivityUsersType> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
let userIDs: ActivityUsersType = [];
|
||||||
|
|
||||||
|
let userRows: { user_id: UserID }[] = await conn.query('SELECT user_id FROM store_service_activity_users WHERE activity_id=?', [activity_id]);
|
||||||
|
|
||||||
|
let allUserIDs: ActivityUsersType = await getAllUsersIDsByStoreID(store_id);
|
||||||
|
|
||||||
|
if (displayMinimal === true) {
|
||||||
|
if (!userRows || userRows.length === 0) userIDs = [];
|
||||||
|
else userIDs = userRows.map((row) => row.user_id);
|
||||||
|
} else {
|
||||||
|
if (!userRows || userRows.length === 0) userIDs = allUserIDs;
|
||||||
|
else userIDs = userRows.map((row) => row.user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(userIDs);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// displayMinimal: if all employees are assinged to the activity, the users object will be empty
|
||||||
|
async function getServices(store_id: StoreID, displayMinimal: boolean): Promise<ServicesType[]> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
const rows = await conn.query('SELECT * FROM store_services WHERE store_id=?', [store_id]);
|
||||||
|
|
||||||
|
let services: ServicesType[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const row = rows[i];
|
||||||
|
|
||||||
|
if (row.store_id != store_id) {
|
||||||
|
console.error('store_id does not match');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const service: ServicesType = {
|
||||||
|
id: row.service_id,
|
||||||
|
name: row.name,
|
||||||
|
activities: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const activityRows = await conn.query('SELECT * FROM store_service_activities WHERE service_id=?', [service.id]);
|
||||||
|
|
||||||
|
for (let j = 0; j < activityRows.length; j++) {
|
||||||
|
const activityRow = activityRows[j];
|
||||||
|
|
||||||
|
if (activityRow.services_id != row.id) {
|
||||||
|
console.error('services_id does not match');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let users: ActivityUsersType = await getUserIDsByActivityID(store_id, activityRow.activity_id, displayMinimal);
|
||||||
|
|
||||||
|
const activity: ActivityType = {
|
||||||
|
id: activityRow.activity_id,
|
||||||
|
name: activityRow.name,
|
||||||
|
description: activityRow.description,
|
||||||
|
price: activityRow.price,
|
||||||
|
duration: activityRow.duration,
|
||||||
|
users: users,
|
||||||
|
};
|
||||||
|
|
||||||
|
service.activities.push(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
services.push(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(services);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getActivity(store_id: StoreID, activity_id: ActivityTypeId, displayMinimal: boolean): Promise<ActivityType> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
const rows = await conn.query('SELECT * FROM store_service_activities WHERE activity_id=?', [activity_id]);
|
||||||
|
|
||||||
|
let activity: ActivityType;
|
||||||
|
|
||||||
|
if (rows.length >= 1) {
|
||||||
|
const row = rows[0];
|
||||||
|
|
||||||
|
if (row.activity_id != activity_id) {
|
||||||
|
reject('activity_id does not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let users: ActivityUsersType = await getUserIDsByActivityID(store_id, activity_id, displayMinimal);
|
||||||
|
|
||||||
|
activity = {
|
||||||
|
id: row.activity_id,
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
price: row.price,
|
||||||
|
duration: row.duration,
|
||||||
|
users: users,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
reject('no activity found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(activity);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getServices, getActivity, ServicesType, ActivityType, ServicesTypeId, ActivityTypeId };
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { Connection } from 'mariadb';
|
||||||
|
import { getConnection } from './initDatabase';
|
||||||
|
import { defaultUserSettings } from '../config';
|
||||||
|
import { User } from '../user/types';
|
||||||
|
import redisClient from '../redis/init';
|
||||||
|
import MyRedisKeys from '../redis/keys';
|
||||||
|
|
||||||
|
interface StoreSettings {
|
||||||
|
calendar_max_future_booking_days: number;
|
||||||
|
calendar_min_earliest_booking_time: number;
|
||||||
|
calendar_using_primary_calendar: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeSettingsKeys: (keyof StoreSettings)[] = ['calendar_max_future_booking_days', 'calendar_min_earliest_booking_time', 'calendar_using_primary_calendar'];
|
||||||
|
|
||||||
|
type StoreID = string;
|
||||||
|
|
||||||
|
interface Store {
|
||||||
|
store_id: StoreID;
|
||||||
|
owner_user_id: string;
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
settings: StoreSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStore(store_id: string): Promise<Store> {
|
||||||
|
try {
|
||||||
|
//check if user is in redis
|
||||||
|
let redisStore = await redisClient.get(MyRedisKeys.storeCache(store_id));
|
||||||
|
if (redisStore) {
|
||||||
|
let store: Store = JSON.parse(redisStore);
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
let conn: Connection | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const rows = await conn.query('SELECT * FROM stores WHERE store_id = ?', [store_id]);
|
||||||
|
|
||||||
|
if (rows.length != 1) throw new Error('invalid store_id, ' + store_id);
|
||||||
|
|
||||||
|
const row = rows[0];
|
||||||
|
|
||||||
|
let _settings: any = {};
|
||||||
|
|
||||||
|
for (const key of storeSettingsKeys) {
|
||||||
|
_settings[key] = row[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings: StoreSettings = { ...defaultUserSettings };
|
||||||
|
|
||||||
|
for (const _key in settings) {
|
||||||
|
const key = _key as keyof StoreSettings;
|
||||||
|
if (_settings[key]) {
|
||||||
|
(settings as any)[key] = _settings[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let store: Store = {
|
||||||
|
store_id: row.store_id,
|
||||||
|
owner_user_id: row.owner_user_id,
|
||||||
|
name: row.name,
|
||||||
|
address: row.address,
|
||||||
|
settings,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await redisClient.set(MyRedisKeys.storeCache(store_id), JSON.stringify(store), { EX: 1 * 60 });
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStoreByUser(user: User): Promise<Store> {
|
||||||
|
return await getStore(user.store_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllStoreIDs(): Promise<StoreID[]> {
|
||||||
|
let conn: Connection | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
|
||||||
|
const rows = await conn.query('SELECT `store_id` FROM `stores`');
|
||||||
|
|
||||||
|
return rows.map((row: any) => row.store_id);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getStore, Store, StoreSettings, getStoreByUser, getAllStoreIDs, storeSettingsKeys, StoreID };
|
||||||
|
|
||||||
|
/*CREATE TABLE `stores` (
|
||||||
|
`store_id` varchar(255) NOT NULL,
|
||||||
|
`owner_user_id` varchar(255) NOT NULL,
|
||||||
|
`name` varchar(255) NOT NULL,
|
||||||
|
`calendar_max_future_booking_days` int(11) NOT NULL,
|
||||||
|
`calendar_min_earliest_booking_time` int(11) NOT NULL,
|
||||||
|
`calendar_primary_calendar_id` varchar(255) NOT NULL,
|
||||||
|
`calendar_using_primary_calendar` tinyint(1) NOT NULL,
|
||||||
|
`createdAt` datetime NOT NULL,
|
||||||
|
`updatedAt` datetime NOT NULL
|
||||||
|
)*/
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { defaultUserSettings } from '../config';
|
||||||
|
import logger, { userLogger } from '../logger/logger';
|
||||||
|
import redisClient from '../redis/init';
|
||||||
|
import MyRedisKeys from '../redis/keys';
|
||||||
|
import { User, UserID } from '../user/types';
|
||||||
|
import { getConnection } from './initDatabase';
|
||||||
|
import { StoreID, getStore, storeSettingsKeys } from './store';
|
||||||
|
|
||||||
|
async function getUser(user_id: UserID): Promise<User> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
//check if user is in redis
|
||||||
|
let redisUser = await redisClient.get(MyRedisKeys.userCache(user_id));
|
||||||
|
if (redisUser) {
|
||||||
|
let user: User = JSON.parse(redisUser);
|
||||||
|
resolve(user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
const rows = await conn.query('SELECT * FROM users WHERE user_id=?', [user_id]);
|
||||||
|
|
||||||
|
let row;
|
||||||
|
|
||||||
|
if (rows.length >= 1) {
|
||||||
|
row = rows[0];
|
||||||
|
} else {
|
||||||
|
reject('no user found ' + user_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.user_id != user_id) {
|
||||||
|
reject('user_id does not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
const store = await getStore(row.store_id);
|
||||||
|
|
||||||
|
let _settings: any = {};
|
||||||
|
|
||||||
|
for (const key of storeSettingsKeys) {
|
||||||
|
_settings[key] = row[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings: User['settings'] = { ...store.settings }; //{ ...defaultUserSettings };
|
||||||
|
|
||||||
|
for (const _key in settings) {
|
||||||
|
const key = _key as keyof User['settings'];
|
||||||
|
|
||||||
|
if (_settings[key] !== undefined && _settings[key] !== null) {
|
||||||
|
if (key === 'calendar_using_primary_calendar') {
|
||||||
|
(settings as any)[key] = _settings[key] === 1;
|
||||||
|
} else {
|
||||||
|
(settings as any)[key] = _settings[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user = {
|
||||||
|
user_id: row.user_id,
|
||||||
|
store_id: row.store_id,
|
||||||
|
username: row.username,
|
||||||
|
lastUpdate: new Date(),
|
||||||
|
isOwner: store.owner_user_id == row.user_id,
|
||||||
|
settings,
|
||||||
|
email: row.email ? row.email : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
//cache user
|
||||||
|
try {
|
||||||
|
await redisClient.set(MyRedisKeys.userCache(user_id), JSON.stringify(user), { EX: 1 * 60 });
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
resolve(user);
|
||||||
|
} catch (err) {
|
||||||
|
userLogger.error(user_id, 'getUser', (err as Error).toString());
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllUsersIDsByStoreID(store_id: StoreID): Promise<UserID[]> {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
const rows = await conn.query('SELECT user_id FROM users WHERE store_id=?', [store_id]);
|
||||||
|
|
||||||
|
let userIDs: UserID[] = [];
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
userIDs.push(row.user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIDs;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllUsersIDsAndNamesByStoreID(store_id: StoreID): Promise<{ [key: UserID]: { name: string } }> {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
const rows = await conn.query('SELECT user_id, username, FROM users WHERE store_id=?', [store_id]);
|
||||||
|
|
||||||
|
let userIDs: { [key: UserID]: { name: string } } = {};
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
userIDs[row.user_id] = { name: row.username };
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIDs;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getUser, getAllUsersIDsByStoreID, getAllUsersIDsAndNamesByStoreID };
|
||||||
|
|
||||||
|
/*
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`user_id` varchar(255) NOT NULL,
|
||||||
|
`store_id` varchar(255) NOT NULL,
|
||||||
|
`account_name` varchar(255) DEFAULT NULL,
|
||||||
|
`username` varchar(255) NOT NULL,
|
||||||
|
`password` varchar(255) NOT NULL,
|
||||||
|
`calendar_max_future_booking_days` int(11) DEFAULT NULL,
|
||||||
|
`calendar_min_earliest_booking_time` int(11) DEFAULT NULL,
|
||||||
|
`calendar_primary_calendar_id` varchar(255) DEFAULT NULL,
|
||||||
|
`calendar_using_primary_calendar` tinyint(1) DEFAULT NULL,
|
||||||
|
`createdAt` datetime NOT NULL,
|
||||||
|
`updatedAt` datetime NOT NULL
|
||||||
|
)
|
||||||
|
*/
|
|
@ -0,0 +1,234 @@
|
||||||
|
import { forceGetFreshOpenHours } from '../calendar/openHoursList';
|
||||||
|
import { clientId, clientSecret } from '../config';
|
||||||
|
import clearRedisCache from '../redis/clearCache';
|
||||||
|
import fillCache from '../redis/fillCache';
|
||||||
|
import { getConnection } from './initDatabase';
|
||||||
|
import { OAuth2Client } from 'google-auth-library';
|
||||||
|
import { getUser } from './user';
|
||||||
|
import calendar_ids from './calendar_ids';
|
||||||
|
|
||||||
|
type GoogleTokenStatus = 'OK' | 'PENDING' | 'ERROR' | 'NOPERM';
|
||||||
|
|
||||||
|
async function initGoogleAccount(user_id: string, access_token: string, refresh_token: string, expiry_date: Date, google_uuid: string) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let alreadySavedTokens = null;
|
||||||
|
try {
|
||||||
|
alreadySavedTokens = await getGoogleTokens(user_id);
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
let rows;
|
||||||
|
if (!alreadySavedTokens) {
|
||||||
|
rows = await conn.query('INSERT INTO user_google_tokens (user_id, access_token, refresh_token, status, expiry_date, google_uuid) VALUES (?, ?, ?, ?, ?, ?)', [
|
||||||
|
user_id,
|
||||||
|
access_token,
|
||||||
|
refresh_token,
|
||||||
|
'PENDING' as GoogleTokenStatus,
|
||||||
|
expiry_date,
|
||||||
|
google_uuid,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
rows = await conn.query('UPDATE user_google_tokens SET access_token=?, refresh_token=?, status=?, expiry_date=?, google_uuid=? WHERE user_id=?', [
|
||||||
|
access_token,
|
||||||
|
refresh_token,
|
||||||
|
'PENDING' as GoogleTokenStatus,
|
||||||
|
expiry_date,
|
||||||
|
google_uuid,
|
||||||
|
user_id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGoogleAccountStatus(user_id: string, status: GoogleTokenStatus) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
let rows;
|
||||||
|
|
||||||
|
rows = await conn.query('UPDATE user_google_tokens SET status=? WHERE user_id=?', [status, user_id]);
|
||||||
|
|
||||||
|
resolve(rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unlinkGoogleAccount(user_id: string) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
let rows;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let alreadySavedTokens = await getGoogleTokens(user_id);
|
||||||
|
|
||||||
|
const oauth2Client = new OAuth2Client({
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
});
|
||||||
|
|
||||||
|
oauth2Client.setCredentials({
|
||||||
|
access_token: alreadySavedTokens.access_token,
|
||||||
|
refresh_token: alreadySavedTokens.refresh_token,
|
||||||
|
//expiry_date: tokens.expiry_date.getTime(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await oauth2Client.revokeToken(alreadySavedTokens.refresh_token);
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rows = await conn.query('SELECT * FROM calendar_ids WHERE user_id=?', [user_id]);
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const calendar = rows[i];
|
||||||
|
|
||||||
|
if (calendar.user_id != user_id) {
|
||||||
|
console.error('user_id does not match');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await calendar_ids.clearCalendarDatabase(calendar.calendar_id);
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.query('UPDATE calendar_ids SET is_disabled=1, sync_token=NULL WHERE user_id=?', [user_id]);
|
||||||
|
|
||||||
|
const user = await getUser(user_id);
|
||||||
|
|
||||||
|
// clear whole redis cache of this store
|
||||||
|
await clearRedisCache(user.store_id);
|
||||||
|
|
||||||
|
// update open hours
|
||||||
|
await forceGetFreshOpenHours(user.store_id);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fillCache(user.store_id);
|
||||||
|
}, 500);
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
rows = await conn.query('DELETE FROM user_google_tokens WHERE user_id=?', [user_id]);
|
||||||
|
|
||||||
|
resolve(rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoogleTokens = {
|
||||||
|
user_id: string;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
expiry_date: Date;
|
||||||
|
google_uuid: string;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getGoogleTokens(userId: string): Promise<GoogleTokens> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
const rows = await conn.query('SELECT * FROM user_google_tokens WHERE user_id=?', [userId]);
|
||||||
|
|
||||||
|
if (rows.length >= 1) {
|
||||||
|
const tokens: GoogleTokens = rows[0];
|
||||||
|
|
||||||
|
if (tokens.user_id != userId) {
|
||||||
|
reject('user_id does not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(tokens);
|
||||||
|
} else {
|
||||||
|
reject('no google tokens found for ' + userId);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateAccessToken(userId: string, accessToken: string, expire_date: Date) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
const rows = await conn.query('UPDATE user_google_tokens SET access_token=?, expiry_date=? WHERE user_id=?', [accessToken, expire_date, userId]);
|
||||||
|
console.log(rows); //[ {val: 1}, meta: ... ]
|
||||||
|
resolve(rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserIds(): Promise<{ user_id: string }[]> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnection();
|
||||||
|
const rows = await conn.query('SELECT user_id FROM user_google_tokens');
|
||||||
|
console.log(rows); //[ {val: 1}, meta: ... ]
|
||||||
|
resolve(rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const GoogleTokensDatabase = {
|
||||||
|
initGoogleAccount,
|
||||||
|
unlinkGoogleAccount,
|
||||||
|
getGoogleTokens,
|
||||||
|
updateAccessToken,
|
||||||
|
getUserIds,
|
||||||
|
updateGoogleAccountStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GoogleTokensDatabase;
|
||||||
|
|
||||||
|
/* Database structure:
|
||||||
|
|
||||||
|
CREATE TABLE `user_google_tokens` (
|
||||||
|
`user_id` varchar(255) NOT NULL,
|
||||||
|
`access_token` text NOT NULL,
|
||||||
|
`refresh_token` text NOT NULL,
|
||||||
|
`expiry_date` timestamp NOT NULL,
|
||||||
|
`google_uuid` text NOT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()
|
||||||
|
)
|
||||||
|
|
||||||
|
*/
|
|
@ -0,0 +1,31 @@
|
||||||
|
const lang = {
|
||||||
|
calendar: {
|
||||||
|
openHours: 'Öffnungszeiten',
|
||||||
|
myEvents: 'Termine',
|
||||||
|
},
|
||||||
|
days: {
|
||||||
|
0: 'Montag',
|
||||||
|
1: 'Dienstag',
|
||||||
|
2: 'Mittwoch',
|
||||||
|
3: 'Donnerstag',
|
||||||
|
4: 'Freitag',
|
||||||
|
5: 'Samstag',
|
||||||
|
6: 'Sonntag',
|
||||||
|
},
|
||||||
|
months: {
|
||||||
|
0: 'Januar',
|
||||||
|
1: 'Februar',
|
||||||
|
2: 'März',
|
||||||
|
3: 'April',
|
||||||
|
4: 'Mai',
|
||||||
|
5: 'Juni',
|
||||||
|
6: 'Juli',
|
||||||
|
7: 'August',
|
||||||
|
8: 'September',
|
||||||
|
9: 'Oktober',
|
||||||
|
10: 'November',
|
||||||
|
11: 'Dezember',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default lang;
|
|
@ -0,0 +1,189 @@
|
||||||
|
import { isBreakOrContinueStatement } from 'typescript';
|
||||||
|
import winston from 'winston';
|
||||||
|
import Transport from 'winston-transport';
|
||||||
|
|
||||||
|
let cachedLogs = new Map();
|
||||||
|
|
||||||
|
export async function initLogHandler() {
|
||||||
|
while (true) {
|
||||||
|
if (cachedLogs.size > 0) {
|
||||||
|
for (let [logType, logs] of cachedLogs) {
|
||||||
|
const url = process.env.LOG_MANAGER_URL!;
|
||||||
|
const defaultLogType = process.env.NODE_APP_NAME!;
|
||||||
|
const body = JSON.stringify({
|
||||||
|
type: logType === undefined ? defaultLogType : logType,
|
||||||
|
logs: logs,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: body,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedLogs.delete(logType);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, Number(process.env.LOG_MANAGER_INTERVAL)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyTransport extends Transport {
|
||||||
|
log(info: any, callback: any) {
|
||||||
|
setImmediate(() => this.emit('logged', info));
|
||||||
|
|
||||||
|
const currentDate = new Date();
|
||||||
|
const formattedDate = `${currentDate.getHours()}:${currentDate.getMinutes()}:${currentDate.getSeconds()}:${currentDate.getMilliseconds()}`;
|
||||||
|
|
||||||
|
let logLevel;
|
||||||
|
|
||||||
|
switch (info['level']) {
|
||||||
|
case 'info':
|
||||||
|
logLevel = 'I';
|
||||||
|
break;
|
||||||
|
case 'debug':
|
||||||
|
logLevel = 'D';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
logLevel = 'E';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logLevel = 'W';
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgLocal = `${logLevel} ${formattedDate} ${info['message']}`;
|
||||||
|
const msg = `${logLevel} ${formattedDate} ${info['logType']} ${info['message']}`;
|
||||||
|
|
||||||
|
if (cachedLogs.has(info['logType'])) {
|
||||||
|
cachedLogs.get(info['logType']).push(msgLocal);
|
||||||
|
|
||||||
|
// for testing purposes send all logs to NODE_APP_NAME
|
||||||
|
if (info['logType'] !== process.env.NODE_APP_NAME!) {
|
||||||
|
cachedLogs.get(process.env.NODE_APP_NAME!).push(msg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cachedLogs.set(info['logType'], [msgLocal]);
|
||||||
|
|
||||||
|
// for testing purposes send all logs to NODE_APP_NAME
|
||||||
|
if (info['logType'] !== process.env.NODE_APP_NAME!) {
|
||||||
|
cachedLogs.set(process.env.NODE_APP_NAME!, [msg]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
const msg = `${logLevel} ${formattedDate} ${info["message"]}`;
|
||||||
|
|
||||||
|
if (cachedLogs.has(info["logType"])) {
|
||||||
|
cachedLogs.get(info["logType"]).push(msg);
|
||||||
|
|
||||||
|
// for testing purposes send all logs to NODE_APP_NAME
|
||||||
|
if (info["logType"] !== process.env.NODE_APP_NAME!) {
|
||||||
|
cachedLogs.get(process.env.NODE_APP_NAME!).push(msg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cachedLogs.set(info["logType"], [msg]);
|
||||||
|
|
||||||
|
// for testing purposes send all logs to NODE_APP_NAME
|
||||||
|
if (info["logType"] !== process.env.NODE_APP_NAME!) {
|
||||||
|
cachedLogs.set(process.env.er!, [msg]);
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const winlogger = winston.createLogger({
|
||||||
|
level: 'debug',
|
||||||
|
format: winston.format.json(),
|
||||||
|
transports: [new MyTransport()],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
winlogger.add(
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: winston.format.simple(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseLogger {
|
||||||
|
constructor(private logTypePrefix: string) {}
|
||||||
|
|
||||||
|
info(id: string, ...messages: string[]) {
|
||||||
|
this.log('info', id, ...messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(id: string, ...messages: string[]) {
|
||||||
|
this.log('error', id, ...messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(id: string, ...messages: string[]) {
|
||||||
|
this.log('warn', id, ...messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(id: string, ...messages: string[]) {
|
||||||
|
this.log('debug', id, ...messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private log(level: string, id: string, ...messages: string[]) {
|
||||||
|
winlogger.log({
|
||||||
|
level: level,
|
||||||
|
logType: `${this.logTypePrefix}-${id}`,
|
||||||
|
message: messages.join(' '),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserLogger extends BaseLogger {
|
||||||
|
constructor() {
|
||||||
|
super('user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StoreLogger extends BaseLogger {
|
||||||
|
constructor() {
|
||||||
|
super('store');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SystemLogger {
|
||||||
|
info(...messages: string[]) {
|
||||||
|
this.log('info', ...messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(...messages: string[]) {
|
||||||
|
this.log('error', ...messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(...messages: string[]) {
|
||||||
|
this.log('warn', ...messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(...messages: string[]) {
|
||||||
|
this.log('debug', ...messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private log(level: string, ...messages: string[]) {
|
||||||
|
winlogger.log({
|
||||||
|
level: level,
|
||||||
|
logType: process.env.NODE_APP_NAME,
|
||||||
|
message: messages.join(' '),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userLogger = new UserLogger();
|
||||||
|
export const storeLogger = new StoreLogger();
|
||||||
|
export const logger = new SystemLogger();
|
||||||
|
|
||||||
|
export default logger;
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { CalendarEvent } from '../database/reservedCustomer';
|
||||||
|
import { getActivity } from '../database/services';
|
||||||
|
import { getUser } from '../database/user';
|
||||||
|
|
||||||
|
interface DateText {
|
||||||
|
activityName: string;
|
||||||
|
username: string;
|
||||||
|
day: string;
|
||||||
|
dayNumber: string;
|
||||||
|
month: string;
|
||||||
|
year: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
}
|
||||||
|
async function getDateText(event: CalendarEvent): Promise<DateText> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const locale = 'de-DE';
|
||||||
|
|
||||||
|
let activityName = 'unknown';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const activity = await getActivity(event.store_id, event.activity_id, false);
|
||||||
|
activityName = activity.name;
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
const user = await getUser(event.user_id);
|
||||||
|
const username = user.username;
|
||||||
|
const day = event.time_start.toLocaleDateString(locale, { weekday: 'long' });
|
||||||
|
const dayNumber = event.time_start.getDate().toString();
|
||||||
|
const month = event.time_start.toLocaleDateString(locale, { month: 'long' });
|
||||||
|
const year = event.time_start.getFullYear().toString();
|
||||||
|
const startTime = event.time_start.toLocaleTimeString(locale, { hour: 'numeric', minute: 'numeric' });
|
||||||
|
const endTime = event.time_end.toLocaleTimeString(locale, { hour: 'numeric', minute: 'numeric' });
|
||||||
|
|
||||||
|
const dateText: DateText = {
|
||||||
|
activityName: activityName,
|
||||||
|
username: username,
|
||||||
|
day: day,
|
||||||
|
dayNumber: dayNumber,
|
||||||
|
month: month,
|
||||||
|
year: year,
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: endTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve(dateText);
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DateText, getDateText };
|
|
@ -0,0 +1,11 @@
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import amqplib from 'amqplib';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
async function getConnection() {
|
||||||
|
const conn = await amqplib.connect(`amqp://${process.env.RABBITMQ_USERNAME}:${process.env.RABBITMQ_PASSWORD}@${process.env.RABBITMQ_HOST}:${process.env.RABBITMQ_PORT}`);
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getConnection;
|
|
@ -0,0 +1,291 @@
|
||||||
|
import { PoolConnection } from 'mariadb';
|
||||||
|
import { GLOBAL_calendar_min_earliest_booking_time, notifyBeforeAppointment } from '../config';
|
||||||
|
import { getConnection as getConnectionDB } from '../database/initDatabase';
|
||||||
|
import getConnectionRabbit from './getConnection';
|
||||||
|
import MyEvents, { CalendarEvent } from '../database/reservedCustomer';
|
||||||
|
import redisClient from '../redis/init';
|
||||||
|
import MyRedisKeys from '../redis/keys';
|
||||||
|
import { getStore } from '../database/store';
|
||||||
|
import { getDateText } from './dateText';
|
||||||
|
import { getUser } from '../database/user';
|
||||||
|
import logger, { userLogger } from '../logger/logger';
|
||||||
|
|
||||||
|
async function checkForNotifications() {
|
||||||
|
let conn: PoolConnection | null = null;
|
||||||
|
try {
|
||||||
|
conn = await getConnectionDB();
|
||||||
|
|
||||||
|
for (const notifyBefore of notifyBeforeAppointment) {
|
||||||
|
const isFirstNotification = notifyBefore === notifyBeforeAppointment[0];
|
||||||
|
let startSoonTime = new Date(new Date().getTime() + 1000 * 60 * notifyBefore);
|
||||||
|
const currentTime = new Date();
|
||||||
|
|
||||||
|
let rows;
|
||||||
|
|
||||||
|
if (isFirstNotification) {
|
||||||
|
rows = await conn.query(
|
||||||
|
'SELECT * FROM `calendar_customer` WHERE `verified_time` IS NOT NULL AND `send_next_notification_at` IS NULL AND `time_start` < ? AND `time_start` > ? ORDER BY RAND() LIMIT 1',
|
||||||
|
[startSoonTime, currentTime]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
rows = await conn.query(
|
||||||
|
'SELECT * FROM `calendar_customer` WHERE `verified_time` IS NOT NULL AND `send_next_notification_at` < ? AND `time_start` < ? AND `time_start` > ? ORDER BY RAND() LIMIT 1',
|
||||||
|
[currentTime, startSoonTime, currentTime]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length >= 1) {
|
||||||
|
const row = rows[0];
|
||||||
|
console.log('rows', rows);
|
||||||
|
const event: CalendarEvent = {
|
||||||
|
store_id: row.store_id,
|
||||||
|
customer_name: row.name,
|
||||||
|
customer_email: row.email,
|
||||||
|
customer_message: row.message ? row.message : undefined,
|
||||||
|
customer_id: row.customer_id,
|
||||||
|
calendar_event_id: row.calendar_event_id,
|
||||||
|
user_id: row.user_id,
|
||||||
|
activity_id: row.activity_id,
|
||||||
|
time_start: row.time_start,
|
||||||
|
time_end: row.time_end,
|
||||||
|
customer_verified_time: row.verified_time ? row.verified_time : undefined,
|
||||||
|
send_next_notification_at: row.send_next_notification_at ? row.send_next_notification_at : undefined,
|
||||||
|
created_at: row.created_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
// redis mutex like lock
|
||||||
|
await new Promise(async (resolve, reject) => {
|
||||||
|
const redisLockKey = MyRedisKeys.appointmentNotificationLock(event.customer_id);
|
||||||
|
const redisLockKeyTTL = 10; // 10 seconds
|
||||||
|
|
||||||
|
let isRedisLocked = null;
|
||||||
|
|
||||||
|
function lock() {
|
||||||
|
return redisClient.set(redisLockKey, 'true', { EX: redisLockKeyTTL, NX: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshLock() {
|
||||||
|
return redisClient.expire(redisLockKey, redisLockKeyTTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isRedisLocked = await lock();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if lock is not set
|
||||||
|
if (isRedisLocked === null) {
|
||||||
|
resolve(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new Date() > event.time_start) {
|
||||||
|
resolve(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let latestNotificationTime = notifyBefore;
|
||||||
|
|
||||||
|
for (let i = notifyBeforeAppointment.length - 1; i >= 0; i--) {
|
||||||
|
const _notifyBefore = notifyBeforeAppointment[i];
|
||||||
|
const _startSoonTime = new Date(new Date().getTime() + 1000 * 60 * _notifyBefore);
|
||||||
|
if (_startSoonTime > event.time_start) {
|
||||||
|
latestNotificationTime = _notifyBefore;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let latestNotificationTimeIndex = notifyBeforeAppointment.indexOf(latestNotificationTime);
|
||||||
|
|
||||||
|
if (latestNotificationTimeIndex === -1) {
|
||||||
|
console.error('latestNotificationTimeIndex === -1', event);
|
||||||
|
resolve(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextStartSoonTime;
|
||||||
|
|
||||||
|
if (latestNotificationTimeIndex + 1 >= notifyBeforeAppointment.length) {
|
||||||
|
nextStartSoonTime = event.time_start;
|
||||||
|
} else {
|
||||||
|
nextStartSoonTime = new Date(event.time_start.getTime() - 1000 * 60 * notifyBeforeAppointment[latestNotificationTimeIndex + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn) {
|
||||||
|
await conn.query('UPDATE `calendar_customer` SET `send_next_notification_at` = ? WHERE `customer_id` = ?', [nextStartSoonTime, event.customer_id]);
|
||||||
|
|
||||||
|
if (event.created_at <= new Date(new Date().getTime() - 1000 * 60 * GLOBAL_calendar_min_earliest_booking_time)) {
|
||||||
|
try {
|
||||||
|
await sendNotificationByEMail(event, latestNotificationTime);
|
||||||
|
} catch (error) {
|
||||||
|
userLogger.error(
|
||||||
|
event.user_id,
|
||||||
|
'Customer Notification failed to send (begins ' + minutesToText('en', latestNotificationTime) + ')',
|
||||||
|
JSON.stringify(event, null, 2),
|
||||||
|
JSON.stringify(error, null, 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject('conn is null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
logger.error('checkForNotifications()', JSON.stringify(error, null, 2));
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function minutesToText(lang: 'de' | 'en', minutes: number) {
|
||||||
|
// convert minutes to text like: 1 Tag, 2 Stunden und 3 Minuten
|
||||||
|
let text = '';
|
||||||
|
|
||||||
|
if (minutes < 0) minutes = 0;
|
||||||
|
|
||||||
|
const days = Math.floor(minutes / 60 / 24);
|
||||||
|
minutes -= days * 60 * 24;
|
||||||
|
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
minutes -= hours * 60;
|
||||||
|
|
||||||
|
switch (lang) {
|
||||||
|
case 'de':
|
||||||
|
if (days > 0) {
|
||||||
|
if (days === 1) text += 'einem';
|
||||||
|
else text += days;
|
||||||
|
|
||||||
|
text += ' Tag';
|
||||||
|
if (days > 1) text += 'e';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
if (text.length > 0) text += ', ';
|
||||||
|
|
||||||
|
if (hours === 1) text += 'einer';
|
||||||
|
else text += hours;
|
||||||
|
|
||||||
|
text += ' Stunde';
|
||||||
|
if (hours > 1) text += 'n';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minutes > 0) {
|
||||||
|
if (text.length > 0) text += ' und ';
|
||||||
|
|
||||||
|
if (minutes === 1) text += 'einer';
|
||||||
|
else text += minutes;
|
||||||
|
text += ' Minute';
|
||||||
|
|
||||||
|
if (minutes > 1) text += 'n';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.length === 0) text = '0 Minuten';
|
||||||
|
|
||||||
|
if (text === 'einem Tag') text = 'morgen';
|
||||||
|
else text = 'in ' + text;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'en':
|
||||||
|
if (days > 0) {
|
||||||
|
if (days === 1) text += 'one';
|
||||||
|
else text += days;
|
||||||
|
|
||||||
|
text += ' day';
|
||||||
|
if (days > 1) text += 's';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
if (text.length > 0) text += ' and ';
|
||||||
|
|
||||||
|
if (hours === 1) text += 'one';
|
||||||
|
else text += hours;
|
||||||
|
|
||||||
|
text += ' hour';
|
||||||
|
if (hours > 1) text += 's';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minutes > 0) {
|
||||||
|
if (text.length > 0) text += ' and ';
|
||||||
|
|
||||||
|
if (minutes === 1) text += 'one';
|
||||||
|
else text += minutes;
|
||||||
|
|
||||||
|
text += ' minute';
|
||||||
|
if (minutes > 1) text += 's';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.length === 0) text = '0 minutes';
|
||||||
|
|
||||||
|
if (text === 'one day') text = 'tomorrow';
|
||||||
|
else text = 'in ' + text;
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
text = minutes + ' minutes';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelEmailNotifications(customer_id: string): Promise<boolean> {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await getConnectionDB();
|
||||||
|
|
||||||
|
let rows = await conn.query('UPDATE `calendar_customer` SET `send_next_notification_at` = `time_end` WHERE `customer_id` = ?', [customer_id]);
|
||||||
|
|
||||||
|
if (rows.affectedRows === 1) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendNotificationByEMail(event: CalendarEvent, inMinutes: number) {
|
||||||
|
const queue = 'kk.mails';
|
||||||
|
const conn = await getConnectionRabbit();
|
||||||
|
|
||||||
|
// Sender
|
||||||
|
const ch2 = await conn.createChannel();
|
||||||
|
|
||||||
|
const store = await getStore(event.store_id);
|
||||||
|
const user = await getUser(event.user_id);
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
m: event.customer_email, // UserMail
|
||||||
|
t: 'embedEmailNotificationZeitAdler', // TemplateId
|
||||||
|
l: 'de', // LanguageId
|
||||||
|
// BodyData
|
||||||
|
b: {
|
||||||
|
name: event.customer_name,
|
||||||
|
cancelURL: MyEvents.generateEMailCancelLink(event.customer_id),
|
||||||
|
address: store.address,
|
||||||
|
timeText: minutesToText('de', inMinutes),
|
||||||
|
cancelNotifyURL: MyEvents.generateEMailCancelNotificationLink(event.customer_id),
|
||||||
|
...(await getDateText(event)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ch2.sendToQueue(queue, Buffer.from(JSON.stringify(data)));
|
||||||
|
|
||||||
|
userLogger.info(event.user_id, 'Customer Notification sent (begins ' + minutesToText('en', inMinutes) + ') to ' + event.customer_email);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { checkForNotifications, cancelEmailNotifications };
|
||||||
|
export default checkForNotifications;
|
|
@ -0,0 +1,117 @@
|
||||||
|
import MyEvents, { CalendarEvent } from '../database/reservedCustomer';
|
||||||
|
import { getStore } from '../database/store';
|
||||||
|
import { getUser } from '../database/user';
|
||||||
|
import { User } from '../user/types';
|
||||||
|
import { getDateText } from './dateText';
|
||||||
|
import getConnection from './getConnection';
|
||||||
|
|
||||||
|
async function sendVerifyMail(event: CalendarEvent) {
|
||||||
|
try {
|
||||||
|
const queue = 'kk.mails';
|
||||||
|
const conn = await getConnection();
|
||||||
|
|
||||||
|
// Sender
|
||||||
|
const ch2 = await conn.createChannel();
|
||||||
|
|
||||||
|
const verifyURL = MyEvents.generateEMailVerificationLink(event.customer_id);
|
||||||
|
const cancelURL = MyEvents.generateEMailCancelLink(event.customer_id);
|
||||||
|
|
||||||
|
const store = await getStore(event.store_id);
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
m: event.customer_email, // UserMail
|
||||||
|
t: 'embedVerificationZeitAdler', // TemplateId
|
||||||
|
l: 'de', // LanguageId
|
||||||
|
// BodyData
|
||||||
|
b: {
|
||||||
|
name: event.customer_name,
|
||||||
|
verifyURL: verifyURL,
|
||||||
|
cancelURL: cancelURL,
|
||||||
|
address: store.address,
|
||||||
|
...(await getDateText(event)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ch2.sendToQueue(queue, Buffer.from(JSON.stringify(data)));
|
||||||
|
console.log('Sent:', data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending verify mail', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMailIsVerifiedMail(event: CalendarEvent) {
|
||||||
|
try {
|
||||||
|
const queue = 'kk.mails';
|
||||||
|
const conn = await getConnection();
|
||||||
|
|
||||||
|
// Sender
|
||||||
|
const ch2 = await conn.createChannel();
|
||||||
|
|
||||||
|
const cancelURL = MyEvents.generateEMailCancelLink(event.customer_id);
|
||||||
|
|
||||||
|
const store = await getStore(event.store_id);
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
m: event.customer_email, // UserMail
|
||||||
|
t: 'embedEmailVerifiedZeitAdler', // TemplateId
|
||||||
|
l: 'de', // LanguageId
|
||||||
|
// BodyData
|
||||||
|
b: {
|
||||||
|
name: event.customer_name,
|
||||||
|
cancelURL: cancelURL,
|
||||||
|
address: store.address,
|
||||||
|
...(await getDateText(event)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ch2.sendToQueue(queue, Buffer.from(JSON.stringify(data)));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendNewAppointmentForEmployeeMail(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error sending sendNewAppointmentForEmployeeMail');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending mail is verified mail', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendNewAppointmentForEmployeeMail(event: CalendarEvent) {
|
||||||
|
try {
|
||||||
|
const user = await getUser(event.user_id);
|
||||||
|
if (!user.email) return;
|
||||||
|
|
||||||
|
const queue = 'kk.mails';
|
||||||
|
const conn = await getConnection();
|
||||||
|
|
||||||
|
// Sender
|
||||||
|
const ch2 = await conn.createChannel();
|
||||||
|
|
||||||
|
const cancelURL = MyEvents.generateEMailCancelLink(event.customer_id);
|
||||||
|
|
||||||
|
const store = await getStore(event.store_id);
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
m: user.email, // UserMail
|
||||||
|
t: 'embedEmployeeNewNotificationZeitAdler', // TemplateId
|
||||||
|
l: 'de', // LanguageId
|
||||||
|
// BodyData
|
||||||
|
b: {
|
||||||
|
name: event.customer_name,
|
||||||
|
customerMessage: event.customer_message,
|
||||||
|
customerEMail: event.customer_email,
|
||||||
|
cancelURL: cancelURL,
|
||||||
|
address: store.address,
|
||||||
|
...(await getDateText(event)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ch2.sendToQueue(queue, Buffer.from(JSON.stringify(data)));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending new appointment for employee mail', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default sendVerifyMail;
|
||||||
|
export { sendMailIsVerifiedMail, sendNewAppointmentForEmployeeMail };
|
|
@ -0,0 +1,75 @@
|
||||||
|
import MyEvents, { CalendarEvent } from '../database/reservedCustomer';
|
||||||
|
import { getStore } from '../database/store';
|
||||||
|
import { getUser } from '../database/user';
|
||||||
|
import { User } from '../user/types';
|
||||||
|
import { getDateText } from './dateText';
|
||||||
|
import getConnection from './getConnection';
|
||||||
|
|
||||||
|
async function sendAppointmentCanceledMail(event: CalendarEvent) {
|
||||||
|
const queue = 'kk.mails';
|
||||||
|
const conn = await getConnection();
|
||||||
|
|
||||||
|
// Sender
|
||||||
|
const ch2 = await conn.createChannel();
|
||||||
|
|
||||||
|
const store = await getStore(event.store_id);
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
m: event.customer_email, // UserMail
|
||||||
|
t: 'embedAppointmentCanceledZeitAdler', // TemplateId
|
||||||
|
l: 'de', // LanguageId
|
||||||
|
// BodyData
|
||||||
|
b: {
|
||||||
|
name: event.customer_name,
|
||||||
|
appointment1: 'Termin',
|
||||||
|
appointment2: event.time_start.toISOString(),
|
||||||
|
appointment3: event.time_end.toISOString(),
|
||||||
|
address: store.address,
|
||||||
|
...(await getDateText(event)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ch2.sendToQueue(queue, Buffer.from(JSON.stringify(data)));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendCancelAppointmentForEmployeeMail(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error sending sendNewAppointmentForEmployeeMail');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Sent:', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendCancelAppointmentForEmployeeMail(event: CalendarEvent) {
|
||||||
|
if (!event.customer_verified_time) return;
|
||||||
|
|
||||||
|
const user = await getUser(event.user_id);
|
||||||
|
if (!user.email) return;
|
||||||
|
|
||||||
|
const queue = 'kk.mails';
|
||||||
|
const conn = await getConnection();
|
||||||
|
|
||||||
|
// Sender
|
||||||
|
const ch2 = await conn.createChannel();
|
||||||
|
|
||||||
|
const store = await getStore(event.store_id);
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
m: user.email, // UserMail
|
||||||
|
t: 'embedEmployeeCancelNotificationZeitAdler', // TemplateId
|
||||||
|
l: 'de', // LanguageId
|
||||||
|
// BodyData
|
||||||
|
b: {
|
||||||
|
name: event.customer_name,
|
||||||
|
customerEMail: event.customer_email,
|
||||||
|
address: store.address,
|
||||||
|
...(await getDateText(event)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ch2.sendToQueue(queue, Buffer.from(JSON.stringify(data)));
|
||||||
|
console.log('Sent:', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default sendAppointmentCanceledMail;
|
|
@ -0,0 +1,35 @@
|
||||||
|
// Configuration
|
||||||
|
import { webPort, webHost } from './config';
|
||||||
|
|
||||||
|
// Web server
|
||||||
|
import app from './web/express';
|
||||||
|
import activateFileServe from './web/serveFiles';
|
||||||
|
|
||||||
|
// Calendar related imports
|
||||||
|
import { refreshSyncToken } from './calendarSync/refreshSync';
|
||||||
|
import updateEvents from './calendarSync/updateEvents';
|
||||||
|
|
||||||
|
// Backend and utilities
|
||||||
|
import { connectToRedis } from './redis/init';
|
||||||
|
import { initGoogleRequestStatUp } from './requestCounter';
|
||||||
|
import updateWebhooks from './calendarSync/updateWebhooks';
|
||||||
|
import checkForNotifications from './mail/notifyBeforeAppointment';
|
||||||
|
import { initLogHandler } from './logger/logger';
|
||||||
|
|
||||||
|
initLogHandler();
|
||||||
|
|
||||||
|
initGoogleRequestStatUp();
|
||||||
|
connectToRedis();
|
||||||
|
|
||||||
|
activateFileServe();
|
||||||
|
|
||||||
|
app.listen(webPort, webHost, function () {
|
||||||
|
console.log('Server is running on http://' + webHost + ':' + webPort);
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(refreshSyncToken, 1000 * 2);
|
||||||
|
setInterval(updateEvents, 1000 * 1);
|
||||||
|
setInterval(updateWebhooks, 1000 * 10);
|
||||||
|
checkForNotifications();
|
||||||
|
setInterval(checkForNotifications, 1000 * 1);
|
||||||
|
refreshSyncToken();
|
|
@ -0,0 +1,14 @@
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
async function generateRandomString(length: number): Promise<string> {
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-';
|
||||||
|
let result = '';
|
||||||
|
const buf = crypto.randomBytes(length);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const randomIndex = buf[i] % chars.length;
|
||||||
|
result += chars[randomIndex];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default generateRandomString;
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { StoreID } from '../database/store';
|
||||||
|
import { getAllUsersIDsByStoreID } from '../database/user';
|
||||||
|
import redisClient from './init';
|
||||||
|
|
||||||
|
async function clearRedisCache(store_id: StoreID) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const keys = await redisClient.keys(`store:${store_id}*`);
|
||||||
|
//console.log('keys', keys);
|
||||||
|
|
||||||
|
if (keys.length > 0) await redisClient.del(keys);
|
||||||
|
|
||||||
|
const users = await getAllUsersIDsByStoreID(store_id);
|
||||||
|
|
||||||
|
for (const user_id of users) {
|
||||||
|
const userKeys = await redisClient.keys(`user:${user_id}*`);
|
||||||
|
//console.log('userKeys', userKeys);
|
||||||
|
|
||||||
|
if (userKeys.length > 0) await redisClient.del(userKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default clearRedisCache;
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { getFreeNextDays } from '../calendar/getFreeTime';
|
||||||
|
import { getServices } from '../database/services';
|
||||||
|
import { StoreID } from '../database/store';
|
||||||
|
import { UserID } from '../user/types';
|
||||||
|
|
||||||
|
async function fillCache(store_id: StoreID) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let services = await getServices(store_id, false);
|
||||||
|
|
||||||
|
let durationList: Number[] = [];
|
||||||
|
|
||||||
|
let timeMeasure = new Date();
|
||||||
|
|
||||||
|
for (const service of services) {
|
||||||
|
for (const activity of service.activities) {
|
||||||
|
let serviceDuration = activity.duration;
|
||||||
|
|
||||||
|
if (durationList.includes(activity.duration)) continue;
|
||||||
|
|
||||||
|
let date = new Date();
|
||||||
|
|
||||||
|
let dateNextMonth = new Date();
|
||||||
|
dateNextMonth.setDate(1);
|
||||||
|
dateNextMonth.setMonth(date.getMonth() + 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// delay 2 seconds
|
||||||
|
//await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
let users: UserID[] = activity.users;
|
||||||
|
|
||||||
|
console.log('fillCache', store_id, users, serviceDuration, date, dateNextMonth);
|
||||||
|
await getFreeNextDays(store_id, users, serviceDuration, date);
|
||||||
|
await getFreeNextDays(store_id, users, serviceDuration, dateNextMonth);
|
||||||
|
} catch (error) {}
|
||||||
|
durationList.push(activity.duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('fillCache time took (ms)', new Date().getTime() - timeMeasure.getTime());
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default fillCache;
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { redis } from 'googleapis/build/src/apis/redis';
|
||||||
|
import { createClient } from 'redis';
|
||||||
|
import { REDIS_URL } from '../config';
|
||||||
|
|
||||||
|
const redisClient = createClient({
|
||||||
|
url: REDIS_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
redisClient.on('error', (err) => console.log('Redis Client Error', err));
|
||||||
|
|
||||||
|
function connectToRedis() {
|
||||||
|
console.log('Connecting to Redis');
|
||||||
|
redisClient.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
redisClient.disconnect();
|
||||||
|
console.log('Disconnected from Redis due to application termination');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
export { connectToRedis };
|
||||||
|
export default redisClient;
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { StoreID } from '../database/store';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
const dayToString = (day: Date) =>
|
||||||
|
day.toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: '2-digit',
|
||||||
|
});
|
||||||
|
|
||||||
|
const MyRedisKeys = {
|
||||||
|
calendarSyncLock: (calendarId: string) => 'calendar:' + calendarId + ':sync:locked',
|
||||||
|
userBookingLock: (userId: string, day: Date) => 'userLockBooking:' + userId + ':' + dayToString(day),
|
||||||
|
userBookingLockUnique: (userId: string, day: Date, date: Date) => MyRedisKeys.userBookingLock(userId, day) + ':' + date.getTime(),
|
||||||
|
appointmentNotificationLock: (customer_id: string) => 'appointmentNotificationLock:' + customer_id,
|
||||||
|
|
||||||
|
calendarOpenHours: (storeId: string) => 'store:' + storeId + ':calendar:openHours',
|
||||||
|
storeCache: (storeId: string) => 'store:' + storeId + ':cache',
|
||||||
|
freeTimeOfDayCache: (storeID: StoreID, _userIDs: string[], bookTimeLength: number, day: Date) => {
|
||||||
|
// sort userIDs by alphabetical order
|
||||||
|
let userIDs = _userIDs.sort();
|
||||||
|
|
||||||
|
//create text from userIDs
|
||||||
|
let text = '';
|
||||||
|
for (const userID of userIDs) {
|
||||||
|
text += userID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create hash 256
|
||||||
|
const hash = crypto.createHash('md5').update(text).digest('hex');
|
||||||
|
|
||||||
|
const dateStr = dayToString(day);
|
||||||
|
|
||||||
|
return 'store:' + storeID + ':' + hash + ':' + dateStr + ':' + bookTimeLength + ':freeTimeOfDay';
|
||||||
|
},
|
||||||
|
|
||||||
|
freeTimeFromOneUserCache: (userId: string, bookTimeLength: number, date: Date) => 'user:' + userId + ':' + dayToString(date) + ':' + bookTimeLength + ':freeTimeFromOneUser',
|
||||||
|
userCache: (userId: string) => 'user:' + userId + ':cache',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MyRedisKeys;
|
|
@ -0,0 +1,55 @@
|
||||||
|
import logger from './logger/logger';
|
||||||
|
|
||||||
|
let counter = 0;
|
||||||
|
let counterTotal = 0;
|
||||||
|
|
||||||
|
interface Statistics {
|
||||||
|
[key: string]: {
|
||||||
|
totalCount: number;
|
||||||
|
count: number;
|
||||||
|
lastUpdated: Date;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let statistics: Statistics = {};
|
||||||
|
|
||||||
|
function countGoogleRequestStatUp(type: string) {
|
||||||
|
counter++;
|
||||||
|
counterTotal++;
|
||||||
|
let stat = statistics[type];
|
||||||
|
|
||||||
|
if (!stat) stat = { totalCount: 0, count: 0, lastUpdated: new Date() };
|
||||||
|
|
||||||
|
stat.totalCount++;
|
||||||
|
stat.count++;
|
||||||
|
stat.lastUpdated = new Date();
|
||||||
|
|
||||||
|
statistics[type] = stat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initGoogleRequestStatUp() {
|
||||||
|
setInterval(() => {
|
||||||
|
logger.info('Google Request Statistics', JSON.stringify(statistics, null, 2));
|
||||||
|
|
||||||
|
console.log('Google Request Statistics', statistics);
|
||||||
|
console.log('Google Request Counter', counter);
|
||||||
|
console.log('Google Request Total Counter', counterTotal);
|
||||||
|
counter = 0;
|
||||||
|
|
||||||
|
for (const type in statistics) {
|
||||||
|
const stat = statistics[type];
|
||||||
|
|
||||||
|
stat.count = 0;
|
||||||
|
stat.lastUpdated = new Date();
|
||||||
|
|
||||||
|
statistics[type] = stat;
|
||||||
|
}
|
||||||
|
}, 1000 * 60 * 15);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
console.log('Google Request Total Counter', counterTotal);
|
||||||
|
}, 1000 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initGoogleRequestStatUp };
|
||||||
|
export default countGoogleRequestStatUp;
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { StoreSettings } from '../database/store';
|
||||||
|
import { getUser } from '../database/user';
|
||||||
|
|
||||||
|
type UserID = string;
|
||||||
|
|
||||||
|
type isOwner = true | false;
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
user_id: UserID;
|
||||||
|
store_id: string;
|
||||||
|
username: string;
|
||||||
|
isOwner: isOwner;
|
||||||
|
settings: StoreSettings;
|
||||||
|
lastUpdate: Date;
|
||||||
|
session?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MasterUser extends User {
|
||||||
|
isOwner: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkerUser extends User {
|
||||||
|
isOwner: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { UserID, User, MasterUser, WorkerUser };
|
|
@ -0,0 +1,10 @@
|
||||||
|
const express = require('express');
|
||||||
|
let app = express();
|
||||||
|
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
export default app;
|
|
@ -0,0 +1,965 @@
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import app from './express';
|
||||||
|
import { GLOBAL_calendar_min_earliest_booking_time, publicFolder, publicURL } from '../config';
|
||||||
|
import { getAccessToken, getOAuthClient, initOauthAccess } from '../calendar/oauthAccess';
|
||||||
|
import { google } from 'googleapis';
|
||||||
|
import calendar_ids from '../database/calendar_ids';
|
||||||
|
import { getFreeNextDays, getFreeTimeOfDay } from '../calendar/getFreeTime';
|
||||||
|
import { getAllUsersIDsAndNamesByStoreID, getAllUsersIDsByStoreID, getUser } from '../database/user';
|
||||||
|
import { MasterUser, UserID } from '../user/types';
|
||||||
|
import { getStore } from '../database/store';
|
||||||
|
import { getActivity, getServices } from '../database/services';
|
||||||
|
import { checkIfTimeIsAvailable } from '../calendar/insertEvent';
|
||||||
|
import MyEvents from '../database/reservedCustomer';
|
||||||
|
import initCalendar, { removeCalendar } from '../calendar/initCalendar';
|
||||||
|
import verifyCaptcha from '../captcha';
|
||||||
|
import sendVerifyMail from '../mail/verify';
|
||||||
|
import GoogleTokensDatabase from '../database/user_google_tokens';
|
||||||
|
import { forceGetFreshOpenHours } from '../calendar/openHoursList';
|
||||||
|
import { scheduleCalendarSync } from '../calendarSync/refreshSync';
|
||||||
|
import MyRedisKeys from '../redis/keys';
|
||||||
|
import redisClient from '../redis/init';
|
||||||
|
import clearRedisCache from '../redis/clearCache';
|
||||||
|
import { getHighestCalendarMaxFutureBookingDays } from '../calendarSync/openHours';
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import { cancelEmailNotifications } from '../mail/notifyBeforeAppointment';
|
||||||
|
import logger, { storeLogger, userLogger } from '../logger/logger';
|
||||||
|
|
||||||
|
import * as EmailValidator from 'email-validator';
|
||||||
|
import { log } from 'console';
|
||||||
|
|
||||||
|
function getIP(req: Request) {
|
||||||
|
return req.headers['x-real-ip'] as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateFileServe() {
|
||||||
|
app.get('/embedPopup/script.js', async function (req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
// read file publicFolder + '/embedPopup/script.js' by fs.readFile
|
||||||
|
const file = await new Promise<Buffer>((resolve, reject) => {
|
||||||
|
fs.readFile(publicFolder + '/embedPopup/script.js', (err, data) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!file) throw new Error('file not found');
|
||||||
|
|
||||||
|
if (publicURL === undefined) throw new Error('publicURL in .env not found');
|
||||||
|
|
||||||
|
// replace %PUBLIC_URL% with config.publicURL
|
||||||
|
const fileString = file.toString().replace(/%PUBLIC_URL%/g, publicURL);
|
||||||
|
|
||||||
|
// send file as response
|
||||||
|
res.setHeader('Content-Type', 'application/javascript');
|
||||||
|
res.send(fileString);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).send('error 500');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.get('/embedPopup/style.css', function (req: Request, res: Response) {
|
||||||
|
res.sendFile(publicFolder + '/embedPopup/style.css');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/ttest', async function (req: Request, res: Response) {
|
||||||
|
res.sendFile(publicFolder + '/index.html');
|
||||||
|
|
||||||
|
let userId = req.cookies['token'] as string | undefined;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
try {
|
||||||
|
const accessToken = await getAccessToken(userId);
|
||||||
|
console.log('accessToken: ' + accessToken);
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/getBooking', async function (req: Request, res: Response) {
|
||||||
|
const customer_id = req.body.bookingID;
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
// check if name is too long
|
||||||
|
if (!customer_id || customer_id.length < 32) {
|
||||||
|
res.status(400).send(JSON.stringify({ error: 'no valid id given' }));
|
||||||
|
logger.warn('REQUEST /getBooking', 'no valid id given', customer_id ? customer_id : '"no id given"', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const event = await MyEvents.getPublicEvent(customer_id);
|
||||||
|
|
||||||
|
if (event && event.time_start > new Date()) {
|
||||||
|
res.status(200).send(JSON.stringify({ event: event }));
|
||||||
|
} else {
|
||||||
|
res.status(404).send(JSON.stringify({ error: 'not found' }));
|
||||||
|
|
||||||
|
logger.warn('/getBooking', 'no valid id given', customer_id ? customer_id : '"no id given"', ip);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('REQUEST /getBooking', 'error', (error as Error).toString(), ip);
|
||||||
|
res.status(500).send(JSON.stringify({ error: 'unknown error' }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/booking', async function (req: Request, res: Response) {
|
||||||
|
// get store_id from body
|
||||||
|
const store_id = req.body.store_id;
|
||||||
|
const activity_id = req.body.activity_id;
|
||||||
|
const date = req.body.time;
|
||||||
|
const name = req.body.name;
|
||||||
|
const email = req.body.email;
|
||||||
|
let message = req.body.message;
|
||||||
|
const employee = req.body.employee;
|
||||||
|
const captcha = req.body.captcha;
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (store_id) {
|
||||||
|
try {
|
||||||
|
const store = await getStore(store_id);
|
||||||
|
|
||||||
|
// check if date is valid
|
||||||
|
if (isNaN(Number(date))) {
|
||||||
|
res.status(400).send(JSON.stringify({ error: 'invalid date' }));
|
||||||
|
storeLogger.warn(store_id, 'REQUEST /booking', 'invalid date', date, ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if name is valid
|
||||||
|
if (name === undefined || name === null || name.length < 3) {
|
||||||
|
res.status(400).send(JSON.stringify({ error: 'invalid name' }));
|
||||||
|
storeLogger.warn(store_id, 'REQUEST /booking', 'invalid name', name, ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if name is too long
|
||||||
|
if (name.length > 32) {
|
||||||
|
res.status(400).send(JSON.stringify({ error: 'name too long' }));
|
||||||
|
storeLogger.warn(store_id, 'REQUEST /booking', 'name too long', name, ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if email is valid
|
||||||
|
if (email === undefined || email === null || !EmailValidator.validate(email)) {
|
||||||
|
res.status(400).send(JSON.stringify({ error: 'invalid email' }));
|
||||||
|
storeLogger.warn(store_id, 'REQUEST /booking', 'invalid email', email, ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if email is too long
|
||||||
|
if (email.length > 255) {
|
||||||
|
res.status(400).send(JSON.stringify({ error: 'email too long' }));
|
||||||
|
storeLogger.warn(store_id, 'REQUEST /booking', 'email too long (' + email.length + ')', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if message is valid
|
||||||
|
if (message === undefined || message === null) {
|
||||||
|
message = '';
|
||||||
|
} else if (message.length > 1502) {
|
||||||
|
res.status(400).send(JSON.stringify({ error: 'message too long' }));
|
||||||
|
storeLogger.warn(store_id, 'REQUEST /booking', 'message too long (' + message.length + ')', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await verifyCaptcha(captcha, ip);
|
||||||
|
} catch (error) {
|
||||||
|
storeLogger.warn(store_id, 'REQUEST /booking', 'captcha error', (error as Error).toString(), ip);
|
||||||
|
res.status(400).send(JSON.stringify({ error: 'captcha' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarLockIDs: {
|
||||||
|
[key: string]: {
|
||||||
|
calendar_id: string | undefined;
|
||||||
|
isLocked: boolean;
|
||||||
|
refreshLock: () => void;
|
||||||
|
removeLock: () => void;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
event: {
|
||||||
|
calendar_id: undefined,
|
||||||
|
isLocked: false,
|
||||||
|
refreshLock: async () => {},
|
||||||
|
removeLock: async () => {},
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
calendar_id: undefined,
|
||||||
|
isLocked: false,
|
||||||
|
refreshLock: async () => {},
|
||||||
|
removeLock: async () => {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const activity = await getActivity(store_id, activity_id, false);
|
||||||
|
|
||||||
|
// check if employee is valid
|
||||||
|
if (employee === undefined || employee === null || !activity.users.includes(employee)) {
|
||||||
|
res.status(400).send(JSON.stringify({ error: 'invalid employee' }));
|
||||||
|
storeLogger.warn(store_id, 'REQUEST /booking', 'invalid employee', employee, ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// start and end time
|
||||||
|
const startTime = new Date(Number(date));
|
||||||
|
const endTime = new Date(Number(date) + activity.duration * 60 * 1000);
|
||||||
|
|
||||||
|
const user = await getUser(employee);
|
||||||
|
|
||||||
|
//wait for calendarSyncLock
|
||||||
|
await new Promise(async (resolve, reject) => {
|
||||||
|
let isLocked = true;
|
||||||
|
|
||||||
|
calendarLockIDs.event.calendar_id = await calendar_ids.getEventCalendarID(user);
|
||||||
|
|
||||||
|
try {
|
||||||
|
calendarLockIDs.primary.calendar_id = (await calendar_ids.getPrimaryCalendar(user.user_id)).calendar_id;
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
while (isLocked) {
|
||||||
|
let hasFoundSomethingLocked = false;
|
||||||
|
|
||||||
|
for (const key in calendarLockIDs) {
|
||||||
|
const calendarID = calendarLockIDs[key];
|
||||||
|
if (calendarID.calendar_id) {
|
||||||
|
const lockKey = MyRedisKeys.calendarSyncLock(calendarID.calendar_id);
|
||||||
|
|
||||||
|
if (calendarID.isLocked) {
|
||||||
|
await calendarID.refreshLock();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lock = await redisClient.set(lockKey, 'locked', { NX: true, EX: 30 });
|
||||||
|
|
||||||
|
if (lock) {
|
||||||
|
calendarID.isLocked = true;
|
||||||
|
calendarID.refreshLock = async () => {
|
||||||
|
await redisClient.expire(lockKey, 30);
|
||||||
|
};
|
||||||
|
calendarID.removeLock = async () => {
|
||||||
|
await redisClient.del(lockKey);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
hasFoundSomethingLocked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFoundSomethingLocked) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
} else {
|
||||||
|
isLocked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
const lockTimeout = 30; // 30 seconds
|
||||||
|
const lockValue = 'locked';
|
||||||
|
let lockDate = new Date();
|
||||||
|
|
||||||
|
let lockKeyUnique = MyRedisKeys.userBookingLockUnique(user.user_id, startTime, lockDate);
|
||||||
|
const lockKey = MyRedisKeys.userBookingLock(user.user_id, startTime);
|
||||||
|
|
||||||
|
const refreshLock = () => {
|
||||||
|
redisClient.expire(lockKeyUnique, lockTimeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// lock user
|
||||||
|
let lock = await redisClient.set(lockKeyUnique, lockValue, { NX: true, EX: lockTimeout });
|
||||||
|
|
||||||
|
while (!lock) {
|
||||||
|
lockDate = new Date();
|
||||||
|
|
||||||
|
lockKeyUnique = MyRedisKeys.userBookingLockUnique(user.user_id, startTime, lockDate);
|
||||||
|
lock = await redisClient.set(lockKeyUnique, lockValue, { NX: true, EX: lockTimeout });
|
||||||
|
|
||||||
|
if (!lock) {
|
||||||
|
// wait 500ms
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500 + Math.random() * 500));
|
||||||
|
}
|
||||||
|
refreshLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitLength = lockKeyUnique.split(':').length;
|
||||||
|
|
||||||
|
let isFirstQueue = false;
|
||||||
|
while (!isFirstQueue) {
|
||||||
|
// list all keys
|
||||||
|
const keys = await redisClient.keys(lockKey + ':*');
|
||||||
|
|
||||||
|
let oldestTime = -1;
|
||||||
|
// check if first in queue
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const key = keys[i];
|
||||||
|
|
||||||
|
const time = Number(key.split(':')[splitLength - 1]);
|
||||||
|
|
||||||
|
if (time < oldestTime || oldestTime === -1) {
|
||||||
|
oldestTime = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldestTime === Number(lockKeyUnique.split(':')[splitLength - 1])) {
|
||||||
|
isFirstQueue = true;
|
||||||
|
} else {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
}
|
||||||
|
refreshLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if time is available
|
||||||
|
try {
|
||||||
|
await clearRedisCache(store_id);
|
||||||
|
await checkIfTimeIsAvailable(store, user, startTime, endTime);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
userLogger.error(user.user_id, 'REQUEST /booking', 'time not available', (error as Error).toString(), ip);
|
||||||
|
res.status(409).send(JSON.stringify({ status: 'already booked' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create event
|
||||||
|
const event = await MyEvents.createEvent(store, user, activity_id, startTime, endTime, email, name, message);
|
||||||
|
|
||||||
|
userLogger.info(user.user_id, 'REQUEST /booking', 'event created', JSON.stringify(event, null, 2), ip);
|
||||||
|
|
||||||
|
// send email
|
||||||
|
try {
|
||||||
|
await sendVerifyMail(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
userLogger.error(user.user_id, 'REQUEST /booking', 'email not sent', (error as Error).toString(), JSON.stringify(event, null, 2), ip);
|
||||||
|
res.status(500).send(JSON.stringify({ error: 'unknown error' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send(JSON.stringify({ status: 'success' }));
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
userLogger.error(user.user_id, 'REQUEST /booking', 'unknown error', (error as Error).toString(), ip);
|
||||||
|
res.status(500).send(JSON.stringify({ error: 'unknown error' }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
storeLogger.error(store_id, 'REQUEST /booking', 'unknown error', (error as Error).toString(), ip);
|
||||||
|
res.status(500).send(JSON.stringify({ error: 'unknown error' }));
|
||||||
|
} finally {
|
||||||
|
for (const key in calendarLockIDs) {
|
||||||
|
const calendarID = calendarLockIDs[key];
|
||||||
|
if (calendarID.isLocked) {
|
||||||
|
await calendarID.removeLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
storeLogger.error(store_id, 'REQUEST /booking', 'unknown error', (error as Error).toString(), ip);
|
||||||
|
res.status(404).send(JSON.stringify({ error: 'store not found' }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn('REQUEST /booking', 'no storeId given', ip);
|
||||||
|
res.status(400).send(JSON.stringify({ error: 'store not found' }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/services', async function (req: Request, res: Response) {
|
||||||
|
// get store_id from body
|
||||||
|
const store_id = req.body.store_id;
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (store_id) {
|
||||||
|
try {
|
||||||
|
const store = await getStore(store_id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const services = await getServices(store.store_id, true);
|
||||||
|
const _users = await getAllUsersIDsByStoreID(store.store_id);
|
||||||
|
let users: { [key: string]: { name: string; cmebt: number } } = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < _users.length; i++) {
|
||||||
|
const user = await getUser(_users[i]);
|
||||||
|
users[user.user_id] = { name: user.username, cmebt: user.settings.calendar_min_earliest_booking_time + GLOBAL_calendar_min_earliest_booking_time };
|
||||||
|
}
|
||||||
|
|
||||||
|
const highest_calendar_max_future_booking_days = await getHighestCalendarMaxFutureBookingDays(store.store_id);
|
||||||
|
|
||||||
|
const maxDate = new Date(new Date().getTime() + highest_calendar_max_future_booking_days * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
res.status(200).send(JSON.stringify({ services: services, users: users, maxDate: maxDate.getTime(), storeName: store.name }));
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
storeLogger.error(store_id, 'REQUEST /services', 'unknown error', (error as Error).toString(), ip);
|
||||||
|
res.status(500).send('error 500');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
logger.warn('REQUEST /services', 'store not found', (error as Error).toString(), ip);
|
||||||
|
res.status(404).send('store not found');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn('REQUEST /services', 'no store id found', ip);
|
||||||
|
res.status(400).send('id not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/freeByActivity', async function (req: Request, res: Response) {
|
||||||
|
const store_id = req.body.store_id;
|
||||||
|
const dateQuery = Number(req.body.date);
|
||||||
|
const activity_id = req.body.activity_id;
|
||||||
|
|
||||||
|
const wishUser = req.body.users;
|
||||||
|
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (store_id) {
|
||||||
|
try {
|
||||||
|
const store = await getStore(store_id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const activity = await getActivity(store_id, activity_id, false);
|
||||||
|
const serviceDuration = activity.duration;
|
||||||
|
|
||||||
|
// check if wishUser is an array
|
||||||
|
if (wishUser !== undefined && !Array.isArray(wishUser)) {
|
||||||
|
logger.warn(store_id, 'REQUEST /freeByActivity', 'invalid users', JSON.stringify(wishUser, null, 2), ip);
|
||||||
|
res.status(400).send('invalid users');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(dateQuery)) throw new Error('invalid date');
|
||||||
|
|
||||||
|
let date = new Date(dateQuery || Date.now());
|
||||||
|
|
||||||
|
let users: UserID[] = [];
|
||||||
|
|
||||||
|
if (wishUser === undefined || wishUser.length == 0) {
|
||||||
|
users = activity.users;
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < wishUser.length; i++) {
|
||||||
|
if (wishUser[i] !== null && wishUser[i] !== undefined) {
|
||||||
|
const userId = wishUser[i].toString();
|
||||||
|
// check if user contains in activity.users
|
||||||
|
if (activity.users.includes(userId)) {
|
||||||
|
users.push(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const freeTime = await getFreeTimeOfDay(store_id, users, serviceDuration, date);
|
||||||
|
|
||||||
|
// convert all dates to timestamps
|
||||||
|
for (const key in freeTime) {
|
||||||
|
const time = freeTime[key].events;
|
||||||
|
for (let i = 0; i < time.length; i++) {
|
||||||
|
const timeSlot = time[i];
|
||||||
|
(timeSlot.start as any) = timeSlot.start.getTime();
|
||||||
|
(timeSlot.end as any) = timeSlot.end.getTime();
|
||||||
|
}
|
||||||
|
} /* [{"userId":"ayg2xg0w0vwswbl1c6w28","events":[{"start":1704964831091,"end":1704966300000},{"start":1704969900000,"end":1704970800000},{"start":1704978900000,"end":1704980700000},{"start":1704988800000,"end":1704990600000},{"start":1704994200000,"end":1704997800000},{"start":1705001400000,"end":1705005000000}]}] */
|
||||||
|
|
||||||
|
res.status(200).send(JSON.stringify({ events: freeTime }));
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
storeLogger.error(store_id, 'REQUEST /freeByActivity', 'error', (error as Error).toString(), ip);
|
||||||
|
res.status(500).send('error 500');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
logger.warn('REQUEST /freeByActivity', 'store not found', (error as Error).toString(), ip);
|
||||||
|
res.status(400).send('no store found');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(400).send('no store found');
|
||||||
|
logger.warn('REQUEST /freeByActivity', 'no store found', ip);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/freeNextDaysByActivity', async function (req: Request, res: Response) {
|
||||||
|
const store_id = req.body.store_id;
|
||||||
|
const dateQuery = Number(req.body.date);
|
||||||
|
const activity_id = req.body.activity_id;
|
||||||
|
|
||||||
|
const wishUser = req.body.users;
|
||||||
|
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (store_id) {
|
||||||
|
try {
|
||||||
|
const store = await getStore(store_id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const activity = await getActivity(store_id, activity_id, false);
|
||||||
|
const serviceDuration = activity.duration;
|
||||||
|
|
||||||
|
// check if wishUser is an array
|
||||||
|
if (wishUser !== undefined && !Array.isArray(wishUser)) {
|
||||||
|
res.status(400).send('invalid users');
|
||||||
|
logger.warn(store_id, 'REQUEST /freeNextDaysByActivity', 'invalid users', JSON.stringify(wishUser, null, 2), ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(dateQuery)) {
|
||||||
|
res.status(400).send('invalid date');
|
||||||
|
logger.warn(store_id, 'REQUEST /freeNextDaysByActivity', 'invalid date', dateQuery.toString(), ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isNaN(serviceDuration) || serviceDuration > 24 * 60) {
|
||||||
|
res.status(400).send('invalid service duration');
|
||||||
|
logger.warn(store_id, 'REQUEST /freeNextDaysByActivity', 'invalid service duration', serviceDuration.toString(), ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert dateQueryNumber to date
|
||||||
|
let date = new Date(dateQuery || Date.now());
|
||||||
|
|
||||||
|
let users: UserID[] = [];
|
||||||
|
|
||||||
|
if (wishUser === undefined || wishUser.length == 0) {
|
||||||
|
users = activity.users;
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < wishUser.length; i++) {
|
||||||
|
if (wishUser[i] !== null && wishUser[i] !== undefined) {
|
||||||
|
const userId = wishUser[i].toString();
|
||||||
|
// check if user contains in activity.users
|
||||||
|
if (activity.users.includes(userId)) {
|
||||||
|
users.push(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _freeDays = await getFreeNextDays(store.store_id, users, serviceDuration, date);
|
||||||
|
let freeDays: number[] = []; // timestamp
|
||||||
|
|
||||||
|
// convert all dates to timestamps
|
||||||
|
for (let i = 0; i < _freeDays.length; i++) {
|
||||||
|
freeDays.push(_freeDays[i].getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send(JSON.stringify({ freeDays: freeDays }));
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).send('error 500');
|
||||||
|
storeLogger.error(store_id, 'REQUEST /freeNextDaysByActivity', 'error', (error as Error).toString(), ip);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(404).send('store not found');
|
||||||
|
logger.warn('REQUEST /freeNextDaysByActivity', 'store not found', (error as Error).toString(), ip);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(404).send('store not found');
|
||||||
|
logger.warn('REQUEST /freeNextDaysByActivity', 'no storeId given', ip);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// for google api webhook
|
||||||
|
app.post('/api/v1/notification/:eventId', async function (req: Request, res: Response) {
|
||||||
|
const eventId = req.params.eventId;
|
||||||
|
|
||||||
|
const headers = req.headers;
|
||||||
|
const token = headers['x-goog-channel-token'] as string;
|
||||||
|
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.log(headers);
|
||||||
|
res.status(404).send('Not found');
|
||||||
|
|
||||||
|
logger.warn('REQUEST /api/v1/notification', 'no token found', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token !== process.env.GOOGLE_WEBHOOK_SECRET) {
|
||||||
|
console.log(headers);
|
||||||
|
res.status(404).send('Not found');
|
||||||
|
|
||||||
|
logger.warn('REQUEST /api/v1/notification', 'invalid token', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = headers['x-goog-resource-state'] as string;
|
||||||
|
const channel_id = headers['x-goog-channel-id'] as string;
|
||||||
|
const channel_expiration = headers['x-goog-channel-expiration'] as string;
|
||||||
|
const resource_id = headers['x-goog-resource-id'] as string;
|
||||||
|
|
||||||
|
if (state === 'sync') {
|
||||||
|
res.status(200).send('success');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('REQUEST /api/v1/notification', 'state', state, 'channel_id', channel_id, 'channel_expiration', channel_expiration, 'resource_id', resource_id, ip);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const channelIDExists = await scheduleCalendarSync(eventId, channel_id);
|
||||||
|
|
||||||
|
if (!channelIDExists) {
|
||||||
|
res.status(404).send('Not found');
|
||||||
|
logger.warn('REQUEST /api/v1/notification', 'channel_id not found', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send('success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('notificationError', error);
|
||||||
|
logger.error('REQUEST /api/v1/notification', 'eventId:', eventId, 'error', (error as Error).toString(), ip);
|
||||||
|
res.status(500).send('error 500');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/v1/changeFutureBookingDays', async function (req: Request, res: Response) {
|
||||||
|
let storeId = req.body.storeId as string;
|
||||||
|
let pass = req.body.pass as string;
|
||||||
|
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (pass !== process.env.BACKEND_PASSPHRASE) {
|
||||||
|
res.status(404).send('Not found');
|
||||||
|
logger.warn('REQUEST /api/v1/changeFutureBookingDays', 'invalid passphrase', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!storeId) {
|
||||||
|
res.status(400).send('no storeId found');
|
||||||
|
logger.warn('REQUEST /api/v1/changeFutureBookingDays', 'no storeId given', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await clearRedisCache(storeId);
|
||||||
|
await forceGetFreshOpenHours(storeId);
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
res.status(200).send('success');
|
||||||
|
} catch (error) {
|
||||||
|
storeLogger.error(storeId, 'REQUEST /api/v1/changeFutureBookingDays', 'error', (error as Error).toString(), ip);
|
||||||
|
res.status(404).send('unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/v1/changedSettings', async function (req: Request, res: Response) {
|
||||||
|
let storeId = req.body.storeId as string;
|
||||||
|
let pass = req.body.pass as string;
|
||||||
|
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (pass !== process.env.BACKEND_PASSPHRASE) {
|
||||||
|
res.status(404).send('Not found');
|
||||||
|
logger.warn('REQUEST /api/v1/changeFutureBookingDays', 'invalid passphrase', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!storeId) {
|
||||||
|
res.status(400).send('no userId found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await clearRedisCache(storeId);
|
||||||
|
await forceGetFreshOpenHours(storeId);
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
res.status(200).send('success');
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
storeLogger.error(storeId, 'REQUEST /api/v1/changedSettings', 'error', (error as Error).toString(), ip);
|
||||||
|
res.status(404).send('user not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/v1/addGoogleAccount', async function (req: Request, res: Response) {
|
||||||
|
let userId = req.body.userId as string;
|
||||||
|
let accessToken = req.body.accessToken as string;
|
||||||
|
let refreshToken = req.body.refreshToken as string;
|
||||||
|
let googleUuid = req.body.sub as string;
|
||||||
|
let pass = req.body.pass as string;
|
||||||
|
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (pass !== process.env.BACKEND_PASSPHRASE) {
|
||||||
|
res.status(404).send('Not found');
|
||||||
|
logger.warn('REQUEST /api/v1/changeFutureBookingDays', 'invalid passphrase', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
res.status(400).send('no userId found');
|
||||||
|
logger.error('REQUEST /api/v1/addGoogleAccount', 'no userId given', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
user = await getUser(userId);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(404).send('user not found');
|
||||||
|
logger.error('REQUEST /api/v1/addGoogleAccount', 'user not found', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
res.status(400).send('no accessToken found');
|
||||||
|
userLogger.error(user.user_id, 'REQUEST /api/v1/addGoogleAccount', 'no accessToken given', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
const oauth2Client = await getOAuthClient(accessToken);
|
||||||
|
|
||||||
|
await oauth2Client.revokeToken(accessToken);
|
||||||
|
|
||||||
|
res.status(400).send('no refreshToken found');
|
||||||
|
userLogger.error(user.user_id, 'REQUEST /api/v1/addGoogleAccount', 'no refreshToken given', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let expireDate = new Date(new Date().getTime() + 1000 * 60 * 30);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await initOauthAccess(userId, refreshToken, accessToken, expireDate, googleUuid);
|
||||||
|
|
||||||
|
res.status(200).send('success');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await forceGetFreshOpenHours(user.store_id);
|
||||||
|
} catch (error) {}
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
const oauth2Client = await getOAuthClient(accessToken);
|
||||||
|
await oauth2Client.revokeToken(accessToken);
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
userLogger.error(user.user_id, 'REQUEST /api/v1/addGoogleAccount', 'error', (error as Error).toString(), ip);
|
||||||
|
res.status(500).send('error 500');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/v1/removeGoogleAccount', async function (req: Request, res: Response) {
|
||||||
|
let userId = req.body.userId as string;
|
||||||
|
let pass = req.body.pass as string;
|
||||||
|
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (pass !== process.env.BACKEND_PASSPHRASE) {
|
||||||
|
res.status(404).send('Not found');
|
||||||
|
logger.warn('REQUEST /api/v1/changeFutureBookingDays', 'invalid passphrase', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
res.status(400).send('no userId found');
|
||||||
|
logger.error('REQUEST /api/v1/removeGoogleAccount', 'no userId given', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
user = await getUser(userId);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('REQUEST /api/v1/removeGoogleAccount', 'user not found', ip);
|
||||||
|
res.status(404).send('user not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await GoogleTokensDatabase.unlinkGoogleAccount(userId);
|
||||||
|
|
||||||
|
res.status(200).send('success');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await forceGetFreshOpenHours(user.store_id);
|
||||||
|
} catch (error) {}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send('error 500');
|
||||||
|
logger.error('REQUEST /api/v1/removeGoogleAccount', 'error', (error as Error).toString(), ip);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/v1/addUser', async function (req: Request, res: Response) {
|
||||||
|
let userId = req.body.userId as string;
|
||||||
|
let pass = req.body.pass as string;
|
||||||
|
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (pass !== process.env.BACKEND_PASSPHRASE) {
|
||||||
|
res.status(404).send('Not found');
|
||||||
|
logger.warn('REQUEST /api/v1/changeFutureBookingDays', 'invalid passphrase', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
res.status(400).send('no userId found');
|
||||||
|
logger.error('REQUEST /api/v1/addUser', 'no userId given', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await getUser(userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await initCalendar(user.user_id);
|
||||||
|
|
||||||
|
res.status(200).send('success');
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
userLogger.error(user.user_id, 'REQUEST /api/v1/addUser', 'error', (error as Error).toString(), ip);
|
||||||
|
res.status(500).send('error 500');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
logger.error('REQUEST /api/v1/addUser', 'user not found', ip);
|
||||||
|
res.status(404).send('user not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/v1/removeUser', async function (req: Request, res: Response) {
|
||||||
|
let userId = req.body.userId as string;
|
||||||
|
let pass = req.body.pass as string;
|
||||||
|
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (pass !== process.env.BACKEND_PASSPHRASE) {
|
||||||
|
res.status(404).send('Not found');
|
||||||
|
logger.warn('REQUEST /api/v1/changeFutureBookingDays', 'invalid passphrase', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
res.status(400).send('no userId found');
|
||||||
|
logger.error('REQUEST /api/v1/removeUser', 'no userId given', ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await getUser(userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeCalendar(user);
|
||||||
|
|
||||||
|
res.status(200).send('success');
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
userLogger.error(user.user_id, 'REQUEST /api/v1/removeUser', 'error', (error as Error).toString(), ip);
|
||||||
|
res.status(500).send('error 500');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
logger.error('REQUEST /api/v1/removeUser', 'user not found', ip);
|
||||||
|
res.status(404).send('user not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/appointment/verify/:customer_id', async function (req: Request, res: Response) {
|
||||||
|
const customer_id = req.params.customer_id;
|
||||||
|
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (customer_id) {
|
||||||
|
try {
|
||||||
|
const event = await MyEvents.verifyEvent(customer_id);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
userLogger.info(event.user_id, 'REQUEST /appointment/verify', 'event verified', ip);
|
||||||
|
res.send(JSON.stringify({ status: 'success' }));
|
||||||
|
} else {
|
||||||
|
logger.warn('REQUEST /appointment/verify', 'event not found', customer_id, ip);
|
||||||
|
res.status(404).sendFile(publicFolder + '/404.html');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
logger.error('REQUEST /appointment/verify', 'error', customer_id, (error as Error).toString(), ip);
|
||||||
|
res.status(500).sendFile(publicFolder + '/error.html');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn('REQUEST /appointment/verify', 'no customer_id given', ip);
|
||||||
|
res.status(400).sendFile(publicFolder + '/error.html');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/appointment/cancel/:customer_id', async function (req: Request, res: Response) {
|
||||||
|
const customer_id = req.params.customer_id;
|
||||||
|
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (customer_id) {
|
||||||
|
try {
|
||||||
|
const event = await MyEvents.cancelEvent(customer_id);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
userLogger.info(event.user_id, 'REQUEST /appointment/cancel', 'event canceled', ip);
|
||||||
|
res.send(JSON.stringify({ status: 'success' }));
|
||||||
|
} else {
|
||||||
|
res.status(404).sendFile(publicFolder + '/404.html');
|
||||||
|
logger.warn('REQUEST /appointment/cancel', 'event not found', customer_id, ip);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
logger.error('REQUEST /appointment/cancel', 'error', customer_id, (error as Error).toString(), ip);
|
||||||
|
res.status(500).sendFile(publicFolder + '/error.html');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn('REQUEST /appointment/cancel', 'no customer_id given', ip);
|
||||||
|
res.status(400).sendFile(publicFolder + '/error.html');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/appointment/cancelNotification/:customer_id', async function (req: Request, res: Response) {
|
||||||
|
const customer_id = req.params.customer_id;
|
||||||
|
|
||||||
|
const ip = getIP(req);
|
||||||
|
|
||||||
|
if (customer_id) {
|
||||||
|
try {
|
||||||
|
const event = await cancelEmailNotifications(customer_id);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
userLogger.info('REQUEST /appointment/cancelNotification', 'notifications canceled', customer_id, ip);
|
||||||
|
res.send(JSON.stringify({ status: 'success' }));
|
||||||
|
} else {
|
||||||
|
res.status(404).sendFile(publicFolder + '/404.html');
|
||||||
|
logger.warn('REQUEST /appointment/cancelNotification', 'event not found', customer_id, ip);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).sendFile(publicFolder + '/error.html');
|
||||||
|
logger.error('REQUEST /appointment/cancelNotification', 'error', customer_id, (error as Error).toString(), ip);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(400).sendFile(publicFolder + '/error.html');
|
||||||
|
logger.warn('REQUEST /appointment/cancelNotification', 'no customer_id given', ip);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//on 404
|
||||||
|
app.use(function (req: Request, res: Response, next: NextFunction) {
|
||||||
|
res.status(404).sendFile(publicFolder + '/404.html');
|
||||||
|
logger.warn('REQUEST', '404', req.url, getIP(req));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default activateFileServe;
|
||||||
|
|
||||||
|
function printDate(str: string, date: Date) {
|
||||||
|
console.log(
|
||||||
|
str,
|
||||||
|
date.toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue