Compare commits

...
11 Commits
45 changed files with 1688 additions and 660 deletions
+1
View File
@@ -1,3 +1,4 @@
build build
node_modules node_modules
test.db test.db
tempdb
+27
View File
@@ -0,0 +1,27 @@
# Server Setup
There is a shell script for doing a mostly automated installation at <https://codeberg.org/timelimit/timelimit-server/raw/branch/master/docs/guides/timelimit-server-setup.sh>. During a test, it took 10 minutes to do a installation with it.
**Read the messages which the script shows** - it is recommend to run it on a clean Debian 10 installation because it overwrites some configuration files.
## Usage
- download it (using wget or a webbrowser)
- make it executable (``chmod +x timelimit-server-setup.sh``)
- run it (``sudo ./timelimit-server-setup.sh``)
- answer the questions
## Requirements
- a **public** IP address **with** a domain name
- a reachable port 80 and 443 (which can need a port forwarding at your router)
- a Debian 10 installation (or something comparable) which does not contain anything (important)
- a mail address for sending automated mails (using SMTP) which should **not** be your primary mail address
## Created setup
- timelimit-server (as a systemd service)
- postgresql (connected to timelimit-server using a unix socket)
- nginx (connected to timelimit-server using a unix socket)
- certbot (to get valid certificates)
- a few helper commands with the ``timelimit-`` prefix
+538
View File
@@ -0,0 +1,538 @@
#! /bin/bash
set -e
# use the test environment
# certenv=live
# certbotparams="--test-cert"
# use the production environment
certenv=live
certbotparams=""
if [ "$(id -u)" != 0 ]; then
echo "You must run this as root (with sudo)"
exit 1
fi
if ! command -v dialog &> /dev/null; then
apt update && apt install -y dialog
fi
dialog --yesno "This script will setup a timelimit-server installation. It assumes running at a clean Debian installation for this. Doing this will overwrite and delete some files - you have been warned. Do you want to continue?" 8 60
dialog --yesno "This device must be reachable from the public internet at port 80 and port 443 and you must have a (sub)domain at which this device can be reached. Do you have got both?" 8 60
dialog --yesno "Do you accept the current Let's Encrypt Subscriber Agreement which can be found at https://letsencrypt.org/repository/?" 8 60
apt update
firewall_mode="$(dialog --menu "Do you want to setup a firewall?" 10 60 3 \
no "do not setup a firewall" \
base "install ufw and allow http and https" \
ssh "install ufw and allow http, https and ssh" \
3>&1 1>&2 2>&3)"
if [ "$firewall_mode" != "no" ]; then
apt install -y ufw
ufw --force reset
ufw default deny incoming
ufw allow http
ufw allow https
if [ "$firewall_mode" = "ssh" ]; then
ufw limit ssh
fi
ufw enable
fi
apt install -y curl certbot nginx python3-certbot-nginx
# minimal nginx setup
rm /etc/nginx/sites-enabled/default || true # ignore if it is already deleted
curl https://ssl-config.mozilla.org/ffdhe2048.txt > /usr/share/mozilla-dhparam
cat > /etc/nginx.conf <<"EOF"
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 512;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_dhparam /usr/share/mozilla-dhparam;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_stapling on;
ssl_stapling_verify on;
gzip off;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
EOF
cat > /etc/nginx/sites-enabled/http-empty <<"EOF"
# this ensures that the certificates can be created/renewed
server {
listen 80;
listen [::]:80;
server_tokens off;
return 403;
}
EOF
systemctl reload nginx
# get data for cert creation
domain="$(dialog --inputbox "At which domain is this device reachable?" 10 60 "" 3>&1 1>&2 2>&3)"
if [[ ! "$domain" =~ ^([0-9a-zA-Z]|.)+$ ]] || [[ "$domain" =~ ^\.+$ ]]; then
echo "This does not look like a valid domain"
exit 1
fi
mail="`dialog --inputbox "What's your mail address? It will be the default mail address which can sign in at your timelimit server and it will be sent to Let's Encrypt for the certificate creation." 10 60 "" 3>&1 1>&2 2>&3`"
if [[ ! $mail =~ ^([0-9a-zA-Z]|-|\.)+@([0-9a-zA-Z]|-|\.)+$ ]]; then
echo "This is not look like a valid mail address"
exit 1
fi
# get certificates
certbot certonly --nginx $certbotparams -m "$mail" --agree-tos --non-interactive -d "$domain"
# add nginx site config
cat > /etc/nginx/sites-enabled/https-timelimit <<EOF
upstream timelimitbackend {
server unix:/var/run/timelimit/server max_fails=0;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name $domain;
server_tokens off;
ssl_certificate /etc/letsencrypt/$certenv/$domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/$certenv/$domain/privkey.pem;
location / {
proxy_pass http://timelimitbackend/;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Host \$http_host;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
client_max_body_size 10m;
}
}
EOF
nginx -t
systemctl reload nginx
# install and configure the database
apt install -y postgresql-11
runuser -u postgres -- createuser timelimitrun
runuser -u postgres -- createdb --encoding=utf8 --owner=timelimitrun timelimit
echo "listen_addresses = ''" > /etc/postgresql/11/main/conf.d/disable-tcp-listen.conf
cat > /etc/postgresql/11/main/pg_hba.conf <<"EOF"
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
local timelimit timelimitrun peer
EOF
systemctl restart postgresql
# install more dependencies
apt install -y pgp git npm jq
# create a config
mkdir --mode=u=rwx,go=rx /etc/timelimit
jq --null-input --arg mailWhitelist "$mail" '{mailWhitelist:$mailWhitelist}' > /etc/timelimit/config.json
chmod u=rw,go= /etc/timelimit/config.json
# create timelimit users
useradd timelimitrun && useradd timelimitbuild
# create directoreis
mkdir --mode=u=rwx,go=rx /var/lib/timelimit
mkdir --mode=u=rwx,go=rx /var/lib/timelimit/build
chown timelimitbuild /var/lib/timelimit/build
# clone the source code
runuser -u timelimitbuild -- git clone --mirror https://codeberg.org/timelimit/timelimit-server.git /var/lib/timelimit/build/mirror
# create helperscript for building the source code and run it
cat > /var/lib/timelimit/build/build.sh <<"EOF"
#! /bin/bash
set -e
cd /var/lib/timelimit/build/mirror
git remote update
current_version=""
if [ -f /var/lib/timelimit/run/version.txt ]; then
current_version="$(cat /var/lib/timelimit/run/version.txt)"
fi
options=()
for option in $(git tag | grep -E '^2[0-9]{3}-[0-9]{2}-[0-9]{2}$' | sort --reverse); do
info=""
if [ "$option" = "$current_version" ]; then
info="(currently installed)"
fi
options+=("$option" "$info")
done
selected_version="$(dialog --menu "Which version do you want to use?" 15 60 10 "${options[@]}" 3>&1 1>&2 2>&3)"
rm -rf .gnupg
curl https://keys.openpgp.org/vks/v1/by-fingerprint/2E5C672DE893055D04F5B7BC36B449FB5364BDC4 | HOME=/var/lib/timelimit/build/mirror gpg --import
echo "2E5C672DE893055D04F5B7BC36B449FB5364BDC4:6:" | HOME=/var/lib/timelimit/build/mirror gpg --import-ownertrust
err=0
HOME=/var/lib/timelimit/build/mirror git verify-tag --raw "$selected_version" 2>&1 | grep -q '\[GNUPG:\] TRUST_ULTIMATE' || err=$?
if [[ $err != 0 ]]; then
echo "invalid signature"
exit
fi
rm -rf /var/lib/timelimit/build/output/
mkdir --mode=u=rwx,go=rx /var/lib/timelimit/build/output
git --work-tree=/var/lib/timelimit/build/output/ checkout "$selected_version" -- .
cd /var/lib/timelimit/build/output/
mkdir .npmcache
export npm_config_cache=/var/lib/timelimit/build/output/.npmcache
# retry a few times
npm install --no-optional || npm install --no-optional || npm install --no-optional || npm install --no-optional
npm run build
npm prune --production
rm -rf src .npmcache
echo "$selected_version" > version.txt
EOF
chown timelimitbuild /var/lib/timelimit/build/build.sh
chmod u+x /var/lib/timelimit/build/build.sh
runuser -u timelimitbuild -- /var/lib/timelimit/build/build.sh
# create helperscript to use the new built version and use it
cat > /var/lib/timelimit/copy-new-build.sh <<"EOF"
#! /bin/bash
set -e
if [[ ! -f /var/lib/timelimit/build/output/version.txt ]]; then
echo "There is no new build"
exit 1
fi
if [[ -f /var/lib/timelimit/run/version.txt ]]; then
NEWVERSION="$(cat /var/lib/timelimit/build/output/version.txt)"
OLDVERSION="$(cat /var/lib/timelimit/run/version.txt)"
if [[ "$OLDVERSION" > "$NEWVERSION" ]]; then
echo ""
echo "IMPORTANT"
echo ""
echo "It looks like you're trying to do a downgrade."
echo "This is not recommend so it's blocked."
echo "You can run 'rm /var/lib/timelimit/run/version.txt' to remove this check temporarily."
echo ""
exit 1
fi
fi
rm -rf /var/lib/timelimit/run
mkdir --mode=u=rwx,go= /var/lib/timelimit/run
cp -r /var/lib/timelimit/build/output/* /var/lib/timelimit/run
chmod -R u-w,go-rwx /var/lib/timelimit/run
chown -R timelimitrun /var/lib/timelimit/run
# allow everyone to see the version
chmod go+x /var/lib/timelimit/run
chmod go+r /var/lib/timelimit/run/version.txt
EOF
chmod u+x /var/lib/timelimit/copy-new-build.sh
/var/lib/timelimit/copy-new-build.sh
# create a helperscript to create a env file and use it
cat > /var/lib/timelimit/build-env.sh <<"EOF"
#! /bin/bash
set -e
umask 077
config="$(cat /etc/timelimit/config.json)"
mail_whitelist="$(jq --argjson config "$config" --null-input --raw-output '$config.mailWhitelist | if type == "string" then . else "" end')"
disable_signup="$(jq --argjson config "$config" --null-input --raw-output '$config.disableSignup | if type == "boolean" and . == true then "yes" else "no" end')"
mail_sender="$(jq --argjson config "$config" --null-input --raw-output '$config.mailTransport.sender | if type == "string" then . else "" end')"
mail_transport="$(jq --compact-output --argjson config "$config" --null-input --raw-output '$config.mailTransport | if type == "object" then { host, port, secure, auth: { user, pass } } else null end')"
mail_imprint="$(jq --argjson config "$config" --null-input --raw-output '$config.mailImprint | if type == "string" then . else "" end')"
encode() {
jq --null-input --raw-output --arg data "$1" '$data | gsub("\\\\"; "\\\\") | gsub("\n"; "\\n")'
}
(
[[ "$mail_whitelist" != "" ]] && echo "MAIL_WHITELIST=$(encode "$mail_whitelist")"
[[ "$mail_sender" != "" ]] && echo "MAIL_SENDER=$(encode "$mail_sender")"
[[ "$mail_transport" != "null" ]] && echo "MAIL_TRANSPORT=$(encode "$mail_transport")"
[[ "$mail_imprint" != "" ]] && echo "MAIL_IMPRINT=$(encode "$mail_imprint")"
echo "DISABLE_SIGNUP=$disable_signup"
) > /etc/timelimit/env
EOF
chmod u+x /var/lib/timelimit/build-env.sh
/var/lib/timelimit/build-env.sh
# create a systemd unit and start the server
cat > /etc/systemd/system/timelimit.service <<"EOF"
[Unit]
Description=the timelimit server application
[Service]
Type=simple
WorkingDirectory=/var/lib/timelimit/run/
RuntimeDirectory=timelimit
ExecStartPre=rm -f /var/run/timelimit/server
ExecStart=/usr/bin/node /var/lib/timelimit/run/build/index.js
User=timelimitrun
Group=timelimitrun
Restart=always
PrivateTmp=yes
ProtectSystem=yes
ProtectHome=yes
ProtectDevices=yes
UMask=0555
Environment="DATABASE_URL=postgres://timelimitrun:unused@localhost/timelimit?host=/var/run/postgresql"
Environment="PORT=/var/run/timelimit/server"
Environment="NODE_ENV=production"
Environment="ALWAYS_PRO=yes"
EnvironmentFile=/etc/timelimit/env
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now timelimit.service
# create timelimit-dump
cat > /bin/timelimit-dump <<"EOF"
#! /bin/bash
set -e
if [ "$(id -u)" != 0 ]; then
echo "You must run this as root (with sudo)"
exit 1
fi
runuser -u postgres -- pg_dump timelimit
EOF
chmod +x /bin/timelimit-dump
# implement timelimit-restore
cat > /bin/timelimit-restore <<"EOF"
#! /bin/bash
set -e
if [ "$(id -u)" != 0 ]; then
echo "You must run this as root (with sudo)"
exit 1
fi
if [ "$#" != 1 ]; then
echo "You must supply one parameter, the file with the data to restore"
exit 1
fi
if [ ! -f "$1" ]; then
echo "You supplied a parameter, but it wasn't a file"
exit 1
fi
TEMPFILE="$(mktemp)"
chown root "$TEMPFILE"
chgrp postgres "$TEMPFILE"
chmod u=rw,g=r,o= "$TEMPFILE"
cat "$1" > "$TEMPFILE"
dialog --yesno "Restoring \"$1\" will delete the current database content. Do you want to continue?" 8 60
systemctl stop timelimit
runuser -u postgres -- dropdb timelimit
runuser -u postgres -- createdb --encoding=utf8 --owner=timelimitrun timelimit
runuser -u postgres -- psql -f "$TEMPFILE" --dbname=timelimit
rm "$TEMPFILE"
systemctl start timelimit
dialog --msgbox "The backup was restored." 8 60
EOF
chmod +x /bin/timelimit-restore
# create timelimit-upgrade
cat > /bin/timelimit-upgrade <<"EOF"
#! /bin/bash
set -e
if [ "$(id -u)" != 0 ]; then
echo "You must run this as root (with sudo)"
exit 1
fi
runuser -u timelimitbuild -- /var/lib/timelimit/build/build.sh
/var/lib/timelimit/copy-new-build.sh
systemctl restart timelimit.service
dialog --msgbox "The new timelimit server version was installed." 8 60
EOF
chmod +x /bin/timelimit-upgrade
# create timelimit-config
cat > /bin/timelimit-config <<"EOF"
#! /bin/bash
set -e
if [ "$(id -u)" != 0 ]; then
echo "You must run this as root (with sudo)"
exit 1
fi
configfilepath=/etc/timelimit/config.json
config_write() {
new_config="$(jq --null-input --argjson 'oldConfig' "$(cat /etc/timelimit/config.json)" "$@")"
[[ "$new_config" == "" ]] && return 1
echo "$new_config" > /etc/timelimit/config.json
/var/lib/timelimit/build-env.sh
systemctl restart timelimit
}
config_whitelist() {
config="$(cat "$configfilepath")"
old_whitelist="$(jq --argjson config "$config" --null-input --raw-output '$config.mailWhitelist | if type == "string" then . else "" end')"
mail_whitelist="`dialog --inputbox "Which users are allowed to sign in? This is a list of mail addresses (someone@somewhere.com) or domains (mailbox.org), separated by comma; if a user requests signing in with a mail address which is not in this list, then the request is rejected" 12 60 "$old_whitelist" 3>&1 1>&2 2>&3`"
[[ $? -eq 0 ]] || return 0
config_write --arg whitelist "$mail_whitelist" '$oldConfig + { mailWhitelist: $whitelist }' || return 1
dialog --msgbox "The whitelist was saved. It is now \"$mail_whitelist\"." 8 60
}
config_mail_delivery() {
config="$(cat "$configfilepath")"
old_sender="$(jq --argjson config "$config" --null-input --raw-output '$config.mailTransport.sender | if type == "string" then . else "" end')"
sender_mail="`dialog --inputbox "From which mail address do you want to send the mails. It's recommend to NOT use your primary mail address." 10 60 "$old_sender" 3>&1 1>&2 2>&3`"
[[ $? -eq 0 ]] || return 0
old_host="$(jq --argjson config "$config" --null-input --raw-output '$config.mailTransport.host | if type == "string" then . else "" end')"
sender_host="`dialog --inputbox "What's the hostname of the SMTP server which should be used?" 10 60 "$old_host" 3>&1 1>&2 2>&3`"
[[ $? -eq 0 ]] || return 0
old_secure="$(jq --argjson config "$config" --null-input --raw-output '$config.mailTransport.secure | if type == "boolean" and . == false then "--defaultno" else "" end')"
sender_secure=true
dialog $old_secure --yesno "Do you want to use TLS when connecting to the mail server?" 8 60 || sender_secure=false
recommend_port=465
if [[ "$sender_secure" == "false" ]]; then
recommend_port=587
fi
old_port="$(jq --arg fallback "$recommend_port" --argjson config "$config" --null-input --raw-output '$config.mailTransport.port | if type == "number" then . else $fallback end')"
sender_port="`dialog --inputbox "Which port should be used for SMTP? It's most likely ${recommend_port}." 10 60 "$old_port" 3>&1 1>&2 2>&3`"
[[ $? -eq 0 ]] || return 0
old_user="$(jq --argjson config "$config" --null-input --raw-output '$config.mailTransport.user | if type == "string" then . else "" end')"
sender_user="`dialog --inputbox "What's the SMTP username?" 10 60 "$old_user" 3>&1 1>&2 2>&3`"
[[ $? -eq 0 ]] || return 0
old_pass="$(jq --argjson config "$config" --null-input --raw-output '$config.mailTransport.pass | if type == "string" then . else "" end')"
sender_pass="`dialog --inputbox "What's the SMTP password?" 10 60 "$old_pass" 3>&1 1>&2 2>&3`"
[[ $? -eq 0 ]] || return 0
dialog --yesno "Do you want to use this settings?\nSender: $sender_mail\nHost: $sender_host\nTLS: $sender_secure\nPort: $sender_port\nUser: $sender_user\nPassword: $sender_pass" 11 60 || return 0
config_write --arg sender "$sender_mail" --arg host "$sender_host" --argjson secure "$sender_secure" --arg port "$sender_port" --arg user "$sender_user" --arg pass "$sender_pass" '$oldConfig + {mailTransport: { sender: $sender, secure: $secure, host: $host, port: $port | tonumber, user: $user, pass: $pass }}' || return 1
dialog --msgbox "Your settings were saved" 8 60
}
config_imprint() {
config="$(cat "$configfilepath")"
old_imprint="$(jq --argjson config "$config" --null-input --raw-output '$config.mailImprint | if type == "string" then . else "" end')"
imprint="`dialog --inputbox "What footer would you like to set for the sent mails?" 10 60 "$old_imprint" 3>&1 1>&2 2>&3`"
[[ $? -eq 0 ]] || return 0
config_write --arg imprint "$imprint" '$oldConfig + { mailImprint: $imprint }' || return 1
dialog --msgbox "The footer was saved. It is now \"$imprint\"." 8 60
}
config_signup() {
old_value="$(jq --argjson config "$config" --null-input --raw-output '$config.disableSignup | if type == "boolean" and . == true then "--defaultno" else "" end')"
value=false
dialog $old_value --no-label "Disable sign up" --yes-label "Enable sign up" --yesno "Do you want to allow new users to sign up?" 8 60 || value=true
config_write --argjson value "$value" '$oldConfig + {disableSignup: $value}' || return 1
if [[ "$value" == "false" ]]; then
dialog --msgbox "Users CAN sign up now" 8 60
else
dialog --msgbox "Users CAN NOT sign up now" 8 60
fi
}
config_main() {
config="$(cat "$configfilepath")"
mail_whitelist="$(jq --argjson config "$config" --null-input --raw-output '$config.mailWhitelist | if type == "string" then . else "" end')"
mail_sender="$(jq --argjson config "$config" --null-input --raw-output '$config.mailTransport.sender | if type == "string" then . else "(not configured)" end')"
mail_imprint="$(jq --argjson config "$config" --null-input --raw-output '$config.mailImprint | if type == "string" then . else "" end')"
disable_signup="$(jq --argjson config "$config" --null-input --raw-output '$config.disableSignup | if type == "boolean" and . == true then "CAN NOT" else "CAN" end')"
option="$(dialog --menu "What would you like to change?" 12 0 5 \
"mail-whitelist" "The allowed mail addresses for signing in - currently \"$mail_whitelist\"" \
"mail-delivery" "The parameters for sending mails - currently using $mail_sender" \
"mail-imprint" "The footer of the mails - currently \"$mail_imprint\"" \
"disable-signup" "Allow/disallow new users; currently new users $disable_signup sign up" \
"nothing" "Close the settings" \
3>&1 1>&2 2>&3)"
[[ $? -eq 0 ]] || return 0
if [[ "$option" == "mail-whitelist" ]]; then
config_whitelist || return 1
elif [[ "$option" == "mail-delivery" ]]; then
config_mail_delivery || return 1
elif [[ "$option" == "mail-imprint" ]]; then
config_imprint || return 1
elif [[ "$option" == "disable-signup" ]]; then
config_signup || return 1
elif [[ "$option" == "nothing" ]]; then
exit 0
else
echo "unknown option"
exit 1
fi
}
while true; do
config_main
done
EOF
chmod +x /bin/timelimit-config
dialog --msgbox "Installation finished. You have to configure the mail delivery before you can use your server. You will get a list of possible commands after confirming this message." 8 60
reset
cat <<"EOF"
There are the following new commands available at your system now:
timelimit-dump
This will output the current database content; redirect the output, e.g.
using 'timelimit-dump > backup' to create a backup
timelimit-restore
Use this to restore a dump using 'timelimit-restore ./backup'
timelimit-upgrade
Use this to upgrade the timelimit server installation
timelimit-config
Use this to adjust your configuration of the timelimit-server
You can get a list of them using 'timelimit-<tab>' or by look at the source code
of the installation script. This message is written at the end of the script.
EOF
+140 -330
View File
@@ -189,14 +189,14 @@
} }
}, },
"@babel/parser": { "@babel/parser": {
"version": "7.12.11", "version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.10.tgz",
"integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==" "integrity": "sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ=="
}, },
"@babel/types": { "@babel/types": {
"version": "7.12.12", "version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz",
"integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", "integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==",
"requires": { "requires": {
"@babel/helper-validator-identifier": "7.12.11", "@babel/helper-validator-identifier": "7.12.11",
"lodash": "4.17.20", "lodash": "4.17.20",
@@ -307,6 +307,11 @@
"@types/node": "14.14.16" "@types/node": "14.14.16"
} }
}, },
"@types/component-emitter": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
"integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg=="
},
"@types/connect": { "@types/connect": {
"version": "3.4.33", "version": "3.4.33",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
@@ -325,6 +330,16 @@
"@types/node": "14.14.16" "@types/node": "14.14.16"
} }
}, },
"@types/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg=="
},
"@types/cors": {
"version": "2.8.10",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz",
"integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="
},
"@types/email-templates": { "@types/email-templates": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/@types/email-templates/-/email-templates-8.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/email-templates/-/email-templates-8.0.0.tgz",
@@ -549,11 +564,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
}, },
"after": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
"integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
},
"ajv": { "ajv": {
"version": "7.0.2", "version": "7.0.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.2.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.2.tgz",
@@ -636,11 +646,6 @@
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="
}, },
"arraybuffer.slice": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
},
"arrify": { "arrify": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
@@ -667,11 +672,6 @@
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
}, },
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
},
"asynckit": { "asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -695,14 +695,9 @@
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
"requires": { "requires": {
"@babel/types": "7.12.12" "@babel/types": "7.13.0"
} }
}, },
"backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
},
"bail": { "bail": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
@@ -715,9 +710,9 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
}, },
"base64-arraybuffer": { "base64-arraybuffer": {
"version": "0.1.5", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
}, },
"base64id": { "base64id": {
"version": "2.0.0", "version": "2.0.0",
@@ -741,25 +736,12 @@
"tweetnacl": "0.14.5" "tweetnacl": "0.14.5"
} }
}, },
"better-assert": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
"integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
"requires": {
"callsite": "1.0.0"
}
},
"big.js": { "big.js": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-6.0.3.tgz", "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.0.3.tgz",
"integrity": "sha512-n6yn1FyVL1EW2DBAr4jlU/kObhRzmr+NNRESl65VIOT8WBJj/Kezpx2zFdhJUqYI6qrtTW7moCStYL5VxeVdPA==", "integrity": "sha512-n6yn1FyVL1EW2DBAr4jlU/kObhRzmr+NNRESl65VIOT8WBJj/Kezpx2zFdhJUqYI6qrtTW7moCStYL5VxeVdPA==",
"dev": true "dev": true
}, },
"blob": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
"integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
},
"bluebird": { "bluebird": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -844,10 +826,14 @@
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
}, },
"callsite": { "call-bind": {
"version": "1.0.0", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"requires": {
"function-bind": "1.1.1",
"get-intrinsic": "1.1.1"
}
}, },
"caseless": { "caseless": {
"version": "0.12.0", "version": "0.12.0",
@@ -900,7 +886,7 @@
"resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
"integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=",
"requires": { "requires": {
"is-regex": "1.1.1" "is-regex": "1.1.2"
} }
}, },
"character-reference-invalid": { "character-reference-invalid": {
@@ -1036,15 +1022,10 @@
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
"dev": true "dev": true
}, },
"component-bind": { "component-emitter": {
"version": "1.0.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"component-inherit": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
"integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
@@ -1069,8 +1050,8 @@
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
"integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==",
"requires": { "requires": {
"@babel/parser": "7.12.11", "@babel/parser": "7.13.10",
"@babel/types": "7.12.12" "@babel/types": "7.13.0"
} }
}, },
"content-disposition": { "content-disposition": {
@@ -1101,6 +1082,15 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
}, },
"cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"requires": {
"object-assign": "4.1.1",
"vary": "1.1.2"
}
},
"country-language": { "country-language": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/country-language/-/country-language-0.1.7.tgz", "resolved": "https://registry.npmjs.org/country-language/-/country-language-0.1.7.tgz",
@@ -1352,27 +1342,28 @@
"integrity": "sha512-bd/DFLAoJetvv7ar/KIpE3CNO8wEuyrt9Xuw6nSMiZ+Vrz/Q21BPsMHvARL2Wz6IKHKXgb+DWZqtRg1vql9cBg==" "integrity": "sha512-bd/DFLAoJetvv7ar/KIpE3CNO8wEuyrt9Xuw6nSMiZ+Vrz/Q21BPsMHvARL2Wz6IKHKXgb+DWZqtRg1vql9cBg=="
}, },
"engine.io": { "engine.io": {
"version": "3.4.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.0.tgz", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.0.0.tgz",
"integrity": "sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w==", "integrity": "sha512-BATIdDV3H1SrE9/u2BAotvsmjJg0t1P4+vGedImSs1lkFAtQdvk4Ev1y4LDiPF7BPWgXWEG+NDY+nLvW3UrMWw==",
"requires": { "requires": {
"accepts": "1.3.7", "accepts": "1.3.7",
"base64id": "2.0.0", "base64id": "2.0.0",
"cookie": "0.3.1", "cookie": "0.4.1",
"debug": "4.1.1", "cors": "2.8.5",
"engine.io-parser": "2.2.0", "debug": "4.3.1",
"ws": "7.2.0" "engine.io-parser": "4.0.2",
"ws": "7.4.4"
}, },
"dependencies": { "dependencies": {
"cookie": { "cookie": {
"version": "0.3.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
}, },
"debug": { "debug": {
"version": "4.1.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"requires": { "requires": {
"ms": "2.1.2" "ms": "2.1.2"
} }
@@ -1384,62 +1375,12 @@
} }
} }
}, },
"engine.io-client": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.0.tgz",
"integrity": "sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA==",
"requires": {
"component-emitter": "1.2.1",
"component-inherit": "0.0.3",
"debug": "4.1.1",
"engine.io-parser": "2.2.0",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"ws": "6.1.4",
"xmlhttprequest-ssl": "1.5.5",
"yeast": "0.1.2"
},
"dependencies": {
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"ws": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
"integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
"requires": {
"async-limiter": "1.0.1"
}
}
}
},
"engine.io-parser": { "engine.io-parser": {
"version": "2.2.0", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz",
"integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==",
"requires": { "requires": {
"after": "0.8.2", "base64-arraybuffer": "0.1.4"
"arraybuffer.slice": "0.0.7",
"base64-arraybuffer": "0.1.5",
"blob": "0.0.5",
"has-binary2": "1.0.3"
} }
}, },
"entities": { "entities": {
@@ -1678,6 +1619,16 @@
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true "dev": true
}, },
"get-intrinsic": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"requires": {
"function-bind": "1.1.1",
"has": "1.0.3",
"has-symbols": "1.0.2"
}
},
"get-paths": { "get-paths": {
"version": "0.0.7", "version": "0.0.7",
"resolved": "https://registry.npmjs.org/get-paths/-/get-paths-0.0.7.tgz", "resolved": "https://registry.npmjs.org/get-paths/-/get-paths-0.0.7.tgz",
@@ -1781,35 +1732,15 @@
"function-bind": "1.1.1" "function-bind": "1.1.1"
} }
}, },
"has-binary2": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
"integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
"requires": {
"isarray": "2.0.1"
},
"dependencies": {
"isarray": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
}
}
},
"has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
},
"has-flag": { "has-flag": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
}, },
"has-symbols": { "has-symbols": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
}, },
"has-unicode": { "has-unicode": {
"version": "2.0.1", "version": "2.0.1",
@@ -1968,11 +1899,6 @@
"minimatch": "3.0.4" "minimatch": "3.0.4"
} }
}, },
"indexof": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
"integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
},
"inflection": { "inflection": {
"version": "1.12.0", "version": "1.12.0",
"resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz",
@@ -2085,11 +2011,12 @@
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="
}, },
"is-regex": { "is-regex": {
"version": "1.1.1", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz",
"integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==",
"requires": { "requires": {
"has-symbols": "1.0.1" "call-bind": "1.0.2",
"has-symbols": "1.0.2"
} }
}, },
"is-typedarray": { "is-typedarray": {
@@ -2773,11 +2700,6 @@
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
}, },
"object-component": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
"integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
},
"on-finished": { "on-finished": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -2881,22 +2803,6 @@
"parse5": "6.0.1" "parse5": "6.0.1"
} }
}, },
"parseqs": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
"integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
"requires": {
"better-assert": "1.0.2"
}
},
"parseuri": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
"integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
"requires": {
"better-assert": "1.0.2"
}
},
"parseurl": { "parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -3062,7 +2968,7 @@
"mailparser": "3.0.1", "mailparser": "3.0.1",
"nodemailer": "6.4.17", "nodemailer": "6.4.17",
"open": "7.3.0", "open": "7.3.0",
"pug": "3.0.0", "pug": "3.0.2",
"uuid": "8.3.2" "uuid": "8.3.2"
}, },
"dependencies": { "dependencies": {
@@ -3109,17 +3015,17 @@
} }
}, },
"pug": { "pug": {
"version": "3.0.0", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz",
"integrity": "sha512-inmsJyFBSHZaiGLaguoFgJGViX0If6AcfcElimvwj9perqjDpUpw79UIEDZbWFmoGVidh08aoE+e8tVkjVJPCw==", "integrity": "sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==",
"requires": { "requires": {
"pug-code-gen": "3.0.1", "pug-code-gen": "3.0.2",
"pug-filters": "4.0.0", "pug-filters": "4.0.0",
"pug-lexer": "5.0.0", "pug-lexer": "5.0.1",
"pug-linker": "4.0.0", "pug-linker": "4.0.0",
"pug-load": "3.0.0", "pug-load": "3.0.0",
"pug-parser": "6.0.0", "pug-parser": "6.0.0",
"pug-runtime": "3.0.0", "pug-runtime": "3.0.1",
"pug-strip-comments": "2.0.0" "pug-strip-comments": "2.0.0"
} }
}, },
@@ -3130,20 +3036,20 @@
"requires": { "requires": {
"constantinople": "4.0.1", "constantinople": "4.0.1",
"js-stringify": "1.0.2", "js-stringify": "1.0.2",
"pug-runtime": "3.0.0" "pug-runtime": "3.0.1"
} }
}, },
"pug-code-gen": { "pug-code-gen": {
"version": "3.0.1", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.1.tgz", "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.2.tgz",
"integrity": "sha512-xJIGvmXTQlkJllq6hqxxjRWcay2F9CU69TuAuiVZgHK0afOhG5txrQOcZyaPHBvSWCU/QQOqEp5XCH94rRZpBQ==", "integrity": "sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==",
"requires": { "requires": {
"constantinople": "4.0.1", "constantinople": "4.0.1",
"doctypes": "1.1.0", "doctypes": "1.1.0",
"js-stringify": "1.0.2", "js-stringify": "1.0.2",
"pug-attrs": "3.0.0", "pug-attrs": "3.0.0",
"pug-error": "2.0.0", "pug-error": "2.0.0",
"pug-runtime": "3.0.0", "pug-runtime": "3.0.1",
"void-elements": "3.1.0", "void-elements": "3.1.0",
"with": "7.0.2" "with": "7.0.2"
} }
@@ -3162,13 +3068,13 @@
"jstransformer": "1.0.0", "jstransformer": "1.0.0",
"pug-error": "2.0.0", "pug-error": "2.0.0",
"pug-walk": "2.0.0", "pug-walk": "2.0.0",
"resolve": "1.19.0" "resolve": "1.20.0"
}, },
"dependencies": { "dependencies": {
"resolve": { "resolve": {
"version": "1.19.0", "version": "1.20.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
"requires": { "requires": {
"is-core-module": "2.2.0", "is-core-module": "2.2.0",
"path-parse": "1.0.6" "path-parse": "1.0.6"
@@ -3177,9 +3083,9 @@
} }
}, },
"pug-lexer": { "pug-lexer": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.0.tgz", "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz",
"integrity": "sha512-52xMk8nNpuyQ/M2wjZBN5gXQLIylaGkAoTk5Y1pBhVqaopaoj8Z0iVzpbFZAqitL4RHNVDZRnJDsqEYe99Ti0A==", "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
"requires": { "requires": {
"character-parser": "2.2.0", "character-parser": "2.2.0",
"is-expression": "4.0.0", "is-expression": "4.0.0",
@@ -3214,9 +3120,9 @@
} }
}, },
"pug-runtime": { "pug-runtime": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz",
"integrity": "sha512-GoEPcmQNnaTsePEdVA05bDpY+Op5VLHKayg08AQiqJBWU/yIaywEYv7TetC5dEQS3fzBBoyb2InDcZEg3mPTIA==" "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg=="
}, },
"pug-strip-comments": { "pug-strip-comments": {
"version": "2.0.0", "version": "2.0.0",
@@ -3621,22 +3527,25 @@
"integrity": "sha1-vQSN23TefRymkV+qSldXCzVQwtc=" "integrity": "sha1-vQSN23TefRymkV+qSldXCzVQwtc="
}, },
"socket.io": { "socket.io": {
"version": "2.3.0", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.0.1.tgz",
"integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", "integrity": "sha512-g8eZB9lV0f4X4gndG0k7YZAywOg1VxYgCUspS4V+sDqsgI/duqd0AW84pKkbGj/wQwxrqrEq+VZrspRfTbHTAQ==",
"requires": { "requires": {
"debug": "4.1.1", "@types/cookie": "0.4.0",
"engine.io": "3.4.0", "@types/cors": "2.8.10",
"has-binary2": "1.0.3", "@types/node": "14.14.16",
"socket.io-adapter": "1.1.1", "accepts": "1.3.7",
"socket.io-client": "2.3.0", "base64id": "2.0.0",
"socket.io-parser": "3.4.0" "debug": "4.3.1",
"engine.io": "5.0.0",
"socket.io-adapter": "2.2.0",
"socket.io-parser": "4.0.4"
}, },
"dependencies": { "dependencies": {
"debug": { "debug": {
"version": "4.1.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"requires": { "requires": {
"ms": "2.1.2" "ms": "2.1.2"
} }
@@ -3649,109 +3558,28 @@
} }
}, },
"socket.io-adapter": { "socket.io-adapter": {
"version": "1.1.1", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz",
"integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" "integrity": "sha512-rG49L+FwaVEwuAdeBRq49M97YI3ElVabJPzvHT9S6a2CWhDKnjSFasvwAwSYPRhQzfn4NtDIbCaGYgOCOU/rlg=="
}, },
"socket.io-client": { "socket.io-parser": {
"version": "2.3.0", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
"integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
"requires": { "requires": {
"backo2": "1.0.2", "@types/component-emitter": "1.2.10",
"base64-arraybuffer": "0.1.5", "component-emitter": "1.3.0",
"component-bind": "1.0.0", "debug": "4.3.1"
"component-emitter": "1.2.1",
"debug": "4.1.1",
"engine.io-client": "3.4.0",
"has-binary2": "1.0.3",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"object-component": "0.0.3",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"socket.io-parser": "3.3.0",
"to-array": "0.1.4"
}, },
"dependencies": { "dependencies": {
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"debug": { "debug": {
"version": "4.1.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"requires": { "requires": {
"ms": "2.1.2" "ms": "2.1.2"
} }
}, },
"isarray": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"socket.io-parser": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz",
"integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==",
"requires": {
"component-emitter": "1.2.1",
"debug": "3.1.0",
"isarray": "2.0.1"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
}
}
},
"socket.io-parser": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.0.tgz",
"integrity": "sha512-/G/VOI+3DBp0+DJKW4KesGnQkQPFmUCbA/oO2QGT6CWxU7hLGWqU3tyuzeSK/dqcyeHsQg1vTe9jiZI8GU9SCQ==",
"requires": {
"component-emitter": "1.2.1",
"debug": "4.1.1",
"isarray": "2.0.1"
},
"dependencies": {
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "2.1.2"
}
},
"isarray": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
},
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -3901,11 +3729,6 @@
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.216.0.tgz", "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.216.0.tgz",
"integrity": "sha512-y9A+eMRKLdAOclcFRTk3durpvCWiEdWcQhCOopCO654pckH9+o5Z5VgBsTTAFqtyxB8yFRXSG1q7BCCeHyrm0w==" "integrity": "sha512-y9A+eMRKLdAOclcFRTk3durpvCWiEdWcQhCOopCO654pckH9+o5Z5VgBsTTAFqtyxB8yFRXSG1q7BCCeHyrm0w=="
}, },
"to-array": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
"integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
},
"to-fast-properties": { "to-fast-properties": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -4425,8 +4248,8 @@
"resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
"requires": { "requires": {
"@babel/parser": "7.12.11", "@babel/parser": "7.13.10",
"@babel/types": "7.12.12", "@babel/types": "7.13.0",
"assert-never": "1.2.1", "assert-never": "1.2.1",
"babel-walk": "3.0.0-canary-5" "babel-walk": "3.0.0-canary-5"
} }
@@ -4514,17 +4337,9 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}, },
"ws": { "ws": {
"version": "7.2.0", "version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw=="
"requires": {
"async-limiter": "1.0.1"
}
},
"xmlhttprequest-ssl": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
"integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
}, },
"xtend": { "xtend": {
"version": "4.0.2", "version": "4.0.2",
@@ -4602,11 +4417,6 @@
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
"integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
"dev": true "dev": true
},
"yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
} }
} }
} }
+2 -2
View File
@@ -5,7 +5,7 @@
"description": "", "description": "",
"scripts": { "scripts": {
"start": "node ./build/index.js", "start": "node ./build/index.js",
"test": "echo \"Error: no test specified\" && exit 1", "test": "node scripts/test-launch-with-different-databases.js",
"lint": "tslint --project .", "lint": "tslint --project .",
"lint:fix": "tslint --project . --fix", "lint:fix": "tslint --project . --fix",
"build": "npm run build:clean && npm run build:json && npm run build:ts && npm run lint && npm run build:doc", "build": "npm run build:clean && npm run build:json && npm run build:ts && npm run lint && npm run build:doc",
@@ -62,7 +62,7 @@
"pg-hstore": "^2.3.3", "pg-hstore": "^2.3.3",
"rate-limiter-flexible": "^2.1.15", "rate-limiter-flexible": "^2.1.15",
"sequelize": "^6.3.5", "sequelize": "^6.3.5",
"socket.io": "^2.3.0", "socket.io": "^4.0.1",
"tokgen": "^1.0.0", "tokgen": "^1.0.0",
"umzug": "^2.3.0" "umzug": "^2.3.0"
}, },
+1 -2
View File
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -29,7 +29,6 @@ const types = [
'CreateFamilyByMailTokenRequest', 'CreateFamilyByMailTokenRequest',
'SignIntoFamilyRequest', 'SignIntoFamilyRequest',
'RecoverParentPasswordRequest', 'RecoverParentPasswordRequest',
'CanRecoverPasswordRequest',
'RegisterChildDeviceRequest', 'RegisterChildDeviceRequest',
'SerializedParentAction', 'SerializedParentAction',
'SerializedAppLogicAction', 'SerializedAppLogicAction',
@@ -0,0 +1,55 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const { databaseLaunchers } = require('./util/database')
const { startMainApp } = require('./util/mainapp.js')
// use export PATH="$PATH:/usr/lib/postgresql/11/bin"
// if the postgres binaries are not in the path
async function main() {
let log = ''
for (const launcher of databaseLaunchers) {
const database = await launcher()
console.log('Test with ' + database.type)
try {
const task = await startMainApp({
DATABASE_URL: database.connectionUrl
})
console.log('test successfull')
log += 'Worked with ' + database.type + '\n'
task.shutdown()
} catch (ex) {
log += 'Failure with ' + database.type + '\n'
console.warn('test failed', ex)
}
database.shutdown()
}
console.log('\nRESULTS\n\n' + log)
process.exit(0)
}
main().catch((ex) => {
console.warn(ex)
})
+22
View File
@@ -0,0 +1,22 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const { resolve } = require('path')
const tempDir = resolve(__dirname, '../../../tempdb')
module.exports = { tempDir }
+25
View File
@@ -0,0 +1,25 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const { startPostgres } = require('./postgres.js')
const { startMariadb } = require('./mariadb.js')
const { startSqlite } = require('./sqlite.js')
module.exports = {
startPostgres,
databaseLaunchers: [ startMariadb, startPostgres, startSqlite ]
}
+82
View File
@@ -0,0 +1,82 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const { resolve } = require('path')
const { spawn } = require('child_process')
const { tempDir } = require('./helper.js')
const { generateShortToken, generateToken } = require('../token.js')
const { rimrafAsync, mkdirAsync, readFileAsync, writeFileAsync } = require('../filesystem.js')
const { spawnAsync } = require('../process.js')
const { sleep } = require('../sleep.js')
async function startMariadb() {
try { await mkdirAsync(tempDir) } catch (ex) {/* ignore */}
const instanceDir = resolve(tempDir, generateShortToken())
const dataDir = resolve(instanceDir, 'data')
const socketPath = resolve(instanceDir, 'socket')
await rimrafAsync(instanceDir)
await mkdirAsync(instanceDir); await mkdirAsync(dataDir)
await spawnAsync('mysql_install_db', ['--datadir=' + dataDir, '--user=' + process.env.USER], { stdio: 'inherit' })
const task = await spawn('mysqld_safe', ['--no-defaults', '--datadir=' + dataDir, '--socket=' + socketPath, '--skip-networking'], { stdio: 'inherit' })
task.on('exit', () => rimrafAsync(instanceDir))
for (let i = 0; i < 100; i++) {
const { status } = await spawnAsync('mysqladmin', ['ping', '-S', socketPath])
if (status === 0) break
await sleep(100)
}
const database = generateShortToken()
const username = generateShortToken()
const password = generateToken()
const commands = [
'CREATE DATABASE `' + database + '` DEFAULT CHARACTER SET `utf8mb4` COLLATE `utf8mb4_bin`',
// all users of the system can see this password because it is passed as command
// line parameter - don't do this for anything important
'CREATE USER `' + username + '`@localhost IDENTIFIED BY \'' + password + '\'',
'GRANT ALL PRIVILEGES ON `' + database + '`.* TO `' + username + '`@localhost'
]
for (command of commands) {
console.log(command)
await spawnAsync('mysql', ['-S', socketPath, '-u', 'root', '-e', command], { stdio: 'inherit' })
}
return {
shutdown: () => {
spawnAsync('mysql', ['-S', socketPath, '-u', 'root', '-e', 'SHUTDOWN;'], { stdio: 'inherit' }).catch((ex) => {
console.warn(ex)
})
},
socketPath,
dataDir,
database,
username,
password,
connectionUrl: 'mariadb://' + username + ':' + password + '@localhost/' + database + '?socketPath=' + encodeURIComponent(socketPath),
type: 'mariadb'
}
}
module.exports = { startMariadb }
+75
View File
@@ -0,0 +1,75 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const { resolve } = require('path')
const { spawn } = require('child_process')
const { tempDir } = require('./helper.js')
const { generateShortToken, generateToken } = require('../token.js')
const { rimrafAsync, mkdirAsync, readFileAsync, writeFileAsync } = require('../filesystem.js')
const { spawnAsync } = require('../process.js')
const { sleep } = require('../sleep.js')
async function startPostgres() {
try { await mkdirAsync(tempDir) } catch (ex) {/* ignore */}
const instanceDir = resolve(tempDir, generateShortToken())
const dataDir = resolve(instanceDir, 'data')
const socketDir = resolve(instanceDir, 'socket')
await rimrafAsync(instanceDir)
await mkdirAsync(instanceDir); await mkdirAsync(dataDir); await mkdirAsync(socketDir)
await spawnAsync('initdb', ['--locale=en_US.UTF-8', '-E', ' UTF8', '-D', dataDir], { stdio: 'inherit' })
const configFilePath = resolve(dataDir, 'postgresql.conf')
const configFileContent = (await readFileAsync(configFilePath)) + '\n'
+ 'unix_socket_directories = \'' + socketDir + '\'' + '\n'
+ 'listen_addresses = \'\' # do not listen using TCP'
await writeFileAsync(configFilePath, configFileContent)
const task = spawn('postgres', ['-D', dataDir], { stdio: 'inherit' })
task.on('exit', () => rimrafAsync(instanceDir))
for (let i = 0; i < 100; i++) {
const { status } = await spawnAsync('pg_isready', ['-h', socketDir])
if (status === 0) break
await sleep(100)
}
const database = generateShortToken()
const username = generateShortToken()
const password = generateToken() // this database accepts anything
await spawnAsync('createuser', ['-h', socketDir, username], { stdio: 'inherit' })
await spawnAsync('createdb', ['-h', socketDir, database], { stdio: 'inherit' })
return {
shutdown: () => task.kill('SIGINT'),
socketDir,
dataDir,
database,
username,
password,
connectionUrl: 'postgres://' + username + ':' + password + '@localhost/' + database + '?host=' + encodeURIComponent(socketDir),
type: 'postgres'
}
}
module.exports = { startPostgres }
+42
View File
@@ -0,0 +1,42 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const { resolve } = require('path')
const { spawn } = require('child_process')
const { tempDir } = require('./helper.js')
const { generateShortToken, generateToken } = require('../token.js')
const { rimrafAsync, mkdirAsync, readFileAsync, writeFileAsync } = require('../filesystem.js')
const { spawnAsync } = require('../process.js')
const { sleep } = require('../sleep.js')
async function startSqlite() {
try { await mkdirAsync(tempDir) } catch (ex) {/* ignore */}
const instanceDir = resolve(tempDir, generateShortToken())
await rimrafAsync(instanceDir)
await mkdirAsync(instanceDir)
return {
shutdown: () => rimrafAsync(instanceDir),
instanceDir,
connectionUrl: 'sqlite:///' + instanceDir + '/test.db',
type: 'sqlite'
}
}
module.exports = { startSqlite }
+57
View File
@@ -0,0 +1,57 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const rimraf = require('rimraf')
const { mkdir, readFile, writeFile } = require('fs')
function mkdirAsync(path) {
return new Promise((resolve, reject) => {
mkdir(path, (err) => {
if (err) reject(err)
else resolve()
})
})
}
function rimrafAsync(path) {
return new Promise((resolve, reject) => {
rimraf(path, (err) => {
if (err) reject(err)
else resolve()
})
})
}
function readFileAsync(path) {
return new Promise((resolve, reject) => {
readFile(path, (err, res) => {
if (err) reject(err)
else resolve(res)
})
})
}
function writeFileAsync(path, content) {
return new Promise((resolve, reject) => {
writeFile(path, content, (err) => {
if (err) reject(err)
else resolve()
})
})
}
module.exports = { mkdirAsync, rimrafAsync, readFileAsync, writeFileAsync }
+51
View File
@@ -0,0 +1,51 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const { spawn } = require('child_process')
const { resolve } = require('path')
function startMainApp(env) {
const initPath = resolve(__dirname, '../../build/index.js')
return new Promise((resolve, reject) => {
const task = spawn('node', [initPath], {
stdio: ['inherit', 'pipe', 'inherit'],
env: { ...process.env, PORT: 0 /* random port */, ...env }
})
task.on('exit', () => reject(new Error('task terminated too early')))
task.on('error', (ex) => reject(ex))
task.stdout.on('data', (data) => {
if (data.toString('utf8').split('\n').indexOf('ready') !== -1) resolve(task)
process.stdout.write(data)
})
setTimeout(() => {
reject(new Error('timeout'))
task.kill('SIGINT')
}, 1000 * 30)
}).then((task) => {
return {
shutdown: () => task.kill('SIGINT')
}
})
}
module.exports = { startMainApp }
+29
View File
@@ -0,0 +1,29 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const { spawn } = require('child_process')
function spawnAsync(command, args, options) {
return new Promise((resolve, reject) => {
const task = spawn(command, args, options)
task.on('error', (ex) => reject(ex))
task.on('exit', (status) => resolve({ status }))
})
}
module.exports = { spawnAsync }
+24
View File
@@ -0,0 +1,24 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function sleep(delay) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), delay)
})
}
module.exports = { sleep }
+33
View File
@@ -0,0 +1,33 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const TokenGenerator = require('tokgen')
const tokenGenerator = new TokenGenerator({
length: 32,
chars: 'a-zA-Z0-9'
})
const shortTokenGenerator = new TokenGenerator({
length: 8,
chars: 'a-zA-Z0-9'
})
function generateToken() { return tokenGenerator.generate() }
function generateShortToken() { return shortTokenGenerator.generate() }
module.exports = { generateToken, generateShortToken }
+2 -21
View File
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -21,7 +21,6 @@ import { BadRequest, Forbidden, Unauthorized } from 'http-errors'
import { config } from '../config' import { config } from '../config'
import { Database, Transaction } from '../database' import { Database, Transaction } from '../database'
import { removeDevice } from '../function/device/remove-device' import { removeDevice } from '../function/device/remove-device'
import { canRecoverPassword } from '../function/parent/can-recover-password'
import { createAddDeviceToken } from '../function/parent/create-add-device-token' import { createAddDeviceToken } from '../function/parent/create-add-device-token'
import { createFamily } from '../function/parent/create-family' import { createFamily } from '../function/parent/create-family'
import { getStatusByMailToken } from '../function/parent/get-status-by-mail-address' import { getStatusByMailToken } from '../function/parent/get-status-by-mail-address'
@@ -30,7 +29,7 @@ import { recoverParentPassword } from '../function/parent/recover-parent-passwor
import { signInIntoFamily } from '../function/parent/sign-in-into-family' import { signInIntoFamily } from '../function/parent/sign-in-into-family'
import { WebsocketApi } from '../websocket' import { WebsocketApi } from '../websocket'
import { import {
isCanRecoverPasswordRequest, isCreateFamilyByMailTokenRequest, isCreateFamilyByMailTokenRequest,
isCreateRegisterDeviceTokenRequest, isLinkParentMailAddressRequest, isCreateRegisterDeviceTokenRequest, isLinkParentMailAddressRequest,
isMailAuthTokenRequestBody, isRecoverParentPasswordRequest, isMailAuthTokenRequestBody, isRecoverParentPasswordRequest,
isRemoveDeviceRequest, isSignIntoFamilyRequest isRemoveDeviceRequest, isSignIntoFamilyRequest
@@ -113,24 +112,6 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
} }
}) })
router.post('/can-recover-password', json(), async (req, res, next) => {
try {
if (!isCanRecoverPasswordRequest(req.body)) {
throw new BadRequest()
}
const canRecover = await canRecoverPassword({
database,
parentUserId: req.body.parentUserId,
mailAuthToken: req.body.mailAuthToken
})
res.json({ canRecover })
} catch (ex) {
next(ex)
}
})
router.post('/recover-parent-password', json(), async (req, res, next) => { router.post('/recover-parent-password', json(), async (req, res, next) => {
try { try {
if (!isRecoverParentPasswordRequest(req.body)) { if (!isRecoverParentPasswordRequest(req.body)) {
+1 -6
View File
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -82,11 +82,6 @@ export interface RecoverParentPasswordRequest {
password: ParentPassword password: ParentPassword
} }
export interface CanRecoverPasswordRequest {
mailAuthToken: string
parentUserId: string
}
export interface RegisterChildDeviceRequest { export interface RegisterChildDeviceRequest {
registerToken: string registerToken: string
childDevice: NewDeviceInfo childDevice: NewDeviceInfo
+1 -19
View File
@@ -1,5 +1,5 @@
// tslint:disable // tslint:disable
import { ClientPushChangesRequest, ClientPullChangesRequest, MailAuthTokenRequestBody, CreateFamilyByMailTokenRequest, SignIntoFamilyRequest, RecoverParentPasswordRequest, CanRecoverPasswordRequest, RegisterChildDeviceRequest, SerializedParentAction, SerializedAppLogicAction, SerializedChildAction, CreateRegisterDeviceTokenRequest, CanDoPurchaseRequest, FinishPurchaseByGooglePlayRequest, LinkParentMailAddressRequest, UpdatePrimaryDeviceRequest, RemoveDeviceRequest, RequestWithAuthToken, SendMailLoginCodeRequest, SignInByMailCodeRequest } from './schema' import { ClientPushChangesRequest, ClientPullChangesRequest, MailAuthTokenRequestBody, CreateFamilyByMailTokenRequest, SignIntoFamilyRequest, RecoverParentPasswordRequest, RegisterChildDeviceRequest, SerializedParentAction, SerializedAppLogicAction, SerializedChildAction, CreateRegisterDeviceTokenRequest, CanDoPurchaseRequest, FinishPurchaseByGooglePlayRequest, LinkParentMailAddressRequest, UpdatePrimaryDeviceRequest, RemoveDeviceRequest, RequestWithAuthToken, SendMailLoginCodeRequest, SignInByMailCodeRequest } from './schema'
import Ajv from 'ajv' import Ajv from 'ajv'
const ajv = new Ajv() const ajv = new Ajv()
@@ -2576,24 +2576,6 @@ export const isRecoverParentPasswordRequest: (value: object) => value is Recover
"definitions": definitions, "definitions": definitions,
"$schema": "http://json-schema.org/draft-07/schema#" "$schema": "http://json-schema.org/draft-07/schema#"
}) })
export const isCanRecoverPasswordRequest: (value: object) => value is CanRecoverPasswordRequest = ajv.compile({
"type": "object",
"properties": {
"mailAuthToken": {
"type": "string"
},
"parentUserId": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"mailAuthToken",
"parentUserId"
],
"definitions": definitions,
"$schema": "http://json-schema.org/draft-07/schema#"
})
export const isRegisterChildDeviceRequest: (value: object) => value is RegisterChildDeviceRequest = ajv.compile({ export const isRegisterChildDeviceRequest: (value: object) => value is RegisterChildDeviceRequest = ajv.compile({
"type": "object", "type": "object",
"properties": { "properties": {
+2 -1
View File
@@ -47,5 +47,6 @@ export const createConfigModel = (sequelize: Sequelize.Sequelize): ConfigModelSt
export const configItemIds = { export const configItemIds = {
statusMessage: 'status_message', statusMessage: 'status_message',
selfTestData: 'self_test_data' selfTestData: 'self_test_data',
secondSelfTestData: 'self_test_data_two'
} }
+4 -128
View File
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -15,130 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as Sequelize from 'sequelize' export { Transaction, Database, defaultDatabase, defaultUmzug } from './main'
import { generateIdWithinFamily } from '../util/token' export { assertNestedTransactionsAreWorking } from './utils/nested-transactions'
import { AddDeviceTokenModelStatic, createAddDeviceTokenModel } from './adddevicetoken' export { assertSerializeableTransactionsAreWorking, shouldRetryWithException } from './utils/serialized'
import { AppModelStatic, createAppModel } from './app'
import { AppActivityModelStatic, createAppActivityModel } from './appactivity'
import { AuthTokenModelStatic, createAuthtokenModel } from './authtoken'
import { CategoryModelStatic, createCategoryModel } from './category'
import { CategoryAppModelStatic, createCategoryAppModel } from './categoryapp'
import { CategoryNetworkIdModelStatic, createCategoryNetworkIdModel } from './categorynetworkid'
import { ChildTaskModelStatic, createChildTaskModel } from './childtask'
import { configItemIds, ConfigModelStatic, createConfigModel } from './config'
import { createDeviceModel, DeviceModelStatic } from './device'
import { createFamilyModel, FamilyModelStatic } from './family'
import { createMailLoginTokenModel, MailLoginTokenModelStatic } from './maillogintoken'
import { createUmzug } from './migration/umzug'
import { createOldDeviceModel, OldDeviceModelStatic } from './olddevice'
import { createPurchaseModel, PurchaseModelStatic } from './purchase'
import { createSessionDurationModel, SessionDurationModelStatic } from './sessionduration'
import { createTimelimitRuleModel, TimelimitRuleModelStatic } from './timelimitrule'
import { createUsedTimeModel, UsedTimeModelStatic } from './usedtime'
import { createUserModel, UserModelStatic } from './user'
import { createUserLimitLoginCategoryModel, UserLimitLoginCategoryModelStatic } from './userlimitlogincategory'
export type Transaction = Sequelize.Transaction
export interface Database {
addDeviceToken: AddDeviceTokenModelStatic
authtoken: AuthTokenModelStatic
app: AppModelStatic
appActivity: AppActivityModelStatic
category: CategoryModelStatic
categoryApp: CategoryAppModelStatic
categoryNetworkId: CategoryNetworkIdModelStatic
childTask: ChildTaskModelStatic
config: ConfigModelStatic
device: DeviceModelStatic
family: FamilyModelStatic
mailLoginToken: MailLoginTokenModelStatic
oldDevice: OldDeviceModelStatic
purchase: PurchaseModelStatic
sessionDuration: SessionDurationModelStatic
timelimitRule: TimelimitRuleModelStatic
usedTime: UsedTimeModelStatic
user: UserModelStatic
userLimitLoginCategory: UserLimitLoginCategoryModelStatic
transaction: <T> (autoCallback: (t: Transaction) => Promise<T>, options?: { transaction: Transaction }) => Promise<T>
dialect: string
}
const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
addDeviceToken: createAddDeviceTokenModel(sequelize),
authtoken: createAuthtokenModel(sequelize),
app: createAppModel(sequelize),
appActivity: createAppActivityModel(sequelize),
category: createCategoryModel(sequelize),
categoryApp: createCategoryAppModel(sequelize),
childTask: createChildTaskModel(sequelize),
categoryNetworkId: createCategoryNetworkIdModel(sequelize),
config: createConfigModel(sequelize),
device: createDeviceModel(sequelize),
family: createFamilyModel(sequelize),
mailLoginToken: createMailLoginTokenModel(sequelize),
oldDevice: createOldDeviceModel(sequelize),
purchase: createPurchaseModel(sequelize),
sessionDuration: createSessionDurationModel(sequelize),
timelimitRule: createTimelimitRuleModel(sequelize),
usedTime: createUsedTimeModel(sequelize),
user: createUserModel(sequelize),
userLimitLoginCategory: createUserLimitLoginCategoryModel(sequelize),
transaction: <T> (autoCallback: (transaction: Transaction) => Promise<T>, options?: { transaction: Transaction }) => (sequelize.transaction({
isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.READ_COMMITTED,
transaction: options?.transaction
}, autoCallback) as any) as Promise<T>,
dialect: sequelize.getDialect()
})
export const sequelize = new Sequelize.Sequelize(process.env.DATABASE_URL || 'sqlite://test.db', {
define: {
timestamps: false
},
logging: false
})
export const defaultDatabase = createDatabase(sequelize)
export const defaultUmzug = createUmzug(sequelize)
class NestedTransactionTestException extends Error {}
class TestRollbackException extends NestedTransactionTestException {}
class NestedTransactionsNotWorkingException extends NestedTransactionTestException { constructor () { super('NestedTransactionsNotWorkingException') } }
class IllegalStateException extends NestedTransactionTestException {}
export async function assertNestedTransactionsAreWorking (database: Database) {
const testValue = generateIdWithinFamily()
// clean up just for the case
await database.config.destroy({ where: { id: configItemIds.selfTestData } })
await database.transaction(async (transaction) => {
const readOne = await database.config.findOne({ where: { id: configItemIds.selfTestData }, transaction })
if (readOne) throw new IllegalStateException()
await database.transaction(async (transaction) => {
await database.config.create({ id: configItemIds.selfTestData, value: testValue }, { transaction })
const readTwo = await database.config.findOne({ where: { id: configItemIds.selfTestData }, transaction })
if (readTwo?.value !== testValue) throw new IllegalStateException()
try {
await database.transaction(async (transaction) => {
await database.config.destroy({ where: { id: configItemIds.selfTestData }, transaction })
throw new TestRollbackException()
}, { transaction })
} catch (ex) {
if (!(ex instanceof TestRollbackException)) throw ex
}
const readThree = await database.config.findOne({ where: { id: configItemIds.selfTestData }, transaction })
if (readThree?.value !== testValue) throw new NestedTransactionsNotWorkingException()
await database.config.destroy({ where: { id: configItemIds.selfTestData }, transaction })
}, { transaction })
})
}
+101
View File
@@ -0,0 +1,101 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { AddDeviceTokenModelStatic, createAddDeviceTokenModel } from './adddevicetoken'
import { AppModelStatic, createAppModel } from './app'
import { AppActivityModelStatic, createAppActivityModel } from './appactivity'
import { AuthTokenModelStatic, createAuthtokenModel } from './authtoken'
import { CategoryModelStatic, createCategoryModel } from './category'
import { CategoryAppModelStatic, createCategoryAppModel } from './categoryapp'
import { CategoryNetworkIdModelStatic, createCategoryNetworkIdModel } from './categorynetworkid'
import { ChildTaskModelStatic, createChildTaskModel } from './childtask'
import { ConfigModelStatic, createConfigModel } from './config'
import { createDeviceModel, DeviceModelStatic } from './device'
import { createFamilyModel, FamilyModelStatic } from './family'
import { createMailLoginTokenModel, MailLoginTokenModelStatic } from './maillogintoken'
import { createUmzug } from './migration/umzug'
import { createOldDeviceModel, OldDeviceModelStatic } from './olddevice'
import { createPurchaseModel, PurchaseModelStatic } from './purchase'
import { createSessionDurationModel, SessionDurationModelStatic } from './sessionduration'
import { createTimelimitRuleModel, TimelimitRuleModelStatic } from './timelimitrule'
import { createUsedTimeModel, UsedTimeModelStatic } from './usedtime'
import { createUserModel, UserModelStatic } from './user'
import { createUserLimitLoginCategoryModel, UserLimitLoginCategoryModelStatic } from './userlimitlogincategory'
export type Transaction = Sequelize.Transaction
export interface Database {
addDeviceToken: AddDeviceTokenModelStatic
authtoken: AuthTokenModelStatic
app: AppModelStatic
appActivity: AppActivityModelStatic
category: CategoryModelStatic
categoryApp: CategoryAppModelStatic
categoryNetworkId: CategoryNetworkIdModelStatic
childTask: ChildTaskModelStatic
config: ConfigModelStatic
device: DeviceModelStatic
family: FamilyModelStatic
mailLoginToken: MailLoginTokenModelStatic
oldDevice: OldDeviceModelStatic
purchase: PurchaseModelStatic
sessionDuration: SessionDurationModelStatic
timelimitRule: TimelimitRuleModelStatic
usedTime: UsedTimeModelStatic
user: UserModelStatic
userLimitLoginCategory: UserLimitLoginCategoryModelStatic
transaction: <T> (autoCallback: (t: Transaction) => Promise<T>, options?: { transaction: Transaction }) => Promise<T>
dialect: string
}
const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
addDeviceToken: createAddDeviceTokenModel(sequelize),
authtoken: createAuthtokenModel(sequelize),
app: createAppModel(sequelize),
appActivity: createAppActivityModel(sequelize),
category: createCategoryModel(sequelize),
categoryApp: createCategoryAppModel(sequelize),
childTask: createChildTaskModel(sequelize),
categoryNetworkId: createCategoryNetworkIdModel(sequelize),
config: createConfigModel(sequelize),
device: createDeviceModel(sequelize),
family: createFamilyModel(sequelize),
mailLoginToken: createMailLoginTokenModel(sequelize),
oldDevice: createOldDeviceModel(sequelize),
purchase: createPurchaseModel(sequelize),
sessionDuration: createSessionDurationModel(sequelize),
timelimitRule: createTimelimitRuleModel(sequelize),
usedTime: createUsedTimeModel(sequelize),
user: createUserModel(sequelize),
userLimitLoginCategory: createUserLimitLoginCategoryModel(sequelize),
transaction: <T> (autoCallback: (transaction: Transaction) => Promise<T>, options?: { transaction: Transaction }) => (sequelize.transaction({
isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE,
transaction: options?.transaction
}, autoCallback) as any) as Promise<T>,
dialect: sequelize.getDialect()
})
export const sequelize = new Sequelize.Sequelize(process.env.DATABASE_URL || 'sqlite://test.db', {
define: {
timestamps: false
},
logging: false
})
export const defaultDatabase = createDatabase(sequelize)
export const defaultUmzug = createUmzug(sequelize)
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -58,12 +58,24 @@ export async function up (queryInterface: QueryInterface, sequelize: Sequelize)
...usedTimeAttributesVersion3 ...usedTimeAttributesVersion3
}, { transaction }) }, { transaction })
const dialect = sequelize.getDialect()
const isMysql = dialect === 'mysql' || dialect === 'mariadb'
if (isMysql) {
await sequelize.query(` await sequelize.query(`
INSERT INTO UsedTimes (familyId, categoryId, dayOfEpoch, usedTime, lastUpdate, startMinuteOfDay, endMinuteOfDay) INSERT INTO UsedTimes (familyId, categoryId, dayOfEpoch, usedTime, lastUpdate, startMinuteOfDay, endMinuteOfDay)
SELECT familyId, categoryId, dayOfEpoch, usedTime, lastUpdate, SELECT familyId, categoryId, dayOfEpoch, usedTime, lastUpdate,
${MinuteOfDay.MIN} AS startMinuteOfDay, ${MinuteOfDay.MAX} AS endMinuteOfDay ${MinuteOfDay.MIN} AS startMinuteOfDay, ${MinuteOfDay.MAX} AS endMinuteOfDay
FROM UsedTimesOld FROM UsedTimesOld
`, { transaction }) `, { transaction })
} else {
await sequelize.query(`
INSERT INTO "UsedTimes" ("familyId", "categoryId", "dayOfEpoch", "usedTime", "lastUpdate", "startMinuteOfDay", "endMinuteOfDay")
SELECT "familyId", "categoryId", "dayOfEpoch", "usedTime", "lastUpdate",
${MinuteOfDay.MIN} AS "startMinuteOfDay", ${MinuteOfDay.MAX} AS "endMinuteOfDay"
FROM "UsedTimesOld"
`, { transaction })
}
await queryInterface.dropTable('UsedTimesOld', { transaction }) await queryInterface.dropTable('UsedTimesOld', { transaction })
}) })
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -25,11 +25,22 @@ export async function up (queryInterface: QueryInterface, sequelize: Sequelize)
await queryInterface.renameTable('Purchases', 'PurchasesOld', { transaction }) await queryInterface.renameTable('Purchases', 'PurchasesOld', { transaction })
await queryInterface.createTable('Purchases', purchaseAttributes, { transaction }) await queryInterface.createTable('Purchases', purchaseAttributes, { transaction })
const dialect = sequelize.getDialect()
const isMysql = dialect === 'mysql' || dialect === 'mariadb'
if (isMysql) {
await sequelize.query(` await sequelize.query(`
INSERT INTO Purchases (familyId, service, transactionId, type, loggedAt, previousFullVersionEndTime, newFullVersionEndTime) INSERT INTO Purchases (familyId, service, transactionId, type, loggedAt, previousFullVersionEndTime, newFullVersionEndTime)
SELECT familyId, service, transactionId, type, 0 AS loggedAt, 0 AS previousFullVersionEndTime, loggedAt AS newFullVersionEndTime SELECT familyId, service, transactionId, type, 0 AS loggedAt, 0 AS previousFullVersionEndTime, loggedAt AS newFullVersionEndTime
FROM PurchasesOld FROM PurchasesOld
`, { transaction }) `, { transaction })
} else {
await sequelize.query(`
INSERT INTO "Purchases" ("familyId", service, "transactionId", type, "loggedAt", "previousFullVersionEndTime", "newFullVersionEndTime")
SELECT "familyId", service, "transactionId", type, 0 AS "loggedAt", 0 AS "previousFullVersionEndTime", "loggedAt" AS "newFullVersionEndTime"
FROM "PurchasesOld"
`, { transaction })
}
await queryInterface.dropTable('PurchasesOld', { transaction }) await queryInterface.dropTable('PurchasesOld', { transaction })
}) })
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -21,6 +21,10 @@ export async function up (queryInterface: QueryInterface, sequelize: Sequelize)
await sequelize.transaction({ await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => { }, async (transaction) => {
const dialect = sequelize.getDialect()
const isMysql = dialect === 'mysql' || dialect === 'mariadb'
if (isMysql) {
await sequelize.query( await sequelize.query(
'CREATE TABLE `UserLimitLoginCategories`' + 'CREATE TABLE `UserLimitLoginCategories`' +
'(`familyId` VARCHAR(10) NOT NULL, `userId` VARCHAR(6) NOT NULL, `categoryId` VARCHAR(6) NOT NULL,' + '(`familyId` VARCHAR(10) NOT NULL, `userId` VARCHAR(6) NOT NULL, `categoryId` VARCHAR(6) NOT NULL,' +
@@ -29,6 +33,16 @@ export async function up (queryInterface: QueryInterface, sequelize: Sequelize)
) )
await sequelize.query('CREATE INDEX `UserLimitLoginCategoriesIndexCategoryId` ON `UserLimitLoginCategories` (`familyId`, `categoryId`)', { transaction }) await sequelize.query('CREATE INDEX `UserLimitLoginCategoriesIndexCategoryId` ON `UserLimitLoginCategories` (`familyId`, `categoryId`)', { transaction })
} else {
await sequelize.query(
'CREATE TABLE "UserLimitLoginCategories"' +
'("familyId" VARCHAR(10) NOT NULL, "userId" VARCHAR(6) NOT NULL, "categoryId" VARCHAR(6) NOT NULL,' +
'PRIMARY KEY("familyId", "userId"), FOREIGN KEY("familyId", "userId") REFERENCES "Users" ("familyId", "userId") ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY("familyId", "categoryId") REFERENCES "Categories" ("familyId", "categoryId") ON UPDATE CASCADE ON DELETE CASCADE )',
{ transaction }
)
await sequelize.query('CREATE INDEX "UserLimitLoginCategoriesIndexCategoryId" ON "UserLimitLoginCategories" ("familyId", "categoryId")', { transaction })
}
}) })
} }
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -21,6 +21,10 @@ export async function up (_: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({ await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => { }, async (transaction) => {
const dialect = sequelize.getDialect()
const isMysql = dialect === 'mysql' || dialect === 'mariadb'
if (isMysql) {
await sequelize.query( await sequelize.query(
'CREATE TABLE `CategoryNetworkIds` ' + 'CREATE TABLE `CategoryNetworkIds` ' +
'(`familyId` VARCHAR(10) NOT NULL, `categoryId` VARCHAR(6) NOT NULL,' + '(`familyId` VARCHAR(10) NOT NULL, `categoryId` VARCHAR(6) NOT NULL,' +
@@ -29,6 +33,16 @@ export async function up (_: QueryInterface, sequelize: Sequelize) {
'REFERENCES `Categories`(`familyId`, `categoryId`) ON UPDATE CASCADE ON DELETE CASCADE )', 'REFERENCES `Categories`(`familyId`, `categoryId`) ON UPDATE CASCADE ON DELETE CASCADE )',
{ transaction } { transaction }
) )
} else {
await sequelize.query(
'CREATE TABLE "CategoryNetworkIds" ' +
'("familyId" VARCHAR(10) NOT NULL, "categoryId" VARCHAR(6) NOT NULL,' +
'"networkItemId" VARCHAR(6) NOT NULL, "hashedNetworkId" VARCHAR(8) NOT NULL,' +
'PRIMARY KEY("familyId", "categoryId", "networkItemId"), FOREIGN KEY("familyId", "categoryId")' +
'REFERENCES "Categories"("familyId", "categoryId") ON UPDATE CASCADE ON DELETE CASCADE )',
{ transaction }
)
}
}) })
} }
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -22,6 +22,10 @@ export async function up (queryInterface: QueryInterface, sequelize: Sequelize)
await sequelize.transaction({ await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => { }, async (transaction) => {
const dialect = sequelize.getDialect()
const isMysql = dialect === 'mysql' || dialect === 'mariadb'
if (isMysql) {
await sequelize.query( await sequelize.query(
'CREATE TABLE `ChildTasks` (' + 'CREATE TABLE `ChildTasks` (' +
'`familyId` VARCHAR(10) NOT NULL, `taskId` VARCHAR(6) NOT NULL,' + '`familyId` VARCHAR(10) NOT NULL, `taskId` VARCHAR(6) NOT NULL,' +
@@ -34,6 +38,20 @@ export async function up (queryInterface: QueryInterface, sequelize: Sequelize)
')', ')',
{ transaction } { transaction }
) )
} else {
await sequelize.query(
'CREATE TABLE "ChildTasks" (' +
'"familyId" VARCHAR(10) NOT NULL, "taskId" VARCHAR(6) NOT NULL,' +
'"categoryId" VARCHAR(6) NOT NULL, "taskTitle" VARCHAR(50) NOT NULL,' +
'"extraTimeDuration" INTEGER NOT NULL, "pendingRequest" INTEGER NOT NULL,' +
'"lastGrantTimestamp" BIGINT NOT NULL,' +
'PRIMARY KEY("familyId", "taskId"),' +
'FOREIGN KEY("familyId", "categoryId") REFERENCES "Categories"("familyId", "categoryId") ' +
'ON UPDATE CASCADE ON DELETE CASCADE' +
')',
{ transaction }
)
}
await queryInterface.addColumn('Categories', 'taskListVersion', { await queryInterface.addColumn('Categories', 'taskListVersion', {
...categoryAttributes.taskListVersion ...categoryAttributes.taskListVersion
+2 -2
View File
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -61,7 +61,7 @@ export const attributesVersion1: SequelizeAttributes<TimelimitRuleAttributesVers
categoryId: { ...idWithinFamilyColumn }, categoryId: { ...idWithinFamilyColumn },
applyToExtraTimeUsage: { ...booleanColumn }, applyToExtraTimeUsage: { ...booleanColumn },
dayMaskAsBitmask: { dayMaskAsBitmask: {
type: Sequelize.TINYINT, type: Sequelize.SMALLINT,
allowNull: false, allowNull: false,
validate: { validate: {
min: 0, min: 0,
+62
View File
@@ -0,0 +1,62 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { generateIdWithinFamily } from '../../util/token'
import { configItemIds } from '../config'
import { Database } from '../main'
class NestedTransactionTestException extends Error {}
class TestRollbackException extends NestedTransactionTestException {}
class NestedTransactionsNotWorkingException extends NestedTransactionTestException { constructor () { super('NestedTransactionsNotWorkingException') } }
class IllegalStateException extends NestedTransactionTestException {}
export async function assertNestedTransactionsAreWorking (database: Database) {
const testValue = generateIdWithinFamily()
// clean up just for the case
await database.config.destroy({ where: { id: configItemIds.selfTestData } })
await database.transaction(async (transaction) => {
const readOne = await database.config.findOne({ where: { id: configItemIds.selfTestData }, transaction })
if (readOne) throw new IllegalStateException()
await database.transaction(async (transaction) => {
await database.config.create({ id: configItemIds.selfTestData, value: testValue }, { transaction })
const readTwo = await database.config.findOne({ where: { id: configItemIds.selfTestData }, transaction })
if (readTwo?.value !== testValue) throw new IllegalStateException()
try {
await database.transaction(async (transaction) => {
await database.config.destroy({ where: { id: configItemIds.selfTestData }, transaction })
throw new TestRollbackException()
}, { transaction })
} catch (ex) {
if (!(ex instanceof TestRollbackException)) throw ex
}
const readThree = await database.config.findOne({ where: { id: configItemIds.selfTestData }, transaction })
if (readThree?.value !== testValue) throw new NestedTransactionsNotWorkingException()
await database.config.destroy({ where: { id: configItemIds.selfTestData }, transaction })
}, { transaction })
})
}
+104
View File
@@ -0,0 +1,104 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { configItemIds } from '../config'
import { Database } from '../main'
export class SerializationFeatureCheckException extends Error {}
export function shouldRetryWithException (database: Database, e: any): boolean {
if (e instanceof Sequelize.TimeoutError) return true
if (!(e instanceof Sequelize.DatabaseError)) return false
const parent = e.parent
if (database.dialect === 'sqlite') {
if (parent.message.startsWith('SQLITE_BUSY:')) return true
} else if (database.dialect === 'postgres') {
// 40001 = serialization_failure
if ((parent as any).code === '40001') return true
// 40P01 = deadlock detected
if ((parent as any).code === '40P01') return true
} else if (database.dialect === 'mariadb') {
const errno = (parent as any).errno
// ER_LOCK_DEADLOCK
// Deadlock found when trying to get lock; try restarting transaction
if (errno === 1213) return true
}
return false
}
export async function assertSerializeableTransactionsAreWorking (database: Database) {
// clean up just for the case
await database.config.destroy({
where: {
id: {
[Sequelize.Op.in]: [ configItemIds.selfTestData, configItemIds.secondSelfTestData ]
}
}
})
// insert specific data
await database.config.bulkCreate([
{
id: configItemIds.selfTestData,
value: '123'
},
{
id: configItemIds.secondSelfTestData,
value: '456'
}
])
try {
// use two parallel transactions
await database.transaction(async (transactionOne) => {
await database.transaction(async (transactionTwo) => {
await database.config.findAll({ transaction: transactionOne })
await database.config.findAll({ transaction: transactionTwo })
await Promise.all([
(async () => {
await database.config.update({ value: 'c' }, { where: { id: configItemIds.selfTestData }, transaction: transactionOne })
})(),
(async () => {
await database.config.update({ value: 'd' }, { where: { id: configItemIds.secondSelfTestData }, transaction: transactionTwo })
})()
])
})
})
throw new SerializationFeatureCheckException()
} catch (ex) {
if (!shouldRetryWithException(database, ex)) {
throw new SerializationFeatureCheckException()
}
}
// finish clean up
await database.config.destroy({
where: {
id: {
[Sequelize.Op.in]: [ configItemIds.selfTestData, configItemIds.secondSelfTestData ]
}
}
})
}
+1 -3
View File
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -16,7 +16,6 @@
*/ */
import { Conflict, InternalServerError, Unauthorized } from 'http-errors' import { Conflict, InternalServerError, Unauthorized } from 'http-errors'
import * as Sequelize from 'sequelize'
import { config } from '../../config' import { config } from '../../config'
import { Database } from '../../database' import { Database } from '../../database'
import { generateVersionId } from '../../util/token' import { generateVersionId } from '../../util/token'
@@ -60,7 +59,6 @@ export const setPrimaryDevice = async ({ database, websocket, deviceAuthToken, c
userId: deviceEntry.currentUserId userId: deviceEntry.currentUserId
}, },
transaction, transaction,
lock: Sequelize.Transaction.LOCK.UPDATE,
attributes: ['currentDevice'] attributes: ['currentDevice']
}) })
@@ -1,41 +0,0 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Database } from '../../database'
import { requireMailByAuthToken } from '../authentication'
export const canRecoverPassword = async ({ database, mailAuthToken, parentUserId }: {
database: Database
mailAuthToken: string
parentUserId: string
// no transaction here because this is directly called from an API endpoint
}): Promise<boolean> => {
return database.transaction(async (transaction) => {
const mail = await requireMailByAuthToken({ mailAuthToken, database, transaction })
const entry = await database.user.findOne({
where: {
mail,
userId: parentUserId,
type: 'parent'
},
transaction
})
return !!entry
})
}
+2 -4
View File
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -16,7 +16,6 @@
*/ */
import { Conflict, Unauthorized } from 'http-errors' import { Conflict, Unauthorized } from 'http-errors'
import * as Sequelize from 'sequelize'
import { Database } from '../../database' import { Database } from '../../database'
import { generateVersionId } from '../../util/token' import { generateVersionId } from '../../util/token'
import { WebsocketApi } from '../../websocket' import { WebsocketApi } from '../../websocket'
@@ -65,8 +64,7 @@ export const linkMailAddress = async ({ mailAuthToken, deviceAuthToken, parentUs
familyId, familyId,
userId: parentUserId userId: parentUserId
}, },
transaction, transaction
lock: Sequelize.Transaction.LOCK.UPDATE
}) })
if (!parentEntry) { if (!parentEntry) {
+2 -4
View File
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -16,7 +16,6 @@
*/ */
import { Conflict } from 'http-errors' import { Conflict } from 'http-errors'
import * as Sequelize from 'sequelize'
import { Database, Transaction } from '../../database' import { Database, Transaction } from '../../database'
import { notifyClientsAboutChangesDelayed } from '../../function/websocket' import { notifyClientsAboutChangesDelayed } from '../../function/websocket'
import { WebsocketApi } from '../../websocket' import { WebsocketApi } from '../../websocket'
@@ -51,8 +50,7 @@ export const addPurchase = async ({ database, familyId, type, transactionId, web
where: { where: {
familyId familyId
}, },
transaction, transaction
lock: Sequelize.Transaction.LOCK.UPDATE
}) })
if (!familyEntry) { if (!familyEntry) {
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -15,10 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as Sequelize from 'sequelize'
import { AddUsedTimeAction } from '../../../../action' import { AddUsedTimeAction } from '../../../../action'
import { MinuteOfDay } from '../../../../util/minuteofday' import { MinuteOfDay } from '../../../../util/minuteofday'
import { Cache } from '../cache' import { Cache } from '../cache'
import { IllegalStateException } from '../exception/illegal-state'
import { MissingCategoryException } from '../exception/missing-item' import { MissingCategoryException } from '../exception/missing-item'
export const getRoundedTimestamp = () => { export const getRoundedTimestamp = () => {
@@ -65,17 +65,32 @@ export async function dispatchAddUsedTime ({ action, cache }: {
currentExtraTime: number currentExtraTime: number
}) => { }) => {
if (action.timeToAdd !== 0) { if (action.timeToAdd !== 0) {
const maxOperator = cache.database.dialect === 'sqlite' ? 'MAX' : 'GREATEST' const oldItem = await cache.database.usedTime.findOne({
const minOperator = cache.database.dialect === 'sqlite' ? 'MIN' : 'LEAST'
// try to update first
const [updatedRows] = await cache.database.usedTime.update({
usedTime: Sequelize.literal(`${maxOperator}(0, ${minOperator}(usedTime + ${action.timeToAdd}, ${dayLengthInMs}))`) as any,
lastUpdate: roundedTimestamp
}, {
where: { where: {
familyId: cache.familyId, familyId: cache.familyId,
categoryId: categoryId, categoryId: action.categoryId,
dayOfEpoch: action.dayOfEpoch,
startMinuteOfDay: MinuteOfDay.MIN,
endMinuteOfDay: MinuteOfDay.MAX
},
transaction: cache.transaction
})
if (oldItem) {
const oldUsedTime = oldItem.usedTime
const newUsedTime = Math.max(0, Math.min(oldUsedTime + action.timeToAdd, dayLengthInMs))
const oldLastUpdate = parseInt(oldItem.lastUpdate, 10)
const newLastUpdate = parseInt(roundedTimestamp, 10)
if (oldUsedTime !== newUsedTime || oldLastUpdate !== newLastUpdate) {
const [updatedRows] = await cache.database.usedTime.update({
usedTime: newUsedTime,
lastUpdate: newLastUpdate.toString(10)
}, {
where: {
familyId: cache.familyId,
categoryId: action.categoryId,
dayOfEpoch: action.dayOfEpoch, dayOfEpoch: action.dayOfEpoch,
startMinuteOfDay: MinuteOfDay.MIN, startMinuteOfDay: MinuteOfDay.MIN,
endMinuteOfDay: MinuteOfDay.MAX endMinuteOfDay: MinuteOfDay.MAX
@@ -83,13 +98,16 @@ export async function dispatchAddUsedTime ({ action, cache }: {
transaction: cache.transaction transaction: cache.transaction
}) })
// otherwise create
if (updatedRows === 0) { if (updatedRows === 0) {
throw new IllegalStateException({ staticMessage: 'could not update fetched row' })
}
}
} else {
await cache.database.usedTime.create({ await cache.database.usedTime.create({
familyId: cache.familyId, familyId: cache.familyId,
categoryId: categoryId, categoryId: action.categoryId,
dayOfEpoch: action.dayOfEpoch, dayOfEpoch: action.dayOfEpoch,
usedTime: Math.min(action.timeToAdd, dayLengthInMs), usedTime: Math.max(0, Math.min(action.timeToAdd, dayLengthInMs)),
lastUpdate: roundedTimestamp, lastUpdate: roundedTimestamp,
startMinuteOfDay: MinuteOfDay.MIN, startMinuteOfDay: MinuteOfDay.MIN,
endMinuteOfDay: MinuteOfDay.MAX endMinuteOfDay: MinuteOfDay.MAX
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -15,12 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as Sequelize from 'sequelize'
import { AddUsedTimeActionVersion2 } from '../../../../action' import { AddUsedTimeActionVersion2 } from '../../../../action'
import { EventHandler } from '../../../../monitoring/eventhandler' import { EventHandler } from '../../../../monitoring/eventhandler'
import { MinuteOfDay } from '../../../../util/minuteofday' import { MinuteOfDay } from '../../../../util/minuteofday'
import { Cache } from '../cache' import { Cache } from '../cache'
import { SourceDeviceNotFoundException } from '../exception/illegal-state' import { IllegalStateException, SourceDeviceNotFoundException } from '../exception/illegal-state'
import { getRoundedTimestamp as getRoundedTimestampForUsedTime } from './addusedtime' import { getRoundedTimestamp as getRoundedTimestampForUsedTime } from './addusedtime'
export const getRoundedTimestampForSessionDuration = () => { export const getRoundedTimestampForSessionDuration = () => {
@@ -92,13 +91,28 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache, ev
const lengthInMinutes = (end - start) + 1 const lengthInMinutes = (end - start) + 1
const lengthInMs = lengthInMinutes * 1000 * 60 const lengthInMs = lengthInMinutes * 1000 * 60
const maxOperator = cache.database.dialect === 'sqlite' ? 'MAX' : 'GREATEST' const oldItem = await cache.database.usedTime.findOne({
const minOperator = cache.database.dialect === 'sqlite' ? 'MIN' : 'LEAST' where: {
familyId: cache.familyId,
categoryId: item.categoryId,
dayOfEpoch: action.dayOfEpoch,
startMinuteOfDay: start,
endMinuteOfDay: end
},
transaction: cache.transaction
})
// try to update first if (oldItem) {
const oldUsedTime = oldItem.usedTime
const newUsedTime = Math.max(0, Math.min(oldUsedTime + item.timeToAdd, lengthInMs))
const oldLastUpdate = parseInt(oldItem.lastUpdate, 10)
const newLastUpdate = parseInt(roundedTimestampForUsedTime, 10)
if (oldUsedTime !== newUsedTime || oldLastUpdate !== newLastUpdate) {
const [updatedRows] = await cache.database.usedTime.update({ const [updatedRows] = await cache.database.usedTime.update({
usedTime: Sequelize.literal(`${maxOperator}(0, ${minOperator}(usedTime + ${item.timeToAdd}, ${lengthInMs}))`) as any, usedTime: newUsedTime,
lastUpdate: roundedTimestampForUsedTime lastUpdate: newLastUpdate.toString(10)
}, { }, {
where: { where: {
familyId: cache.familyId, familyId: cache.familyId,
@@ -110,13 +124,16 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache, ev
transaction: cache.transaction transaction: cache.transaction
}) })
// otherwise create
if (updatedRows === 0) { if (updatedRows === 0) {
throw new IllegalStateException({ staticMessage: 'could not update fetched row' })
}
}
} else {
await cache.database.usedTime.create({ await cache.database.usedTime.create({
familyId: cache.familyId, familyId: cache.familyId,
categoryId: item.categoryId, categoryId: item.categoryId,
dayOfEpoch: action.dayOfEpoch, dayOfEpoch: action.dayOfEpoch,
usedTime: Math.min(item.timeToAdd, lengthInMs), usedTime: Math.max(0, Math.min(item.timeToAdd, lengthInMs)),
lastUpdate: roundedTimestampForUsedTime, lastUpdate: roundedTimestampForUsedTime,
startMinuteOfDay: start, startMinuteOfDay: start,
endMinuteOfDay: end endMinuteOfDay: end
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -15,7 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as Sequelize from 'sequelize'
import { ChildChangePasswordAction } from '../../../../action' import { ChildChangePasswordAction } from '../../../../action'
import { Cache } from '../cache' import { Cache } from '../cache'
import { SourceUserNotFoundException } from '../exception/illegal-state' import { SourceUserNotFoundException } from '../exception/illegal-state'
@@ -31,8 +30,7 @@ export const dispatchChildChangePassword = async ({ action, childUserId, cache }
userId: childUserId, userId: childUserId,
type: 'child' type: 'child'
}, },
transaction: cache.transaction, transaction: cache.transaction
lock: Sequelize.Transaction.LOCK.UPDATE
}) })
if (!childEntry) { if (!childEntry) {
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -15,7 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as Sequelize from 'sequelize'
import { ChangeParentPasswordAction, InvalidChangeParentPasswordIntegrityException } from '../../../../action/changeparentpassword' import { ChangeParentPasswordAction, InvalidChangeParentPasswordIntegrityException } from '../../../../action/changeparentpassword'
import { Cache } from '../cache' import { Cache } from '../cache'
import { ApplyActionException } from '../exception/index' import { ApplyActionException } from '../exception/index'
@@ -31,8 +30,7 @@ export async function dispatchChangeParentPassword ({ action, cache }: {
userId: action.parentUserId, userId: action.parentUserId,
type: 'parent' type: 'parent'
}, },
transaction: cache.transaction, transaction: cache.transaction
lock: Sequelize.Transaction.LOCK.UPDATE
}) })
if (!parentEntry) { if (!parentEntry) {
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -106,8 +106,7 @@ export async function dispatchRemoveUser ({ action, cache, parentUserId }: {
familyId: cache.familyId, familyId: cache.familyId,
childId: action.userId childId: action.userId
}, },
transaction: cache.transaction, transaction: cache.transaction
lock: Sequelize.Transaction.LOCK.UPDATE
}) })
await cache.database.categoryApp.destroy({ await cache.database.categoryApp.destroy({
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -15,7 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as Sequelize from 'sequelize'
import { SetChildPasswordAction } from '../../../../action' import { SetChildPasswordAction } from '../../../../action'
import { Cache } from '../cache' import { Cache } from '../cache'
import { MissingUserException } from '../exception/missing-item' import { MissingUserException } from '../exception/missing-item'
@@ -30,8 +29,7 @@ export async function dispatchSetChildPassword ({ action, cache }: {
userId: action.childUserId, userId: action.childUserId,
type: 'child' type: 'child'
}, },
transaction: cache.transaction, transaction: cache.transaction
lock: Sequelize.Transaction.LOCK.UPDATE
}) })
if (!childEntry) { if (!childEntry) {
+6 -2
View File
@@ -18,7 +18,7 @@
import { BadRequest } from 'http-errors' import { BadRequest } from 'http-errors'
import { ClientPushChangesRequest } from '../../../api/schema' import { ClientPushChangesRequest } from '../../../api/schema'
import { VisibleConnectedDevicesManager } from '../../../connected-devices' import { VisibleConnectedDevicesManager } from '../../../connected-devices'
import { Database } from '../../../database' import { Database, shouldRetryWithException } from '../../../database'
import { EventHandler } from '../../../monitoring/eventhandler' import { EventHandler } from '../../../monitoring/eventhandler'
import { WebsocketApi } from '../../../websocket' import { WebsocketApi } from '../../../websocket'
import { notifyClientsAboutChangesDelayed } from '../../websocket' import { notifyClientsAboutChangesDelayed } from '../../websocket'
@@ -104,7 +104,11 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con
} }
}) })
} catch (ex) { } catch (ex) {
if (ex instanceof ApplyActionException) { if (shouldRetryWithException(ex, database)) {
eventHandler.countEvent('applyActionsFromDevice got exception which should cause retry')
throw ex
} else if (ex instanceof ApplyActionException) {
eventHandler.countEvent('applyActionsFromDevice errorDispatchingAction:' + ex.staticMessage) eventHandler.countEvent('applyActionsFromDevice errorDispatchingAction:' + ex.staticMessage)
} else { } else {
const stack = ex instanceof Error && ex.stack ? ex.stack.substring(0, 4096) : 'no stack' const stack = ex instanceof Error && ex.stack ? ex.stack.substring(0, 4096) : 'no stack'
+3 -2
View File
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -19,7 +19,7 @@ import { Server } from 'http'
import { createApi } from './api' import { createApi } from './api'
import { config } from './config' import { config } from './config'
import { VisibleConnectedDevicesManager } from './connected-devices' import { VisibleConnectedDevicesManager } from './connected-devices'
import { assertNestedTransactionsAreWorking, defaultDatabase, defaultUmzug } from './database' import { assertNestedTransactionsAreWorking, assertSerializeableTransactionsAreWorking, defaultDatabase, defaultUmzug } from './database'
import { EventHandler } from './monitoring/eventhandler' import { EventHandler } from './monitoring/eventhandler'
import { InMemoryEventHandler } from './monitoring/inmemoryeventhandler' import { InMemoryEventHandler } from './monitoring/inmemoryeventhandler'
import { createWebsocketHandler } from './websocket' import { createWebsocketHandler } from './websocket'
@@ -31,6 +31,7 @@ async function main () {
const eventHandler: EventHandler = new InMemoryEventHandler() const eventHandler: EventHandler = new InMemoryEventHandler()
await assertNestedTransactionsAreWorking(database) await assertNestedTransactionsAreWorking(database)
await assertSerializeableTransactionsAreWorking(database)
const connectedDevicesManager = new VisibleConnectedDevicesManager({ const connectedDevicesManager = new VisibleConnectedDevicesManager({
database database
+7 -5
View File
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -16,7 +16,7 @@
*/ */
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import * as io from 'socket.io' import { Server } from 'socket.io'
import { ConnectedDevicesManager, VisibleConnectedDevicesManager } from '../connected-devices' import { ConnectedDevicesManager, VisibleConnectedDevicesManager } from '../connected-devices'
import { Database } from '../database' import { Database } from '../database'
import { deviceByAuthTokenRoom } from './rooms' import { deviceByAuthTokenRoom } from './rooms'
@@ -25,7 +25,7 @@ export const createWebsocketHandler = ({ connectedDevicesManager, database }: {
connectedDevicesManager: VisibleConnectedDevicesManager connectedDevicesManager: VisibleConnectedDevicesManager
database: Database database: Database
}): { }): {
websocketServer: io.Server websocketServer: Server
websocketApi: WebsocketApi websocketApi: WebsocketApi
} => { } => {
const events = new EventEmitter() const events = new EventEmitter()
@@ -37,14 +37,16 @@ export const createWebsocketHandler = ({ connectedDevicesManager, database }: {
const eventTriggerImportantSyncForAll = 'triggerimportantsyncforall' const eventTriggerImportantSyncForAll = 'triggerimportantsyncforall'
let socketCounter = 0 let socketCounter = 0
const server = new io() const server = new Server({
allowEIO3: true
})
server.on('connection', (socket) => { server.on('connection', (socket) => {
socketCounter++ socketCounter++
socket.on('disconnect', () => socketCounter--) socket.on('disconnect', () => socketCounter--)
socket.on('devicelogin', (deviceAuthToken: any, ack: any) => { socket.on('devicelogin', (deviceAuthToken: any, ack: any) => {
socket.leaveAll() socket.rooms.forEach((room) => socket.leave(room))
if (typeof deviceAuthToken !== 'string') { if (typeof deviceAuthToken !== 'string') {
return return
+1 -2
View File
@@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2021 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -53,7 +53,6 @@ async function deleteDeprecatedPurchases ({ database, websocket }: {
}, },
attributes: ['familyId'], attributes: ['familyId'],
transaction, transaction,
lock: Sequelize.Transaction.LOCK.UPDATE,
limit: 100 limit: 100
})).map((item) => item.familyId) })).map((item) => item.familyId)