Compare commits

...

24 Commits
master ... 6.0

Author SHA1 Message Date
Tamas Soltesz fe894ef8a0
fix: version bump for backport release (#1187)
* fix: version bump for backport release

* Update CHANGELOG.md

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

---------

Co-authored-by: Sattvik Chakravarthy <sattvik@supertokens.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-09-03 11:51:53 +05:30
Sattvik Chakravarthy c0fb714688
fix: Create do-release.yml 2025-08-11 11:11:11 +05:30
Sattvik Chakravarthy 42d4ac0027
fix: add-dev-tag.yml 2025-08-09 12:16:38 +05:30
Sattvik Chakravarthy 68325eb117
fix: Create add-dev-tag.yml 2025-08-09 12:06:22 +05:30
Sattvik Chakravarthy 7bcd27cd25
fix: Create wait-for-docker.py 2025-08-09 11:56:22 +05:30
Sattvik Chakravarthy 2447f87ca9
fix: for backport release 6.0 (#1176)
* fix: remove circle ci, add github action tests

* fix: update supertokens-root branch

* fix: dev docker image

* fix: supertokens-root branch

* experiment: parallel running tests

* experiment: parallel running tests

* experiment: parallel running tests

* fix: dependencies

* fix: test command

* fix: test gen

* fix: remove unnecessary checkout

* fix: test command

* fix: glob

* fix: test names

* fix: test naming

* fix: failing test

* fix: devtag and stress test flows

---------

Co-authored-by: tamassoltesz <tamas@supertokens.com>
2025-08-09 11:53:52 +05:30
Mihály Lengyel f13f2ba4e4
Merge pull request #1169 from supertokens/backport/logs_to_otel_60
backport: logs to otel 60
2025-08-04 12:57:13 +02:00
tamassoltesz e774df6cf6 backport: logs to otel
fix: add implementationDependencies.json dependencies

chore: build version and changelog

fix: add missing config and devConfig entries

fix: remove accidentally merged lines
2025-07-25 15:06:54 +02:00
rishabhpoddar 5069bf35ed adding dev-v6.0.19 tag to this commit to ensure building 2024-03-29 18:27:39 +05:30
Sattvik Chakravarthy 08079bc5d5
fix: backport to core 6.0 (#972) 2024-03-29 17:54:03 +05:30
rishabhpoddar e1b37b0be3 adding dev-v6.0.18 tag to this commit to ensure building 2024-02-27 18:13:32 +05:30
Sattvik Chakravarthy 93df2080fc
fix: vulnerability fix (backport to 6.0) (#930)
* fix: vulnerability fix

* fix: backport

* fix: test
2024-02-27 18:09:21 +05:30
rishabhpoddar 1d18e368cb adding dev-v6.0.17 tag to this commit to ensure building 2024-02-10 14:29:15 +05:30
Sattvik Chakravarthy 48754e2b7c
fix: load only cud config in core (#920)
* fix: load only cud

* fix: connection pool handling

* fix: tests

* fix: version update

* fix: tests
2024-02-10 14:24:53 +05:30
rishabhpoddar 01cef02d41 adding dev-v6.0.16 tag to this commit to ensure building 2023-11-03 16:22:49 +05:30
Sattvik Chakravarthy b3585da402
fix: requests stats (#876) 2023-11-03 16:21:35 +05:30
rishabhpoddar 21ea25a722 adding dev-v6.0.15 tag to this commit to ensure building 2023-10-18 18:59:57 +05:30
Sattvik Chakravarthy 497fe8623e
fix: skip postgres tests (#857)
* fix: skip postgres tests

* fix: typo
2023-10-18 18:58:54 +05:30
rishabhpoddar a97b7e88d6 adding dev-v6.0.15 tag to this commit to ensure building 2023-10-18 13:13:18 +05:30
Sattvik Chakravarthy 3d8564bf5c
fix: test (#855) 2023-10-18 13:12:33 +05:30
rishabhpoddar b4b266f950 adding dev-v6.0.15 tag to this commit to ensure building 2023-10-18 12:50:25 +05:30
Sattvik Chakravarthy 50d860218d
fix: crontask per app issue (#854) 2023-10-18 12:48:54 +05:30
rishabhpoddar 6a6a1d4879 adding dev-v6.0.14 tag to this commit to ensure building 2023-10-12 11:34:45 +05:30
Sattvik Chakravarthy c270d27337
fix: duplicate cron task (#835) 2023-10-12 11:33:35 +05:30
98 changed files with 4358 additions and 1022 deletions

View File

@ -1,57 +0,0 @@
FROM ubuntu:16.04
RUN apt-get update && apt-get upgrade -y
RUN apt-get install build-essential -y
RUN echo "mysql-server mysql-server/root_password password root" | debconf-set-selections
RUN echo "mysql-server mysql-server/root_password_again password root" | debconf-set-selections
RUN apt install mysql-server -y
RUN usermod -d /var/lib/mysql/ mysql
RUN mkdir /var/run/mysqld
ADD ./runMySQL.sh /runMySQL.sh
RUN chmod +x /runMySQL.sh
RUN apt-get install -y git-core
RUN apt-get install -y wget
# Install OpenJDK 12
RUN wget https://download.java.net/java/GA/jdk12.0.2/e482c34c86bd4bf8b56c0b35558996b9/10/GPL/openjdk-12.0.2_linux-x64_bin.tar.gz
RUN mkdir /usr/java
RUN mv openjdk-12.0.2_linux-x64_bin.tar.gz /usr/java
RUN cd /usr/java && tar -xzvf openjdk-12.0.2_linux-x64_bin.tar.gz
RUN echo 'JAVA_HOME=/usr/java/jdk-12.0.2' >> /etc/profile
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
RUN apt-get install jq -y
RUN apt-get install curl -y
RUN apt-get install unzip -y
# Install OpenJDK 15.0.1
RUN wget https://download.java.net/java/GA/jdk15.0.1/51f4f36ad4ef43e39d0dfdbaf6549e32/9/GPL/openjdk-15.0.1_linux-x64_bin.tar.gz
RUN mv openjdk-15.0.1_linux-x64_bin.tar.gz /usr/java
RUN cd /usr/java && tar -xzvf openjdk-15.0.1_linux-x64_bin.tar.gz
RUN echo 'JAVA_HOME=/usr/java/jdk-15.0.1' >> /etc/profile
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
RUN echo 'export JAVA_HOME' >> /etc/profile
RUN echo 'export JRE_HOME' >> /etc/profile
RUN echo 'export PATH' >> /etc/profile
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-12.0.2/bin/java" 1
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-12.0.2/bin/javac" 1

View File

@ -1,57 +0,0 @@
FROM ubuntu:18.04
RUN apt-get update && apt-get upgrade -y
RUN apt-get install build-essential -y
RUN echo "mysql-server mysql-server/root_password password root" | debconf-set-selections
RUN echo "mysql-server mysql-server/root_password_again password root" | debconf-set-selections
RUN apt install mysql-server -y
RUN usermod -d /var/lib/mysql/ mysql
RUN mkdir /var/run/mysqld
ADD ./runMySQL.sh /runMySQL.sh
RUN chmod +x /runMySQL.sh
RUN apt-get install -y git-core
RUN apt-get install -y wget
# Install OpenJDK 12
RUN wget https://download.java.net/java/GA/jdk12.0.2/e482c34c86bd4bf8b56c0b35558996b9/10/GPL/openjdk-12.0.2_linux-x64_bin.tar.gz
RUN mkdir /usr/java
RUN mv openjdk-12.0.2_linux-x64_bin.tar.gz /usr/java
RUN cd /usr/java && tar -xzvf openjdk-12.0.2_linux-x64_bin.tar.gz
RUN echo 'JAVA_HOME=/usr/java/jdk-12.0.2' >> /etc/profile
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
RUN apt-get install jq -y
RUN apt-get install curl -y
RUN apt-get install unzip -y
# Install OpenJDK 15.0.1
RUN wget https://download.java.net/java/GA/jdk15.0.1/51f4f36ad4ef43e39d0dfdbaf6549e32/9/GPL/openjdk-15.0.1_linux-x64_bin.tar.gz
RUN mv openjdk-15.0.1_linux-x64_bin.tar.gz /usr/java
RUN cd /usr/java && tar -xzvf openjdk-15.0.1_linux-x64_bin.tar.gz
RUN echo 'JAVA_HOME=/usr/java/jdk-15.0.1' >> /etc/profile
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
RUN echo 'export JAVA_HOME' >> /etc/profile
RUN echo 'export JRE_HOME' >> /etc/profile
RUN echo 'export PATH' >> /etc/profile
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-12.0.2/bin/java" 1
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-12.0.2/bin/javac" 1

View File

@ -1,30 +0,0 @@
version: 2.1
orbs:
slack: circleci/slack@3.4.2
jobs:
test:
docker:
- image: rishabhpoddar/supertokens_core_testing
- image: mongo
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: root
resource_class: large
steps:
- checkout
- run: echo $'\n[mysqld]\ncharacter_set_server=utf8mb4\nmax_connections=10000' >> /etc/mysql/mysql.cnf
- run: (cd .circleci/ && ./doTests.sh)
- slack/status
workflows:
version: 2
tagged-build:
jobs:
- test:
context:
- slack-notification
filters:
tags:
only: /dev-v[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/

View File

@ -1,212 +0,0 @@
function cleanup {
if test -f "pluginInterfaceExactVersionsOutput"; then
rm pluginInterfaceExactVersionsOutput
fi
}
trap cleanup EXIT
cleanup
pinnedDBJson=$(curl -s -X GET \
'https://api.supertokens.io/0/plugin/pinned?planType=FREE' \
-H 'api-version: 0')
pinnedDBLength=$(echo "$pinnedDBJson" | jq ".plugins | length")
pinnedDBArray=$(echo "$pinnedDBJson" | jq ".plugins")
echo "got pinned dbs..."
pluginInterfaceJson=$(cat ../pluginInterfaceSupported.json)
pluginInterfaceLength=$(echo "$pluginInterfaceJson" | jq ".versions | length")
pluginInterfaceArray=$(echo "$pluginInterfaceJson" | jq ".versions")
echo "got plugin interface relations"
coreDriverJson=$(cat ../coreDriverInterfaceSupported.json)
coreDriverArray=$(echo "$coreDriverJson" | jq ".versions")
echo "got core driver relations"
./getPluginInterfaceExactVersions.sh "$pluginInterfaceLength" "$pluginInterfaceArray"
if [[ $? -ne 0 ]]
then
echo "all plugin interfaces found... failed. exiting!"
exit 1
else
echo "all plugin interfaces found..."
fi
# get core version
coreVersion=$(cat ../build.gradle | grep -e "version =" -e "version=")
while IFS='"' read -ra ADDR; do
counter=0
for i in "${ADDR[@]}"; do
if [ $counter == 1 ]
then
coreVersion=$i
fi
counter=$(($counter+1))
done
done <<< "$coreVersion"
responseStatus=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
https://api.supertokens.io/0/core \
-H 'Content-Type: application/json' \
-H 'api-version: 0' \
-d "{
\"password\": \"$SUPERTOKENS_API_KEY\",
\"planType\":\"FREE\",
\"version\":\"$coreVersion\",
\"pluginInterfaces\": $pluginInterfaceArray,
\"coreDriverInterfaces\": $coreDriverArray
}")
if [ "$responseStatus" -ne "200" ]
then
echo "failed core PUT API status code: $responseStatus. Exiting!"
exit 1
fi
someTestsRan=false
while read -u 10 line
do
if [[ $line = "" ]]; then
continue
fi
i=0
currTag=$(echo "$line" | jq .tag)
currTag=$(echo "$currTag" | tr -d '"')
currVersion=$(echo "$line" | jq .version)
currVersion=$(echo "$currVersion" | tr -d '"')
piX=$(cut -d'.' -f1 <<<"$currVersion")
piY=$(cut -d'.' -f2 <<<"$currVersion")
piVersion="$piX.$piY"
while [ $i -lt "$pinnedDBLength" ]; do
someTestsRan=true
currPinnedDb=$(echo "$pinnedDBArray" | jq ".[$i]")
currPinnedDb=$(echo "$currPinnedDb" | tr -d '"')
i=$((i+1))
if [[ $currPinnedDb == "sqlite" ]]
then
# shellcheck disable=SC2034
continue=1
else
response=$(curl -s -X GET \
"https://api.supertokens.io/0/plugin-interface/dependency/plugin/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$piVersion&pluginName=$currPinnedDb" \
-H 'api-version: 0')
if [[ $(echo "$response" | jq .plugin) == "null" ]]
then
echo "fetching latest X.Y version for $currPinnedDb given plugin-interface X.Y version: $piVersion gave response: $response"
exit 1
fi
pinnedDbVersionX2=$(echo $response | jq .plugin | tr -d '"')
response=$(curl -s -X GET \
"https://api.supertokens.io/0/plugin/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$pinnedDbVersionX2&name=$currPinnedDb" \
-H 'api-version: 0')
if [[ $(echo "$response" | jq .tag) == "null" ]]
then
echo "fetching latest X.Y.Z version for $currPinnedDb, X.Y version: $pinnedDbVersionX2 gave response: $response"
exit 1
fi
pinnedDbVersionTag=$(echo "$response" | jq .tag | tr -d '"')
pinnedDbVersion=$(echo "$response" | jq .version | tr -d '"')
./startDb.sh "$currPinnedDb"
fi
cd ../../
git clone git@github.com:supertokens/supertokens-root.git
cd supertokens-root
update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 2
update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 2
coreX=$(cut -d'.' -f1 <<<"$coreVersion")
coreY=$(cut -d'.' -f2 <<<"$coreVersion")
if [[ $currPinnedDb == "sqlite" ]]
then
echo -e "core,$coreX.$coreY\nplugin-interface,$piVersion" > modules.txt
else
echo -e "core,$coreX.$coreY\nplugin-interface,$piVersion\n$currPinnedDb-plugin,$pinnedDbVersionX2" > modules.txt
fi
./loadModules
cd supertokens-core
git checkout dev-v$coreVersion
cd ../supertokens-plugin-interface
git checkout $currTag
if [[ $currPinnedDb == "sqlite" ]]
then
# shellcheck disable=SC2034
continue=1
else
cd ../supertokens-$currPinnedDb-plugin
git checkout $pinnedDbVersionTag
fi
cd ../
echo $SUPERTOKENS_API_KEY > apiPassword
./startTestingEnv --cicd
if [[ $? -ne 0 ]]
then
cat logs/*
cd ../project/
echo "test failed... exiting!"
exit 1
fi
cd ../
rm -rf supertokens-root
if [[ $currPinnedDb == "sqlite" ]]
then
# shellcheck disable=SC2034
continue=1
else
curl -o supertokens.zip -s -X GET \
"https://api.supertokens.io/0/app/download?pluginName=$currPinnedDb&os=linux&mode=DEV&binary=FREE&targetCore=$coreVersion&targetPlugin=$pinnedDbVersion" \
-H 'api-version: 0'
unzip supertokens.zip -d .
rm supertokens.zip
cd supertokens
../project/.circleci/testCli.sh
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
cd ../
fi
rm -rf supertokens
cd project/.circleci
if [[ $currPinnedDb == "sqlite" ]]
then
# shellcheck disable=SC2034
continue=1
else
./stopDb.sh $currPinnedDb
fi
done
done 10<pluginInterfaceExactVersionsOutput
if [[ $someTestsRan = "true" ]]
then
echo "calling /core PATCH to make testing passed"
responseStatus=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH \
https://api.supertokens.io/0/core \
-H 'Content-Type: application/json' \
-H 'api-version: 0' \
-d "{
\"password\": \"$SUPERTOKENS_API_KEY\",
\"planType\":\"FREE\",
\"version\":\"$coreVersion\",
\"testPassed\": true
}")
if [ "$responseStatus" -ne "200" ]
then
echo "patch api failed"
exit 1
fi
else
echo "no test ran"
exit 1
fi

View File

@ -1,19 +0,0 @@
# args: <length of array> <array like ["0.0", "0.1"]>
touch pluginInterfaceExactVersionsOutput
i=0
while [ $i -lt $1 ]; do
currVersion=`echo $2 | jq ".[$i]"`
currVersion=`echo $currVersion | tr -d '"'`
i=$((i+1))
# now we have the current version like 0.0.
# We now have to find something that matches dev-v0.0.* or v0.0.*
response=`curl -s -X GET \
"https://api.supertokens.io/0/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$currVersion" \
-H 'api-version: 0'`
if [[ `echo $response | jq .tag` == "null" ]]
then
echo $response
exit 1
fi
echo $response >> pluginInterfaceExactVersionsOutput
done

View File

@ -1 +0,0 @@
chown -R mysql:mysql /var/lib/mysql /var/run/mysqld && service mysql start

View File

@ -1,57 +0,0 @@
case $1 in
mysql)
(cd / && ./runMySQL.sh)
mysql -u root --password=root -e "CREATE DATABASE supertokens;"
mysql -u root --password=root -e "CREATE DATABASE st0;"
mysql -u root --password=root -e "CREATE DATABASE st1;"
mysql -u root --password=root -e "CREATE DATABASE st2;"
mysql -u root --password=root -e "CREATE DATABASE st3;"
mysql -u root --password=root -e "CREATE DATABASE st4;"
mysql -u root --password=root -e "CREATE DATABASE st5;"
mysql -u root --password=root -e "CREATE DATABASE st6;"
mysql -u root --password=root -e "CREATE DATABASE st7;"
mysql -u root --password=root -e "CREATE DATABASE st8;"
mysql -u root --password=root -e "CREATE DATABASE st9;"
mysql -u root --password=root -e "CREATE DATABASE st10;"
mysql -u root --password=root -e "CREATE DATABASE st11;"
mysql -u root --password=root -e "CREATE DATABASE st12;"
mysql -u root --password=root -e "CREATE DATABASE st13;"
mysql -u root --password=root -e "CREATE DATABASE st14;"
mysql -u root --password=root -e "CREATE DATABASE st15;"
mysql -u root --password=root -e "CREATE DATABASE st16;"
mysql -u root --password=root -e "CREATE DATABASE st17;"
mysql -u root --password=root -e "CREATE DATABASE st18;"
mysql -u root --password=root -e "CREATE DATABASE st19;"
mysql -u root --password=root -e "CREATE DATABASE st20;"
mysql -u root --password=root -e "CREATE DATABASE st21;"
mysql -u root --password=root -e "CREATE DATABASE st22;"
mysql -u root --password=root -e "CREATE DATABASE st23;"
mysql -u root --password=root -e "CREATE DATABASE st24;"
mysql -u root --password=root -e "CREATE DATABASE st25;"
mysql -u root --password=root -e "CREATE DATABASE st26;"
mysql -u root --password=root -e "CREATE DATABASE st27;"
mysql -u root --password=root -e "CREATE DATABASE st28;"
mysql -u root --password=root -e "CREATE DATABASE st29;"
mysql -u root --password=root -e "CREATE DATABASE st30;"
mysql -u root --password=root -e "CREATE DATABASE st31;"
mysql -u root --password=root -e "CREATE DATABASE st32;"
mysql -u root --password=root -e "CREATE DATABASE st33;"
mysql -u root --password=root -e "CREATE DATABASE st34;"
mysql -u root --password=root -e "CREATE DATABASE st35;"
mysql -u root --password=root -e "CREATE DATABASE st36;"
mysql -u root --password=root -e "CREATE DATABASE st37;"
mysql -u root --password=root -e "CREATE DATABASE st38;"
mysql -u root --password=root -e "CREATE DATABASE st39;"
mysql -u root --password=root -e "CREATE DATABASE st40;"
mysql -u root --password=root -e "CREATE DATABASE st41;"
mysql -u root --password=root -e "CREATE DATABASE st42;"
mysql -u root --password=root -e "CREATE DATABASE st43;"
mysql -u root --password=root -e "CREATE DATABASE st44;"
mysql -u root --password=root -e "CREATE DATABASE st45;"
mysql -u root --password=root -e "CREATE DATABASE st46;"
mysql -u root --password=root -e "CREATE DATABASE st47;"
mysql -u root --password=root -e "CREATE DATABASE st48;"
mysql -u root --password=root -e "CREATE DATABASE st49;"
mysql -u root --password=root -e "CREATE DATABASE st50;"
;;
esac

View File

@ -1,5 +0,0 @@
case $1 in
mysql)
service mysql stop
;;
esac

View File

@ -1,72 +0,0 @@
# inside supertokens downloaded zip
./install
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
supertokens start --port=8888
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
supertokens list
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
sed -i 's/# mysql_user:/mysql_user: root/g' /usr/lib/supertokens/config.yaml
sed -i 's/# mysql_password:/mysql_password: root/g' /usr/lib/supertokens/config.yaml
sed -i 's/# mongodb_connection_uri:/mongodb_connection_uri: mongodb:\/\/root:root@localhost:27017/g' /usr/lib/supertokens/config.yaml
sed -i 's/# disable_telemetry:/disable_telemetry: true/g' /usr/lib/supertokens/config.yaml
supertokens start --port=8889
supertokens list
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
curl http://localhost:8889/hello
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
curl http://localhost:8888/hello
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
supertokens stop
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
supertokens uninstall
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi

View File

@ -0,0 +1,56 @@
import json
import os
import http.client
def register_core_version(supertokens_api_key, core_version, plugin_interface_array, core_driver_array):
print("Core Version: ", core_version)
print("Plugin Interface Array: ", plugin_interface_array)
print("Core Driver Array: ", core_driver_array)
conn = http.client.HTTPSConnection("api.supertokens.io")
payload = {
"password": supertokens_api_key,
"planType": "FREE",
"version": core_version,
"pluginInterfaces": plugin_interface_array,
"coreDriverInterfaces": core_driver_array
}
headers = {
'Content-Type': 'application/json',
'api-version': '0'
}
conn.request("PUT", "/0/core", json.dumps(payload), headers)
response = conn.getresponse()
if response.status != 200:
print(f"failed core PUT API status code: {response.status}. Exiting!")
exit(1)
conn.close()
def read_core_version():
with open('build.gradle', 'r') as file:
for line in file:
if 'version =' in line:
return line.split('=')[1].strip().strip("'\"")
raise Exception("Could not find version in build.gradle")
core_version = read_core_version()
with open('pluginInterfaceSupported.json', 'r') as fd:
plugin_interface_array = json.load(fd)['versions']
with open('coreDriverInterfaceSupported.json', 'r') as fd:
core_driver_array = json.load(fd)['versions']
register_core_version(
supertokens_api_key=os.environ.get("SUPERTOKENS_API_KEY"),
core_version=core_version,
plugin_interface_array=plugin_interface_array,
core_driver_array=core_driver_array
)

View File

@ -0,0 +1,68 @@
import json
import os
import subprocess
import http.client
def register_plugin_version(supertokens_api_key, plugin_version, plugin_interface_array, plugin_name):
print("Plugin Version: ", plugin_version)
print("Plugin Interface Array: ", plugin_interface_array)
print("Plugin Name: ", plugin_name)
conn = http.client.HTTPSConnection("api.supertokens.io")
payload = {
"password": supertokens_api_key,
"planType": "FREE",
"version": plugin_version,
"pluginInterfaces": plugin_interface_array,
"name": plugin_name
}
headers = {
'Content-Type': 'application/json',
'api-version': '0'
}
conn.request("PUT", "/0/plugin", json.dumps(payload), headers)
response = conn.getresponse()
if response.status != 200:
print(f"failed plugin PUT API status code: {response.status}. Exiting!")
print(f"response: {str(response.read())}")
exit(1)
conn.close()
def read_plugin_version():
with open('build.gradle', 'r') as file:
for line in file:
if 'version =' in line:
return line.split('=')[1].strip().strip("'\"")
raise Exception("Could not find version in build.gradle")
plugin_version = read_plugin_version()
with open('pluginInterfaceSupported.json', 'r') as fd:
plugin_interface_array = json.load(fd)['versions']
def check_if_tag_exists(tag):
try:
result = subprocess.run(['git', 'tag', '-l', tag], capture_output=True, text=True)
return tag in result.stdout
except subprocess.CalledProcessError:
print(f"Error checking for tag {tag}")
return False
dev_tag = f"dev-v{plugin_version}"
if not check_if_tag_exists(dev_tag):
print(f"Tag {dev_tag} does not exist. Exiting!")
exit(0)
register_plugin_version(
supertokens_api_key=os.environ.get("SUPERTOKENS_API_KEY"),
plugin_version=plugin_version,
plugin_interface_array=plugin_interface_array,
plugin_name=os.environ.get("PLUGIN_NAME")
)

39
.github/helpers/release-docker.sh vendored Normal file
View File

@ -0,0 +1,39 @@
#!/bin/bash
set -e
# Check for required arguments
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <source-image:tag> <target-image:tag>"
exit 1
fi
SOURCE_IMAGE="$1"
TARGET_IMAGE="$2"
# Platforms to support
PLATFORMS=("linux/amd64" "linux/arm64")
TEMP_IMAGES=()
# Pull, retag, and push platform-specific images
for PLATFORM in "${PLATFORMS[@]}"; do
ARCH=$(echo $PLATFORM | cut -d'/' -f2)
TEMP_TAG="${TARGET_IMAGE}-${ARCH}"
TEMP_IMAGES+=("$TEMP_TAG")
echo "Pulling $SOURCE_IMAGE for $PLATFORM..."
docker pull --platform $PLATFORM "$SOURCE_IMAGE"
echo "Tagging as $TEMP_TAG..."
docker tag "$SOURCE_IMAGE" "$TEMP_TAG"
echo "Pushing $TEMP_TAG..."
docker push "$TEMP_TAG"
done
# Create and push manifest for multi-arch image
echo "Creating and pushing multi-arch manifest for $TARGET_IMAGE..."
docker manifest create "$TARGET_IMAGE" "${TEMP_IMAGES[@]}"
docker manifest push "$TARGET_IMAGE"
echo "✅ Multi-arch image pushed as $TARGET_IMAGE"

55
.github/helpers/wait-for-docker.py vendored Normal file
View File

@ -0,0 +1,55 @@
import http.client
import json
import time
import os
import sys
REPO = "supertokens/supertokens-core"
SHA = os.environ.get("GITHUB_SHA")
NAME = os.environ.get("WORKFLOW_NAME", "Publish Dev Docker Image")
st = time.time()
def get_latest_actions():
conn = http.client.HTTPSConnection("api.github.com")
url = f"/repos/{REPO}/actions/runs"
headers = {"User-Agent": "Python-http.client"}
conn.request("GET", url, headers=headers)
response = conn.getresponse()
if response.status == 200:
data = response.read()
runs = json.loads(data)['workflow_runs']
found = False
for run in runs:
if run['head_sha'] == SHA and run['name'] == NAME:
found = True
break
if not found:
print("No matching workflow run found.")
sys.exit(1)
if run["status"] == "completed":
if run["conclusion"] == "success":
print("Workflow completed successfully.")
return True
else:
print(f"Workflow failed with conclusion: {run['conclusion']}")
sys.exit(1)
else:
print(f"Failed to fetch workflow runs: {response.status} {response.reason}")
sys.exit(1)
return False
time.sleep(30) # Wait for 30 seconds before checking
while not get_latest_actions():
print("Waiting for the latest actions to complete...")
time.sleep(10)
if time.time() - st > 600:
print("Timed out waiting for the latest actions.")
sys.exit(1)

107
.github/workflows/add-dev-tag.yml vendored Normal file
View File

@ -0,0 +1,107 @@
name: Add dev tags for release
on:
workflow_dispatch:
inputs:
core-version:
description: 'Core version'
required: true
type: string
plugin-interface-version:
description: 'Plugin interface version'
required: true
type: string
new-release-for-plugin-interface:
description: 'New release for plugin interface'
required: true
type: boolean
postgresql-plugin-version:
description: 'Postgres plugin version'
required: true
new-release-for-postgresql-plugin:
description: 'New release for postgres plugin'
required: true
type: boolean
jobs:
dependency-branches:
name: Dependency Branches
environment: publish
runs-on: ubuntu-latest
outputs:
branches: ${{ steps.result.outputs.branches }}
steps:
- uses: actions/checkout@v4
- uses: supertokens/get-core-dependencies-action@main
id: result
with:
run-for: add-dev-tag
core-version: ${{ github.event.inputs.core-version }}
plugin-interface-version: ${{ github.event.inputs.plugin-interface-version }}
postgresql-plugin-version: ${{ github.event.inputs.postgresql-plugin-version }}
add-dev-tag:
environment: publish
runs-on: ubuntu-latest
needs: dependency-branches
steps:
- name: Set up JDK 15.0.1
uses: actions/setup-java@v2
with:
java-version: 15.0.1
distribution: zulu
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-root
path: ./supertokens-root
ref: for_jdk_15_releases
- name: Checkout supertokens-core
run: |
cd supertokens-root
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-core.git
cd supertokens-core
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['core'] }}
- name: Checkout supertokens-plugin-interface
run: |
cd supertokens-root
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-plugin-interface.git
cd supertokens-plugin-interface
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['plugin-interface'] }}
- name: Checkout supertokens-postgresql-plugin
run: |
cd supertokens-root
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-postgresql-plugin.git
cd supertokens-postgresql-plugin
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['postgresql'] }}
- name: Load Modules
run: |
cd supertokens-root
echo "core,master
plugin-interface,master
postgresql-plugin,master
" > modules.txt
cat modules.txt
./loadModules
- name: Setup test env
run: cd supertokens-root && ./utils/setupTestEnv --local
- name: Git config
run: |
git config --global user.name "Supertokens Bot"
git config --global user.email "<>"
- name: Add dev tag to plugin interface
if: ${{ github.event.inputs.new-release-for-plugin-interface == 'true' }}
run: |
echo "Adding dev tag to plugin interface"
cd supertokens-root/supertokens-plugin-interface
./addDevTag
- name: Add dev tag to postgres plugin
if: ${{ github.event.inputs.new-release-for-postgresql-plugin == 'true' }}
run: |
echo "Adding dev tag to postgres plugin"
cd supertokens-root/supertokens-postgresql-plugin
./addDevTag
- name: Add dev tag to core
run: |
echo "Adding dev tag to core"
cd supertokens-root/supertokens-core
./addDevTag

153
.github/workflows/dev-tag.yml vendored Normal file
View File

@ -0,0 +1,153 @@
name: Checks for release
on:
push:
branches:
- '[0-9]+.[0-9]+'
tags:
- 'dev-*'
jobs:
dependency-versions:
name: Dependency Versions
runs-on: ubuntu-latest
outputs:
versions: ${{ steps.result.outputs.versions }}
branches: ${{ steps.result.outputs.branches }}
steps:
- uses: actions/checkout@v4
- uses: supertokens/get-core-dependencies-action@main
with:
run-for: PR
id: result
new-core-version:
environment: publish
name: New core version
runs-on: ubuntu-latest
needs: [dependency-versions]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Run script
env:
SUPERTOKENS_API_KEY: ${{ secrets.SUPERTOKENS_API_KEY }}
run: |
python .github/helpers/register-new-core-version.py
new-plugin-versions:
environment: publish
name: New plugin versions
runs-on: ubuntu-latest
needs: [dependency-versions]
strategy:
fail-fast: false
matrix:
plugin:
- postgresql
# no longer supported
# - mysql
# - mongodb
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v4
with:
path: ./supertokens-plugin
repository: supertokens/supertokens-${{ matrix.plugin }}-plugin
ref: ${{ fromJson(needs.dependency-versions.outputs.branches)[matrix.plugin] }}
fetch-depth: 0
fetch-tags: true
- name: Run script
env:
SUPERTOKENS_API_KEY: ${{ secrets.SUPERTOKENS_API_KEY }}
PLUGIN_NAME: ${{ matrix.plugin }}
run: |
cd supertokens-plugin
python ../.github/helpers/register-new-plugin-version.py
unit-tests:
name: Run unit tests
needs: [new-core-version, new-plugin-versions]
uses: ./.github/workflows/unit-test.yml
wait-for-docker:
name: Wait for Docker
runs-on: ubuntu-latest
needs: [new-core-version, new-plugin-versions]
outputs:
tag: ${{ steps.set_tag.outputs.TAG }}
steps:
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Checkout
uses: actions/checkout@v4
- name: Wait for Docker build
env:
SHA: ${{ github.sha }}
run: |
python .github/helpers/wait-for-docker.py
- name: set tag
id: set_tag
run: |
echo "TAG=${GITHUB_REF}" | sed 's/refs\/heads\///g' | sed 's/\//_/g' >> $GITHUB_OUTPUT
stress-tests:
needs: [wait-for-docker]
uses: ./.github/workflows/stress-tests.yml
with:
tag: ${{ needs.wait-for-docker.outputs.tag }}
mark-as-passed:
environment: publish
needs: [dependency-versions, unit-tests, stress-tests]
name: Mark as passed
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
plugin:
- sqlite
- postgresql
# no longer supported
# - mysql
# - mongodb
steps:
- name: Mark plugin as passed
if: matrix.plugin != 'sqlite' && fromJson(needs.dependency-versions.outputs.versions)[matrix.plugin] != ''
uses: muhfaris/request-action@main
with:
url: https://api.supertokens.io/0/plugin
method: PATCH
headers: |
{
"Content-Type": "application/json",
"api-version": "0"
}
body: |
{
"password": "${{ secrets.SUPERTOKENS_API_KEY }}",
"version": "${{ fromJson(needs.dependency-versions.outputs.versions)[matrix.plugin] }}",
"planType": "FREE",
"name": "${{ matrix.plugin }}",
"testPassed": true
}
- name: Mark core as passed
if: matrix.plugin == 'sqlite' && fromJson(needs.dependency-versions.outputs.versions)['core'] != ''
uses: muhfaris/request-action@main
with:
url: https://api.supertokens.io/0/core
method: PATCH
headers: |
{
"Content-Type": "application/json",
"api-version": "0"
}
body: |
{
"password": "${{ secrets.SUPERTOKENS_API_KEY }}",
"version": "${{ fromJson(needs.dependency-versions.outputs.versions)['core'] }}",
"planType": "FREE",
"testPassed": true
}

148
.github/workflows/do-release.yml vendored Normal file
View File

@ -0,0 +1,148 @@
name: Do Release
on:
workflow_dispatch:
inputs:
core-version:
description: 'Core version'
required: true
type: string
plugin-interface-version:
description: 'Plugin interface version'
required: true
type: string
new-release-for-plugin-interface:
description: 'New release for plugin interface'
required: true
type: boolean
postgresql-plugin-version:
description: 'Postgres plugin version'
required: true
new-release-for-postgresql-plugin:
description: 'New release for postgres plugin'
required: true
type: boolean
is-latest-release:
description: 'Is this the latest release?'
required: true
type: boolean
jobs:
dependency-branches:
name: Dependency Branches
environment: publish
runs-on: ubuntu-latest
outputs:
branches: ${{ steps.result.outputs.branches }}
versions: ${{ steps.result.outputs.versions }}
steps:
- uses: actions/checkout@v4
- uses: supertokens/get-core-dependencies-action@main
id: result
with:
run-for: add-dev-tag
core-version: ${{ github.event.inputs.core-version }}
plugin-interface-version: ${{ github.event.inputs.plugin-interface-version }}
postgresql-plugin-version: ${{ github.event.inputs.postgresql-plugin-version }}
release-docker:
environment: publish
name: Release Docker
runs-on: ubuntu-latest
needs: dependency-branches
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up JDK 15.0.1
uses: actions/setup-java@v2
with:
java-version: 15.0.1
distribution: zulu
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Tag and Push Docker Image
run: |
tag=${{ github.event.inputs.core-version }}
major=$(echo $tag | cut -d. -f1)
minor=$(echo $tag | cut -d. -f1,2)
bash .github/helpers/release-docker.sh supertokens/supertokens-dev-postgresql:$minor supertokens/supertokens-postgresql:$major
bash .github/helpers/release-docker.sh supertokens/supertokens-dev-postgresql:$minor supertokens/supertokens-postgresql:$minor
bash .github/helpers/release-docker.sh supertokens/supertokens-dev-postgresql:$minor supertokens/supertokens-postgresql:$tag
if [ "${{ github.event.inputs.is-latest-release }}" == "true" ]; then
bash .github/helpers/release-docker.sh supertokens/supertokens-dev-postgresql:$minor supertokens/supertokens-postgresql:latest
fi
add-release-tag:
environment: publish
runs-on: ubuntu-latest
needs: [dependency-branches, release-docker]
steps:
- name: Set up JDK 15.0.1
uses: actions/setup-java@v2
with:
java-version: 15.0.1
distribution: zulu
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-root
path: ./supertokens-root
ref: for_jdk_15_releases
- name: Checkout supertokens-core
run: |
cd supertokens-root
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-core.git
cd supertokens-core
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['core'] }}
- name: Checkout supertokens-plugin-interface
run: |
cd supertokens-root
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-plugin-interface.git
cd supertokens-plugin-interface
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['plugin-interface'] }}
- name: Checkout supertokens-postgresql-plugin
run: |
cd supertokens-root
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-postgresql-plugin.git
cd supertokens-postgresql-plugin
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['postgresql'] }}
- name: Add release password
run: |
cd supertokens-root
echo "${{ secrets.SUPERTOKENS_API_KEY }}" > releasePassword
echo "${{ secrets.SUPERTOKENS_API_KEY }}" > apiPassword
- name: Load Modules
run: |
cd supertokens-root
echo "core,master
plugin-interface,master
postgresql-plugin,master
" > modules.txt
cat modules.txt
./loadModules
- name: Setup test env
run: cd supertokens-root && ./utils/setupTestEnv --local
- name: Git config
run: |
git config --global user.name "Supertokens Bot"
git config --global user.email "<>"
- name: Add release tag to plugin interface
if: ${{ github.event.inputs.new-release-for-plugin-interface == 'true' }}
run: |
echo "Adding release tag to plugin interface"
cd supertokens-root/supertokens-plugin-interface
./addReleaseTag
- name: Add release tag to postgres plugin
if: ${{ github.event.inputs.new-release-for-postgresql-plugin == 'true' }}
run: |
echo "Adding release tag to postgres plugin"
cd supertokens-root/supertokens-postgresql-plugin
./addReleaseTag
- name: Add release tag to core
run: |
echo "Adding release tag to core"
cd supertokens-root/supertokens-core
./addReleaseTag

View File

@ -1,15 +0,0 @@
name: "Enforcing changelog in PRs Workflow"
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
jobs:
# Enforces the update of a changelog file on every pull request
changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dangoslen/changelog-enforcer@v2
with:
changeLogPath: 'CHANGELOG.md'
skipLabels: 'Skip-Changelog'

View File

@ -1,20 +0,0 @@
name: "Lint PR Title"
on:
pull_request:
types:
- opened
- reopened
- edited
- synchronize
jobs:
pr-title:
name: Lint PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
validateSingleCommit: true

27
.github/workflows/pr-checks.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: PR Checks
on:
pull_request:
types: [ opened, synchronize, reopened, ready_for_review, labeled, unlabeled ]
jobs:
pr-title:
name: Lint PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
validateSingleCommit: true
changelog:
name: Enforce Changelog
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dangoslen/changelog-enforcer@v2
with:
changeLogPath: 'CHANGELOG.md'
skipLabels: 'Skip-Changelog'
unit-tests:
name: Run unit tests
uses: ./.github/workflows/unit-test.yml

102
.github/workflows/publish-dev-docker.yml vendored Normal file
View File

@ -0,0 +1,102 @@
name: Publish Dev Docker Image
on:
push:
branches:
- "**"
tags:
- 'dev-*'
jobs:
dependency-branches:
name: Dependency Branches
runs-on: ubuntu-latest
outputs:
branches: ${{ steps.result.outputs.branches }}
steps:
- uses: actions/checkout@v4
- uses: supertokens/get-core-dependencies-action@main
id: result
with:
run-for: PR
docker:
name: Docker
runs-on: ubuntu-latest
needs: dependency-branches
outputs:
tag: ${{ steps.set_tag.outputs.TAG }}
strategy:
fail-fast: false
matrix:
plugin:
- postgresql
# no longer supported
# - mysql
# - mongodb
steps:
- name: Set up JDK 15.0.1
uses: actions/setup-java@v2
with:
java-version: 15.0.1
distribution: zulu
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-root
path: ./supertokens-root
ref: for_jdk_15_releases
- uses: actions/checkout@v2
with:
path: ./supertokens-root/supertokens-core
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-plugin-interface
path: ./supertokens-root/supertokens-plugin-interface
ref: ${{ fromJson(needs.dependency-branches.outputs.branches)['plugin-interface'] }}
- uses: actions/checkout@v2
if: matrix.plugin != 'sqlite'
with:
repository: supertokens/supertokens-${{ matrix.plugin }}-plugin
path: ./supertokens-root/supertokens-${{ matrix.plugin }}-plugin
ref: ${{ fromJson(needs.dependency-branches.outputs.branches)[matrix.plugin] }}
- name: Load Modules
run: |
cd supertokens-root
echo "core,master
plugin-interface,master
${{ matrix.plugin }}-plugin,master
" > modules.txt
cat modules.txt
./loadModules
- name: Setup test env
run: cd supertokens-root && ./utils/setupTestEnv --local
- name: Generate config file
run: |
cd supertokens-root
touch config_temp.yaml
cat supertokens-core/config.yaml >> config_temp.yaml
cat supertokens-${{ matrix.plugin }}-plugin/config.yaml >> config_temp.yaml
mv config_temp.yaml config.yaml
- name: set tag
id: set_tag
run: |
echo "TAG=${GITHUB_REF}" | sed 's/refs\/heads\///g' | sed 's/\//_/g' >> $GITHUB_OUTPUT
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
context: ./supertokens-root
tags: supertokens/supertokens-dev-${{ matrix.plugin }}:${{ steps.set_tag.outputs.TAG }}
file: ./supertokens-root/supertokens-${{ matrix.plugin }}-plugin/.github/helpers/docker/Dockerfile
platforms: linux/amd64,linux/arm64

47
.github/workflows/stress-tests.yml vendored Normal file
View File

@ -0,0 +1,47 @@
name: Stress Tests
on:
workflow_call:
inputs:
tag:
description: 'Docker image tag to use'
required: true
type: string
jobs:
stress-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: |
cd stress-tests
npm install
- name: Update Docker image in compose
run: |
cd stress-tests
sed -i 's|supertokens/supertokens-postgresql|supertokens/supertokens-dev-postgresql:${{ inputs.tag }}|' docker-compose.yml
cat docker-compose.yml
- name: Bring up the services
run: |
cd stress-tests
docker compose up -d
- name: Generate user jsons
run: |
cd stress-tests
npm run generate-users
- name: Run one million users test
id: one-million-users
run: |
cd stress-tests
npm run one-million-users | tee stress-tests.log
- name: Display Test Statistics
run: |
echo "## Stress Test Results" >> $GITHUB_STEP_SUMMARY
echo "| Test | Duration |" >> $GITHUB_STEP_SUMMARY
echo "|------|----------|" >> $GITHUB_STEP_SUMMARY
jq -r '.measurements[] | "| \(.title) | \(.formatted) |"' stress-tests/stats.json >> $GITHUB_STEP_SUMMARY

View File

@ -1,24 +0,0 @@
name: "Check if \"Run tests\" action succeeded"
on:
pull_request:
types:
- opened
- reopened
- edited
- synchronize
jobs:
pr-run-test-action:
name: Check if "Run tests" action succeeded
timeout-minutes: 60
concurrency:
group: ${{ github.head_ref }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: node install
run: cd ./.github/helpers && npm i
- name: Calling github API
run: cd ./.github/helpers && GITHUB_TOKEN=${{ github.token }} REPO=${{ github.repository }} RUN_ID=${{ github.run_id }} BRANCH=${{ github.head_ref }} JOB_ID=${{ github.job }} SOURCE_OWNER=${{ github.event.pull_request.head.repo.owner.login }} CURRENT_SHA=${{ github.event.pull_request.head.sha }} node node_modules/github-workflow-helpers/test-pass-check-pr.js

View File

@ -1,37 +0,0 @@
name: "Run tests"
on:
workflow_dispatch:
inputs:
pluginRepoOwnerName:
description: 'supertokens-plugin-interface repo owner name'
default: supertokens
required: true
pluginInterfaceBranch:
description: 'supertokens-plugin-interface repos branch name'
default: master
required: true
jobs:
test_job:
name: Run tests
timeout-minutes: 60
runs-on: ubuntu-latest
container: rishabhpoddar/supertokens_core_testing
steps:
- uses: actions/checkout@v2
- name: Cloning supertokens-root
run: cd ../ && git clone https://github.com/supertokens/supertokens-root.git
- name: Update Java 1
run: update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 2
- name: Update Java 2
run: update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 2
- name: Modifying modules.txt in supertokens-root
run: cd ../supertokens-root && echo "core,master\nplugin-interface,${{ github.event.inputs.pluginInterfaceBranch }},${{ github.event.inputs.pluginRepoOwnerName }}" > modules.txt
- name: Contents of modules.txt
run: cat ../supertokens-root/modules.txt
- name: Running loadModules in supertokens-root
run: cd ../supertokens-root && ./loadModules
- name: Copying current supertokens-core branch into supertokens-root
run: cd ../supertokens-root && rm -rf ./supertokens-core && cp -r ../supertokens-core ./
- name: Building and running tests
run: cd ../supertokens-root && ./startTestingEnv

123
.github/workflows/unit-test.yml vendored Normal file
View File

@ -0,0 +1,123 @@
name: Unit Tests
on:
workflow_call:
env:
total-runners: 12
jobs:
dependency-branches:
name: Dependency Branches
runs-on: ubuntu-latest
outputs:
branches: ${{ steps.result.outputs.branches }}
steps:
- uses: actions/checkout@v4
- uses: supertokens/get-core-dependencies-action@main
id: result
with:
run-for: PR
runner-indexes:
runs-on: ubuntu-latest
name: Generate runner indexes
needs: dependency-branches
outputs:
json: ${{ steps.generate-index-list.outputs.json }}
steps:
- id: generate-index-list
run: |
MAX_INDEX=$((${{ env.total-runners }}-1))
INDEX_LIST=$(seq 0 ${MAX_INDEX})
INDEX_JSON=$(jq --null-input --compact-output '. |= [inputs]' <<< ${INDEX_LIST})
echo "::set-output name=json::${INDEX_JSON}"
unit-tests:
runs-on: ubuntu-latest
name: "Unit tests: ${{ matrix.plugin }} plugin, runner #${{ matrix.runner-index }}"
needs:
- dependency-branches
- runner-indexes
strategy:
fail-fast: false
matrix:
runner-index: ${{ fromjson(needs.runner-indexes.outputs.json) }}
plugin:
- sqlite
- postgresql
steps:
- name: Set up JDK 15.0.1
uses: actions/setup-java@v2
with:
java-version: 15.0.1
distribution: zulu
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-root
path: ./supertokens-root
ref: for_jdk_15_releases
- uses: actions/checkout@v2
with:
path: ./supertokens-root/supertokens-core
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-plugin-interface
path: ./supertokens-root/supertokens-plugin-interface
ref: ${{ fromJson(needs.dependency-branches.outputs.branches)['plugin-interface'] }}
- uses: actions/checkout@v2
if: matrix.plugin != 'sqlite'
with:
repository: supertokens/supertokens-${{ matrix.plugin }}-plugin
path: ./supertokens-root/supertokens-${{ matrix.plugin }}-plugin
ref: ${{ fromJson(needs.dependency-branches.outputs.branches)[matrix.plugin] }}
- name: Load Modules
run: |
cd supertokens-root
echo "core,master
plugin-interface,master
${{ matrix.plugin }}-plugin,master
" > modules.txt
cat modules.txt
./loadModules
- name: Setup test env
run: cd supertokens-root && ./utils/setupTestEnv --local
- name: Start ${{ matrix.plugin }} server
if: matrix.plugin != 'sqlite'
run: cd supertokens-root/supertokens-${{ matrix.plugin }}-plugin && ./startDb.sh
- uses: chaosaffe/split-tests@v1-alpha.1
id: split-tests
name: Split tests
with:
glob: 'supertokens-root/*/src/test/java/**/*.java'
split-total: ${{ env.total-runners }}
split-index: ${{ matrix.runner-index }}
- run: 'echo "This runner will execute the following tests: ${{ steps.split-tests.outputs.test-suite }}"'
- name: Run tests
env:
ST_PLUGIN_NAME: ${{ matrix.plugin }}
run: |
cd supertokens-root
echo "./gradlew test \\" > test.sh
chmod +x test.sh
IFS=' ' read -ra TESTS <<< "${{ steps.split-tests.outputs.test-suite }}"
for test in "${TESTS[@]}"; do
test_name="${test%.java}"
test_name="${test_name#supertokens-root/supertokens-core/src/test/java/}"
test_name="${test_name//\//.}"
echo " --tests $test_name \\" >> test.sh
done
echo "" >> test.sh
echo "this is the test command:"
cat test.sh
echo "--------------------------------"
./test.sh
- name: Publish Test Report
uses: mikepenz/action-junit-report@v5
if: always()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
detailed_summary: true
include_passed: false
annotate_notice: true

View File

@ -5,7 +5,46 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [unreleased]
## [Unreleased]
## [6.0.21]
- Placeholder backport for releasing the opentelemetry connection uri env variable
## [6.0.20]
- Adds internal opentelemetry support for logging
## [6.0.19] - 2024-03-29
- Fixes userIdMapping queries
- Adds a new required `useDynamicSigningKey` into the request body of `RefreshSessionAPI`
- This enables smooth switching between `useDynamicAccessTokenSigningKey` settings by allowing refresh calls to
change the signing key type of a session
## [6.0.18] - 2024-02-20
- Fixes vulnerabilities in dependencies
- Updates telemetry payload
- Fixes Active User tracking to use the right storage
## [6.0.17] - 2024-02-06
- Adds new config `supertokens_saas_load_only_cud` that makes the core instance load a particular CUD only, irrespective of the CUDs present in the db.
- Fixes connection pool handling when connection pool size changes for a tenant.
## [6.0.16] - 2023-11-03
- Collects requests stats per app
- Adds `/requests/stats` API to return requests stats for the last day
## [6.0.15] - 2023-10-18
- Fixes issue with cron tasks that run per app and tenant
## [6.0.14] - 2023-10-12
- Fixes issue with duplicate cron task
## [6.0.13] - 2023-09-15

View File

@ -1,155 +1,155 @@
# Contributing
We're so excited you're interested in helping with SuperTokens! We are happy to help you get started, even if you don't
have any previous open-source experience :blush:
## New to Open Source?
1. Take a look
at [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github)
2. Go through
the [SuperTokens Code of Conduct](https://github.com/supertokens/supertokens-core/blob/master/CODE_OF_CONDUCT.md)
## Where to ask Questions?
1. Check our [Github Issues](https://github.com/supertokens/supertokens-core/issues) to see if someone has already
answered your question.
2. Join our community on [Discord](https://supertokens.io/discord) and feel free to ask us your questions
As you gain experience with SuperTokens, please help answer other people's questions! :pray:
## What to Work On?
You can get started by taking a look at our [Github issues](https://github.com/supertokens/supertokens-core/issues)
If you find one that looks interesting and no one else is already working on it, comment in the issue that you are going
to work on it.
Please ask as many questions as you need, either directly in the issue or on [Discord](https://supertokens.io/discord).
We're happy to help!:raised_hands:
### Contributions that are ALWAYS welcome
1. More tests
2. Contributing to discussions that can be
found [here](https://github.com/supertokens/supertokens-core/issues?q=is%3Aissue+is%3Aopen+label%3Adiscussions)
3. Improved error messages
4. Educational content like blogs, videos, courses
## Development Setup
### With Gitpod
1. Navigate to the [supertokens-root](https://github.com/supertokens/supertokens-root) repository
2. Click on the `Open in Gitpod` button
### Local Setup Prerequisites
- OS: Linux or macOS. Or if using Windows, you need to use [wsl2](https://docs.microsoft.com/en-us/windows/wsl/about).
- JDK: openjdk 15.0.1. Installation instructions for Mac and Linux can be found
in [our wiki](https://github.com/supertokens/supertokens-core/wiki/Installing-OpenJDK-for-Mac-and-Linux)
- IDE: [IntelliJ](https://www.jetbrains.com/idea/download/)(recommended) or equivalent IDE
### Familiarize yourself with SuperTokens
1. [Architecture of SuperTokens](https://github.com/supertokens/supertokens-core/wiki/SuperTokens-Architecture)
2. [SuperTokens code and file structure overview](https://github.com/supertokens/supertokens-core/wiki/Code-and-file-structure-overview)
3. [Versioning methodology](https://github.com/supertokens/supertokens-core/wiki/Versioning,-git-and-releases)
### Project Setup
1. Fork the [supertokens-core](https://github.com/supertokens/supertokens-core) repository (**Skip this step if you are
NOT modifying supertokens-core**)
2. `git clone https://github.com/supertokens/supertokens-root.git`
3. `cd supertokens-root`
4. Open the `modules.txt` file in an editor (**Skip this step if you are NOT modifying supertokens-core**):
- The `modules.txt` file contains the core, plugin-interface, the type of plugin and their branches(versions)
- By default the `master` branch is used but you can change the branch depending on which version you want to modify
- The `sqlite-plugin` is used as the default plugin as it is an in-memory database and requires no setup
- [core](https://github.com/supertokens/supertokens-core)
- [plugin-interface](https://github.com/supertokens/supertokens-plugin-interface)
- Check the repository branches by clicking on the links listed above, click the branch tab and check for all
the available versions
- Add your github `username` separated by a ',' after `core,master` in `modules.txt`
- If, for example, your github `username` is `helloworld` then modules.txt should look like...
```
// put module name like module name,branch name,github username(if contributing with a forked repository) and then call ./loadModules script
core,master,helloworld
plugin-interface,master
sqlite-plugin,master
```
5. Run loadModules to clone the required repositories
`./loadModules`
## Modifying code
1. Open `supetokens-root` in your IDE
2. After gradle has imported all the dependencies you can start modifying the code
## Testing
### On your local machine
1. Navigate to the `supertokens-root` repository
2. Run all tests
`./startTestEnv`
3. If all tests pass the terminal should display
- core tests:
![core tests passing](https://github.com/supertokens/supertokens-logo/blob/master/images/core-tests-passing.png)
- plugin tests:
![plugin tests passing](https://github.com/supertokens/supertokens-logo/blob/master/images/plugin-tests-passing.png)
### Using github actions
1. Go to the supertokens-core repo on github (or your forked version of it).
2. Navigate to the Actions tab.
3. Find the action named "Run tests" and navigate to it.
4. Click on the "Run workflow" button.
5. Set the config variables in the drop down:
- **supertokens-plugin-interface repo owner name**: If you have forked the supertokens-plugin-interface repo, then
set the value of this to your github username.
- **supertokens-plugin-interface repos branch name**: If the core version you are working on is compatible with a
plugin-interface version that is not in the master branch, then set the correct branch name in this value.
6. Click on "Run workflow".
## Running the core manually
1. Run `startTestEnv --wait` in a terminal, and keep it running
2. Then open `supertokens-root` in another terminal and run `cp ./temp/config.yaml .`
3. Then run `java -classpath "./core/*:./plugin-interface/*:./ee/*" io.supertokens.Main ./ DEV`. This will start the
core to listen on `http://localhost:3567`
## Pull Request
1. Before submitting a pull request make sure all tests have passed
2. Reference the relevant issue or pull request and give a clear description of changes/features added when submitting a
pull request
3. Make sure the PR title follows [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) specification
## Install the supertokens CLI manually
1. Setup test env and keep it running
2. In `supertokens-root`, run `cp temp/config.yaml .`
3. On a different terminal, go to `supertokens-root` folder and
run `java -classpath "./cli/*" io.supertokens.cli.Main true install`
## SuperTokens Community
SuperTokens is made possible by a passionate team and a strong community of developers. If you have any questions or
would like to get more involved in the SuperTokens community you can check out:
- [Github Issues](https://github.com/supertokens/supertokens-core/issues)
- [Discord](https://supertokens.io/discord)
- [Twitter](https://twitter.com/supertokensio)
- or [email us](mailto:team@supertokens.io)
Additional resources you might find useful:
- [SuperTokens Docs](https://supertokens.io/docs/community/getting-started/installation)
- [Blog Posts](https://supertokens.io/blog/)
- [Development guideline for the backend and frontend recipes](https://github.com/supertokens/supertokens-core/wiki/Development-guideline-for-the-backend-and-frontend-recipes)
# Contributing
We're so excited you're interested in helping with SuperTokens! We are happy to help you get started, even if you don't
have any previous open-source experience :blush:
## New to Open Source?
1. Take a look
at [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github)
2. Go through
the [SuperTokens Code of Conduct](https://github.com/supertokens/supertokens-core/blob/master/CODE_OF_CONDUCT.md)
## Where to ask Questions?
1. Check our [Github Issues](https://github.com/supertokens/supertokens-core/issues) to see if someone has already
answered your question.
2. Join our community on [Discord](https://supertokens.io/discord) and feel free to ask us your questions
As you gain experience with SuperTokens, please help answer other people's questions! :pray:
## What to Work On?
You can get started by taking a look at our [Github issues](https://github.com/supertokens/supertokens-core/issues)
If you find one that looks interesting and no one else is already working on it, comment in the issue that you are going
to work on it.
Please ask as many questions as you need, either directly in the issue or on [Discord](https://supertokens.io/discord).
We're happy to help!:raised_hands:
### Contributions that are ALWAYS welcome
1. More tests
2. Contributing to discussions that can be
found [here](https://github.com/supertokens/supertokens-core/issues?q=is%3Aissue+is%3Aopen+label%3Adiscussions)
3. Improved error messages
4. Educational content like blogs, videos, courses
## Development Setup
### With Gitpod
1. Navigate to the [supertokens-root](https://github.com/supertokens/supertokens-root) repository
2. Click on the `Open in Gitpod` button
### Local Setup Prerequisites
- OS: Linux or macOS. Or if using Windows, you need to use [wsl2](https://docs.microsoft.com/en-us/windows/wsl/about).
- JDK: openjdk 15.0.1. Installation instructions for Mac and Linux can be found
in [our wiki](https://github.com/supertokens/supertokens-core/wiki/Installing-OpenJDK-for-Mac-and-Linux)
- IDE: [IntelliJ](https://www.jetbrains.com/idea/download/)(recommended) or equivalent IDE
### Familiarize yourself with SuperTokens
1. [Architecture of SuperTokens](https://github.com/supertokens/supertokens-core/wiki/SuperTokens-Architecture)
2. [SuperTokens code and file structure overview](https://github.com/supertokens/supertokens-core/wiki/Code-and-file-structure-overview)
3. [Versioning methodology](https://github.com/supertokens/supertokens-core/wiki/Versioning,-git-and-releases)
### Project Setup
1. Fork the [supertokens-core](https://github.com/supertokens/supertokens-core) repository (**Skip this step if you are
NOT modifying supertokens-core**)
2. `git clone https://github.com/supertokens/supertokens-root.git`
3. `cd supertokens-root`
4. Open the `modules.txt` file in an editor (**Skip this step if you are NOT modifying supertokens-core**):
- The `modules.txt` file contains the core, plugin-interface, the type of plugin and their branches(versions)
- By default the `master` branch is used but you can change the branch depending on which version you want to modify
- The `sqlite-plugin` is used as the default plugin as it is an in-memory database and requires no setup
- [core](https://github.com/supertokens/supertokens-core)
- [plugin-interface](https://github.com/supertokens/supertokens-plugin-interface)
- Check the repository branches by clicking on the links listed above, click the branch tab and check for all
the available versions
- Add your github `username` separated by a ',' after `core,master` in `modules.txt`
- If, for example, your github `username` is `helloworld` then modules.txt should look like...
```
// put module name like module name,branch name,github username(if contributing with a forked repository) and then call ./loadModules script
core,master,helloworld
plugin-interface,master
sqlite-plugin,master
```
5. Run loadModules to clone the required repositories
`./loadModules`
## Modifying code
1. Open `supetokens-root` in your IDE
2. After gradle has imported all the dependencies you can start modifying the code
## Testing
### On your local machine
1. Navigate to the `supertokens-root` repository
2. Run all tests
`./startTestEnv`
3. If all tests pass the terminal should display
- core tests:
![core tests passing](https://github.com/supertokens/supertokens-logo/blob/master/images/core-tests-passing.png)
- plugin tests:
![plugin tests passing](https://github.com/supertokens/supertokens-logo/blob/master/images/plugin-tests-passing.png)
### Using github actions
1. Go to the supertokens-core repo on github (or your forked version of it).
2. Navigate to the Actions tab.
3. Find the action named "Run tests" and navigate to it.
4. Click on the "Run workflow" button.
5. Set the config variables in the drop down:
- **supertokens-plugin-interface repo owner name**: If you have forked the supertokens-plugin-interface repo, then
set the value of this to your github username.
- **supertokens-plugin-interface repos branch name**: If the core version you are working on is compatible with a
plugin-interface version that is not in the master branch, then set the correct branch name in this value.
6. Click on "Run workflow".
## Running the core manually
1. Run `startTestEnv --wait` in a terminal, and keep it running
2. Then open `supertokens-root` in another terminal and run `cp ./temp/config.yaml .`
3. Then run `java -classpath "./core/*:./plugin-interface/*:./ee/*" io.supertokens.Main ./ DEV`. This will start the
core to listen on `http://localhost:3567`
## Pull Request
1. Before submitting a pull request make sure all tests have passed
2. Reference the relevant issue or pull request and give a clear description of changes/features added when submitting a
pull request
3. Make sure the PR title follows [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) specification
## Install the supertokens CLI manually
1. Setup test env and keep it running
2. In `supertokens-root`, run `cp temp/config.yaml .`
3. On a different terminal, go to `supertokens-root` folder and
run `java -classpath "./cli/*" io.supertokens.cli.Main true install`
## SuperTokens Community
SuperTokens is made possible by a passionate team and a strong community of developers. If you have any questions or
would like to get more involved in the SuperTokens community you can check out:
- [Github Issues](https://github.com/supertokens/supertokens-core/issues)
- [Discord](https://supertokens.io/discord)
- [Twitter](https://twitter.com/supertokensio)
- or [email us](mailto:team@supertokens.io)
Additional resources you might find useful:
- [SuperTokens Docs](https://supertokens.io/docs/community/getting-started/installation)
- [Blog Posts](https://supertokens.io/blog/)
- [Development guideline for the backend and frontend recipes](https://github.com/supertokens/supertokens-core/wiki/Development-guideline-for-the-backend-and-frontend-recipes)

View File

@ -19,8 +19,7 @@ compileTestJava { options.encoding = "UTF-8" }
// }
//}
version = "6.0.13"
version = "6.0.21"
repositories {
mavenCentral()
@ -33,22 +32,19 @@ dependencies {
implementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.14.0'
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.14.0'
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1'
// https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core
implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.1'
implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18'
// https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305
implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
// https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc
implementation group: 'org.xerial', name: 'sqlite-jdbc', version: '3.30.1'
implementation group: 'org.xerial', name: 'sqlite-jdbc', version: '3.45.1.0'
// https://mvnrepository.com/artifact/org.mindrot/jbcrypt
implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.4'
@ -71,6 +67,23 @@ dependencies {
// https://mvnrepository.com/artifact/commons-codec/commons-codec
implementation group: 'commons-codec', name: 'commons-codec', version: '1.15'
// https://mvnrepository.com/artifact/com.googlecode.libphonenumber/libphonenumber/
implementation group: 'com.googlecode.libphonenumber', name: 'libphonenumber', version: '8.13.25'
implementation platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha:2.17.0-alpha")
implementation("ch.qos.logback:logback-core:1.5.18")
implementation("ch.qos.logback:logback-classic:1.5.18")
// OpenTelemetry core
implementation("io.opentelemetry:opentelemetry-sdk")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
implementation("io.opentelemetry:opentelemetry-exporter-logging")
implementation("io.opentelemetry:opentelemetry-api")
implementation("io.opentelemetry.semconv:opentelemetry-semconv")
compileOnly project(":supertokens-plugin-interface")
testImplementation project(":supertokens-plugin-interface")

View File

@ -19,10 +19,10 @@ dependencies {
implementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.10.0'
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.0'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1'
// https://mvnrepository.com/artifact/de.mkammerer/argon2-jvm
implementation group: 'de.mkammerer', name: 'argon2-jvm', version: '2.11'

View File

@ -7,29 +7,29 @@
"src": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.10.0/jackson-dataformat-yaml-2.10.0.jar",
"name": "Jackson Dataformat 2.10.0",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.10.0/jackson-dataformat-yaml-2.10.0-sources.jar"
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1.jar",
"name": "Jackson Dataformat 2.16.1",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.24/snakeyaml-1.24.jar",
"name": "SnakeYAML 1.24",
"src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.24/snakeyaml-1.24-sources.jar"
"jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar",
"name": "SnakeYAML 2.2",
"src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.10.0/jackson-core-2.10.0.jar",
"name": "Jackson core 2.10.0",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.10.0/jackson-core-2.10.0-sources.jar"
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1.jar",
"name": "Jackson core 2.16.1",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.10.0/jackson-databind-2.10.0.jar",
"name": "Jackson databind 2.10.0",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.10.0/jackson-databind-2.10.0-sources.jar"
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1.jar",
"name": "Jackson databind 2.16.1",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.10.0/jackson-annotations-2.10.0.jar",
"name": "Jackson annotation 2.10.0",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.10.0/jackson-annotations-2.10.0-sources.jar"
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1.jar",
"name": "Jackson annotation 2.16.1",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar",

Binary file not shown.

View File

@ -146,3 +146,11 @@ core_config_version: 0
# when CDI version is not specified in the request. When set to null, the core will assume the latest version of the
# CDI.
# supertokens_max_cdi_version:
# (OPTIONAL | Default: null) string value. If specified, the supertokens service will only load the specified CUD even
# if there are more CUDs in the database and block all other CUDs from being used from this instance.
# supertokens_saas_load_only_cud:
# (OPTIONAL | Default: http://localhost:4317) string value. The URL of the OpenTelemetry collector to which the core
# will send telemetry data. This should be in the format http://<host>:<port> or https://<host>:<port>.
# otel_collector_connection_uri:

View File

@ -147,3 +147,11 @@ disable_telemetry: true
# when CDI version is not specified in the request. When set to null, the core will assume the latest version of the
# CDI.
# supertokens_max_cdi_version:
# (OPTIONAL | Default: null) string value. If specified, the supertokens service will only load the specified CUD even
# if there are more CUDs in the database and block all other CUDs from being used from this instance.
# supertokens_saas_load_only_cud:
# (OPTIONAL | Default: http://localhost:4317) string value. The URL of the OpenTelemetry collector to which the core
# will send telemetry data. This should be in the format http://<host>:<port> or https://<host>:<port>.
# otel_collector_connection_uri:

Binary file not shown.

View File

@ -35,10 +35,10 @@ dependencies {
testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.1.0'
// https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core
testImplementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.1'
testImplementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18'
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'
testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14'
// https://mvnrepository.com/artifact/com.google.code.gson/gson
testImplementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
@ -46,10 +46,10 @@ dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit4:0.22.0'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml
testImplementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.14.0'
testImplementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.14.0'
testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1'
testImplementation group: 'org.jetbrains', name: 'annotations', version: '13.0'
}

Binary file not shown.

View File

@ -193,20 +193,19 @@ public class EEFeatureFlag implements io.supertokens.featureflag.EEFeatureFlagIn
// TODO Active users are present only on public tenant and TOTP users may be present on different storages
Storage publicTenantStorage = StorageLayer.getStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main);
final long now = System.currentTimeMillis();
for (int i = 0; i < 30; i++) {
long today = now - (now % (24 * 60 * 60 * 1000L));
long timestamp = today - (i * 24 * 60 * 60 * 1000L);
final long now = System.currentTimeMillis();
for (int i = 1; i <= 31; i++) {
long timestamp = now - (i * 24 * 60 * 60 * 1000L);
int totpMau = 0;
// TODO Need to figure out a way to combine the data from different storages to get the final stats
// for (Storage storage : storages) {
totpMau += ((ActiveUsersStorage) publicTenantStorage).countUsersEnabledTotpAndActiveSince(this.appIdentifier, timestamp);
// }
totpMauArr.add(new JsonPrimitive(totpMau));
}
int totpMau = 0;
// TODO Need to figure out a way to combine the data from different storages to get the final stats
// for (Storage storage : storages) {
totpMau += ((ActiveUsersStorage) publicTenantStorage).countUsersEnabledTotpAndActiveSince(this.appIdentifier, timestamp);
// }
totpMauArr.add(new JsonPrimitive(totpMau));
}
totpStats.add("maus", totpMauArr);
totpStats.add("maus", totpMauArr);
int totpTotalUsers = 0;
for (Storage storage : storages) {
@ -274,10 +273,10 @@ public class EEFeatureFlag implements io.supertokens.featureflag.EEFeatureFlagIn
private JsonArray getMAUs() throws StorageQueryException, TenantOrAppNotFoundException {
JsonArray mauArr = new JsonArray();
for (int i = 0; i < 30; i++) {
long now = System.currentTimeMillis();
long today = now - (now % (24 * 60 * 60 * 1000L));
long timestamp = today - (i * 24 * 60 * 60 * 1000L);
long now = System.currentTimeMillis();
for (int i = 1; i <= 31; i++) {
long timestamp = now - (i * 24 * 60 * 60 * 1000L);
ActiveUsersStorage activeUsersStorage = (ActiveUsersStorage) StorageLayer.getStorage(
this.appIdentifier.getAsPublicTenantIdentifier(), main);
int mau = activeUsersStorage.countUsersActiveSince(this.appIdentifier, timestamp);

View File

@ -1326,7 +1326,7 @@ public class EETest extends Mockito {
JsonObject paidFeatureUsageStats = j.getAsJsonObject("paidFeatureUsageStats");
JsonArray mauArr = paidFeatureUsageStats.get("maus").getAsJsonArray();
assertEquals(paidFeatureUsageStats.entrySet().size(), 1);
assertEquals(mauArr.size(), 30);
assertEquals(mauArr.size(), 31);
assertEquals(mauArr.get(0).getAsInt(), 0);
assertEquals(mauArr.get(29).getAsInt(), 0);
}

View File

@ -54,7 +54,7 @@ public class GetFeatureFlagAPITest {
if (StorageLayer.getStorage(process.getProcess()).getType() == STORAGE_TYPE.SQL) {
JsonArray mauArr = usageStats.get("maus").getAsJsonArray();
assertEquals(1, usageStats.entrySet().size());
assertEquals(30, mauArr.size());
assertEquals(31, mauArr.size());
assertEquals(0, mauArr.get(0).getAsInt());
assertEquals(0, mauArr.get(29).getAsInt());
} else {
@ -92,7 +92,7 @@ public class GetFeatureFlagAPITest {
if (StorageLayer.getStorage(process.getProcess()).getType() == STORAGE_TYPE.SQL) {
JsonArray mauArr = usageStats.get("maus").getAsJsonArray();
assertEquals(1, usageStats.entrySet().size());
assertEquals(30, mauArr.size());
assertEquals(31, mauArr.size());
assertEquals(0, mauArr.get(0).getAsInt());
assertEquals(0, mauArr.get(29).getAsInt());
} else {

View File

@ -1,115 +1,125 @@
{
"_comment": "Contains list of implementation dependencies URL for this project",
"list": [
{
"jar": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1.jar",
"name": "Gson 2.3.1",
"src": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.14.2/jackson-dataformat-yaml-2.14.2.jar",
"name": "Jackson Dataformat 2.14.2",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.14.2/jackson-dataformat-yaml-2.14.2-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.33/snakeyaml-1.33.jar",
"name": "SnakeYAML 1.33",
"src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.33/snakeyaml-1.33-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.14.2/jackson-core-2.14.2.jar",
"name": "Jackson core 2.14.2",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.14.2/jackson-core-2.14.2-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.14.2/jackson-databind-2.14.2.jar",
"name": "Jackson databind 2.14.2",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.14.2/jackson-databind-2.14.2-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.14.2/jackson-annotations-2.14.2.jar",
"name": "Jackson annotation 2.14.2",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.14.2/jackson-annotations-2.14.2-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar",
"name": "Logback classic 1.2.3",
"src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar",
"name": "Logback core 1.2.3",
"src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar",
"name": "SLF4j API 1.7.25",
"src": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.1/tomcat-annotations-api-10.1.1.jar",
"name": "Tomcat annotations API 10.1.1",
"src": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.1/tomcat-annotations-api-10.1.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.1/tomcat-embed-core-10.1.1.jar",
"name": "Tomcat embed core API 10.1.1",
"src": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.1/tomcat-embed-core-10.1.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar",
"name": "JSR305 3.0.2",
"src": "https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.jar",
"name": "JSR305 3.0.2",
"src": "https://repo1.maven.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.30.1/sqlite-jdbc-3.30.1.jar",
"name": "SQLite JDBC Driver 3.30.1",
"src": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.30.1/sqlite-jdbc-3.30.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar",
"name": "SQLite JDBC Driver 3.30.1",
"src": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/auth0/java-jwt/4.4.0/java-jwt-4.4.0.jar",
"name": "Auth0 Java JWT",
"src": "https://repo1.maven.org/maven2/com/auth0/java-jwt/4.4.0/java-jwt-4.4.0-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar",
"name": "Argon2-jvm 2.11",
"src": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm-nolibs/2.11/argon2-jvm-nolibs-2.11.jar",
"name": "Argon2-jvm no libs 2.11",
"src": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm-nolibs/2.11/argon2-jvm-nolibs-2.11-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0.jar",
"name": "JNA 5.8.0",
"src": "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0.jar",
"name": "Scrypt 1.4.0",
"src": "https://repo1.maven.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/eatthepath/java-otp/0.4.0/java-otp-0.4.0.jar",
"name": "Java OTP 0.4.0",
"src": "https://repo1.maven.org/maven2/com/eatthepath/java-otp/0.4.0/java-otp-0.4.0-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15.jar",
"name": "Commons Codec 1.15",
"src": "https://repo1.maven.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15-sources.jar"
}
]
"_comment": "Contains list of implementation dependencies URL for this project. This is a generated file, don't modify the contents by hand.",
"list": [
{
"jar":"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1.jar",
"name":"gson 2.3.1",
"src":"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1.jar",
"name":"jackson-dataformat-yaml 2.16.1",
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar",
"name":"snakeyaml 2.2",
"src":"https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1.jar",
"name":"jackson-databind 2.16.1",
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18.jar",
"name":"tomcat-embed-core 10.1.18",
"src":"https://repo.maven.apache.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.18/tomcat-annotations-api-10.1.18.jar",
"name":"tomcat-annotations-api 10.1.18",
"src":"https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.18/tomcat-annotations-api-10.1.18-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar",
"name":"jsr305 3.0.2",
"src":"https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar",
"name":"sqlite-jdbc 3.45.1.0",
"src":"https://repo.maven.apache.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar",
"name":"slf4j-api 2.0.17",
"src":"https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar",
"name":"jbcrypt 0.4",
"src":"https://repo.maven.apache.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.jar",
"name":"annotations 13.0",
"src":"https://repo.maven.apache.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar",
"name":"argon2-jvm 2.11",
"src":"https://repo.maven.apache.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/auth0/java-jwt/4.4.0/java-jwt-4.4.0.jar",
"name":"java-jwt 4.4.0",
"src":"https://repo.maven.apache.org/maven2/com/auth0/java-jwt/4.4.0/java-jwt-4.4.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0.jar",
"name":"scrypt 1.4.0",
"src":"https://repo.maven.apache.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/eatthepath/java-otp/0.4.0/java-otp-0.4.0.jar",
"name":"java-otp 0.4.0",
"src":"https://repo.maven.apache.org/maven2/com/eatthepath/java-otp/0.4.0/java-otp-0.4.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15.jar",
"name":"commons-codec 1.15",
"src":"https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.25/libphonenumber-8.13.25.jar",
"name":"libphonenumber 8.13.25",
"src":"https://repo.maven.apache.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.25/libphonenumber-8.13.25-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/ch/qos/logback/logback-core/1.5.18/logback-core-1.5.18.jar",
"name":"logback-core 1.5.18",
"src":"https://repo.maven.apache.org/maven2/ch/qos/logback/logback-core/1.5.18/logback-core-1.5.18-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/ch/qos/logback/logback-classic/1.5.18/logback-classic-1.5.18.jar",
"name":"logback-classic 1.5.18",
"src":"https://repo.maven.apache.org/maven2/ch/qos/logback/logback-classic/1.5.18/logback-classic-1.5.18-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-api/1.51.0/opentelemetry-api-1.51.0.jar",
"name":"opentelemetry-api 1.51.0",
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-api/1.51.0/opentelemetry-api-1.51.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-exporter-logging/1.51.0/opentelemetry-exporter-logging-1.51.0.jar",
"name":"opentelemetry-exporter-logging 1.51.0",
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-exporter-logging/1.51.0/opentelemetry-exporter-logging-1.51.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-sdk/1.51.0/opentelemetry-sdk-1.51.0.jar",
"name":"opentelemetry-sdk 1.51.0",
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-sdk/1.51.0/opentelemetry-sdk-1.51.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-exporter-otlp/1.51.0/opentelemetry-exporter-otlp-1.51.0.jar",
"name":"opentelemetry-exporter-otlp 1.51.0",
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-exporter-otlp/1.51.0/opentelemetry-exporter-otlp-1.51.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/semconv/opentelemetry-semconv/1.34.0/opentelemetry-semconv-1.34.0.jar",
"name":"opentelemetry-semconv 1.34.0",
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/semconv/opentelemetry-semconv/1.34.0/opentelemetry-semconv-1.34.0-sources.jar"
}
]
}

View File

@ -44,6 +44,7 @@ import io.supertokens.signingkeys.AccessTokenSigningKey;
import io.supertokens.signingkeys.JWTSigningKey;
import io.supertokens.signingkeys.SigningKeys;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.telemetry.TelemetryProvider;
import io.supertokens.version.Version;
import io.supertokens.webserver.Webserver;
import org.jetbrains.annotations.TestOnly;
@ -159,6 +160,8 @@ public class Main {
Logging.info(this, TenantIdentifier.BASE_TENANT, "Completed config.yaml loading.", true);
TelemetryProvider.initialize(this);
// loading storage layer
try {
StorageLayer.initPrimary(this, CLIOptions.get(this).getInstallationPath() + "plugin/",
@ -254,6 +257,9 @@ public class Main {
// starts DeleteExpiredAccessTokenSigningKeys cronjob if the access token signing keys can change
Cronjobs.addCronjob(this, DeleteExpiredAccessTokenSigningKeys.init(this, uniqueUserPoolIdsTenants));
// this is to ensure tenantInfos are in sync for the new cron job as well
MultitenancyHelper.getInstance(this).refreshCronjobs();
// creates password hashing pool
PasswordHashing.init(this);
@ -417,6 +423,7 @@ public class Main {
StorageLayer.close(this);
removeDotStartedFileForThisProcess();
Logging.stopLogging(this);
TelemetryProvider.closeTelemetry(this);
// uncomment this when you want to confirm that processes are actually shut.
// printRunningThreadNames();

View File

@ -30,7 +30,9 @@ import io.supertokens.config.annotations.NotConflictingInApp;
import io.supertokens.pluginInterface.LOG_LEVEL;
import io.supertokens.pluginInterface.exceptions.InvalidConfigException;
import io.supertokens.utils.SemVer;
import io.supertokens.webserver.Utils;
import io.supertokens.webserver.WebserverAPI;
import jakarta.servlet.ServletException;
import org.apache.catalina.filters.RemoteAddrFilter;
import org.jetbrains.annotations.TestOnly;
@ -197,12 +199,20 @@ public class CoreConfig {
@JsonProperty
private String supertokens_max_cdi_version = null;
@ConfigYamlOnly
@JsonProperty
private String supertokens_saas_load_only_cud = null;
@IgnoreForAnnotationCheck
private Set<LOG_LEVEL> allowedLogLevels = null;
@IgnoreForAnnotationCheck
private boolean isNormalizedAndValid = false;
@ConfigYamlOnly
@JsonProperty
private String otel_collector_connection_uri = "http://localhost:4317";
public static Set<String> getValidFields() {
CoreConfig coreConfig = new CoreConfig();
JsonObject coreConfigObj = new GsonBuilder().serializeNulls().create().toJsonTree(coreConfig).getAsJsonObject();
@ -254,6 +264,10 @@ public class CoreConfig {
return base_path;
}
public String getSuperTokensLoadOnlyCUD() {
return supertokens_saas_load_only_cud;
}
public enum PASSWORD_HASHING_ALG {
ARGON2, BCRYPT, FIREBASE_SCRYPT
}
@ -388,6 +402,10 @@ public class CoreConfig {
return webserver_https_enabled;
}
public String getOtelCollectorConnectionURI() {
return otel_collector_connection_uri;
}
private String getConfigFileLocation(Main main) {
return new File(CLIOptions.get(main).getConfigFilePath() == null
? CLIOptions.get(main).getInstallationPath() + "config.yaml"
@ -663,6 +681,15 @@ public class CoreConfig {
host = cliHost;
}
if (supertokens_saas_load_only_cud != null) {
try {
supertokens_saas_load_only_cud =
Utils.normalizeAndValidateConnectionUriDomain(supertokens_saas_load_only_cud, true);
} catch (ServletException e) {
throw new InvalidConfigException("supertokens_saas_load_only_cud is invalid");
}
}
access_token_validity = access_token_validity * 1000;
access_token_dynamic_signing_key_update_interval = access_token_dynamic_signing_key_update_interval * 3600 * 1000;
refresh_token_validity = refresh_token_validity * 60 * 1000;

View File

@ -25,6 +25,7 @@ import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.storageLayer.StorageLayer;
import org.jetbrains.annotations.TestOnly;
import java.util.ArrayList;
import java.util.HashSet;
@ -187,6 +188,11 @@ public abstract class CronTask extends ResourceDistributor.SingletonResource imp
}
}
@TestOnly
public List<List<TenantIdentifier>> getTenantsInfo() {
return this.tenantsInfo;
}
// the list belongs to tenants that are a part of the same user pool ID
protected void doTaskPerStorage(Storage storage) throws Exception {

View File

@ -91,9 +91,9 @@ public class Cronjobs extends ResourceDistributor.SingletonResource {
}
Cronjobs instance = getInstance(main);
synchronized (instance.lock) {
instance.executor.scheduleWithFixedDelay(task, task.getInitialWaitTimeSeconds(),
task.getIntervalTimeSeconds(), TimeUnit.SECONDS);
if (!instance.tasks.contains(task)) {
instance.executor.scheduleWithFixedDelay(task, task.getInitialWaitTimeSeconds(),
task.getIntervalTimeSeconds(), TimeUnit.SECONDS);
instance.tasks.add(task);
}
}
@ -103,4 +103,13 @@ public class Cronjobs extends ResourceDistributor.SingletonResource {
public List<CronTask> getTasks() {
return this.tasks;
}
@TestOnly
public List<List<List<TenantIdentifier>>> getTenantInfos() {
List<List<List<TenantIdentifier>>> tenantsInfos = new ArrayList<>();
for (CronTask task : this.tasks) {
tenantsInfos.add(task.getTenantsInfo());
}
return tenantsInfos;
}
}

View File

@ -16,20 +16,26 @@
package io.supertokens.cronjobs.telemetry;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import io.supertokens.Main;
import io.supertokens.ProcessState;
import io.supertokens.authRecipe.AuthRecipe;
import io.supertokens.config.Config;
import io.supertokens.cronjobs.CronTask;
import io.supertokens.cronjobs.CronTaskTest;
import io.supertokens.dashboard.Dashboard;
import io.supertokens.httpRequest.HttpRequest;
import io.supertokens.httpRequest.HttpRequestMocking;
import io.supertokens.pluginInterface.ActiveUsersStorage;
import io.supertokens.pluginInterface.KeyValueInfo;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.dashboard.DashboardUser;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.storageLayer.StorageLayer;
@ -90,22 +96,63 @@ public class Telemetry extends CronTask {
json.addProperty("telemetryId", telemetryId.value);
json.addProperty("superTokensVersion", coreVersion);
if (StorageLayer.getBaseStorage(main).getType() == STORAGE_TYPE.SQL) {
ActiveUsersStorage activeUsersStorage = (ActiveUsersStorage) StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main);
json.addProperty("mau", activeUsersStorage.countUsersActiveSince(app, System.currentTimeMillis() - 30 * 24 * 3600 * 1000L));
} else {
json.addProperty("mau", -1);
}
json.addProperty("appId", app.getAppId());
json.addProperty("connectionUriDomain", app.getConnectionUriDomain());
if (StorageLayer.getBaseStorage(main).getType() == STORAGE_TYPE.SQL) {
{ // Users count across all tenants
Storage[] storages = StorageLayer.getStoragesForApp(main, app);
AppIdentifierWithStorage appIdentifierWithAllTenantStorages = new AppIdentifierWithStorage(
app.getConnectionUriDomain(), app.getAppId(),
StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main), storages
);
json.addProperty("usersCount",
AuthRecipe.getUsersCountAcrossAllTenants(appIdentifierWithAllTenantStorages, null));
}
{ // Dashboard user emails
// Dashboard APIs are app specific and are always stored on the public tenant
DashboardUser[] dashboardUsers = Dashboard.getAllDashboardUsers(
app.withStorage(StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main)), main);
JsonArray dashboardUserEmails = new JsonArray();
for (DashboardUser user : dashboardUsers) {
dashboardUserEmails.add(new JsonPrimitive(user.email));
}
json.add("dashboardUserEmails", dashboardUserEmails);
}
{ // MAUs
// Active users are always tracked on the public tenant, so we use the public tenant's storage
ActiveUsersStorage activeUsersStorage = (ActiveUsersStorage) StorageLayer.getStorage(
app.getAsPublicTenantIdentifier(), main);
JsonArray mauArr = new JsonArray();
long now = System.currentTimeMillis();
for (int i = 1; i <= 31; i++) {
long timestamp = now - (i * 24 * 60 * 60 * 1000L);
int mau = activeUsersStorage.countUsersActiveSince(app, timestamp);
mauArr.add(new JsonPrimitive(mau));
}
json.add("maus", mauArr);
}
} else {
json.addProperty("usersCount", -1);
json.add("dashboardUserEmails", new JsonArray());
json.add("maus", new JsonArray());
}
String url = "https://api.supertokens.io/0/st/telemetry";
// we call the API only if we are not testing the core, of if the request can be mocked (in case a test
// wants
// to use this)
if (!Main.isTesting || HttpRequestMocking.getInstance(main).getMockURL(REQUEST_ID, url) != null) {
HttpRequest.sendJsonPOSTRequest(main, REQUEST_ID, url, json, 10000, 10000, 4);
HttpRequest.sendJsonPOSTRequest(main, REQUEST_ID, url, json, 10000, 10000, 5);
ProcessState.getInstance(main).addState(ProcessState.PROCESS_STATE.SENT_TELEMETRY, null);
}
}

View File

@ -511,10 +511,11 @@ public class Start
@Override
public void updateSessionInfo_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con,
String sessionHandle, String refreshTokenHash2,
long expiry) throws StorageQueryException {
long expiry, boolean useStaticKey) throws StorageQueryException {
Connection sqlCon = (Connection) con.getConnection();
try {
SessionQueries.updateSessionInfo_Transaction(this, sqlCon, tenantIdentifier, sessionHandle, refreshTokenHash2, expiry);
SessionQueries.updateSessionInfo_Transaction(this, sqlCon, tenantIdentifier, sessionHandle,
refreshTokenHash2, expiry, useStaticKey);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@ -2154,10 +2155,11 @@ public class Start
}
@Override
public HashMap<String, String> getUserIdMappingForSuperTokensIds(ArrayList<String> userIds)
public HashMap<String, String> getUserIdMappingForSuperTokensIds(AppIdentifier appIdentifier,
ArrayList<String> userIds)
throws StorageQueryException {
try {
return UserIdMappingQueries.getUserIdMappingWithUserIds(this, userIds);
return UserIdMappingQueries.getUserIdMappingWithUserIds(this, appIdentifier, userIds);
} catch (SQLException e) {
throw new StorageQueryException(e);
}

View File

@ -124,18 +124,19 @@ public class SessionQueries {
public static void updateSessionInfo_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier,
String sessionHandle,
String refreshTokenHash2, long expiry)
String refreshTokenHash2, long expiry, boolean useStaticKey)
throws SQLException, StorageQueryException {
String QUERY = "UPDATE " + getConfig(start).getSessionInfoTable()
+ " SET refresh_token_hash_2 = ?, expires_at = ?"
+ " SET refresh_token_hash_2 = ?, expires_at = ?, use_static_key = ?"
+ " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?";
update(con, QUERY, pst -> {
pst.setString(1, refreshTokenHash2);
pst.setLong(2, expiry);
pst.setString(3, tenantIdentifier.getAppId());
pst.setString(4, tenantIdentifier.getTenantId());
pst.setString(5, sessionHandle);
pst.setBoolean(3, useStaticKey);
pst.setString(4, tenantIdentifier.getAppId());
pst.setString(5, tenantIdentifier.getTenantId());
pst.setString(6, sessionHandle);
});
}

View File

@ -115,7 +115,9 @@ public class UserIdMappingQueries {
}
public static HashMap<String, String> getUserIdMappingWithUserIds(Start start, ArrayList<String> userIds)
public static HashMap<String, String> getUserIdMappingWithUserIds(Start start,
AppIdentifier appIdentifier,
ArrayList<String> userIds)
throws SQLException, StorageQueryException {
if (userIds.size() == 0) {
@ -124,7 +126,8 @@ public class UserIdMappingQueries {
// No need to filter based on tenantId because the id list is already filtered for a tenant
StringBuilder QUERY = new StringBuilder(
"SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE supertokens_user_id IN (");
"SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND " +
"supertokens_user_id IN (");
for (int i = 0; i < userIds.size(); i++) {
QUERY.append("?");
if (i != userIds.size() - 1) {
@ -134,9 +137,10 @@ public class UserIdMappingQueries {
}
QUERY.append(")");
return execute(start, QUERY.toString(), pst -> {
pst.setString(1, appIdentifier.getAppId());
for (int i = 0; i < userIds.size(); i++) {
// i+1 cause this starts with 1 and not 0
pst.setString(i + 1, userIds.get(i));
// i+2 cause this starts with 1 and not 0, and 1 is the app_id
pst.setString(i + 2, userIds.get(i));
}
}, result -> {
HashMap<String, String> userIdMappings = new HashMap<>();

View File

@ -51,9 +51,20 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
private Main main;
private TenantConfig[] tenantConfigs;
// when the core has `supertokens_saas_load_only_cud` set, the tenantConfigs array will be filtered
// based on the config value. However, we need to keep all the list of CUDs from the db to be able
// to check if the CUD is present in the DB or not, while processing the requests.
private final Set<String> dangerous_allCUDsFromDb = new HashSet<>();
private MultitenancyHelper(Main main) throws StorageQueryException {
this.main = main;
this.tenantConfigs = getAllTenantsFromDb();
TenantConfig[] allTenantsFromDb = getAllTenantsFromDb();
this.tenantConfigs = this.getFilteredTenantConfigs(allTenantsFromDb);
this.dangerous_allCUDsFromDb.clear();
for (TenantConfig config : allTenantsFromDb) {
this.dangerous_allCUDsFromDb.add(config.tenantIdentifier.getConnectionUriDomain());
}
}
public static MultitenancyHelper getInstance(Main main) {
@ -81,7 +92,7 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
// instance of eeFeatureFlag. This is applicable only when the core is starting on
// an empty database as no tenants are loaded from the db yet.
} catch (CannotModifyBaseConfigException | BadPermissionException | FeatureNotEnabledException |
InvalidConfigException | InvalidProviderConfigException | TenantOrAppNotFoundException e) {
InvalidConfigException | InvalidProviderConfigException | TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
}
@ -108,10 +119,11 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
return main.getResourceDistributor().withResourceDistributorLock(() -> {
try {
TenantConfig[] tenantsFromDb = getAllTenantsFromDb();
TenantConfig[] filteredTenantsFromDb = this.getFilteredTenantConfigs(tenantsFromDb);
Map<ResourceDistributor.KeyClass, JsonObject> normalizedTenantsFromDb =
Config.getNormalisedConfigsForAllTenants(
tenantsFromDb, Config.getBaseConfigAsJsonObject(main));
filteredTenantsFromDb, Config.getBaseConfigAsJsonObject(main));
Map<ResourceDistributor.KeyClass, JsonObject> normalizedTenantsFromMemory =
Config.getNormalisedConfigsForAllTenants(
@ -129,9 +141,14 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
}
}
boolean sameNumberOfTenants = tenantsFromDb.length == this.tenantConfigs.length;
boolean sameNumberOfTenants =
filteredTenantsFromDb.length == this.tenantConfigs.length;
this.tenantConfigs = tenantsFromDb;
this.dangerous_allCUDsFromDb.clear();
for (TenantConfig tenant : tenantsFromDb) {
this.dangerous_allCUDsFromDb.add(tenant.tenantIdentifier.getConnectionUriDomain());
}
this.tenantConfigs = filteredTenantsFromDb;
if (tenantsThatChanged.size() == 0 && sameNumberOfTenants) {
return tenantsThatChanged;
}
@ -190,7 +207,7 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
public void loadFeatureFlag(List<TenantIdentifier> tenantsThatChanged) {
List<AppIdentifier> apps = new ArrayList<>();
Set<AppIdentifier> appsSet = new HashSet<>();
for (TenantConfig t : tenantConfigs) {
for (TenantConfig t : this.tenantConfigs) {
if (appsSet.contains(t.tenantIdentifier.toAppIdentifier())) {
continue;
}
@ -204,7 +221,7 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
throws UnsupportedJWTSigningAlgorithmException {
List<AppIdentifier> apps = new ArrayList<>();
Set<AppIdentifier> appsSet = new HashSet<>();
for (TenantConfig t : tenantConfigs) {
for (TenantConfig t : this.tenantConfigs) {
if (appsSet.contains(t.tenantIdentifier.toAppIdentifier())) {
continue;
}
@ -217,7 +234,7 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
SigningKeys.loadForAllTenants(main, apps, tenantsThatChanged);
}
private void refreshCronjobs() {
public void refreshCronjobs() {
List<List<TenantIdentifier>> list = StorageLayer.getTenantsWithUniqueUserPoolId(main);
Cronjobs.getInstance(main).setTenantsInfo(list);
}
@ -238,4 +255,21 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
throw new IllegalStateException(e);
}
}
}
private TenantConfig[] getFilteredTenantConfigs(TenantConfig[] inputTenantConfigs) {
String loadOnlyCUD = Config.getBaseConfig(main).getSuperTokensLoadOnlyCUD();
if (loadOnlyCUD == null) {
return inputTenantConfigs;
}
return Arrays.stream(inputTenantConfigs)
.filter(tenantConfig -> tenantConfig.tenantIdentifier.getConnectionUriDomain().equals(loadOnlyCUD)
|| tenantConfig.tenantIdentifier.getConnectionUriDomain().equals(TenantIdentifier.DEFAULT_CONNECTION_URI))
.toArray(TenantConfig[]::new);
}
public boolean isConnectionUriDomainPresentInDb(String cud) {
return this.dangerous_allCUDsFromDb.contains(cud);
}
}

View File

@ -29,6 +29,7 @@ import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.telemetry.TelemetryProvider;
import io.supertokens.utils.Utils;
import io.supertokens.webserver.Webserver;
import org.slf4j.LoggerFactory;
@ -109,6 +110,7 @@ public class Logging extends ResourceDistributor.SingletonResource {
msg = prependTenantIdentifierToMessage(tenantIdentifier, msg);
if (getInstance(main) != null) {
getInstance(main).infoLogger.debug(msg);
TelemetryProvider.createLogEvent(main, tenantIdentifier, msg, "debug");
}
} catch (NullPointerException e) {
// sometimes logger.debug throws a null pointer exception...
@ -132,6 +134,8 @@ public class Logging extends ResourceDistributor.SingletonResource {
if (getInstance(main) != null) {
getInstance(main).infoLogger.info(msg);
}
TelemetryProvider.createLogEvent(main, tenantIdentifier, msg, "info");
} catch (NullPointerException ignored) {
}
}
@ -145,6 +149,8 @@ public class Logging extends ResourceDistributor.SingletonResource {
msg = prependTenantIdentifierToMessage(tenantIdentifier, msg);
if (getInstance(main) != null) {
getInstance(main).errorLogger.warn(msg);
TelemetryProvider.createLogEvent(main, tenantIdentifier, msg, "warn");
}
} catch (NullPointerException ignored) {
}
@ -166,6 +172,7 @@ public class Logging extends ResourceDistributor.SingletonResource {
err = prependTenantIdentifierToMessage(tenantIdentifier, err);
if (getInstance(main) != null) {
getInstance(main).errorLogger.error(err);
TelemetryProvider.createLogEvent(main, tenantIdentifier, err, "error");
}
if (toConsoleAsWell || getInstance(main) == null) {
systemErr(err);
@ -199,6 +206,9 @@ public class Logging extends ResourceDistributor.SingletonResource {
message = prependTenantIdentifierToMessage(tenantIdentifier, message);
if (getInstance(main) != null) {
getInstance(main).errorLogger.error(message);
TelemetryProvider
.createLogEvent(main, tenantIdentifier, message,
"error");
}
if (toConsoleAsWell || getInstance(main) == null) {
systemErr(message);

View File

@ -351,7 +351,7 @@ public class Session {
accessToken.sessionHandle,
Utils.hashSHA256(accessToken.refreshTokenHash1),
System.currentTimeMillis() +
config.getRefreshTokenValidity());
config.getRefreshTokenValidity(), sessionInfo.useStaticKey);
}
storage.commitTransaction(con);
@ -423,7 +423,7 @@ public class Session {
Utils.hashSHA256(accessToken.refreshTokenHash1),
System.currentTimeMillis() + Config.getConfig(tenantIdentifierWithStorage, main)
.getRefreshTokenValidity(),
sessionInfo.lastUpdatedSign);
sessionInfo.lastUpdatedSign, sessionInfo.useStaticKey);
if (!success) {
continue;
}
@ -473,7 +473,7 @@ public class Session {
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError {
try {
return refreshSession(new AppIdentifier(null, null), main, refreshToken, antiCsrfToken,
enableAntiCsrf, accessTokenVersion);
enableAntiCsrf, accessTokenVersion, null);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -482,7 +482,8 @@ public class Session {
public static SessionInformationHolder refreshSession(AppIdentifier appIdentifier, Main main,
@Nonnull String refreshToken,
@Nullable String antiCsrfToken, boolean enableAntiCsrf,
AccessToken.VERSION accessTokenVersion)
AccessToken.VERSION accessTokenVersion,
Boolean shouldUseStaticKey)
throws StorageTransactionLogicException,
UnauthorisedException, StorageQueryException, TokenTheftDetectedException,
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError, TenantOrAppNotFoundException {
@ -498,14 +499,15 @@ public class Session {
return refreshSessionHelper(refreshTokenInfo.tenantIdentifier.withStorage(
StorageLayer.getStorage(refreshTokenInfo.tenantIdentifier, main)),
main, refreshToken, refreshTokenInfo, enableAntiCsrf, accessTokenVersion);
main, refreshToken, refreshTokenInfo, enableAntiCsrf, accessTokenVersion, shouldUseStaticKey);
}
private static SessionInformationHolder refreshSessionHelper(
TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String refreshToken,
RefreshToken.RefreshTokenInfo refreshTokenInfo,
boolean enableAntiCsrf,
AccessToken.VERSION accessTokenVersion)
AccessToken.VERSION accessTokenVersion,
Boolean shouldUseStaticKey)
throws StorageTransactionLogicException, UnauthorisedException, StorageQueryException,
TokenTheftDetectedException, UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError,
TenantOrAppNotFoundException {
@ -530,7 +532,16 @@ public class Session {
throw new UnauthorisedException("Session missing in db or has expired");
}
boolean useStaticKey = shouldUseStaticKey != null ? shouldUseStaticKey : sessionInfo.useStaticKey;
if (sessionInfo.refreshTokenHash2.equals(Utils.hashSHA256(Utils.hashSHA256(refreshToken)))) {
if (useStaticKey != sessionInfo.useStaticKey) {
// We do not update anything except the static key status
storage.updateSessionInfo_Transaction(tenantIdentifierWithStorage, con, sessionHandle,
sessionInfo.refreshTokenHash2, sessionInfo.expiry,
useStaticKey);
}
// at this point, the input refresh token is the parent one.
storage.commitTransaction(con);
String antiCsrfToken = enableAntiCsrf ? UUID.randomUUID().toString() : null;
@ -542,7 +553,7 @@ public class Session {
main, sessionHandle,
sessionInfo.userId, Utils.hashSHA256(newRefreshToken.token),
Utils.hashSHA256(refreshToken), sessionInfo.userDataInJWT, antiCsrfToken,
null, accessTokenVersion, sessionInfo.useStaticKey);
null, accessTokenVersion, useStaticKey);
TokenInfo idRefreshToken = new TokenInfo(UUID.randomUUID().toString(),
newRefreshToken.expiry, newRefreshToken.createdTime);
@ -560,13 +571,13 @@ public class Session {
.equals(sessionInfo.refreshTokenHash2))) {
storage.updateSessionInfo_Transaction(tenantIdentifierWithStorage, con, sessionHandle,
Utils.hashSHA256(Utils.hashSHA256(refreshToken)),
System.currentTimeMillis() + config.getRefreshTokenValidity());
System.currentTimeMillis() + config.getRefreshTokenValidity(), useStaticKey);
storage.commitTransaction(con);
return refreshSessionHelper(tenantIdentifierWithStorage, main, refreshToken,
refreshTokenInfo, enableAntiCsrf,
accessTokenVersion);
accessTokenVersion, shouldUseStaticKey);
}
storage.commitTransaction(con);
@ -613,7 +624,19 @@ public class Session {
throw new UnauthorisedException("Session missing in db or has expired");
}
boolean useStaticKey = shouldUseStaticKey != null ? shouldUseStaticKey : sessionInfo.useStaticKey;
if (sessionInfo.refreshTokenHash2.equals(Utils.hashSHA256(Utils.hashSHA256(refreshToken)))) {
if (sessionInfo.useStaticKey != useStaticKey) {
// We do not update anything except the static key status
boolean success = storage.updateSessionInfo_Transaction(sessionHandle,
sessionInfo.refreshTokenHash2, sessionInfo.expiry,
sessionInfo.lastUpdatedSign, useStaticKey);
if (!success) {
continue;
}
}
// at this point, the input refresh token is the parent one.
String antiCsrfToken = enableAntiCsrf ? UUID.randomUUID().toString() : null;
@ -624,7 +647,7 @@ public class Session {
sessionHandle,
sessionInfo.userId, Utils.hashSHA256(newRefreshToken.token),
Utils.hashSHA256(refreshToken), sessionInfo.userDataInJWT, antiCsrfToken,
null, accessTokenVersion, sessionInfo.useStaticKey);
null, accessTokenVersion, useStaticKey);
TokenInfo idRefreshToken = new TokenInfo(UUID.randomUUID().toString(), newRefreshToken.expiry,
newRefreshToken.createdTime);
@ -644,13 +667,13 @@ public class Session {
Utils.hashSHA256(Utils.hashSHA256(refreshToken)),
System.currentTimeMillis() +
Config.getConfig(tenantIdentifierWithStorage, main).getRefreshTokenValidity(),
sessionInfo.lastUpdatedSign);
sessionInfo.lastUpdatedSign, useStaticKey);
if (!success) {
continue;
}
return refreshSessionHelper(tenantIdentifierWithStorage, main, refreshToken, refreshTokenInfo,
enableAntiCsrf,
accessTokenVersion);
accessTokenVersion, shouldUseStaticKey);
}
throw new TokenTheftDetectedException(sessionHandle, sessionInfo.userId);

View File

@ -242,7 +242,7 @@ public class StorageLayer extends ResourceDistributor.SingletonResource {
}
main.getResourceDistributor().clearAllResourcesWithResourceKey(RESOURCE_KEY);
Set<String> userPoolsInUse = new HashSet<>();
Set<String> uniquePoolsInUse = new HashSet<>();
for (ResourceDistributor.KeyClass key : resourceKeyToStorageMap.keySet()) {
Storage currStorage = resourceKeyToStorageMap.get(key);
@ -259,11 +259,16 @@ public class StorageLayer extends ResourceDistributor.SingletonResource {
main.getResourceDistributor().setResource(key.getTenantIdentifier(), RESOURCE_KEY,
new StorageLayer(resourceKeyToStorageMap.get(key)));
userPoolsInUse.add(userPoolId);
uniquePoolsInUse.add(uniqueId);
}
for (ResourceDistributor.KeyClass key : existingStorageMap.keySet()) {
if (!userPoolsInUse.contains(((StorageLayer) existingStorageMap.get(key)).storage.getUserPoolId())) {
Storage existingStorage = ((StorageLayer) existingStorageMap.get(key)).storage;
String userPoolId = existingStorage.getUserPoolId();
String connectionPoolId = existingStorage.getConnectionPoolId();
String uniqueId = userPoolId + "~" + connectionPoolId;
if (!uniquePoolsInUse.contains(uniqueId)) {
((StorageLayer) existingStorageMap.get(key)).storage.close();
((StorageLayer) existingStorageMap.get(key)).storage.stopLogging();
}

View File

@ -0,0 +1,167 @@
/*
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.supertokens.telemetry;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.supertokens.Main;
import io.supertokens.ResourceDistributor;
import io.supertokens.config.Config;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import org.jetbrains.annotations.TestOnly;
import java.util.concurrent.TimeUnit;
import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME;
public class TelemetryProvider extends ResourceDistributor.SingletonResource {
private static final String RESOURCE_ID = "io.supertokens.telemetry.TelemetryProvider";
private final OpenTelemetry openTelemetry;
private static synchronized TelemetryProvider getInstance(Main main) {
TelemetryProvider instance = null;
try {
instance = (TelemetryProvider) main.getResourceDistributor()
.getResource(TenantIdentifier.BASE_TENANT, RESOURCE_ID);
} catch (TenantOrAppNotFoundException ignored) {
}
return instance;
}
public static void initialize(Main main) {
main.getResourceDistributor()
.setResource(TenantIdentifier.BASE_TENANT, RESOURCE_ID, new TelemetryProvider(main));
}
public static void createLogEvent(Main main, TenantIdentifier tenantIdentifier, String logMessage,
String logLevel) {
getInstance(main).openTelemetry.getTracer("core-tracer")
.spanBuilder(logLevel)
.setParent(Context.current())
.setAttribute("tenant.connectionUriDomain", tenantIdentifier.getConnectionUriDomain())
.setAttribute("tenant.appId", tenantIdentifier.getAppId())
.setAttribute("tenant.tenantId", tenantIdentifier.getTenantId())
.startSpan()
.addEvent("log",
Attributes.builder()
.put("message", logMessage)
.build(),
System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.end();
}
public static Span startSpan(Main main, TenantIdentifier tenantIdentifier, String spanName) {
Span span = getInstance(main).openTelemetry.getTracer("core-tracer")
.spanBuilder(spanName)
.setParent(Context.current())
.setAttribute("tenant.connectionUriDomain", tenantIdentifier.getConnectionUriDomain())
.setAttribute("tenant.appId", tenantIdentifier.getAppId())
.setAttribute("tenant.tenantId", tenantIdentifier.getTenantId())
.startSpan();
span.makeCurrent(); // Set the span as the current context
return span;
}
public static Span endSpan(Span span) {
if (span != null) {
span.end();
}
return span;
}
public static Span addEventToSpan(Span span, String eventName, Attributes attributes) {
if (span != null) {
span.addEvent(eventName, attributes, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
return span;
}
private static OpenTelemetry initializeOpenTelemetry(Main main) {
if (getInstance(main) != null && getInstance(main).openTelemetry != null) {
return getInstance(main).openTelemetry; // already initialized
}
Resource resource = Resource.getDefault().toBuilder()
.put(SERVICE_NAME, "supertokens-core")
.build();
String collectorUri = Config.getBaseConfig(main).getOtelCollectorConnectionURI();
SdkTracerProvider sdkTracerProvider =
SdkTracerProvider.builder()
.setResource(resource)
.addSpanProcessor(SimpleSpanProcessor.create(OtlpGrpcSpanExporter.builder()
.setEndpoint(collectorUri) // otel collector
.build()))
.build();
OpenTelemetrySdk sdk =
OpenTelemetrySdk.builder()
.setTracerProvider(sdkTracerProvider)
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.setLoggerProvider(
SdkLoggerProvider.builder()
.setResource(resource)
.addLogRecordProcessor(
BatchLogRecordProcessor.builder(
OtlpGrpcLogRecordExporter.builder()
.setEndpoint(collectorUri)
.build())
.build())
.build())
.build();
// Add hook to close SDK, which flushes logs
Runtime.getRuntime().addShutdownHook(new Thread(sdk::close));
return sdk;
}
@TestOnly
public static void resetForTest() {
GlobalOpenTelemetry.resetForTest();
}
public static void closeTelemetry(Main main) {
OpenTelemetry telemetry = getInstance(main).openTelemetry;
if (telemetry instanceof OpenTelemetrySdk) {
((OpenTelemetrySdk) telemetry).close();
}
}
private TelemetryProvider(Main main) {
openTelemetry = initializeOpenTelemetry(main);
}
}

View File

@ -258,7 +258,8 @@ public class UserIdMapping {
ArrayList<String> userIds)
throws StorageQueryException {
// userIds are already filtered for a tenant, so this becomes a tenant specific operation.
return tenantIdentifierWithStorage.getUserIdMappingStorage().getUserIdMappingForSuperTokensIds(userIds);
return tenantIdentifierWithStorage.getUserIdMappingStorage().getUserIdMappingForSuperTokensIds(
tenantIdentifierWithStorage.toAppIdentifier(), userIds);
}
@TestOnly

View File

@ -0,0 +1,143 @@
/*
* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.supertokens.webserver;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import io.supertokens.Main;
import io.supertokens.ResourceDistributor;
import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
public class RequestStats extends ResourceDistributor.SingletonResource {
public static final String RESOURCE_KEY = "io.supertokens.webserver.RequestStats";
private final int MAX_MINUTES = 24 * 60;
private long currentMinute; // current minute since epoch
private final int[] currentMinuteRequestCounts; // array of 60 items representing number of requests at each second in the current minute
// The 2 arrays below contains stats for a day for every minute
// the array is stored in such a way that array[currentMinute % MAX_MINUTES] contains the stats for a day ago
// until array[(currentMinute - 1) % MAX_MINUTES] which contains the stats for the last minute, circling around
// from end of array to the beginning
// for e.g. if currentMinute % MAX_MINUTES = 250,
// then array[250] contains stats for now - 1440 minutes
// array[251] contains stats for now - 1439 minutes
// ...
// array[1439] contains stats for now - 1191 minutes
// array[0] contains stats for now - 1190 minutes
// array[1] contains stats for now - 1189 minutes
// ...
// array[249] contains stats for now - 1 minute
private final double[] averageRequestsPerSecond;
private final int[] peakRequestsPerSecond;
private RequestStats() {
currentMinute = System.currentTimeMillis() / 60000;
currentMinuteRequestCounts = new int[60];
averageRequestsPerSecond = new double[MAX_MINUTES];
peakRequestsPerSecond = new int[MAX_MINUTES];
for (int i = 0; i < MAX_MINUTES; i++) {
averageRequestsPerSecond[i] = -1;
peakRequestsPerSecond[i] = -1;
}
}
private void checkAndUpdateMinute(long currentSecond) {
if (currentSecond / 60 == currentMinute) {
return; // stats update not required
}
int sum = 0;
int max = 0;
for (int i = 0; i < 60; i++) {
sum += currentMinuteRequestCounts[i];
max = Math.max(max, currentMinuteRequestCounts[i]);
}
averageRequestsPerSecond[(int) (currentMinute % MAX_MINUTES)] = sum / 60.0;
peakRequestsPerSecond[(int) (currentMinute % MAX_MINUTES)] = max;
// fill zeros for passed minutes
for (long i = currentMinute + 1; i < currentSecond / 60; i++) {
averageRequestsPerSecond[(int) (i % MAX_MINUTES)] = 0;
peakRequestsPerSecond[(int) (i % MAX_MINUTES)] = 0;
}
currentMinute = currentSecond / 60;
for (int i = 0; i < 60; i++) {
currentMinuteRequestCounts[i] = 0;
}
}
private void updateCounts(long currentSecond) {
currentMinuteRequestCounts[(int) (currentSecond % 60)]++;
}
public static RequestStats getInstance(Main main, AppIdentifier appIdentifier) throws TenantOrAppNotFoundException {
try {
return (RequestStats) main.getResourceDistributor()
.getResource(appIdentifier.getAsPublicTenantIdentifier(), RESOURCE_KEY);
} catch (TenantOrAppNotFoundException e) {
// appIdentifier parameter is coming from the API request and hence we need to check if the app exists
// before creating a resource for it, otherwise someone could fill up memory by making requests for apps
// that don't exist.
// The other resources are created during init or while refreshing tenants from the db, so we don't need
// this kind of pattern for those resources.
if (Multitenancy.getTenantInfo(main, appIdentifier.getAsPublicTenantIdentifier()) == null) {
throw e;
}
return (RequestStats) main.getResourceDistributor()
.setResource(appIdentifier.getAsPublicTenantIdentifier(), RESOURCE_KEY, new RequestStats());
}
}
public void updateRequestStats() {
this.updateRequestStats(true);
}
synchronized private void updateRequestStats(boolean updateCounts) {
long now = System.currentTimeMillis() / 1000;
this.checkAndUpdateMinute(now);
if (updateCounts) { this.updateCounts(now); }
}
public JsonObject getStats() {
this.updateRequestStats(false);
JsonArray avgRps = new JsonArray();
JsonArray peakRps = new JsonArray();
long atMinute = System.currentTimeMillis() / 60000;
int offset = (int) (atMinute % MAX_MINUTES);
for (int i = 0; i < MAX_MINUTES; i++) {
avgRps.add(new JsonPrimitive(this.averageRequestsPerSecond[(i + offset) % MAX_MINUTES]));
peakRps.add(new JsonPrimitive(this.peakRequestsPerSecond[(i + offset) % MAX_MINUTES]));
}
JsonObject result = new JsonObject();
result.addProperty("atMinute", atMinute);
result.add("averageRequestsPerSecond", avgRps);
result.add("peakRequestsPerSecond", peakRps);
return result;
}
}

View File

@ -250,6 +250,8 @@ public class Webserver extends ResourceDistributor.SingletonResource {
addAPI(new AssociateUserToTenantAPI(main));
addAPI(new DisassociateUserFromTenant(main));
addAPI(new RequestStatsAPI(main));
StandardContext context = tomcatReference.getContext();
Tomcat tomcat = tomcatReference.getTomcat();

View File

@ -24,11 +24,13 @@ import io.supertokens.config.Config;
import io.supertokens.config.CoreConfig;
import io.supertokens.exceptions.QuitProgramException;
import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
import io.supertokens.multitenancy.MultitenancyHelper;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.output.Logging;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
@ -286,15 +288,18 @@ public abstract class WebserverAPI extends HttpServlet {
String connectionUriDomain = req.getServerName();
connectionUriDomain = Utils.normalizeAndValidateConnectionUriDomain(connectionUriDomain, false);
try {
if (Config.getConfig(new TenantIdentifier(connectionUriDomain, null, null), main) ==
Config.getConfig(new TenantIdentifier(null, null, null), main)) {
return null;
if (MultitenancyHelper.getInstance(main).isConnectionUriDomainPresentInDb(connectionUriDomain)) {
CoreConfig baseConfig = Config.getBaseConfig(main);
if (baseConfig.getSuperTokensLoadOnlyCUD() != null) {
if (!connectionUriDomain.equals(baseConfig.getSuperTokensLoadOnlyCUD())) {
throw new ServletException(new BadRequestException("Connection URI domain is disallowed"));
}
}
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
return connectionUriDomain;
}
return connectionUriDomain;
return null;
}
@TestOnly
@ -338,6 +343,15 @@ public abstract class WebserverAPI extends HttpServlet {
storage, storages);
}
protected AppIdentifierWithStorage getPublicTenantStorage(HttpServletRequest req)
throws ServletException, TenantOrAppNotFoundException {
AppIdentifier appIdentifier = new AppIdentifier(this.getConnectionUriDomain(req), this.getAppId(req));
Storage storage = StorageLayer.getStorage(appIdentifier.getAsPublicTenantIdentifier(), main);
return appIdentifier.withStorage(storage);
}
protected TenantIdentifierWithStorageAndUserIdMapping getTenantIdentifierWithStorageAndUserIdMappingFromRequest(
HttpServletRequest req, String userId, UserIdType userIdType)
throws StorageQueryException, TenantOrAppNotFoundException, UnknownUserIdException, ServletException {
@ -480,6 +494,13 @@ public abstract class WebserverAPI extends HttpServlet {
}
Logging.info(main, tenantIdentifier, "API ended: " + req.getRequestURI() + ". Method: " + req.getMethod(),
false);
if (tenantIdentifier != null) {
try {
RequestStats.getInstance(main, tenantIdentifier.toAppIdentifier()).updateRequestStats();
} catch (TenantOrAppNotFoundException e) {
// Ignore the error as we would have already sent the response for tenantNotFound
}
}
}
protected String getRIDFromRequest(HttpServletRequest req) {

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2020, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.supertokens.webserver.api.core;
import com.google.gson.JsonObject;
import io.supertokens.Main;
import io.supertokens.cliOptions.CLIOptions;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.webserver.InputParser;
import io.supertokens.webserver.RequestStats;
import io.supertokens.webserver.WebserverAPI;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
public class RequestStatsAPI extends WebserverAPI {
private static final long serialVersionUID = -4641988458637882374L;
public RequestStatsAPI(Main main) {
super(main, "");
}
@Override
public String getPath() {
return "/requests/stats";
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
// API is app specific
try {
AppIdentifier appIdentifier = getAppIdentifierWithStorageFromRequestAndEnforcePublicTenant(req);
JsonObject stats = RequestStats.getInstance(main, appIdentifier).getStats();
stats.addProperty("status", "OK");
super.sendJsonResponse(200, stats, resp);
} catch (BadPermissionException | TenantOrAppNotFoundException e) {
throw new ServletException(e);
}
}
}

View File

@ -79,7 +79,7 @@ public class SignInAPI extends WebserverAPI {
try {
UserInfo user = EmailPassword.signIn(tenantIdentifierWithStorage, super.main, normalisedEmail, password);
ActiveUsers.updateLastActive(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), main, user.id); // use the internal user id
ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, user.id); // use the internal user id
// if a userIdMapping exists, pass the externalUserId to the response
UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(

View File

@ -81,7 +81,7 @@ public class SignUpAPI extends WebserverAPI {
try {
UserInfo user = EmailPassword.signUp(this.getTenantIdentifierWithStorageFromRequest(req), super.main, normalisedEmail, password);
ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, user.id);
ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, user.id);
JsonObject result = new JsonObject();
result.addProperty("status", "OK");

View File

@ -19,6 +19,8 @@ package io.supertokens.webserver.api.multitenancy;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.supertokens.Main;
import io.supertokens.config.Config;
import io.supertokens.config.CoreConfig;
import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.multitenancy.exception.BadPermissionException;
@ -49,6 +51,14 @@ public abstract class BaseCreateOrUpdate extends WebserverAPI {
HttpServletResponse resp)
throws ServletException, IOException {
CoreConfig baseConfig = Config.getBaseConfig(main);
if (baseConfig.getSuperTokensLoadOnlyCUD() != null) {
if (!(targetTenantIdentifier.getConnectionUriDomain().equals(TenantIdentifier.DEFAULT_CONNECTION_URI) || targetTenantIdentifier.getConnectionUriDomain().equals(baseConfig.getSuperTokensLoadOnlyCUD()))) {
throw new ServletException(new BadRequestException("Creation of connection uri domain or app or " +
"tenant is disallowed"));
}
}
TenantConfig tenantConfig = Multitenancy.getTenantInfo(main,
new TenantIdentifier(targetTenantIdentifier.getConnectionUriDomain(), targetTenantIdentifier.getAppId(),
targetTenantIdentifier.getTenantId()));

View File

@ -87,7 +87,7 @@ public class ConsumeCodeAPI extends WebserverAPI {
deviceId, deviceIdHash,
userInputCode, linkCode);
ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, consumeCodeResponse.user.id);
ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, consumeCodeResponse.user.id);
UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(
this.getAppIdentifierWithStorage(req),

View File

@ -61,10 +61,13 @@ public class RefreshSessionAPI extends WebserverAPI {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
// API is app specific, but session is updated based on tenantId obtained from the refreshToken
SemVer version = super.getVersionFromRequest(req);
JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
String refreshToken = InputParser.parseStringOrThrowError(input, "refreshToken", false);
String antiCsrfToken = InputParser.parseStringOrThrowError(input, "antiCsrfToken", true);
Boolean enableAntiCsrf = InputParser.parseBooleanOrThrowError(input, "enableAntiCsrf", false);
Boolean useDynamicSigningKey = version.greaterThanOrEqualTo(SemVer.v3_0) ?
InputParser.parseBooleanOrThrowError(input, "useDynamicSigningKey", true) : null;
assert enableAntiCsrf != null;
assert refreshToken != null;
@ -75,13 +78,13 @@ public class RefreshSessionAPI extends WebserverAPI {
throw new ServletException(e);
}
SemVer version = super.getVersionFromRequest(req);
try {
AccessToken.VERSION accessTokenVersion = AccessToken.getAccessTokenVersionForCDI(version);
SessionInformationHolder sessionInfo = Session.refreshSession(appIdentifierWithStorage, main,
refreshToken, antiCsrfToken,
enableAntiCsrf, accessTokenVersion);
enableAntiCsrf, accessTokenVersion,
useDynamicSigningKey == null ? null : Boolean.FALSE.equals(useDynamicSigningKey));
if (StorageLayer.getStorage(this.getTenantIdentifierWithStorageFromRequest(req), main).getType() ==
STORAGE_TYPE.SQL) {
@ -90,10 +93,10 @@ public class RefreshSessionAPI extends WebserverAPI {
this.getAppIdentifierWithStorage(req),
sessionInfo.session.userId, UserIdType.ANY);
if (userIdMapping != null) {
ActiveUsers.updateLastActive(appIdentifierWithStorage, main,
ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main,
userIdMapping.superTokensUserId);
} else {
ActiveUsers.updateLastActive(appIdentifierWithStorage, main,
ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main,
sessionInfo.session.userId);
}
} catch (StorageQueryException ignored) {

View File

@ -111,10 +111,10 @@ public class SessionAPI extends WebserverAPI {
this.getAppIdentifierWithStorage(req),
sessionInfo.session.userId, UserIdType.ANY);
if (userIdMapping != null) {
ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main,
ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main,
userIdMapping.superTokensUserId);
} else {
ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main,
ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main,
sessionInfo.session.userId);
}
} catch (StorageQueryException ignored) {

View File

@ -105,10 +105,10 @@ public class SessionRemoveAPI extends WebserverAPI {
this.getAppIdentifierWithStorage(req),
userId, UserIdType.ANY);
if (userIdMapping != null) {
ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main,
ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main,
userIdMapping.superTokensUserId);
} else {
ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, userId);
ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, userId);
}
} catch (StorageQueryException ignored) {
}

View File

@ -78,7 +78,7 @@ public class SignInUpAPI extends WebserverAPI {
thirdPartyId,
thirdPartyUserId, email, isEmailVerified);
ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.id);
ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, response.user.id);
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
@ -118,7 +118,7 @@ public class SignInUpAPI extends WebserverAPI {
this.getTenantIdentifierWithStorageFromRequest(req), super.main, thirdPartyId, thirdPartyUserId,
email);
ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.id);
ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, response.user.id);
//
io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = UserIdMapping

View File

@ -4,10 +4,15 @@ import com.google.gson.JsonObject;
import io.supertokens.ActiveUsers;
import io.supertokens.Main;
import io.supertokens.ProcessState;
import io.supertokens.featureflag.EE_FEATURES;
import io.supertokens.featureflag.FeatureFlagTestContent;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.test.httpRequest.HttpRequestForTesting;
import io.supertokens.test.httpRequest.HttpResponseException;
import io.supertokens.test.multitenant.api.TestMultitenancyAPIHelper;
import io.supertokens.utils.SemVer;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
@ -212,4 +217,80 @@ public class ActiveUsersTest {
assert res.get("count").getAsInt() == 2;
}
@Test
public void testThatActiveUserDataIsSavedInPublicTenantStorage() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
FeatureFlagTestContent.getInstance(process.getProcess())
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY});
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}
{ // Create a tenant
JsonObject coreConfig = new JsonObject();
StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())
.modifyConfigToAddANewUserPoolForTesting(coreConfig, 1);
TestMultitenancyAPIHelper.createTenant(
process.getProcess(),
new TenantIdentifier(null, null, null),
"t1", true, true, true,
coreConfig);
}
{ // no active users yet
HashMap<String, String> params = new HashMap<>();
params.put("since", "0");
JsonObject res = HttpRequestForTesting.sendGETRequest(
process.getProcess(),
"",
"http://localhost:3567/users/count/active",
params,
1000,
1000,
null,
Utils.getCdiVersionStringLatestForTests(),
"");
assert res.get("status").getAsString().equals("OK");
assert res.get("count").getAsInt() == 0;
}
{ // Sign up, which updates active users
JsonObject responseBody = new JsonObject();
responseBody.addProperty("email", "random@gmail.com");
responseBody.addProperty("password", "validPass123");
JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/t1/recipe/signup", responseBody, 1000, 1000, null, SemVer.v3_0.get(),
"emailpassword");
}
{ // 1 active user in the public tenant
HashMap<String, String> params = new HashMap<>();
params.put("since", "0");
JsonObject res = HttpRequestForTesting.sendGETRequest(
process.getProcess(),
"",
"http://localhost:3567/users/count/active",
params,
1000,
1000,
null,
Utils.getCdiVersionStringLatestForTests(),
"");
assert res.get("status").getAsString().equals("OK");
assert res.get("count").getAsInt() == 1;
}
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
}

View File

@ -33,6 +33,7 @@ import io.supertokens.session.Session;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.thirdparty.ThirdParty;
import io.supertokens.usermetadata.UserMetadata;
import io.supertokens.version.Version;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
@ -488,6 +489,8 @@ public class AuthRecipeTest {
fail();
}
boolean isMySQL = Version.getVersion(process.getProcess()).getPluginName().equals("mysql");
for (int limit : limits) {
// now we paginate in asc order
@ -497,6 +500,9 @@ public class AuthRecipeTest {
if (o1.timeJoined != o2.timeJoined) {
return (int) (o1.timeJoined - o2.timeJoined);
}
if (isMySQL) {
return o1.id.compareTo(o2.id);
}
return o2.id.compareTo(o1.id);
});

View File

@ -43,6 +43,7 @@ import org.junit.rules.TestRule;
import org.reflections.Reflections;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.*;
@ -308,6 +309,49 @@ public class CronjobTest {
}
}
static class CounterCronJob extends CronTask {
private static final String RESOURCE_ID = "io.supertokens.test.CronjobTest.CounterCronJob";
private static AtomicInteger count = new AtomicInteger();
private CounterCronJob(Main main, List<List<TenantIdentifier>> tenantsInfo) {
super("CounterCronJob", main, tenantsInfo, false);
}
public static CounterCronJob getInstance(Main main) {
try {
return (CounterCronJob) main.getResourceDistributor()
.getResource(new TenantIdentifier(null, null, null), RESOURCE_ID);
} catch (TenantOrAppNotFoundException e) {
List<TenantIdentifier> tenants = new ArrayList<>();
tenants.add(new TenantIdentifier(null, null, null));
List<List<TenantIdentifier>> finalList = new ArrayList<>();
finalList.add(tenants);
return (CounterCronJob) main.getResourceDistributor()
.setResource(new TenantIdentifier(null, null, null), RESOURCE_ID,
new CounterCronJob(main, finalList));
}
}
@Override
public int getIntervalTimeSeconds() {
return 1;
}
@Override
public int getInitialWaitTimeSeconds() {
return 0;
}
@Override
protected void doTaskPerStorage(Storage storage) throws Exception {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
@Rule
public TestRule watchman = Utils.getOnFailure();
@ -780,6 +824,122 @@ public class CronjobTest {
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void testThatCronJobsHaveTenantsInfoAfterRestart() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
FeatureFlagTestContent.getInstance(process.getProcess())
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY});
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}
if (StorageLayer.isInMemDb(process.getProcess())) {
return;
}
JsonObject coreConfig = new JsonObject();
StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())
.modifyConfigToAddANewUserPoolForTesting(coreConfig, 1);
// create CUD and apps
Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig(
new TenantIdentifier("127.0.0.1", null, null),
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
coreConfig
), false, false, true);
Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig(
new TenantIdentifier("127.0.0.1", "a1", null),
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
coreConfig
), false, false, true);
Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig(
new TenantIdentifier("127.0.0.1", "a2", null),
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
coreConfig
), false, false, true);
Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig(
new TenantIdentifier("127.0.0.1", "a3", null),
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
coreConfig
), false, false, true);
{
List<List<List<TenantIdentifier>>> tenantsInfos = Cronjobs.getInstance(process.getProcess()).getTenantInfos();
assertEquals(10, tenantsInfos.size());
int count = 0;
for (List<List<TenantIdentifier>> tenantsInfo : tenantsInfos) {
if (tenantsInfo != null) {
assertEquals(2, tenantsInfo.size());
assertEquals(1, tenantsInfo.get(0).size());
assertEquals(4, tenantsInfo.get(1).size());
count++;
}
}
assertEquals(9, count);
}
process.kill(false);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
process = TestingProcessManager.start(args, false);
FeatureFlagTestContent.getInstance(process.getProcess())
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY});
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
// we expect the state of the tenantsInfo to be same after core restart
{
List<List<List<TenantIdentifier>>> tenantsInfos = Cronjobs.getInstance(process.getProcess()).getTenantInfos();
assertEquals(10, tenantsInfos.size());
int count = 0;
for (List<List<TenantIdentifier>> tenantsInfo : tenantsInfos) {
if (tenantsInfo != null) {
assertEquals(2, tenantsInfo.size());
assertEquals(1, tenantsInfo.get(0).size());
assertEquals(4, tenantsInfo.get(1).size());
count++;
}
}
assertEquals(9, count);
}
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void testThatReAddingSameCronTaskDoesNotScheduleMoreExecutors() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
for (int i=0; i<10; i++) {
Cronjobs.addCronjob(process.getProcess(), CounterCronJob.getInstance(process.getProcess()));
Thread.sleep(50);
}
Thread.sleep(5000);
assertTrue(CounterCronJob.getInstance(process.getProcess()).getCount() > 3 && CounterCronJob.getInstance(process.getProcess()).getCount() < 8);
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void testThatNoCronJobIntervalIsMoreThanADay() throws Exception {
String[] args = {"../"};

View File

@ -93,7 +93,7 @@ public class FeatureFlagTest {
JsonObject stats = FeatureFlag.getInstance(process.getProcess()).getPaidFeatureStats();
Assert.assertEquals(stats.entrySet().size(), 1);
Assert.assertEquals(stats.get("maus").getAsJsonArray().size(), 30);
Assert.assertEquals(stats.get("maus").getAsJsonArray().size(), 31);
Assert.assertEquals(stats.get("maus").getAsJsonArray().get(0).getAsInt(), 0);
Assert.assertEquals(stats.get("maus").getAsJsonArray().get(29).getAsInt(), 0);
@ -188,7 +188,7 @@ public class FeatureFlagTest {
assert features.size() == 1;
}
assert features.contains(new JsonPrimitive("totp"));
assert maus.size() == 30;
assert maus.size() == 31;
assert maus.get(0).getAsInt() == 0;
assert maus.get(29).getAsInt() == 0;
@ -196,7 +196,7 @@ public class FeatureFlagTest {
JsonArray totpMaus = totpStats.get("maus").getAsJsonArray();
int totalTotpUsers = totpStats.get("total_users").getAsInt();
assert totpMaus.size() == 30;
assert totpMaus.size() == 31;
assert totpMaus.get(0).getAsInt() == 0;
assert totpMaus.get(29).getAsInt() == 0;
@ -247,7 +247,7 @@ public class FeatureFlagTest {
}
assert features.contains(new JsonPrimitive("totp"));
assert maus.size() == 30;
assert maus.size() == 31;
assert maus.get(0).getAsInt() == 2; // 2 users have signed up
assert maus.get(29).getAsInt() == 2;
@ -255,7 +255,7 @@ public class FeatureFlagTest {
JsonArray totpMaus = totpStats.get("maus").getAsJsonArray();
int totalTotpUsers = totpStats.get("total_users").getAsInt();
assert totpMaus.size() == 30;
assert totpMaus.size() == 31;
assert totpMaus.get(0).getAsInt() == 1; // only 1 user has TOTP enabled
assert totpMaus.get(29).getAsInt() == 1;

View File

@ -1533,7 +1533,9 @@ public class PathRouterTest extends Mockito {
public void tenantNotFoundTest3()
throws InterruptedException, IOException, io.supertokens.httpRequest.HttpResponseException,
InvalidConfigException,
io.supertokens.test.httpRequest.HttpResponseException, TenantOrAppNotFoundException {
io.supertokens.test.httpRequest.HttpResponseException, TenantOrAppNotFoundException,
InvalidProviderConfigException, StorageQueryException, FeatureNotEnabledException,
CannotModifyBaseConfigException, BadPermissionException {
String[] args = {"../"};
Utils.setValueInConfig("host", "\"0.0.0.0\"");
@ -1556,15 +1558,26 @@ public class PathRouterTest extends Mockito {
StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())
.modifyConfigToAddANewUserPoolForTesting(tenantConfig, 2);
Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{
new TenantConfig(new TenantIdentifier("localhost", null, null), new EmailPasswordConfig(false),
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantConfig(
new TenantIdentifier("localhost", null, null),
new EmailPasswordConfig(false),
new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]),
new PasswordlessConfig(false),
tenantConfig),
new TenantConfig(new TenantIdentifier("localhost", null, "t1"), new EmailPasswordConfig(false),
false
);
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantConfig(
new TenantIdentifier("localhost", null, "t1"),
new EmailPasswordConfig(false),
new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]),
new PasswordlessConfig(false),
tenantConfig)}, new ArrayList<>());
tenantConfig),
false
);
Webserver.getInstance(process.getProcess()).addAPI(new WebserverAPI(process.getProcess(), "") {
@ -2788,7 +2801,9 @@ public class PathRouterTest extends Mockito {
public void tenantNotFoundWithAppIdTest3()
throws InterruptedException, IOException, io.supertokens.httpRequest.HttpResponseException,
InvalidConfigException,
io.supertokens.test.httpRequest.HttpResponseException, TenantOrAppNotFoundException {
io.supertokens.test.httpRequest.HttpResponseException, TenantOrAppNotFoundException,
InvalidProviderConfigException, StorageQueryException, FeatureNotEnabledException,
CannotModifyBaseConfigException, BadPermissionException {
String[] args = {"../"};
Utils.setValueInConfig("host", "\"0.0.0.0\"");
@ -2811,15 +2826,26 @@ public class PathRouterTest extends Mockito {
StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())
.modifyConfigToAddANewUserPoolForTesting(tenantConfig, 2);
Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{
new TenantConfig(new TenantIdentifier("localhost", null, null), new EmailPasswordConfig(false),
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantConfig(
new TenantIdentifier("localhost", null, null),
new EmailPasswordConfig(false),
new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]),
new PasswordlessConfig(false),
tenantConfig),
new TenantConfig(new TenantIdentifier("localhost", "app1", "t1"), new EmailPasswordConfig(false),
false
);
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantConfig(
new TenantIdentifier("localhost", "app1", "t1"),
new EmailPasswordConfig(false),
new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]),
new PasswordlessConfig(false),
tenantConfig)}, new ArrayList<>());
tenantConfig),
false
);
Webserver.getInstance(process.getProcess()).addAPI(new WebserverAPI(process.getProcess(), "") {
@ -2875,4 +2901,4 @@ public class PathRouterTest extends Mockito {
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
}
}

View File

@ -0,0 +1,289 @@
/*
* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.supertokens.test;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.supertokens.ProcessState;
import io.supertokens.featureflag.EE_FEATURES;
import io.supertokens.featureflag.FeatureFlagTestContent;
import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.multitenancy.*;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.test.httpRequest.HttpRequestForTesting;
import io.supertokens.test.httpRequest.HttpResponseException;
import io.supertokens.test.multitenant.api.TestMultitenancyAPIHelper;
import io.supertokens.webserver.RequestStats;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.*;
public class RequestStatsTest {
@Rule
public TestRule watchman = Utils.getOnFailure();
@AfterClass
public static void afterTesting() {
Utils.afterTesting();
}
@Before
public void beforeEach() {
Utils.reset();
}
@Test
public void testLastMinuteStats() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}
// Wait for a minute to pass
Thread.sleep(60000 - (System.currentTimeMillis() % 60000) + 100);
ExecutorService ex = Executors.newFixedThreadPool(100);
int numRequests = 1000;
for (int i = 0; i < numRequests; i++) {
int finalI = i;
ex.execute(() -> {
try {
TestMultitenancyAPIHelper.epSignUp(TenantIdentifier.BASE_TENANT, "test" + finalI + "@example.com", "password", process.getProcess());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
ex.shutdown();
ex.awaitTermination(45, TimeUnit.SECONDS); // should finish in 45 seconds
// Wait for a minute to pass
Thread.sleep(60000 - (System.currentTimeMillis() % 60000) + 100);
JsonObject stats = HttpRequestForTesting
.sendGETRequest(process.getProcess(), "", "http://localhost:3567/requests/stats", null, 5000,
5000, null, Utils.getCdiVersionStringLatestForTests(), null);
JsonArray avgRps = stats.get("averageRequestsPerSecond").getAsJsonArray();
JsonArray peakRps = stats.get("peakRequestsPerSecond").getAsJsonArray();
double avg = 10000;
int count = 0;
for (JsonElement e : avgRps) {
if (e.getAsDouble() == -1) {
count++;
} else {
assertEquals(numRequests, Math.round(e.getAsDouble() * 60));
avg = e.getAsDouble();
}
}
assertEquals(1439, count);
count = 0;
for (JsonElement e : peakRps) {
if (e.getAsInt() == -1) {
count++;
} else {
assertTrue(e.getAsInt() > avg);
}
}
assertEquals(1439, count);
assertEquals(System.currentTimeMillis() / 60000, stats.get("atMinute").getAsLong());
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void testLastMinuteStatsPerApp() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
FeatureFlagTestContent.getInstance(process.getProcess())
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
EE_FEATURES.MULTI_TENANCY});
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}
Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig(
new TenantIdentifier(null, "a1", null),
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
new JsonObject()
), false);
// Wait for a minute to pass
Thread.sleep(60000 - (System.currentTimeMillis() % 60000) + 100);
ExecutorService ex = Executors.newFixedThreadPool(100);
int numRequests = 500;
for (int i = 0; i < numRequests; i++) {
int finalI = i;
ex.execute(() -> {
try {
TestMultitenancyAPIHelper.epSignUp(TenantIdentifier.BASE_TENANT, "test" + finalI + "@example.com", "password", process.getProcess());
} catch (Exception e) {
// ignore
}
if (finalI < 400) {
try {
TestMultitenancyAPIHelper.epSignUp(new TenantIdentifier(null, "a1", null), "test" + finalI + "@example.com", "password", process.getProcess());
} catch (Exception e) {
// ignore
}
}
});
}
ex.shutdown();
ex.awaitTermination(45, TimeUnit.SECONDS); // should finish in 45 seconds
// Wait for a minute to pass
Thread.sleep(60000 - (System.currentTimeMillis() % 60000) + 100);
{
JsonObject stats = HttpRequestForTesting
.sendGETRequest(process.getProcess(), "", "http://localhost:3567/requests/stats", null, 5000,
5000, null, Utils.getCdiVersionStringLatestForTests(), null);
JsonArray avgRps = stats.get("averageRequestsPerSecond").getAsJsonArray();
JsonArray peakRps = stats.get("peakRequestsPerSecond").getAsJsonArray();
double avg = 10000;
int count = 0;
for (JsonElement e : avgRps) {
if (e.getAsDouble() == -1) {
count++;
} else {
assertEquals(numRequests, Math.round(e.getAsDouble() * 60));
avg = e.getAsDouble();
}
}
assertEquals(1439, count);
count = 0;
for (JsonElement e : peakRps) {
if (e.getAsInt() == -1) {
count++;
} else {
assertTrue(e.getAsInt() > avg);
}
}
assertEquals(1439, count);
assertEquals(System.currentTimeMillis() / 60000, stats.get("atMinute").getAsLong());
}
{
JsonObject stats = HttpRequestForTesting
.sendGETRequest(process.getProcess(), "", "http://localhost:3567/appid-a1/requests/stats", null, 1000,
1000, null, Utils.getCdiVersionStringLatestForTests(), null);
JsonArray avgRps = stats.get("averageRequestsPerSecond").getAsJsonArray();
JsonArray peakRps = stats.get("peakRequestsPerSecond").getAsJsonArray();
double avg = 10000;
int count = 0;
for (JsonElement e : avgRps) {
if (e.getAsDouble() == -1) {
count++;
} else {
assertEquals(400, Math.round(e.getAsDouble() * 60));
avg = e.getAsDouble();
}
}
assertEquals(1439, count);
count = 0;
for (JsonElement e : peakRps) {
if (e.getAsInt() == -1) {
count++;
} else {
assertTrue(e.getAsInt() > avg);
}
}
assertEquals(1439, count);
assertEquals(System.currentTimeMillis() / 60000, stats.get("atMinute").getAsLong());
}
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void testWithNonExistantApp() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
FeatureFlagTestContent.getInstance(process.getProcess())
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
EE_FEATURES.MULTI_TENANCY});
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}
try {
RequestStats.getInstance(process.getProcess(), new AppIdentifier(null, "a1"));
fail();
} catch (TenantOrAppNotFoundException e) {
// ok
}
try {
JsonObject stats = HttpRequestForTesting
.sendGETRequest(process.getProcess(), "", "http://localhost:3567/appid-a1/requests/stats", null, 1000,
1000, null, Utils.getCdiVersionStringLatestForTests(), null);
fail();
} catch (HttpResponseException e) {
assertEquals(400, e.statusCode);
assertEquals("Http error. Status Code: 400. Message: AppId or tenantId not found => Tenant with the following connectionURIDomain, appId and tenantId combination not found: (, a1, public)", e.getMessage());
}
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
}

View File

@ -94,7 +94,7 @@ public class StorageLayerTest {
// This error will be different in Postgres and MySQL
// We added (CHECK (LENGTH(code) <= 8)) to the table definition in SQLite
String totpUsedCodeTable = Config.getConfig(start).getTotpUsedCodesTable();
assert e.getMessage().contains("CHECK constraint failed: " + totpUsedCodeTable);
assert e.getMessage().contains("CHECK constraint failed: ");
}
// Try code with length < 8

View File

@ -21,7 +21,10 @@ import com.google.gson.JsonParser;
import io.supertokens.ProcessState;
import io.supertokens.ProcessState.PROCESS_STATE;
import io.supertokens.cronjobs.telemetry.Telemetry;
import io.supertokens.dashboard.Dashboard;
import io.supertokens.httpRequest.HttpRequestMocking;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.test.TestingProcessManager.TestingProcess;
import io.supertokens.version.Version;
import org.junit.AfterClass;
@ -111,6 +114,15 @@ public class TelemetryTest extends Mockito {
String[] args = { "../" };
TestingProcess process = TestingProcessManager.start(args, false);
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
if (StorageLayer.getBaseStorage(process.getProcess()).getType() == STORAGE_TYPE.SQL) {
Dashboard.signUpDashboardUser(process.getProcess(), "test@example.com", "password123");
}
// Restarting the process to send telemetry again
process.kill(false);
process = TestingProcessManager.start(args, false);
ByteArrayOutputStream output = new ByteArrayOutputStream();
final HttpURLConnection mockCon = mock(HttpURLConnection.class);
@ -149,13 +161,26 @@ public class TelemetryTest extends Mockito {
assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.SENT_TELEMETRY));
JsonObject telemetryData = new JsonParser().parse(output.toString()).getAsJsonObject();
assertEquals(7, telemetryData.entrySet().size());
assertTrue(telemetryData.has("telemetryId"));
assertEquals(telemetryData.get("superTokensVersion").getAsString(),
Version.getVersion(process.getProcess()).getCoreVersion());
assertEquals(telemetryData.get("appId").getAsString(), "public");
assertEquals(telemetryData.get("connectionUriDomain").getAsString(), "");
assertTrue(telemetryData.has("mau"));
assertTrue(telemetryData.has("maus"));
assertTrue(telemetryData.has("dashboardUserEmails"));
if (StorageLayer.getBaseStorage(process.getProcess()).getType() == STORAGE_TYPE.SQL) {
assertEquals(1, telemetryData.get("dashboardUserEmails").getAsJsonArray().size());
assertEquals("test@example.com", telemetryData.get("dashboardUserEmails").getAsJsonArray().get(0).getAsString());
assertEquals(31, telemetryData.get("maus").getAsJsonArray().size());
assertEquals(0, telemetryData.get("usersCount").getAsInt());
} else {
assertEquals(0, telemetryData.get("dashboardUserEmails").getAsJsonArray().size());
assertEquals(0, telemetryData.get("maus").getAsJsonArray().size());
assertEquals(-1, telemetryData.get("usersCount").getAsInt());
}
process.kill();
assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STOPPED));

View File

@ -0,0 +1,159 @@
/*
* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.supertokens.test;
import io.supertokens.ProcessState;
import io.supertokens.featureflag.EE_FEATURES;
import io.supertokens.featureflag.FeatureFlagTestContent;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.thirdparty.ThirdParty;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.junit.Assert.assertNotNull;
public class TestGetUserSpeed {
@Rule
public TestRule watchman = Utils.getOnFailure();
@AfterClass
public static void afterTesting() {
Utils.afterTesting();
}
@Before
public void beforeEach() {
Utils.reset();
}
public void testUserCreationLinkingAndGetByIdSpeedsCommon(TestingProcessManager.TestingProcess process,
long createTime, long getTime) throws Exception {
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}
if (StorageLayer.isInMemDb(process.getProcess())) {
return;
}
int numberOfUsers = 10000;
List<String> userIds = new ArrayList<>();
List<String> userIds2 = new ArrayList<>();
Lock lock = new ReentrantLock();
{
ExecutorService es = Executors.newFixedThreadPool(32);
long start = System.currentTimeMillis();
for (int i = 0; i < numberOfUsers; i++) {
int finalI = i;
es.execute(() -> {
try {
String email = "user" + finalI + "@example.com";
AuthRecipeUserInfo user = ThirdParty.signInUp(
process.getProcess(), "google", "googleid" + finalI, email).user;
lock.lock();
userIds.add(user.id);
userIds2.add(user.id);
lock.unlock();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
es.shutdown();
es.awaitTermination(5, TimeUnit.MINUTES);
long end = System.currentTimeMillis();
System.out.println("Created users " + numberOfUsers + " in " + (end - start) + "ms");
assert end - start < createTime; // 25 sec
}
Thread.sleep(10000); // wait for index
Thread.sleep(10000); // wait for index
{
ExecutorService es = Executors.newFixedThreadPool(32);
long start = System.currentTimeMillis();
for (String userId : userIds2) {
es.execute(() -> {
try {
ThirdParty.getUser(process.getProcess(), userId);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
es.shutdown();
es.awaitTermination(5, TimeUnit.MINUTES);
long end = System.currentTimeMillis();
System.out.println("Time taken for " + numberOfUsers + " users: " + (end - start) + "ms");
assert end - start < getTime; // 20 sec
}
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void testUserCreationLinkingAndGetByIdSpeedsWithoutMinIdle() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
Utils.setValueInConfig("postgresql_connection_pool_size", "100");
Utils.setValueInConfig("mysql_connection_pool_size", "100");
FeatureFlagTestContent.getInstance(process.getProcess())
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
testUserCreationLinkingAndGetByIdSpeedsCommon(process, 25000, 5000);
}
@Test
public void testUserCreationLinkingAndGetByIdSpeedsWithMinIdle() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
Utils.setValueInConfig("postgresql_connection_pool_size", "100");
Utils.setValueInConfig("mysql_connection_pool_size", "100");
Utils.setValueInConfig("postgresql_minimum_idle_connections", "1");
Utils.setValueInConfig("mysql_minimum_idle_connections", "1");
FeatureFlagTestContent.getInstance(process.getProcess())
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
testUserCreationLinkingAndGetByIdSpeedsCommon(process, 60000, 5000);
}
}

View File

@ -22,6 +22,7 @@ import io.supertokens.Main;
import io.supertokens.pluginInterface.PluginInterfaceTesting;
import io.supertokens.pluginInterface.useridmapping.UserIdMapping;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.telemetry.TelemetryProvider;
import io.supertokens.test.httpRequest.HttpRequestForTesting;
import io.supertokens.test.httpRequest.HttpResponseException;
import io.supertokens.useridmapping.UserIdType;
@ -69,6 +70,8 @@ public abstract class Utils extends Mockito {
} catch (Exception ignored) {
}
TelemetryProvider.resetForTest();
} catch (Exception e) {
e.printStackTrace();
}

View File

@ -20,6 +20,8 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.supertokens.ProcessState;
import io.supertokens.authRecipe.AuthRecipe;
import io.supertokens.authRecipe.UserPaginationContainer;
import io.supertokens.emailpassword.EmailPassword;
import io.supertokens.featureflag.EE_FEATURES;
import io.supertokens.featureflag.FeatureFlagTestContent;
@ -30,6 +32,7 @@ import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException;
import io.supertokens.passwordless.Passwordless;
import io.supertokens.passwordless.exceptions.*;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.emailpassword.UserInfo;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
import io.supertokens.pluginInterface.exceptions.InvalidConfigException;
@ -38,6 +41,7 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicExceptio
import io.supertokens.pluginInterface.multitenancy.*;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException;
import io.supertokens.pluginInterface.thirdparty.sqlStorage.ThirdPartySQLStorage;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.test.TestingProcessManager;
import io.supertokens.test.Utils;
@ -325,4 +329,66 @@ public class UserPaginationTest {
}
}
}
@Test
public void testUserPaginationWithSameTimeJoined() throws Exception {
if (StorageLayer.getBaseStorage(process.main).getType() != STORAGE_TYPE.SQL) {
return;
}
ThirdPartySQLStorage storage = (ThirdPartySQLStorage) StorageLayer.getBaseStorage(process.getProcess());
Set<String> userIds = new HashSet<>();
long timeJoined = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
String userId = io.supertokens.utils.Utils.getUUID();
storage.signUp(TenantIdentifier.BASE_TENANT, userId, "test"+i+"@example.com", new io.supertokens.pluginInterface.thirdparty.UserInfo.ThirdParty("google", userId), timeJoined);
userIds.add(userId);
}
// Test ascending
{
Set<String> paginationUserIds = new HashSet<>();
UserPaginationContainer usersRes = AuthRecipe.getUsers(process.getProcess(), 10,
"ASC", null, null, null);
while (true) {
for (UserPaginationContainer.UsersContainer user : usersRes.users) {
paginationUserIds.add(user.user.id);
}
if (usersRes.nextPaginationToken == null) {
break;
}
usersRes = AuthRecipe.getUsers(process.getProcess(), 10,
"ASC", usersRes.nextPaginationToken, null, null);
}
assertEquals(userIds.size(), paginationUserIds.size());
assertEquals(userIds, paginationUserIds);
}
// Test descending
{
Set<String> paginationUserIds = new HashSet<>();
UserPaginationContainer usersRes = AuthRecipe.getUsers(process.getProcess(), 10,
"DESC", null, null, null);
while (true) {
for (UserPaginationContainer.UsersContainer user : usersRes.users) {
paginationUserIds.add(user.user.id);
}
if (usersRes.nextPaginationToken == null) {
break;
}
usersRes = AuthRecipe.getUsers(process.getProcess(), 10,
"DESC", usersRes.nextPaginationToken, null, null);
}
assertEquals(userIds.size(), paginationUserIds.size());
assertEquals(userIds, paginationUserIds);
}
}
}

View File

@ -290,7 +290,7 @@ public class DashboardTest {
JsonObject usageStats = response.get("usageStats").getAsJsonObject();
JsonArray mauArr = usageStats.get("maus").getAsJsonArray();
assertEquals(1, usageStats.entrySet().size());
assertEquals(30, mauArr.size());
assertEquals(31, mauArr.size());
assertEquals(0, mauArr.get(0).getAsInt());
assertEquals(0, mauArr.get(29).getAsInt());
}
@ -312,7 +312,7 @@ public class DashboardTest {
JsonObject usageStats = response.get("usageStats").getAsJsonObject();
JsonArray mauArr = usageStats.get("maus").getAsJsonArray();
assertEquals(1, usageStats.entrySet().size());
assertEquals(30, mauArr.size());
assertEquals(31, mauArr.size());
assertEquals(0, mauArr.get(0).getAsInt());
assertEquals(0, mauArr.get(29).getAsInt());
}
@ -338,7 +338,7 @@ public class DashboardTest {
JsonObject usageStats = response.get("usageStats").getAsJsonObject();
JsonObject dashboardLoginObject = usageStats.get("dashboard_login").getAsJsonObject();
assertEquals(2, usageStats.entrySet().size());
assertEquals(30, usageStats.get("maus").getAsJsonArray().size());
assertEquals(31, usageStats.get("maus").getAsJsonArray().size());
assertEquals(1, dashboardLoginObject.entrySet().size());
assertEquals(1, dashboardLoginObject.get("user_count").getAsInt());
}
@ -366,7 +366,7 @@ public class DashboardTest {
JsonObject usageStats = response.get("usageStats").getAsJsonObject();
JsonObject dashboardLoginObject = usageStats.get("dashboard_login").getAsJsonObject();
assertEquals(2, usageStats.entrySet().size());
assertEquals(30, usageStats.get("maus").getAsJsonArray().size());
assertEquals(31, usageStats.get("maus").getAsJsonArray().size());
assertEquals(1, dashboardLoginObject.entrySet().size());
assertEquals(4, dashboardLoginObject.get("user_count").getAsInt());
}

View File

@ -1914,6 +1914,7 @@ public class ConfigTest {
"argon2_memory_kb",
"argon2_parallelism",
"bcrypt_log_rounds",
"supertokens_saas_load_only_cud"
};
Object[] disallowedValues = new Object[]{
3567, // port
@ -1930,6 +1931,7 @@ public class ConfigTest {
87795, // argon2_memory_kb
2, // argon2_parallelism
11, // bcrypt_log_rounds
"mydomain.com", // supertokens_saas_load_only_cud
};
process.kill();
@ -1995,7 +1997,7 @@ public class ConfigTest {
new Object[]{true, false}, // disable_telemetry
new Object[]{"BCRYPT", "ARGON2"}, // password_hashing_alg
new Object[]{"abcd1234abcd1234abcd1234abcd1234", "qwer1234qwer1234qwer1234qwer1234"}, // firebase_password_hashing_signer_key
new Object[]{"2.21", "3.0"} // supertokens_max_cdi_version
new Object[]{"2.21", "3.0"}, // supertokens_max_cdi_version
};
for (int i=0; i<conflictingInSameUserPool.length; i++) {

View File

@ -0,0 +1,280 @@
/*
* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.supertokens.test.multitenant;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.supertokens.Main;
import io.supertokens.ProcessState;
import io.supertokens.cronjobs.CronTask;
import io.supertokens.cronjobs.Cronjobs;
import io.supertokens.featureflag.EE_FEATURES;
import io.supertokens.featureflag.FeatureFlagTestContent;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.test.TestingProcessManager;
import io.supertokens.test.Utils;
import io.supertokens.test.httpRequest.HttpResponseException;
import io.supertokens.test.multitenant.api.TestMultitenancyAPIHelper;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.*;
public class LoadOnlyCUDTest {
@Rule
public TestRule watchman = Utils.getOnFailure();
@AfterClass
public static void afterTesting() {
Utils.afterTesting();
}
@Before
public void beforeEach() {
Utils.reset();
}
@Test
public void testAPIChecksForLoadOnlyCUD() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
FeatureFlagTestContent.getInstance(process.getProcess())
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY});
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}
if (StorageLayer.isInMemDb(process.getProcess())) {
return;
}
JsonObject coreConfig = new JsonObject();
StorageLayer.getStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(coreConfig, 1);
TestMultitenancyAPIHelper.createConnectionUriDomain(process.getProcess(), TenantIdentifier.BASE_TENANT,
"127.0.0.1", true, true, true, coreConfig);
StorageLayer.getStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(coreConfig, 2);
TestMultitenancyAPIHelper.createConnectionUriDomain(process.getProcess(), TenantIdentifier.BASE_TENANT,
"localhost", true, true, true, coreConfig);
process.kill(false);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
Utils.setValueInConfig("supertokens_saas_load_only_cud", "127.0.0.1:3567");
process = TestingProcessManager.start(args, false);
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
try {
TestMultitenancyAPIHelper.epSignUp(new TenantIdentifier("localhost", null, null), "test@example.com",
"password123", process.getProcess());
fail();
} catch (HttpResponseException e) {
assertEquals(400, e.statusCode);
}
// check that it's allowed with 127.0.0.1
TestMultitenancyAPIHelper.epSignUp(new TenantIdentifier("127.0.0.1", null, null), "test@example.com",
"password123", process.getProcess());
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void testCreationOfCUDWithLoadOnlyCUD() throws Exception {
String[] args = {"../"};
Utils.setValueInConfig("supertokens_saas_load_only_cud", "127.0.0.1:3567");
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
FeatureFlagTestContent.getInstance(process.getProcess())
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY});
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}
if (StorageLayer.isInMemDb(process.getProcess())) {
return;
}
try {
TestMultitenancyAPIHelper.createConnectionUriDomain(process.getProcess(), TenantIdentifier.BASE_TENANT,
"localhost:3567", true, true, true, new JsonObject());
fail();
} catch (HttpResponseException e) {
assertEquals(400, e.statusCode);
}
JsonObject coreConfig = new JsonObject();
StorageLayer.getStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(coreConfig, 1);
// This should pass
TestMultitenancyAPIHelper.createConnectionUriDomain(process.getProcess(), TenantIdentifier.BASE_TENANT,
"127.0.0.1:3567", true, true, true, coreConfig);
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void testThatResourcesAreNotLoadedWithLoadOnlyCUD() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
FeatureFlagTestContent.getInstance(process.getProcess())
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY});
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}
if (StorageLayer.isInMemDb(process.getProcess())) {
return;
}
JsonObject coreConfig = new JsonObject();
StorageLayer.getStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(coreConfig, 1);
TestMultitenancyAPIHelper.createConnectionUriDomain(process.getProcess(), TenantIdentifier.BASE_TENANT,
"127.0.0.1", true, true, true, coreConfig);
StorageLayer.getStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(coreConfig, 2);
TestMultitenancyAPIHelper.createConnectionUriDomain(process.getProcess(), TenantIdentifier.BASE_TENANT,
"localhost.org", true, true, true, coreConfig);
process.kill(false);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
Utils.setValueInConfig("supertokens_saas_load_only_cud", "127.0.0.1:3567");
process = TestingProcessManager.start(args, false);
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
JsonObject result = TestMultitenancyAPIHelper.listConnectionUriDomains(TenantIdentifier.BASE_TENANT,
process.getProcess());
assertEquals("OK", result.get("status").getAsString());
assertEquals(2, result.get("connectionUriDomains").getAsJsonArray().size());
for (JsonElement elem : result.get("connectionUriDomains").getAsJsonArray()) {
assertNotEquals("localhost.org", elem.getAsJsonObject().get("connectionUriDomain").getAsString());
}
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void testCronDoesNotRunForOtherCUDsWithLoadOnlyCUD() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
FeatureFlagTestContent.getInstance(process.getProcess())
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY});
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}
if (StorageLayer.isInMemDb(process.getProcess())) {
return;
}
JsonObject coreConfig = new JsonObject();
StorageLayer.getStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(coreConfig, 1);
TestMultitenancyAPIHelper.createConnectionUriDomain(process.getProcess(), TenantIdentifier.BASE_TENANT,
"127.0.0.1", true, true, true, coreConfig);
StorageLayer.getStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(coreConfig, 2);
TestMultitenancyAPIHelper.createConnectionUriDomain(process.getProcess(), TenantIdentifier.BASE_TENANT,
"localhost.org", true, true, true, coreConfig);
process.kill(false);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
Utils.setValueInConfig("supertokens_saas_load_only_cud", "127.0.0.1:3567");
process = TestingProcessManager.start(args, false);
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
List<List<TenantIdentifier>> uniqueUserPoolIdsTenants = StorageLayer.getTenantsWithUniqueUserPoolId(process.getProcess());
Cronjobs.addCronjob(process.getProcess(), LoadOnlyCUDTest.PerAppCronjob.getInstance(process.getProcess(), uniqueUserPoolIdsTenants));
Thread.sleep(3000);
Set<AppIdentifier> appIdentifiersFromCron = PerAppCronjob.getInstance(process.getProcess(), uniqueUserPoolIdsTenants).appIdentifiers;
assertEquals(2, appIdentifiersFromCron.size());
for (AppIdentifier app : appIdentifiersFromCron) {
assertNotEquals("localhost.org", app.getConnectionUriDomain());
}
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
static class PerAppCronjob extends CronTask {
private static final String RESOURCE_ID = "io.supertokens.test.CronjobTest.NormalCronjob";
private PerAppCronjob(Main main, List<List<TenantIdentifier>> tenantsInfo) {
super("PerTenantCronjob", main, tenantsInfo, true);
}
Set<AppIdentifier> appIdentifiers = new HashSet<>();
public static LoadOnlyCUDTest.PerAppCronjob getInstance(Main main, List<List<TenantIdentifier>> tenantsInfo) {
try {
return (LoadOnlyCUDTest.PerAppCronjob) main.getResourceDistributor().getResource(new TenantIdentifier(null, null, null), RESOURCE_ID);
} catch (TenantOrAppNotFoundException e) {
return (LoadOnlyCUDTest.PerAppCronjob) main.getResourceDistributor()
.setResource(new TenantIdentifier(null, null, null), RESOURCE_ID, new LoadOnlyCUDTest.PerAppCronjob(main, tenantsInfo));
}
}
@Override
public int getIntervalTimeSeconds() {
return 1;
}
@Override
public int getInitialWaitTimeSeconds() {
return 0;
}
@Override
protected void doTaskPerApp(AppIdentifier app) throws Exception {
appIdentifiers.add(app);
}
}
}

View File

@ -108,6 +108,7 @@ public class RefreshTokenTest {
TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STARTED));
long createdTime = System.currentTimeMillis();
TokenInfo tokenInfo = RefreshToken.createNewRefreshToken(process.getProcess(), "sessionHandle", "userId",
"parentRefreshTokenHash1", "antiCsrfToken");
@ -129,9 +130,8 @@ public class RefreshTokenTest {
assertEquals("antiCsrfToken", infoFromToken.antiCsrfToken);
assertNull(infoFromToken.parentRefreshTokenHash2);
assertSame(infoFromToken.type, TYPE.FREE_OPTIMISED);
// -5000 for some grace period for creation and checking above
assertTrue(tokenInfo.expiry > System.currentTimeMillis()
+ Config.getConfig(process.getProcess()).getRefreshTokenValidity() - 5000);
assertTrue(tokenInfo.expiry >= createdTime
+ Config.getConfig(process.getProcess()).getRefreshTokenValidity());
process.kill();
assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STOPPED));

View File

@ -0,0 +1,202 @@
/*
* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.supertokens.test.session;
import com.google.gson.JsonObject;
import io.supertokens.ProcessState;
import io.supertokens.exceptions.TryRefreshTokenException;
import io.supertokens.exceptions.UnauthorisedException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.session.SessionStorage;
import io.supertokens.session.Session;
import io.supertokens.session.accessToken.AccessToken;
import io.supertokens.session.info.SessionInformationHolder;
import io.supertokens.session.jwt.JWT;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.test.TestingProcessManager;
import io.supertokens.test.Utils;
import org.junit.*;
import org.junit.rules.TestRule;
import static junit.framework.TestCase.*;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
public class SessionTest6 {
@Rule
public TestRule watchman = Utils.getOnFailure();
@AfterClass
public static void afterTesting() {
Utils.afterTesting();
}
@Before
public void beforeEach() {
Utils.reset();
}
@Test
public void createRefreshSwitchVerify() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
String userId = "userId";
JsonObject userDataInJWT = new JsonObject();
userDataInJWT.addProperty("key", "value");
JsonObject userDataInDatabase = new JsonObject();
userDataInDatabase.addProperty("key", "value");
SessionInformationHolder sessionInfo = Session.createNewSession(process.getProcess(), userId, userDataInJWT,
userDataInDatabase, false, AccessToken.getLatestVersion(), false);
checkIfUsingStaticKey(sessionInfo, false);
sessionInfo = Session.refreshSession(new AppIdentifier(null, null), process.getProcess(), sessionInfo.refreshToken.token,
sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion(), true);
assert sessionInfo.refreshToken != null;
assert sessionInfo.accessToken != null;
checkIfUsingStaticKey(sessionInfo, true);
SessionInformationHolder verifiedSession = Session.getSession(process.getProcess(), sessionInfo.accessToken.token,
sessionInfo.antiCsrfToken, false, true, false);
checkIfUsingStaticKey(verifiedSession, true);
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void createRefreshSwitchRegen() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
String userId = "userId";
JsonObject userDataInJWT = new JsonObject();
userDataInJWT.addProperty("key", "value");
JsonObject userDataInDatabase = new JsonObject();
userDataInDatabase.addProperty("key", "value");
SessionInformationHolder sessionInfo = Session.createNewSession(process.getProcess(), userId, userDataInJWT,
userDataInDatabase, false, AccessToken.getLatestVersion(), false);
checkIfUsingStaticKey(sessionInfo, false);
sessionInfo = Session.refreshSession(new AppIdentifier(null, null), process.getProcess(), sessionInfo.refreshToken.token,
sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion(), true);
assert sessionInfo.refreshToken != null;
assert sessionInfo.accessToken != null;
checkIfUsingStaticKey(sessionInfo, true);
SessionInformationHolder newSessionInfo = Session.regenerateToken(process.getProcess(),
sessionInfo.accessToken.token, userDataInJWT);
checkIfUsingStaticKey(newSessionInfo, true);
SessionInformationHolder getSessionResponse = Session.getSession(process.getProcess(),
newSessionInfo.accessToken.token, sessionInfo.antiCsrfToken, false, true, false);
checkIfUsingStaticKey(getSessionResponse, true);
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void createRefreshRefreshSwitchVerify() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
String userId = "userId";
JsonObject userDataInJWT = new JsonObject();
userDataInJWT.addProperty("key", "value");
JsonObject userDataInDatabase = new JsonObject();
userDataInDatabase.addProperty("key", "value");
SessionInformationHolder sessionInfo = Session.createNewSession(process.getProcess(), userId, userDataInJWT,
userDataInDatabase, false, AccessToken.getLatestVersion(), false);
checkIfUsingStaticKey(sessionInfo, false);
sessionInfo = Session.refreshSession(new AppIdentifier(null, null), process.getProcess(), sessionInfo.refreshToken.token,
sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion(), false);
sessionInfo = Session.refreshSession(new AppIdentifier(null, null), process.getProcess(), sessionInfo.refreshToken.token,
sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion(), true);
assert sessionInfo.refreshToken != null;
assert sessionInfo.accessToken != null;
checkIfUsingStaticKey(sessionInfo, true);
SessionInformationHolder verifiedSession = Session.getSession(process.getProcess(), sessionInfo.accessToken.token,
sessionInfo.antiCsrfToken, false, true, false);
checkIfUsingStaticKey(verifiedSession, true);
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void createRefreshRefreshSwitchRegen() throws Exception {
String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
String userId = "userId";
JsonObject userDataInJWT = new JsonObject();
userDataInJWT.addProperty("key", "value");
JsonObject userDataInDatabase = new JsonObject();
userDataInDatabase.addProperty("key", "value");
SessionInformationHolder sessionInfo = Session.createNewSession(process.getProcess(), userId, userDataInJWT,
userDataInDatabase, false, AccessToken.getLatestVersion(), false);
checkIfUsingStaticKey(sessionInfo, false);
sessionInfo = Session.refreshSession(new AppIdentifier(null, null), process.getProcess(), sessionInfo.refreshToken.token,
sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion(), false);
sessionInfo = Session.refreshSession(new AppIdentifier(null, null), process.getProcess(), sessionInfo.refreshToken.token,
sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion(), true);
assert sessionInfo.refreshToken != null;
assert sessionInfo.accessToken != null;
checkIfUsingStaticKey(sessionInfo, true);
SessionInformationHolder newSessionInfo = Session.regenerateToken(process.getProcess(),
sessionInfo.accessToken.token, userDataInJWT);
checkIfUsingStaticKey(newSessionInfo, true);
SessionInformationHolder getSessionResponse = Session.getSession(process.getProcess(),
newSessionInfo.accessToken.token, sessionInfo.antiCsrfToken, false, true, false);
checkIfUsingStaticKey(getSessionResponse, true);
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
private static void checkIfUsingStaticKey(SessionInformationHolder info, boolean shouldBeStatic) throws JWT.JWTException {
assert info.accessToken != null;
JWT.JWTPreParseInfo tokenInfo = JWT.preParseJWTInfo(info.accessToken.token);
assert tokenInfo.kid != null;
if (shouldBeStatic) {
assert tokenInfo.kid.startsWith("s-");
} else {
assert tokenInfo.kid.startsWith("d-");
}
}
}

View File

@ -76,7 +76,7 @@ public class RefreshSessionAPITest2_21 {
JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null,
Utils.getCdiVersionStringLatestForTests(), "session");
SemVer.v2_21.get(), "session");
assertEquals(response.entrySet().size(), 2);
assertEquals(response.get("status").getAsString(), "UNAUTHORISED");

View File

@ -0,0 +1,207 @@
/*
* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.supertokens.test.session.api;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import io.supertokens.ProcessState;
import io.supertokens.session.jwt.JWT;
import io.supertokens.test.TestingProcessManager;
import io.supertokens.test.Utils;
import io.supertokens.test.httpRequest.HttpRequestForTesting;
import io.supertokens.utils.SemVer;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertNotNull;
public class RefreshSessionAPITest3_0 {
@Rule
public TestRule watchman = Utils.getOnFailure();
@AfterClass
public static void afterTesting() {
Utils.afterTesting();
}
@Before
public void beforeEach() {
Utils.reset();
}
@Test
public void successOutputWithValidRefreshTokenTest() throws Exception {
String[] args = { "../" };
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
String userId = "userId";
JsonObject userDataInJWT = new JsonObject();
userDataInJWT.add("nullProp", JsonNull.INSTANCE);
userDataInJWT.addProperty("key", "value");
JsonObject userDataInDatabase = new JsonObject();
userDataInDatabase.addProperty("key", "value");
JsonObject request = new JsonObject();
request.addProperty("userId", userId);
request.add("userDataInJWT", userDataInJWT);
request.add("userDataInDatabase", userDataInDatabase);
request.addProperty("enableAntiCsrf", false);
JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/recipe/session", request, 1000, 1000, null, SemVer.v2_7.get(),
"session");
assertEquals(sessionInfo.get("status").getAsString(), "OK");
JsonObject sessionRefreshBody = new JsonObject();
sessionRefreshBody.addProperty("refreshToken",
sessionInfo.get("refreshToken").getAsJsonObject().get("token").getAsString());
sessionRefreshBody.addProperty("enableAntiCsrf", false);
JsonObject sessionRefreshResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null,
SemVer.v3_0.get(), "session");
checkRefreshSessionResponse(sessionRefreshResponse, process, userId, userDataInJWT, false, false);
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void successOutputUpgradeWithNonStaticKeySessionTest() throws Exception {
String[] args = { "../" };
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
String userId = "userId";
JsonObject userDataInJWT = new JsonObject();
userDataInJWT.add("nullProp", JsonNull.INSTANCE);
userDataInJWT.addProperty("key", "value");
JsonObject userDataInDatabase = new JsonObject();
userDataInDatabase.addProperty("key", "value");
JsonObject request = new JsonObject();
request.addProperty("userId", userId);
request.add("userDataInJWT", userDataInJWT);
request.add("userDataInDatabase", userDataInDatabase);
request.addProperty("enableAntiCsrf", false);
JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/recipe/session", request, 1000, 1000, null, SemVer.v2_7.get(),
"session");
assertEquals(sessionInfo.get("status").getAsString(), "OK");
JsonObject sessionRefreshBody = new JsonObject();
sessionRefreshBody.addProperty("refreshToken",
sessionInfo.get("refreshToken").getAsJsonObject().get("token").getAsString());
sessionRefreshBody.addProperty("enableAntiCsrf", false);
sessionRefreshBody.addProperty("useDynamicSigningKey", true);
JsonObject sessionRefreshResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null,
SemVer.v3_0.get(), "session");
checkRefreshSessionResponse(sessionRefreshResponse, process, userId, userDataInJWT, false, false);
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
@Test
public void successOutputUpgradeWithStaticKeySessionTest() throws Exception {
String[] args = { "../" };
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
String userId = "userId";
JsonObject userDataInJWT = new JsonObject();
userDataInJWT.add("nullProp", JsonNull.INSTANCE);
userDataInJWT.addProperty("key", "value");
JsonObject userDataInDatabase = new JsonObject();
userDataInDatabase.addProperty("key", "value");
JsonObject request = new JsonObject();
request.addProperty("userId", userId);
request.add("userDataInJWT", userDataInJWT);
request.add("userDataInDatabase", userDataInDatabase);
request.addProperty("enableAntiCsrf", false);
JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/recipe/session", request, 1000, 1000, null, SemVer.v2_7.get(),
"session");
assertEquals(sessionInfo.get("status").getAsString(), "OK");
JsonObject sessionRefreshBody = new JsonObject();
sessionRefreshBody.addProperty("refreshToken",
sessionInfo.get("refreshToken").getAsJsonObject().get("token").getAsString());
sessionRefreshBody.addProperty("enableAntiCsrf", false);
sessionRefreshBody.addProperty("useDynamicSigningKey", false);
JsonObject sessionRefreshResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null,
SemVer.v3_0.get(), "session");
checkRefreshSessionResponse(sessionRefreshResponse, process, userId, userDataInJWT, false, true);
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
private static void checkRefreshSessionResponse(JsonObject response, TestingProcessManager.TestingProcess process,
String userId, JsonObject userDataInJWT, boolean hasAntiCsrf, boolean useStaticKey) throws
JWT.JWTException {
assertNotNull(response.get("session").getAsJsonObject().get("handle").getAsString());
assertEquals(response.get("session").getAsJsonObject().get("userId").getAsString(), userId);
assertEquals(response.get("session").getAsJsonObject().get("tenantId").getAsString(), "public");
assertEquals(response.get("session").getAsJsonObject().get("userDataInJWT").getAsJsonObject().toString(),
userDataInJWT.toString());
assertEquals(response.get("session").getAsJsonObject().entrySet().size(), 4);
assertTrue(response.get("accessToken").getAsJsonObject().has("token"));
assertTrue(response.get("accessToken").getAsJsonObject().has("expiry"));
assertTrue(response.get("accessToken").getAsJsonObject().has("createdTime"));
assertEquals(response.get("accessToken").getAsJsonObject().entrySet().size(), 3);
JWT.JWTPreParseInfo tokenInfo = JWT.preParseJWTInfo(response.get("accessToken").getAsJsonObject().get("token").getAsString());
if (useStaticKey) {
assert(tokenInfo.kid.startsWith("s-"));
} else {
assert(tokenInfo.kid.startsWith("d-"));
}
assertTrue(response.get("refreshToken").getAsJsonObject().has("token"));
assertTrue(response.get("refreshToken").getAsJsonObject().has("expiry"));
assertTrue(response.get("refreshToken").getAsJsonObject().has("createdTime"));
assertEquals(response.get("refreshToken").getAsJsonObject().entrySet().size(), 3);
assertEquals(response.has("antiCsrfToken"), hasAntiCsrf);
assertEquals(response.entrySet().size(), hasAntiCsrf ? 5 : 4);
}
}

View File

@ -107,6 +107,7 @@ public class SessionRegenerateAPITest2_21 {
sessionRefreshBody.addProperty("refreshToken",
sessionInfo.get("refreshToken").getAsJsonObject().get("token").getAsString());
sessionRefreshBody.addProperty("enableAntiCsrf", false);
sessionRefreshBody.addProperty("useDynamicSigningKey", true);
JsonObject sessionRefreshResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null,

View File

@ -583,7 +583,8 @@ public class UserIdMappingStorageTest {
storage.createUserIdMapping(new AppIdentifier(null, null), superTokensUserId, externalUserId,
null);
}
HashMap<String, String> response = storage.getUserIdMappingForSuperTokensIds(superTokensUserIdList);
HashMap<String, String> response = storage.getUserIdMappingForSuperTokensIds(
new AppIdentifier(null, null), superTokensUserIdList);
assertEquals(AuthRecipe.USER_PAGINATION_LIMIT, response.size());
for (int i = 0; i < response.size(); i++) {
assertEquals(externalUserIdList.get(i), response.get(superTokensUserIdList.get(i)));
@ -606,7 +607,8 @@ public class UserIdMappingStorageTest {
UserIdMappingStorage storage = (UserIdMappingStorage) StorageLayer.getStorage(process.main);
ArrayList<String> emptyList = new ArrayList<>();
HashMap<String, String> response = storage.getUserIdMappingForSuperTokensIds(emptyList);
HashMap<String, String> response = storage.getUserIdMappingForSuperTokensIds(
new AppIdentifier(null, null), emptyList);
assertEquals(0, response.size());
process.kill();
@ -631,7 +633,8 @@ public class UserIdMappingStorageTest {
superTokensUserIdList.add(userInfo.id);
}
HashMap<String, String> userIdMapping = storage.getUserIdMappingForSuperTokensIds(superTokensUserIdList);
HashMap<String, String> userIdMapping = storage.getUserIdMappingForSuperTokensIds(
new AppIdentifier(null, null), superTokensUserIdList);
assertEquals(0, userIdMapping.size());
process.kill();
@ -668,7 +671,8 @@ public class UserIdMappingStorageTest {
}
// retrieve UserIDMapping
HashMap<String, String> response = storage.getUserIdMappingForSuperTokensIds(superTokensUserIdList);
HashMap<String, String> response = storage.getUserIdMappingForSuperTokensIds(
new AppIdentifier(null, null), superTokensUserIdList);
assertEquals(5, response.size());
// check that the last 5 users have their ids mapped

55
stress-tests/.gitignore vendored Normal file
View File

@ -0,0 +1,55 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
# Environment variables
.env
.env.local
.env.*.local
# Build output
dist/
build/
out/
# Logs
logs/
*.log
# IDE and editor files
.idea/
.vscode/
*.swp
*.swo
.DS_Store
Thumbs.db
# Testing
coverage/
.nyc_output/
# Temporary files
*.tmp
*.temp
.cache/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
users/

8
stress-tests/.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}

View File

@ -0,0 +1,66 @@
version: '3'
services:
# Note: If you are assigning a custom name to your db service on the line below, make sure it does not contain underscores
db:
image: 'postgres:latest'
environment:
POSTGRES_USER: supertokens
POSTGRES_PASSWORD: supertokens
POSTGRES_DB: supertokens
command: postgres -c shared_preload_libraries='pg_stat_statements' -c pg_stat_statements.track=all -c max_connections=1000 -c shared_buffers=1GB -c synchronous_commit=off -c wal_buffers=16MB -c checkpoint_timeout=30min -c max_wal_size=4GB
ports:
- 5432:5432
networks:
- app_network
restart: unless-stopped
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'supertokens', '-d', 'supertokens']
interval: 5s
timeout: 5s
retries: 5
supertokens:
image: supertokens/supertokens-dev-postgresql:for-backport-release-6-0
# platform: linux/amd64
depends_on:
db:
condition: service_healthy
ports:
- 3567:3567
environment:
POSTGRESQL_CONNECTION_URI: "postgresql://supertokens:supertokens@db:5432/supertokens"
PASSWORD_HASHING_ALG: "ARGON2"
ARGON2_ITERATIONS: 1
ARGON2_MEMORY_KB: 8
ARGON2_PARALLELISM: 1
ARGON2_HASHING_POOL_SIZE: 8
API_KEYS: "qwertyuiopasdfghjklzxcvbnm"
BULK_MIGRATION_PARALLELISM: "4"
BULK_MIGRATION_BATCH_SIZE: "500"
networks:
- app_network
restart: unless-stopped
healthcheck:
test: >
bash -c 'exec 3<>/dev/tcp/127.0.0.1/3567 && echo -e "GET /hello HTTP/1.1\r\nhost: 127.0.0.1:3567\r\nConnection: close\r\n\r\n" >&3 && cat <&3 | grep "Hello"'
interval: 10s
timeout: 5s
retries: 5
pghero:
image: ankane/pghero
environment:
DATABASE_URL: "postgres://supertokens:supertokens@db:5432/supertokens"
ports:
- 8080:8080
networks:
- app_network
depends_on:
- db
restart: unless-stopped
networks:
app_network:
driver: bridge

27
stress-tests/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "stress-tests",
"version": "1.0.0",
"description": "Stress tests for SuperTokens",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"generate-users": "rm -rf users && mkdir -p users && ts-node src/oneMillionUsers/generateUsers.ts",
"one-million-users": "ts-node src/oneMillionUsers/index.ts",
"format": "prettier --write \"**/*.{ts,js,json}\""
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^20.11.24",
"prettier": "^3.5.3",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"dependencies": {
"@types/uuid": "^10.0.0",
"supertokens-node": "15.2.3",
"uuid": "^11.1.0"
}
}

View File

@ -0,0 +1,143 @@
import * as fs from 'fs';
export const LICENSE_FOR_TEST =
'E1yITHflaFS4BPm7n0bnfFCjP4sJoTERmP0J=kXQ5YONtALeGnfOOe2rf2QZ0mfOh0aO3pBqfF-S0jb0ABpat6pySluTpJO6jieD6tzUOR1HrGjJO=50Ob3mHi21tQH1';
export const createStInstanceForTest = async () => {
return {
deployment_id: '1234567890',
core_url: 'http://localhost:3567',
api_key: 'qwertyuiopasdfghjklzxcvbnm',
};
};
export const deleteStInstance = async (deploymentId: string) => {
// noop
};
export const formatTime = (ms: number): string => {
const seconds = Math.floor(ms / 1000);
if (seconds < 60) {
return `${seconds}s`;
}
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}m ${remainingSeconds}s`;
};
export const workInBatches = async <T>(
count: number,
numberOfBatches: number,
work: (idx: number) => Promise<T>
): Promise<T[]> => {
const batchSize = Math.ceil(count / numberOfBatches);
const batches = [];
let workCount = 0;
const st = Date.now();
let done = numberOfBatches;
for (let b = 0; b < numberOfBatches; b++) {
batches.push(
(async () => {
const startIndex = b * batchSize;
const endIndex = Math.min(startIndex + batchSize, count);
const batchResults: T[] = [];
for (let i = startIndex; i < endIndex; i++) {
batchResults.push(await work(i));
workCount++;
}
done--;
return batchResults;
})()
);
}
batches.push(
(async () => {
while (done > 0) {
await new Promise((resolve) => setTimeout(resolve, 5000));
const en = Date.now();
console.log(
` Progress: Time=${formatTime(en - st)}, Completed=${workCount}, Throughput=${Math.round((workCount / (en - st)) * 10000) / 10}/s`
);
}
return [];
})()
);
const results = await Promise.all(batches);
return results.flat();
};
export const setupLicense = async (coreUrl: string, apiKey: string) => {
try {
const response = await fetch(`${coreUrl}/ee/license`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'api-key': apiKey,
},
body: JSON.stringify({
licenseKey: LICENSE_FOR_TEST,
}),
});
if (!response.ok) {
throw new Error(`Failed with status: ${response.status}`);
}
const responseText = await response.text();
console.log('License response:', responseText);
console.log('License key set successfully');
} catch (error) {
console.error('Failed to set license key:', error);
throw error;
}
};
export class StatsCollector {
private static instance: StatsCollector;
private measurements: { title: string; timeMs: number }[] = [];
private constructor() {}
public static getInstance(): StatsCollector {
if (!StatsCollector.instance) {
StatsCollector.instance = new StatsCollector();
}
return StatsCollector.instance;
}
public addMeasurement(title: string, timeMs: number) {
this.measurements.push({ title, timeMs });
}
public getStats() {
return this.measurements;
}
public writeToFile() {
const formattedMeasurements = this.measurements.map((measurement) => ({
title: measurement.title,
ms: measurement.timeMs,
formatted: formatTime(measurement.timeMs),
}));
const stats = {
measurements: formattedMeasurements,
timestamp: new Date().toISOString(),
};
fs.writeFileSync('stats.json', JSON.stringify(stats, null, 2));
}
}
export const measureTime = async <T>(title: string, fn: () => Promise<T>): Promise<T> => {
const st = Date.now();
const result = await fn();
const et = Date.now();
const timeMs = et - st;
console.log(` ${title} took ${formatTime(timeMs)}`);
StatsCollector.getInstance().addMeasurement(title, timeMs);
return result;
};

View File

@ -0,0 +1,18 @@
import SuperTokens from 'supertokens-node';
import UserRoles from 'supertokens-node/recipe/userroles';
import { measureTime, workInBatches } from '../common/utils';
export const addRoles = async (
users: { recipeUserId: string; email?: string; phoneNumber?: string }[]
) => {
console.log('\n\n4. Adding roles');
await measureTime('Adding roles', async () => {
await UserRoles.createNewRoleOrAddPermissions('admin', ['p1', 'p2']);
await workInBatches(users.length, 8, async (idx) => {
const user = users[idx]!;
await UserRoles.addRoleToUser('public', user.recipeUserId, 'admin');
});
});
};

View File

@ -0,0 +1,19 @@
import SuperTokens from 'supertokens-node';
import Session from 'supertokens-node/recipe/session';
import { measureTime, workInBatches } from '../common/utils';
export const createSessions = async (
users: { recipeUserId: string; email?: string; phoneNumber?: string }[]
) => {
console.log('\n\n5. Creating sessions');
await measureTime('Creating sessions', async () => {
await workInBatches(users.length, 8, async (idx) => {
const user = users[idx]!;
await Session.createNewSessionWithoutRequestResponse(
'public',
user.recipeUserId
);
});
});
};

View File

@ -0,0 +1,25 @@
import { measureTime, workInBatches } from '../common/utils';
import SuperTokens from 'supertokens-node';
export const createUserIdMappings = async (
users: { recipeUserId: string; email?: string; phoneNumber?: string }[]
) => {
console.log('\n\n3. Create user id mappings');
await measureTime('Create user id mappings', async () => {
await workInBatches(users.length, 8, async (idx) => {
const user = users[idx]!;
if (Math.random() < 0.5) {
const newUserId = Array(64)
.fill(0)
.map(() => String.fromCharCode(97 + Math.floor(Math.random() * 26)))
.join('');
await SuperTokens.createUserIdMapping({
superTokensUserId: user.recipeUserId,
externalUserId: newUserId,
});
user.recipeUserId = newUserId;
}
});
});
};

View File

@ -0,0 +1,128 @@
import EmailPassword from 'supertokens-node/recipe/emailpassword';
import Passwordless from 'supertokens-node/recipe/passwordless';
import ThirdParty from 'supertokens-node/recipe/thirdparty';
import { workInBatches, measureTime } from '../common/utils';
const TOTAL_USERS = 10000;
const createEmailPasswordUsers = async () => {
console.log(` Creating EmailPassword users...`);
return await workInBatches(Math.floor(TOTAL_USERS / 5), 4, async (idx) => {
const email =
Array(64)
.fill(0)
.map(() => String.fromCharCode(97 + Math.floor(Math.random() * 26)))
.join('') + '@example.com';
const createdUser = await EmailPassword.signUp('public', email, 'password');
// expect(createdUser.status).toBe("OK");
if (createdUser.status === 'OK') {
return {
recipeUserId: createdUser.user.id,
email: email,
};
}
});
};
const createPasswordlessUsersWithEmail = async () => {
console.log(` Creating Passwordless users (with email)...`);
return await workInBatches(Math.floor(TOTAL_USERS / 5), 4, async (idx) => {
const email =
Array(64)
.fill(0)
.map(() => String.fromCharCode(97 + Math.floor(Math.random() * 26)))
.join('') + '@example.com';
const createdUser = await Passwordless.signInUp({
tenantId: 'public',
email,
});
// expect(createdUser.status).toBe("OK");
if (createdUser.status === 'OK') {
return {
recipeUserId: createdUser.user.id,
email,
};
}
});
};
const createPasswordlessUsersWithPhone = async () => {
console.log(` Creating Passwordless users (with phone)...`);
return await workInBatches(Math.floor(TOTAL_USERS / 5), 4, async (idx) => {
const phoneNumber = `+1${Math.floor(Math.random() * 10000000000)}`;
const createdUser = await Passwordless.signInUp({
tenantId: 'public',
phoneNumber,
});
// expect(createdUser.status).toBe("OK");
if (createdUser.status === 'OK') {
return {
recipeUserId: createdUser.user.id,
phoneNumber,
};
}
});
};
const createThirdPartyUsers = async (thirdPartyId: string) => {
console.log(` Creating ThirdParty (${thirdPartyId}) users...`);
return await workInBatches(Math.floor(TOTAL_USERS / 5), 4, async (idx) => {
const email =
Array(64)
.fill(0)
.map(() => String.fromCharCode(97 + Math.floor(Math.random() * 26)))
.join('') + '@example.com';
const tpUserId = Array(64)
.fill(0)
.map(() => String.fromCharCode(97 + Math.floor(Math.random() * 26)))
.join('');
const createdUser = await ThirdParty.manuallyCreateOrUpdateUser(
'public',
thirdPartyId,
tpUserId,
email,
true
);
// expect(createdUser.status).toBe("OK");
if (createdUser.status === 'OK') {
return {
recipeUserId: createdUser.user.id,
email,
};
}
});
};
export const createUsers = async () => {
console.log('\n\n1. Create one million users');
const epUsers = await measureTime('Emailpassword users creation', createEmailPasswordUsers);
const plessEmailUsers = await measureTime(
'Passwordless users (with email) creation',
createPasswordlessUsersWithEmail
);
const plessPhoneUsers = await measureTime(
'Passwordless users (with phone) creation',
createPasswordlessUsersWithPhone
);
const tpUsers1 = await measureTime('ThirdParty users (google) creation', () =>
createThirdPartyUsers('google')
);
const tpUsers2 = await measureTime('ThirdParty users (facebook) creation', () =>
createThirdPartyUsers('facebook')
);
return {
epUsers,
plessEmailUsers,
plessPhoneUsers,
tpUsers1,
tpUsers2,
};
};

View File

@ -0,0 +1,193 @@
import * as fs from 'fs';
import { v4 as uuidv4 } from 'uuid';
const USERS_TO_GENERATE = 1000000;
const USERS_PER_JSON = 10000;
const n = Math.floor(USERS_TO_GENERATE / USERS_PER_JSON);
const generatedEmails = new Set<string>();
const generatedPhoneNumbers = new Set<string>();
const generatedUserIds = new Set<string>();
interface LoginMethod {
tenantIds: string[];
email: string;
recipeId: string;
passwordHash?: string;
hashingAlgorithm?: string;
thirdPartyId?: string;
thirdPartyUserId?: string;
phoneNumber?: string;
isVerified: boolean;
isPrimary: boolean;
timeJoinedInMSSinceEpoch: number;
}
interface User {
externalUserId: string;
userRoles: Array<{
role: string;
tenantIds: string[];
}>;
loginMethods: LoginMethod[];
}
function createEmailLoginMethod(email: string, tenantIds: string[]): LoginMethod {
return {
tenantIds,
email,
recipeId: 'emailpassword',
passwordHash: '$argon2d$v=19$m=12,t=3,p=1$aGI4enNvMmd0Zm0wMDAwMA$r6p7qbr6HD+8CD7sBi4HVw',
hashingAlgorithm: 'argon2',
isVerified: true,
isPrimary: false,
timeJoinedInMSSinceEpoch:
Math.floor(Math.random() * (Date.now() - 3 * 365 * 24 * 60 * 60 * 1000)) +
3 * 365 * 24 * 60 * 60 * 1000,
};
}
function createThirdPartyLoginMethod(email: string, tenantIds: string[]): LoginMethod {
return {
tenantIds,
recipeId: 'thirdparty',
email,
thirdPartyId: 'google',
thirdPartyUserId: String(hashCode(email)),
isVerified: true,
isPrimary: false,
timeJoinedInMSSinceEpoch:
Math.floor(Math.random() * (Date.now() - 3 * 365 * 24 * 60 * 60 * 1000)) +
3 * 365 * 24 * 60 * 60 * 1000,
};
}
function createPasswordlessLoginMethod(
email: string,
tenantIds: string[],
phoneNumber: string
): LoginMethod {
return {
tenantIds,
email,
recipeId: 'passwordless',
phoneNumber,
isVerified: true,
isPrimary: false,
timeJoinedInMSSinceEpoch:
Math.floor(Math.random() * (Date.now() - 3 * 365 * 24 * 60 * 60 * 1000)) +
3 * 365 * 24 * 60 * 60 * 1000,
};
}
function hashCode(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return hash;
}
function generateRandomString(length: number, chars: string): string {
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
function generateRandomEmail(): string {
return `${generateRandomString(24, 'abcdefghijklmnopqrstuvwxyz')}@example.com`;
}
function generateRandomPhoneNumber(): string {
return `+91${generateRandomString(10, '0123456789')}`;
}
function genUser(): User {
const user: User = {
externalUserId: '',
userRoles: [
{ role: 'role1', tenantIds: ['public'] },
{ role: 'role2', tenantIds: ['public'] },
],
loginMethods: [],
};
let userId = `e-${uuidv4()}`;
while (generatedUserIds.has(userId)) {
userId = `e-${uuidv4()}`;
}
generatedUserIds.add(userId);
user.externalUserId = userId;
const tenantIds = ['public'];
let email = generateRandomEmail();
while (generatedEmails.has(email)) {
email = generateRandomEmail();
}
generatedEmails.add(email);
const loginMethods: LoginMethod[] = [];
// Always add email login method
loginMethods.push(createEmailLoginMethod(email, tenantIds));
// 50% chance to add third party login
if (Math.random() < 0.5) {
loginMethods.push(createThirdPartyLoginMethod(email, tenantIds));
}
// 50% chance to add passwordless login
if (Math.random() < 0.5) {
let phoneNumber = generateRandomPhoneNumber();
while (generatedPhoneNumbers.has(phoneNumber)) {
phoneNumber = generateRandomPhoneNumber();
}
generatedPhoneNumbers.add(phoneNumber);
loginMethods.push(createPasswordlessLoginMethod(email, tenantIds, phoneNumber));
}
// If no methods were added, randomly add one
if (loginMethods.length === 0) {
const methodNumber = Math.floor(Math.random() * 3);
if (methodNumber === 0) {
loginMethods.push(createEmailLoginMethod(email, tenantIds));
} else if (methodNumber === 1) {
loginMethods.push(createThirdPartyLoginMethod(email, tenantIds));
} else {
let phoneNumber = generateRandomPhoneNumber();
while (generatedPhoneNumbers.has(phoneNumber)) {
phoneNumber = generateRandomPhoneNumber();
}
generatedPhoneNumbers.add(phoneNumber);
loginMethods.push(createPasswordlessLoginMethod(email, tenantIds, phoneNumber));
}
}
loginMethods[Math.floor(Math.random() * loginMethods.length)].isPrimary = true;
user.loginMethods = loginMethods;
return user;
}
// Create users directory if it doesn't exist
if (!fs.existsSync('users')) {
fs.mkdirSync('users');
}
for (let i = 0; i < n; i++) {
console.log(`Generating ${USERS_PER_JSON} users for ${i}`);
const users: User[] = [];
for (let j = 0; j < USERS_PER_JSON; j++) {
users.push(genUser());
}
fs.writeFileSync(
`users/users-${i.toString().padStart(4, '0')}.json`,
JSON.stringify({ users }, null, 2)
);
}

View File

@ -0,0 +1,148 @@
import {
createStInstanceForTest,
deleteStInstance,
setupLicense,
StatsCollector,
} from '../common/utils';
import SuperTokens from 'supertokens-node';
import EmailPassword from 'supertokens-node/recipe/emailpassword';
import Passwordless from 'supertokens-node/recipe/passwordless';
import ThirdParty from 'supertokens-node/recipe/thirdparty';
import UserRoles from 'supertokens-node/recipe/userroles';
import Session from 'supertokens-node/recipe/session';
import { createUsers } from './createUsers';
import { createUserIdMappings } from './createUserIdMappings';
import { addRoles } from './addRoles';
import { createSessions } from './createSessions';
function stInit(connectionURI: string, apiKey: string) {
SuperTokens.init({
appInfo: {
appName: 'SuperTokens',
apiDomain: 'http://localhost:3001',
websiteDomain: 'http://localhost:3000',
apiBasePath: '/auth',
websiteBasePath: '/auth',
},
supertokens: {
connectionURI: connectionURI,
apiKey: apiKey,
},
recipeList: [
EmailPassword.init(),
Passwordless.init({
contactMethod: 'EMAIL_OR_PHONE',
flowType: 'USER_INPUT_CODE',
}),
ThirdParty.init({
signInAndUpFeature: {
providers: [
{
config: { thirdPartyId: 'google' },
},
{
config: { thirdPartyId: 'facebook' },
},
],
},
}),
UserRoles.init(),
Session.init(),
],
});
}
async function main() {
const deployment = await createStInstanceForTest();
console.log(`Deployment created: ${deployment.core_url}`);
try {
stInit(deployment.core_url, deployment.api_key);
await setupLicense(deployment.core_url, deployment.api_key);
// 1. Create one million users
const users = await createUsers();
// Randomly create groups of users for linking
const allUsers: ({ recipeUserId: string; email?: string; phoneNumber?: string } | undefined)[] =
[
...users.epUsers,
...users.plessEmailUsers,
...users.plessPhoneUsers,
...users.tpUsers1,
...users.tpUsers2,
];
const usersToLink: { recipeUserId: string; email?: string; phoneNumber?: string }[][] = [];
while (allUsers.length > 0) {
const userSet: { recipeUserId: string; email?: string; phoneNumber?: string }[] = [];
const numAccounts = Math.min(Math.floor(Math.random() * 5 + 1), allUsers.length);
for (let i = 0; i < numAccounts; i++) {
const randomIndex = Math.floor(Math.random() * allUsers.length);
userSet.push(allUsers[randomIndex]!);
allUsers.splice(randomIndex, 1);
}
usersToLink.push(userSet);
}
// 2. Link accounts
// await doAccountLinking(usersToLink);
// 3. Create user id mappings
const allUsersForMapping = [
...users.epUsers,
...users.plessEmailUsers,
...users.plessPhoneUsers,
...users.tpUsers1,
...users.tpUsers2,
].filter((user) => user !== undefined) as {
recipeUserId: string;
email?: string;
phoneNumber?: string;
}[];
await createUserIdMappings(allUsersForMapping);
// 4. Add roles
await addRoles(allUsersForMapping);
// 5. Create sessions
await createSessions(allUsersForMapping);
// 6. List all users
console.log('\n\n6. Listing all users');
let userCount = 0;
let paginationToken: string | undefined;
while (true) {
const result = await SuperTokens.getUsersNewestFirst({
tenantId: 'public',
paginationToken,
});
for (const user of result.users) {
userCount++;
}
paginationToken = result.nextPaginationToken;
if (result.nextPaginationToken === undefined) break;
}
console.log(`Users count: ${userCount}`);
// 7. Count users
console.log('\n\n7. Count users');
const total = await SuperTokens.getUserCount();
console.log(`Users count: ${total}`);
// Write stats to file
StatsCollector.getInstance().writeToFile();
console.log('\nStats written to stats.json');
} catch (error) {
console.error('An error occurred during execution:', error);
throw error;
} finally {
await deleteStInstance(deployment.deployment_id);
}
}
main();

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}