From 3370145b4246c38dec9ad6645e21468626aeda15 Mon Sep 17 00:00:00 2001 From: Julio SANTILARIO BERTHILIER <62715763+jujusb@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:44:17 +0200 Subject: [PATCH 001/245] fix SSE + BFF + views + profiling in dev + dockerfile adding WebSocketManager adding webSocket dependency fix import fix typo adding server side event client manager adding more events to track adding trigger custom events remove unnecesary require fix on message to call depending on data.type received using events in study fix fix frontend jwt adding new endpoint adding update game activity fix fix in gameplay painter fix username fix percent fix fix fix game play fix fix fix fix fix refresh for scheduler + gameplay painter fix adding construct map parameters for signature for signed url adding manual and fixing gameplay activity adding limesurvey painter update activity update limesurvey events fix limesurvey activity painter adding edit activity button (not enabled for the moment) edit test adding token signature encoder fix import generation of signature hmac in sse remove comment fix edit study, test and activities fix fix percentage + edit study name fix using patch adding hmac signature trying to import key after generating it fix using browserify ./public/libs/SSEClientManager.js -o ./public/libs/SSEClientManager-bundle.js adding import/export study adding image + fix file upload fix update activity progress function in utils instead of repeating functions adding edit group name fix config fix using hmacKeyConfig for hmac config remove extra variables adding duplicate test button fix using generated presigned url in simva api adding update limesurvey button edit limesurvey + update owner of survey fix copy survey when editing remove extra file remove ws package delete group fix get survey list when edit fix update survey list adding name in confirm when removing group owner participants, study, test, activity + reorder partial done in progress set study active / archived redirect to first not completed study fix activity result fix see backup fix update activity painter fix update activity result limesurvey fix redirect to scheduler view redirect to studies and groups page when accessing to a group you don't have access fix error bad gateway fix update result gameplay fix gameplay activity completion via adding checkbox in teacher dashboard Adding refresh button fix Adding redirect tor openid with only token to enter fix openidscheduler fix register token users + update xasu config choose with new and previous generation + update passport to keycloak to only use one custom KeyCloakStrategy with 2 possibles additionals parameters fix save username + participant row pdf fix getUsernameOrToken in activities display fix url fix group page as deprecated fix group view disable simva event remove some extra logs for debugging fix activity painter backup result adding pino in backend for frontend adding presigned events url adding kafka consumer fix processMessage from kafka and send them to clients adding display of student token in simva disable access to sso page when it is a token fix display adding ping message at first + every 5 minutes when new message received from another client move events to /events instead of /studies/id/events and /scheduler/id/events + fix students events fix sse adding schedule task fix schedule task fix refresh token fix refresh auth scheduler fix some some to debug use ping event to refresh simva token fix update jwt token fix using client list for case of multiple tabs opened fix logout fix remove client fix refresh token in all pages fix reconnection and close connection on error in SSE adding bff call to simva to remove afterward jwt from frontend adding bff all requests fix some bff functions adding shlink config url tiny generation fix generateShlinkURL fix survey all compaction in bff instead of backend simva api fix update auth token in bff get complete study in /studies/:studyid route in bff update group view to paint participants from updated group.participants in bff fix concatenation in bff fix status completion close connection before unload page fix spaces fix activity controler + others calls to bff instead of client fix refresh auth also in requests fix refresh auth in requests fix getRefreshSessionsList fix reloadStudy in bff fix request to api after check auth and Refresh with callback export study in bff fix bff error response + export import of study fix import study from export fix filter fix duplicate test in bff fix xasu config refresh auth only on scheduler and study page when receiving refreshAuth type message using new function isAuthRefresh to check the token expiration only fix index redirectOpenId and authExpiredAndRefreshAuthWithCallback in auth fix auth combining function for oauth2 with Keycloak and redirect in usertools fix auth for bff calls fix refresh access token update refresh time for schedule task fix auth function fix cookie maxage in config fix add group remove mongoose fix delete complete in group and study + fix patch studies//tests/ instead of /test/ fix remove session fix updateParticipants fix study, test and activity bff calls removed + fix refresh study adding language selection for survey fix duplicate test fix surveyid not changed format Fix generate shlink + possibility of delete url fix utils adding domain shlink fix gameplay progress for gameplay activtity rounding result when updating activity progress fix update activityprogress to update when restart game adding logs gitignore fix logger update gameplaypainter game uri edition adding profiling in dev script adding gitignore on profiling files adding perf output for functions graph adding docker file fix docker file fix dockerfile fix docker startup fix dockerfile fix dockerfile adding publish new version to docker hub when pushing in master fix build dockerfile fix chmod fix logger fix console.log in logger adding heat profiling fix profiling in api fix gitignore fix profiling Update gameplaypainter.js Adding progress through game Adding Activity progression desactivate setInterval for now fix partial fix backup result fix order fix email as example.com and not dummy.dum remove call to LTI platform in simva api in study view (todo create a new view for the lti plateforms) fix default revert changes --- .dockerignore | 4 + .github/workflows/docker-publish.yml | 96 ++ .gitignore | 3 + Dockerfile | 28 + README.md | 0 bin/www | 13 +- config.js | 21 + docker-compose.yaml | 0 docker-startup.sh | 1 + logger.js | 61 ++ logs/.gitignore | 4 + package-lock.json | 863 ++++++++++++------ package.json | 13 +- profiling.js | 49 + public/activities/activitypainter.js | 419 ++++++++- public/activities/gameplaypainter.js | 243 ++--- public/activities/imspackagepainter.js | 81 +- public/activities/limesurveypainter.js | 285 +++--- public/activities/ltitoolpainter.js | 68 +- public/activities/manualpainter.js | 180 ++-- public/activities/miniopainter.js | 43 +- public/activities/rageanalyticspainter.js | 43 +- public/activities/rageminiopainter.js | 43 +- public/allocators/defaultallocatorpainter.js | 5 +- public/allocators/groupallocatorpainter.js | 3 - public/config.jpg | Bin 0 -> 5389 bytes public/css/loader.css | 0 public/css/style.css | 0 public/favicon.ico | Bin public/icon/back.png | Bin public/icon/check.png | Bin public/icon/editar.png | Bin public/icon/gear.png | Bin public/icon/group.png | Bin public/icon/home.png | Bin public/icon/info.png | Bin public/icon/pdf.png | Bin public/icon/power.png | Bin public/icon/refresh.png | Bin 0 -> 94109 bytes public/icon/study.png | Bin public/icon/survey.png | Bin public/icon/user.png | Bin public/icon/user_white.png | Bin public/icon/word.png | Bin public/jquery.js | 0 public/libs/SSEClientManager.js | 96 ++ public/libs/jsonTree/icons.svg | 0 public/libs/jsonTree/jsonTree.css | 0 public/libs/jsonTree/jsonTree.js | 0 public/libs/wsConnection.js | 62 ++ public/login.png | Bin public/logo.png | Bin public/logo.psd | Bin public/logo_text.png | Bin public/logo_text.psd | Bin public/simva.js | 283 +++--- public/toast/jquery.toast.css | 0 public/toast/jquery.toast.js | 0 public/traces/.gitignore | 0 public/ua.png | Bin public/ua.psd | Bin public/utils.js | 67 +- public/wallpaper.png | Bin routes/index.js | 104 +-- routes/lib/activitiescontroler.js | 164 ++++ routes/lib/date.js | 142 +++ routes/lib/groupcontroler.js | 46 + routes/lib/hMacKey/base58-universal/README.md | 1 + routes/lib/hMacKey/base58-universal/baseN.js | 157 ++++ routes/lib/hMacKey/base58-universal/index.js | 31 + routes/lib/hMacKey/crypto.js | 253 +++++ routes/lib/hMacKey/tokens.js | 55 ++ routes/lib/kafka.js | 79 ++ routes/lib/simva.js | 378 ++++++++ routes/lib/sseClientsListManager.js | 71 ++ routes/lib/sseManager.js | 86 ++ routes/lib/studycontroler.js | 136 +++ routes/lib/testscontroler.js | 69 ++ routes/lib/userClientsListManager.js | 104 +++ routes/lib/usertools.js | 196 +++- routes/lib/utils.js | 78 ++ routes/routes/activities.js | 0 routes/routes/bff.js | 612 +++++++++++++ routes/routes/events.js | 71 ++ routes/routes/groups.js | 4 +- routes/routes/previous-groups.js | 15 + routes/routes/scheduler.js | 0 routes/routes/studies.js | 118 ++- routes/routes/users.js | 65 +- views/about.ejs | 2 +- views/activities_list.ejs | 2 +- views/css/css.css | 0 views/groups_list.ejs | 78 -- views/home.ejs | 2 +- views/layout.ejs | 19 +- views/layout_with_menu.ejs | 19 +- views/layout_with_menu_and_sse.ejs | 103 +++ views/new_group_view.ejs | 443 +++++++++ views/new_groups_list.ejs | 157 ++++ ...group_view.ejs => previous_group_view.ejs} | 163 ++-- views/previous_groups_list.ejs | 157 ++++ views/scheduler.ejs | 107 ++- views/studenthome.ejs | 5 +- views/studies_list.ejs | 148 ++- views/studies_play.ejs | 29 +- views/study_view.ejs | 725 ++++++++++++--- views/users_contact_admin.ejs | 0 views/users_login.ejs | 0 views/users_role_edit.ejs | 5 - 109 files changed, 6752 insertions(+), 1524 deletions(-) create mode 100755 .dockerignore create mode 100755 .github/workflows/docker-publish.yml mode change 100644 => 100755 .gitignore create mode 100755 Dockerfile mode change 100644 => 100755 README.md mode change 100644 => 100755 bin/www mode change 100644 => 100755 config.js mode change 100644 => 100755 docker-compose.yaml create mode 100755 logger.js create mode 100755 logs/.gitignore mode change 100644 => 100755 package-lock.json mode change 100644 => 100755 package.json create mode 100755 profiling.js mode change 100644 => 100755 public/activities/activitypainter.js mode change 100644 => 100755 public/activities/gameplaypainter.js mode change 100644 => 100755 public/activities/imspackagepainter.js mode change 100644 => 100755 public/activities/limesurveypainter.js mode change 100644 => 100755 public/activities/ltitoolpainter.js mode change 100644 => 100755 public/activities/manualpainter.js mode change 100644 => 100755 public/activities/miniopainter.js mode change 100644 => 100755 public/activities/rageanalyticspainter.js mode change 100644 => 100755 public/activities/rageminiopainter.js mode change 100644 => 100755 public/allocators/defaultallocatorpainter.js mode change 100644 => 100755 public/allocators/groupallocatorpainter.js create mode 100755 public/config.jpg mode change 100644 => 100755 public/css/loader.css mode change 100644 => 100755 public/css/style.css mode change 100644 => 100755 public/favicon.ico mode change 100644 => 100755 public/icon/back.png mode change 100644 => 100755 public/icon/check.png mode change 100644 => 100755 public/icon/editar.png mode change 100644 => 100755 public/icon/gear.png mode change 100644 => 100755 public/icon/group.png mode change 100644 => 100755 public/icon/home.png mode change 100644 => 100755 public/icon/info.png mode change 100644 => 100755 public/icon/pdf.png mode change 100644 => 100755 public/icon/power.png create mode 100755 public/icon/refresh.png mode change 100644 => 100755 public/icon/study.png mode change 100644 => 100755 public/icon/survey.png mode change 100644 => 100755 public/icon/user.png mode change 100644 => 100755 public/icon/user_white.png mode change 100644 => 100755 public/icon/word.png mode change 100644 => 100755 public/jquery.js create mode 100755 public/libs/SSEClientManager.js mode change 100644 => 100755 public/libs/jsonTree/icons.svg mode change 100644 => 100755 public/libs/jsonTree/jsonTree.css mode change 100644 => 100755 public/libs/jsonTree/jsonTree.js create mode 100755 public/libs/wsConnection.js mode change 100644 => 100755 public/login.png mode change 100644 => 100755 public/logo.png mode change 100644 => 100755 public/logo.psd mode change 100644 => 100755 public/logo_text.png mode change 100644 => 100755 public/logo_text.psd mode change 100644 => 100755 public/simva.js mode change 100644 => 100755 public/toast/jquery.toast.css mode change 100644 => 100755 public/toast/jquery.toast.js mode change 100644 => 100755 public/traces/.gitignore mode change 100644 => 100755 public/ua.png mode change 100644 => 100755 public/ua.psd mode change 100644 => 100755 public/utils.js mode change 100644 => 100755 public/wallpaper.png mode change 100644 => 100755 routes/index.js create mode 100755 routes/lib/activitiescontroler.js create mode 100755 routes/lib/date.js create mode 100755 routes/lib/groupcontroler.js create mode 100755 routes/lib/hMacKey/base58-universal/README.md create mode 100755 routes/lib/hMacKey/base58-universal/baseN.js create mode 100755 routes/lib/hMacKey/base58-universal/index.js create mode 100755 routes/lib/hMacKey/crypto.js create mode 100755 routes/lib/hMacKey/tokens.js create mode 100755 routes/lib/kafka.js create mode 100755 routes/lib/simva.js create mode 100755 routes/lib/sseClientsListManager.js create mode 100755 routes/lib/sseManager.js create mode 100755 routes/lib/studycontroler.js create mode 100755 routes/lib/testscontroler.js create mode 100755 routes/lib/userClientsListManager.js mode change 100644 => 100755 routes/lib/usertools.js create mode 100755 routes/lib/utils.js mode change 100644 => 100755 routes/routes/activities.js create mode 100755 routes/routes/bff.js create mode 100755 routes/routes/events.js mode change 100644 => 100755 routes/routes/groups.js create mode 100755 routes/routes/previous-groups.js mode change 100644 => 100755 routes/routes/scheduler.js mode change 100644 => 100755 routes/routes/studies.js mode change 100644 => 100755 routes/routes/users.js mode change 100644 => 100755 views/about.ejs mode change 100644 => 100755 views/activities_list.ejs mode change 100644 => 100755 views/css/css.css delete mode 100644 views/groups_list.ejs mode change 100644 => 100755 views/home.ejs mode change 100644 => 100755 views/layout.ejs mode change 100644 => 100755 views/layout_with_menu.ejs create mode 100755 views/layout_with_menu_and_sse.ejs create mode 100755 views/new_group_view.ejs create mode 100755 views/new_groups_list.ejs rename views/{group_view.ejs => previous_group_view.ejs} (71%) mode change 100644 => 100755 create mode 100755 views/previous_groups_list.ejs mode change 100644 => 100755 views/scheduler.ejs mode change 100644 => 100755 views/studenthome.ejs mode change 100644 => 100755 views/studies_list.ejs mode change 100644 => 100755 views/studies_play.ejs mode change 100644 => 100755 views/study_view.ejs mode change 100644 => 100755 views/users_contact_admin.ejs mode change 100644 => 100755 views/users_login.ejs mode change 100644 => 100755 views/users_role_edit.ejs diff --git a/.dockerignore b/.dockerignore new file mode 100755 index 00000000..0eb2006c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.github +Dockerfile +.dockerignore \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100755 index 00000000..04b97511 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,96 @@ +name: Docker + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +on: + push: + branches: [ "master" ] + # Publish semver tags as releases. + tags: [ 'v*.*.*' ] + pull_request: + branches: [ "master" ] + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: docker.io + # github.repository as / + IMAGE_NAME: ${{ github.repository }} + + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Install the cosign tool except on PR + # https://github.com/sigstore/cosign-installer + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 + with: + cosign-release: 'v2.2.4' + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} \ No newline at end of file diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index dcaac7f9..fc0146e5 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ # Config files config-test.js +*.log +*.heapsnapshot +*.crt \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 00000000..7cf85c20 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM node:20.18.3-bullseye + +# Install ca-certificates and update them +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates && \ + update-ca-certificates && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app + +# Set the working directory +WORKDIR /home/node/app + +COPY --chown=node:node package*.json ./ + +USER node + +RUN npm install + +# Copy the current directory contents into the container at /app +COPY --chown=node:node . . + +RUN mkdir -p /home/node/logs && chown -R node:node /home/node/logs + +# Make port 3050 available to the world outside this container +EXPOSE 3050 + +CMD [ "/home/node/app/docker-startup.sh" ] \ No newline at end of file diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/bin/www b/bin/www old mode 100644 new mode 100755 index afa3550e..5f4e5b21 --- a/bin/www +++ b/bin/www @@ -6,8 +6,7 @@ const app = require('../routes'); const http = require('http'); const config = require('../config'); - - +const logger = require('../logger'); //XXX //require('https').globalAgent.options.ca = require('ssl-root-cas/latest').create(); @@ -58,17 +57,17 @@ function onError (error) { const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`; - console.log(error.code); - console.log(error); + logger.info(error.code); + logger.info(error); // handle specific listen errors with friendly messages switch (error.code) { case 'EACCES': - console.log(`${bind} requires elevated privileges`); + logger.info(`${bind} requires elevated privileges`); process.exit(1); break; case 'EADDRINUSE': - console.log(`${bind} is already in use`); + logger.info(`${bind} is already in use`); process.exit(1); break; default: @@ -82,5 +81,5 @@ function onError (error) { function onListening () { const addr = server.address(); const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`; - console.log(`Listening on ${bind}`); + logger.info(`Listening on ${bind}`); } diff --git a/config.js b/config.js old mode 100644 new mode 100755 index ae78075a..2448b676 --- a/config.js +++ b/config.js @@ -11,6 +11,7 @@ config.simva.host = process.env.SIMVA_HOST || 'simva.external.test' config.simva.protocol = process.env.SIMVA_PROTOCOL || 'https' let simvaPort = ((default_protocol_ports[config.simva.protocol] !== config.simva.port) ? `:${config.simva.port}` : '') config.simva.url = process.env.SIMVA_URL || `${config.simva.protocol}://${config.simva.host}${simvaPort}`; +config.simva.cookieMaxAgeInMin=process.env.SIMVA_COOKIE_MAX_AGE_IN_MIN || 4*60 config.mongo = {} config.mongo.host = process.env.MONGO_HOST || 'localhost:27017' @@ -56,7 +57,27 @@ config.limesurvey.url = `${config.limesurvey.protocol}://${config.limesurvey.ho config.limesurvey.adminUser = process.env.LIMESURVEY_ADMIN_USER || 'admin' config.limesurvey.adminPassword = process.env.LIMESURVEY_ADMIN_PASSWORD || 'password' +config.hmac = {} +config.hmac.password = process.env.HMAC_PASSWORD || 'mypassword' +config.hmac.salt = process.env.HMAC_SALT || 'mysalt' +config.hmac.key = process.env.HMAC_KEY || 'mykey' +config.hmac.hmacKey = null + config.lti = {} config.lti.enabled = process.env.LTI_ENABLED || 'false' +config.kafka = {} +config.kafka.clientId= process.env.SIMVA_KAFKA_CLIENTID || 'my-client-id' +config.kafka.brokers= [ process.env.SIMVA_KAFKA_BROKER ] || ['localhost:9092'] +config.kafka.groupId= process.env.SIMVA_KAFKA_GROUPID || 'my-group-id' +config.kafka.topic= process.env.SIMVA_KAFKA_SIMVA_EVENTS_TOPIC || 'minio-events' + +config.shlink = {} +config.shlink.apihost = process.env.SHLINK_SERVER_HOST || 'shlink.external.test' +config.shlink.protocol = process.env.SHLINK_PROTOCOL || 'https' +config.shlink.port = process.env.SHLINK_PORT || '443' +config.shlink.apiurl = `${config.shlink.protocol}://${config.shlink.apihost}:${config.shlink.port}` +config.shlink.apikey = process.env.SHLINK_SERVER_API_KEY || 'myapikey' + + module.exports = config; \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml old mode 100644 new mode 100755 diff --git a/docker-startup.sh b/docker-startup.sh index b1a3c5cf..dc186efd 100755 --- a/docker-startup.sh +++ b/docker-startup.sh @@ -8,6 +8,7 @@ fi if [[ "${NODE_ENV:-production}" == "development" ]]; then npm run dev + #perf record -e cycles:u -g -- npm run dev > perf.out else npm start fi \ No newline at end of file diff --git a/logger.js b/logger.js new file mode 100755 index 00000000..ef3e8d4c --- /dev/null +++ b/logger.js @@ -0,0 +1,61 @@ +const pino = require('pino'); +const config = require('./config.js'); +const path = require('path'); +const logsFolder =process.env.LOG_FOLDER || path.join(__dirname, '../../logs'); +var now = new Date(); +const logFile = `${logsFolder}/${now.toISOString().replace(/:/g, '-')}.log`; +/** @type {{targets:import('pino').TransportTargetOptions[]}} */ + +/** @type {import('pino').LoggerOptions} */ +const options = { + level: (process.env.LOG_LEVEL || 'info').toLowerCase(), + redact: { + paths: ['clientSecret','password', 'api.adminPassword', 'JWT.secret', 'limesurvey.adminPassword', 'sso.clientSecret', 'sso.adminPassword', 'a2.adminPassword', 'LTI.platform.mongo.password', 'LTI.platform.key'], + censor: '**REDACTED**' + }, + customLevels: { log: 30 }, + serializers: { + err: pino.stdSerializers.err, + req: pino.stdSerializers.req, + res: pino.stdSerializers.res + } +} +let targets = [ + { + target: 'pino/file', + level: (process.env.LOG_LEVEL || 'info').toLowerCase(), + options: { + destination: logFile, + singleLine: true + } + } +]; +if (process.env.NODE_ENV !== 'production') { +targets.push( + { + target: 'pino-pretty', + level: (process.env.LOG_LEVEL || 'info').toLowerCase(), + options: { + ignore: 'pid,hostname' + } + } +); +} + + +const transport = pino.transport({ +targets: targets +}); + +const logger = pino(options, transport); + +process.on('uncaughtException', err => { + logger.fatal(err, 'uncaughtException') + process.exitCode = 1 +}); + +process.on('unhandledRejection', reason => + logger.fatal(reason, 'unhandledRejection') +); + +module.exports = logger; \ No newline at end of file diff --git a/logs/.gitignore b/logs/.gitignore new file mode 100755 index 00000000..21910320 --- /dev/null +++ b/logs/.gitignore @@ -0,0 +1,4 @@ +# Ignore all files in this dir... +* +# ... except for this one. +!.gitignore \ No newline at end of file diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 index 919873ed..2e0fa172 --- a/package-lock.json +++ b/package-lock.json @@ -19,49 +19,22 @@ "express-session": "1.15.6", "html": "^1.0.0", "jsonwebtoken": "^9.0.0", - "mongoose": "^5.7.8", + "kafkajs": "^2.2.4", + "node-cron": "3.0.3", "passport": "^0.6.0", - "passport-keycloak-oauth2-oidc": "^1.0.3" + "passport-keycloak-oauth2-oidc": "^1.0.3", + "pino": "^9.6.0" }, "devDependencies": { - "nodemon": "^3.1.4" + "nodemon": "^3.1.4", + "pino-pretty": "^13.0.0" } }, - "node_modules/@types/bson": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.5.tgz", - "integrity": "sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/mongodb": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", - "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", - "dependencies": { - "@types/bson": "*", - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "20.11.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.23.tgz", - "integrity": "sha512-ZUarKKfQuRILSNYt32FuPL20HS7XwNT7/uRwSV8tiHWfyyVwDLYZNF6DZKc2bove++pgfsXn9sUwII/OsQ82cQ==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -74,6 +47,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -89,6 +63,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -100,22 +75,34 @@ "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" }, "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -123,55 +110,44 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "license": "MIT", "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" } }, - "node_modules/bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -197,11 +173,13 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { @@ -209,6 +187,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -220,6 +199,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==", + "license": "Apache-2.0", "engines": { "node": ">=0.6.19" } @@ -227,12 +207,14 @@ "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" }, "node_modules/busboy": { "version": "1.6.0", @@ -249,6 +231,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -276,6 +259,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -292,6 +276,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -315,6 +300,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -325,12 +311,21 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -341,7 +336,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" }, "node_modules/concat-stream": { "version": "1.6.2", @@ -350,6 +346,7 @@ "engines": [ "node >= 0.8" ], + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -361,6 +358,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-3.2.0.tgz", "integrity": "sha512-0Mx88079Z20CG909wCFlR3UxhMYGg6Ibn1hkIje1hwsqOLWtL9HJV+XD0DAjUvQScK6WqY/FA8tSVQM9rR64Rw==", + "license": "MIT", "dependencies": { "mongodb": "^3.1.0" } @@ -369,6 +367,7 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -380,6 +379,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -388,6 +388,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz", "integrity": "sha512-mWkFhcL+HVG1KjeCjEBVJJ7s4sAGMLiBDFSDs4bzzvgLZt7rW8BhP6XV/8b1+pNvx/skd3yYxPuaF3Z6LlQzyw==", + "license": "MIT", "engines": { "node": "*" } @@ -396,6 +397,7 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.3.5.tgz", "integrity": "sha512-YN/8nzPcK5o6Op4MIzAd4H4qUal5+3UaMhVIeaafFYL0pKvBQA/9Yhzo7ZwvBpjdGshsiTAb1+FC37M6RdPDFg==", + "license": "MIT", "dependencies": { "cookie": "0.1.3", "cookie-signature": "1.0.6" @@ -407,22 +409,36 @@ "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" }, "node_modules/crc": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/crc/-/crc-3.4.4.tgz", - "integrity": "sha512-wcAOOnkzlwFAlFCCF20ZAiGn25JgSBy+oQrdOeszuk0bxI2nc29YFFmlCbDEfZJJljuw4XVqHrGV34J89910yA==" + "integrity": "sha512-wcAOOnkzlwFAlFCCF20ZAiGn25JgSBy+oQrdOeszuk0bxI2nc29YFFmlCbDEfZJJljuw4XVqHrGV34J89910yA==", + "license": "MIT" + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -448,6 +464,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -456,6 +473,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==", + "license": "Apache-2.0", "engines": { "node": ">=0.10" } @@ -464,6 +482,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -472,6 +491,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -481,6 +501,7 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" } @@ -488,12 +509,14 @@ "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" }, @@ -513,6 +536,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -595,6 +628,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/express-ejs-extend/-/express-ejs-extend-0.0.1.tgz", "integrity": "sha512-XsqcWrDm5rvNqJisFe93mFTXX8mP12eIzBcTzuzqtGu/zQetkxkvgbCaP4RLHJTpdmkc89dIvTlurLB8VHmP/A==", + "license": "MIT", "dependencies": { "ejs": ">=2.4.1" } @@ -603,6 +637,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.4.0.tgz", "integrity": "sha512-RjzLCHxkv3umDeZKeFeMg8w7qe0V09w3B7oGZprr/oO2H/ISCgNzuqzn7gV3HRWb37GjRk429CCpSLS2KNTqMQ==", + "license": "MIT", "dependencies": { "busboy": "^1.6.0" }, @@ -614,6 +649,7 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.6.tgz", "integrity": "sha512-r0nrHTCYtAMrFwZ0kBzZEXa1vtPVrw0dKvGSrKP4dahwBQ1BJpF2/y1Pp4sCD/0kvxV4zZeclyvfmw0B4RMJQA==", + "license": "MIT", "dependencies": { "cookie": "0.3.1", "cookie-signature": "1.0.6", @@ -633,6 +669,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -641,6 +678,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -649,22 +687,57 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" } }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -677,6 +750,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -703,15 +777,16 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -721,10 +796,25 @@ } } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -744,6 +834,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -785,6 +876,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -793,12 +885,15 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", + "integrity": "sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==", "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3" + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -808,6 +903,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -825,10 +921,13 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz", + "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==", "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, "engines": { "node": ">= 0.4" }, @@ -837,9 +936,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -860,10 +959,18 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, "node_modules/html": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/html/-/html-1.0.0.tgz", "integrity": "sha512-lw/7YsdKiP3kk5PnR1INY17iJuzdAtJewxr14ozKJWbbR97znovZ0mh+WEMZ8rjc3lgTK+ID/htTjuyGKB52Kw==", + "license": "BSD", "dependencies": { "concat-stream": "^1.4.7" }, @@ -875,6 +982,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -890,6 +998,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -901,17 +1010,20 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -921,6 +1033,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -933,6 +1046,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -942,6 +1056,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -954,6 +1069,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -961,12 +1077,14 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" }, "node_modules/jake": { - "version": "10.8.7", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", - "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "license": "Apache-2.0", "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", @@ -980,15 +1098,32 @@ "node": ">=10" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", "dependencies": { "jws": "^3.2.2", - "lodash": "^4.17.21", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", "ms": "^2.1.1", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">=12", @@ -998,12 +1133,14 @@ "node_modules/jsonwebtoken/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -1014,25 +1151,74 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, - "node_modules/kareem": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz", - "integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==" + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1041,6 +1227,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT", "optional": true }, "node_modules/merge-descriptors": { @@ -1056,6 +1243,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1076,6 +1264,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1084,6 +1273,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -1095,6 +1285,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1102,19 +1293,21 @@ "node": "*" } }, - "node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/mongodb": { "version": "3.7.4", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz", "integrity": "sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw==", + "license": "Apache-2.0", "dependencies": { "bl": "^2.2.1", "bson": "^1.1.4", @@ -1149,109 +1342,39 @@ } } }, - "node_modules/mongoose": { - "version": "5.13.22", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.22.tgz", - "integrity": "sha512-p51k/c4X/MfqeQ3I1ranlDiggLzNumZrTDD9CeezHwZxt2/btf+YZD7MCe07RAY2NgFYVMayq6jMamw02Jmf9w==", - "dependencies": { - "@types/bson": "1.x || 4.0.x", - "@types/mongodb": "^3.5.27", - "bson": "^1.1.4", - "kareem": "2.3.2", - "mongodb": "3.7.4", - "mongoose-legacy-pluralize": "1.0.2", - "mpath": "0.8.4", - "mquery": "3.2.5", - "ms": "2.1.2", - "optional-require": "1.0.x", - "regexp-clone": "1.0.0", - "safe-buffer": "5.2.1", - "sift": "13.5.2", - "sliced": "1.0.1" - }, - "engines": { - "node": ">=4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/mongoose-legacy-pluralize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", - "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==", - "peerDependencies": { - "mongoose": "*" - } - }, - "node_modules/mongoose/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/mongoose/node_modules/optional-require": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz", - "integrity": "sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/mpath": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz", - "integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.5.tgz", - "integrity": "sha512-VjOKHHgU84wij7IUoZzFRU07IAxd5kWJaDmyUzQlbjHjyoeK5TNeeo8ZsFDtTYnSgpW6n/nMNIHvE3u8Lbrf4A==", - "dependencies": { - "bluebird": "3.5.1", - "debug": "3.1.0", - "regexp-clone": "^1.0.0", - "safe-buffer": "5.1.2", - "sliced": "1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/mquery/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", - "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", "dev": true, + "license": "MIT", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", @@ -1276,12 +1399,13 @@ } }, "node_modules/nodemon/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1297,21 +1421,24 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/nodemon/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/nodemon/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -1319,26 +1446,12 @@ "node": ">=4" } }, - "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1346,12 +1459,13 @@ "node_modules/oauth": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", - "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==" + "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==", + "license": "MIT" }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -1360,10 +1474,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -1375,14 +1499,26 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optional-require": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz", "integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==", + "license": "Apache-2.0", "dependencies": { "require-at": "^1.0.6" }, @@ -1394,6 +1530,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1402,6 +1539,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "license": "MIT", "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -1419,6 +1557,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/passport-keycloak-oauth2-oidc/-/passport-keycloak-oauth2-oidc-1.0.5.tgz", "integrity": "sha512-uvZGTFu7MozqOIocDdSlEImEQTaPd8g5Lwbt83SbFqOVvCS3PelKjMQOYoJ7ERL7ELOY6puV0BY+Pw8ExM4EYw==", + "license": "MIT", "dependencies": { "lodash": "^4.17.20", "passport-oauth2": "^1.5.0" @@ -1428,6 +1567,7 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", "dependencies": { "base64url": "3.x.x", "oauth": "0.10.x", @@ -1467,6 +1607,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1474,15 +1615,95 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz", + "integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz", + "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -1494,13 +1715,26 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, "node_modules/qs": { "version": "6.13.0", @@ -1517,10 +1751,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1538,6 +1779,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -1552,6 +1794,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1565,13 +1808,15 @@ "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -1579,15 +1824,20 @@ "node": ">=8.10.0" } }, - "node_modules/regexp-clone": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", - "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } }, "node_modules/require-at": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz", "integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==", + "license": "Apache-2.0", "engines": { "node": ">=4" } @@ -1609,17 +1859,29 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/saslprep": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "license": "MIT", "optional": true, "dependencies": { "sparse-bitfield": "^3.0.3" @@ -1628,10 +1890,18 @@ "node": ">=6" } }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1713,7 +1983,8 @@ "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" }, "node_modules/side-channel": { "version": "1.0.6", @@ -1733,16 +2004,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sift": { - "version": "13.5.2", - "resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz", - "integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA==" - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -1750,24 +2017,39 @@ "node": ">=10" } }, - "node_modules/sliced": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", - "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==" + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", "optional": true, "dependencies": { "memory-pager": "^1.0.2" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1784,6 +2066,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -1791,12 +2074,27 @@ "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -1804,11 +2102,21 @@ "node": ">=8" } }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -1820,18 +2128,17 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", "engines": { "node": ">=0.6" } }, "node_modules/touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", "dev": true, - "dependencies": { - "nopt": "~1.0.10" - }, + "license": "ISC", "bin": { "nodetouch": "bin/nodetouch.js" } @@ -1840,6 +2147,7 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -1851,12 +2159,14 @@ "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", "dependencies": { "random-bytes": "~1.0.0" }, @@ -1867,23 +2177,21 @@ "node_modules/uid2": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", - "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "dev": true, + "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1891,23 +2199,42 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", "engines": { "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" } } } diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 405191a0..ad743e20 --- a/package.json +++ b/package.json @@ -4,10 +4,12 @@ "version": "1.0.0", "scripts": { "start": "node bin/www", - "dev": "nodemon --inspect=0.0.0.0:9229 bin/www" + "dev": "nodemon --inspect=0.0.0.0:9229 --prof bin/www" }, "dependencies": { + "axios": "^1.7.4", "body-parser": "1.20.3", + "connect-mongo": "3", "cookie-parser": "1.3.5", "ejs": "^3.1.10", "express": "4.21.0", @@ -16,13 +18,14 @@ "express-session": "1.15.6", "html": "^1.0.0", "jsonwebtoken": "^9.0.0", - "mongoose": "^5.7.8", - "connect-mongo": "3", "passport": "^0.6.0", + "pino": "^9.6.0", + "kafkajs":"^2.2.4", "passport-keycloak-oauth2-oidc": "^1.0.3", - "axios": "^1.7.4" + "node-cron": "3.0.3" }, "devDependencies": { - "nodemon": "^3.1.4" + "nodemon": "^3.1.4", + "pino-pretty": "^13.0.0" } } diff --git a/profiling.js b/profiling.js new file mode 100755 index 00000000..d461a202 --- /dev/null +++ b/profiling.js @@ -0,0 +1,49 @@ +const config = require('./config.js'); +const path = require('path'); +const { now, convertTimeToCron } = require("./routes/lib/date.js"); +const logger = require("./logger.js"); +const cron = require('node-cron'); + +if(process.env.NODE_ENV == "development") { + logger.info("Profiling in progress..."); + const profilingFolder = process.env.PROFILING_FOLDER || path.join(__dirname, '../profiling'); + // Schedule a task to run every x + let intervalInMin=30; + const cronTime = convertTimeToCron(intervalInMin); + logger.info(cronTime); + cron.schedule(cronTime, async () => { + logger.info(`schedule task for profiling running...`); + //let filename=`${profilingFolder}/Heap.${now().toISOString()}.heapsnapshot`; + let filename=`${profilingFolder}/${require('v8').writeHeapSnapshot()}`; + logger.info(`Saved heapdump into ${require('v8').writeHeapSnapshot(filename)}`); + }); +} + +/* +import { + Worker, + isMainThread, + parentPort, +} from 'node:worker_threads'; + +if (isMainThread) { + const worker = new Worker(__filename); + + worker.once('message', (filename) => { + logger.info(`worker heapdump: ${filename}`); + // Now get a heapdump for the main thread. + logger.info(`main thread heapdump: ${v8.writeHeapSnapshot(`${profilingFolder}/Heap.${now().toISOString()}.heapsnapshot`)}`); + }); + + // Tell the worker to create a heapdump. + worker.postMessage('heapdump'); +} else { + parentPort.once('message', (message) => { + if (message === 'heapdump') { + // Generate a heapdump for the worker + // and return the filename to the parent. + parentPort.postMessage(v8.writeHeapSnapshot(`${profilingFolder}/Heap.${now().toISOString()}.heapsnapshot`)); + } + }); +} +*/ \ No newline at end of file diff --git a/public/activities/activitypainter.js b/public/activities/activitypainter.js old mode 100644 new mode 100755 index 0432ffdb..dc4fcf21 --- a/public/activities/activitypainter.js +++ b/public/activities/activitypainter.js @@ -17,10 +17,36 @@ var ActivityPainter = { this.utils = utils; }, + getUsernameOrToken : function (user) { + if(user.isToken) { + return user.token; + } else { + return user.username; + } + }, + getExtraForm: function () { return ''; }, + getEditExtraForm: function () { + return ''; + }, + + updateInputEditExtraForm(activity) { + }, + + extractEditInformation: function(form, actualActivity, callback){ + let jform = $(form); + let formdata = Utils.getFormData(jform); + let activity = {}; + if(actualActivity.name !== formdata.name) { + activity.name = formdata.name; + } + callback(null, activity); + }, + + extractInformation: function(form, callback){ let activity = {}; @@ -35,49 +61,51 @@ var ActivityPainter = { fullyPaintActivity: function(activity){ this.paintActivity(activity, participants); - let tmp = this; - this.updateParticipants(activity); - setInterval(function(){ - tmp.updateParticipants(activity); - }, 5000); }, updateParticipants: function(activity){ - let tmp = this; - activity.tmp = {}; + this.paintActivityCompletion(activity, activity.data.completion, true); + this.paintActivityResult(activity, activity.data.hasresult, true); + if(activity.data.openable){ + PainterFactory.Painters["activity"].paintActivityTargets(activity, activity.data.target); + } + }, - Simva.getActivityCompletion(activity._id, function(error, result){ - tmp.paintActivityCompletion(activity, result); - }); + paintActivityTargets: function(activity, results){ + let usernames = Object.keys(results); - Simva.hasActivityResult(activity._id, function(error, result){ - tmp.paintActivityResult(activity, result); - }); + let done = 0, partial = 0; + + for (var i = 0; i < usernames.length; i++) { + $(`#${activity._id}_${usernames[i]}_target`).attr('href', results[usernames[i]]); + } }, - + paintActivity: function(activity, participants){ $(`#test_${activity.test} .activities`).append(`

${activity.name}

-
+ +

${this.simpleName}

Default Activity

-
Completed: 0%
-
Results: 0(0)%
- this.paintActivityParticipantsTable(activity, participants)`); +

Result: ⬇️

+
Completed: 0% [ 0/0 ]
+
Results: 0 (0) % [ 0 (0) /0 ]
+
Progress:0 (0) % [ 0 (0) /0 ]
+ ${this.paintActivityParticipantsTable(activity, participants, true)}`); }, - paintActivityParticipantsTable: function(activity, participants){ + paintActivityParticipantsTable: function(activity, participants, checkbox=false){ let toret = ''; for (var i = 0; i < participants.length; i++) { if(!AllocatorFactory.Painters[allocator.type].isAllocatedToActivity(participants[i].username, activity)){ continue; } - - toret += ` - - `; + toret += ``; + toret += `${this.paintCompletionRow(activity._id,participants[i].username, checkbox)} + ${this.paintResultRow(activity._id,participants[i].username)}`; } toret += '
UserCompletedResult
${participants[i].username}------
${this.paintUsernameOrToken(activity, participants[i])}
'; @@ -85,7 +113,35 @@ var ActivityPainter = { return toret; }, - paintActivityCompletion: function(activity, status){ + paintUsernameOrToken(activity, participant, ok=null) { + let openable = activity.data.openable; + if(ok !== '') { + openable = openable || ok; + } + let toret=""; + if(openable) { + toret += `${this.getUsernameOrToken(participant)}`; + } else { + toret += `${this.getUsernameOrToken(participant)}`; + } + return toret; + }, + + paintCompletionRow(activity, participant, checkbox=false) { + if(checkbox) { + return ` + + `; + } else { + return `---`; + } + }, + + paintResultRow(activity, participant) { + return `---`; + }, + + paintActivityCompletion: function(activity, status, checkbox=false){ let usernames = Object.keys(status); let done = 0; @@ -94,11 +150,22 @@ var ActivityPainter = { if(status[usernames[i]]){ done++; } - - let completion = `${status[usernames[i]]}`; - $(`#completion_${activity._id}_${usernames[i]}`).addClass(!status[usernames[i]] ? 'red' : 'green'); - $(`#completion_${activity._id}_${usernames[i]}`).empty(); - $(`#completion_${activity._id}_${usernames[i]}`).append(completion); + if(checkbox) { + if(status[usernames[i]]){ + $(`#completion_${activity._id}_${usernames[i]}`).addClass('green'); + $(`#completion_${activity._id}_${usernames[i]}`).removeClass('red'); + }else{ + $(`#completion_${activity._id}_${usernames[i]}`).removeClass('green'); + $(`#completion_${activity._id}_${usernames[i]}`).addClass('red'); + } + + $(`#completion_${activity._id}_${usernames[i]}`).find('input[type="checkbox"]').prop('checked', status[usernames[i]]); + } else { + let completion = `${status[usernames[i]]}` + $(`#completion_${activity._id}_${usernames[i]}`).addClass(!status[usernames[i]] ? 'red' : 'green'); + $(`#completion_${activity._id}_${usernames[i]}`).empty(); + $(`#completion_${activity._id}_${usernames[i]}`).append(completion); + } } let progress = Math.round((done / usernames.length) * 1000) / 10; @@ -109,43 +176,243 @@ var ActivityPainter = { $(`#completion_progress_${activity._id} .done`).css('width', `${progress}%` ); $(`#completion_progress_${activity._id} done`).text(progress); + $(`#completion_progress_${activity._id} doneres`).text(done); + $(`#completion_progress_${activity._id} total`).text(usernames.length); + }, + + paintActivityProgress: function(activity, status){ + let usernames = Object.keys(status); + let done = 0, partial = 0; + + for (var i = 0; i < usernames.length; i++) { + if(status[usernames[i]] !== 0){ + partial++; + if(status[usernames[i]] == 1){ + done++; + } + } + + let tmpprogress = status[usernames[i]]*100; + $(`#progress_${activity._id}_${usernames[i]} .done`).css('width', `${tmpprogress}%` ); + $(`#progress_${activity._id}_${usernames[i]} done`).text(tmpprogress); + } + + let progress = Math.round((done / usernames.length) * 1000) / 10; + if(isNaN(progress)){ + progress = 0; + } + $(`#progress_${activity._id} .done`).css('width', `${progress}%` ); + $(`#progress_${activity._id} done`).text(progress); + $(`#progress_${activity._id} doneres`).text(done); + + let partialprogress = Math.round((partial / usernames.length) * 1000) / 10; + if(isNaN(partialprogress)){ + partialprogress = 0; + } + $(`#progress_${activity._id} .partial`).css('width', `${partialprogress}%` ); + $(`#progress_${activity._id} partial`).text(partialprogress); + $(`#progress_${activity._id} partialres`).text(partial); + $(`#progress_${activity._id} total`).text(usernames.length); }, - paintActivityResult: function(activity, results){ + paintActivityResult: function(activity, results, defaultValue='No Results', displayDefaultValue='No Results', partialValue=null,displayPartialValue=null, finalValue="true", displayFinalValue="See Results",painter="PainterFactory.Painters['activity']"){ let usernames = Object.keys(results); let done = 0, partial = 0; for (var i = 0; i < usernames.length; i++) { let status = results[usernames[i]]; - let result = 'No results' - + let result= `${displayDefaultValue}`; + let color = 'red'; + let state = defaultValue; if(status){ - done++; - result = `See Results`; + if(status == finalValue){ + color = 'green'; + state = displayFinalValue; + done++; + partial++; + } else if(status == partialValue) { + color = 'yellow'; + state = displayPartialValue; + partial++; + } else { + color = 'red'; + state = defaultValue; + } + if(state == defaultValue) { + result = `${displayDefaultValue}`; + } else { + result = ` + ${state} + ⬇️ + `; + } + } - - - $(`#result_${activity._id}_${usernames[i]}`).addClass(status ? 'green' : 'red'); + $(`#result_${activity._id}_${usernames[i]}`).removeClass(); + $(`#result_${activity._id}_${usernames[i]}`).addClass(color); $(`#result_${activity._id}_${usernames[i]}`).empty(); $(`#result_${activity._id}_${usernames[i]}`).append(result); } let progress = Math.round((done / usernames.length) * 1000) / 10; - let partialprogress = Math.round((partial / usernames.length) * 1000) / 10; - if(isNaN(progress)){ progress = 0; } + $(`#result_progress_${activity._id} .done`).css('width', `${progress}%` ); + $(`#result_progress_${activity._id} done`).text(progress); + $(`#result_progress_${activity._id} doneres`).text(done); + + let partialprogress = Math.round((partial / usernames.length) * 1000) / 10; if(isNaN(partialprogress)){ partialprogress = 0; } - - $(`#result_progress_${activity._id} .done`).css('width', `${progress}%` ); $(`#result_progress_${activity._id} .partial`).css('width', `${partialprogress}%` ); - $(`#result_progress_${activity._id} done`).text(progress); $(`#result_progress_${activity._id} partial`).text(partialprogress); + $(`#result_progress_${activity._id} partialres`).text(partial); + $(`#result_progress_${activity._id} total`).text(usernames.length); + }, + + updateActivityCompletion: function(activityId, username, completion, checkbox=false) { + var users = parseInt(document.querySelector(`#completion_progress_${activityId} total`).textContent); + var res= parseInt(document.querySelector(`#completion_progress_${activityId} doneres`).textContent); + var newRes; + if(checkbox) { + var previous= $(`#completion_${activityId}_${username}`).find('input[type="checkbox"]').prop('checked'); + if(completion) { + $(`#completion_${activityId}_${username}`).addClass('green'); + $(`#completion_${activityId}_${username}`).removeClass('red'); + newRes=res+1; + if(! previous) { + $(`#completion_${activityId}_${username}`).find('input[type="checkbox"]').prop('checked', true); + } + } else { + $(`#completion_${activityId}_${username}`).removeClass('green'); + $(`#completion_${activityId}_${username}`).addClass('red'); + newRes=res-1; + if(previous) { + $(`#completion_${activityId}_${username}`).find('input[type="checkbox"]').prop('checked', false); + } + } + } else { + var previousCompletion= document.querySelector(`#completion_${activityId}_${username}`).textContent; + $(`#completion_${activityId}_${username}`).addClass(completion == 'false' ? 'red' : 'green'); + $(`#completion_${activityId}_${username}`).empty(); + $(`#completion_${activityId}_${username}`).append(completion); + if(previousCompletion == "false") { + newRes = res + 1; + } + } + $(`#completion_progress_${activityId} doneRes`).text(newRes); + var progress = Math.round((newRes / users) * 1000) / 10; + $(`#completion_progress_${activityId} .done`).css('width', `${progress}%` ); + $(`#completion_progress_${activityId} done`).text(progress); + }, + + updateActivityResult: function(activityId, username, result, defaultValue='No Results', displayDefaultValue='No Results', partialValue=null,displayPartialValue=null, finalValue="true", displayFinalValue="See Results", painter="PainterFactory.Painters['activity']") { + var users=parseInt(document.querySelector(`#result_progress_${activityId} total`).textContent); + var res=parseInt(document.querySelector(`#result_progress_${activityId} doneres`).textContent); + var partialRes=parseInt(document.querySelector(`#result_progress_${activityId} partialres`).textContent); + var prev=document.querySelector(`#result_${activityId}_${username}`).textContent; + var span; + var newRes, newPartialRes; + if(result){ + if(result == finalValue){ + color = 'green'; + state = displayFinalValue; + if(! prev.includes(finalValue)) { + newRes = res+1; + } + } else if(result == partialValue) { + color = 'yellow'; + state = displayPartialValue; + if(! prev.includes(partialValue)) { + newPartialRes = partialRes+1; + } + } else { + color = 'red'; + state = displayDefaultValue; + if(!prev.includes(displayDefaultValue)) { + if(prev.includes(displayPartialValue)) { + newPartialRes = partialRes-1; + } + if(prev.includes(displayFinalValue)) { + newRes = res+1; + } + } + } + span = ` + ${state} + ⬇️ + `; + } else { + span = `${displayDefaultValue}`; + } + $(`#result_${activityId}_${username}`).removeClass(); + $(`#result_${activityId}_${username}`).addClass(color); + $(`#result_${activityId}_${username}`).empty(); + $(`#result_${activityId}_${username}`).append(span); + if(finalValue) { + if(!newRes) { + newRes = res; + } + $(`#result_progress_${activityId} doneRes`).text(newRes); + var progress = Math.round((newRes / users) * 1000) / 10; + $(`#result_progress_${activityId} .done`).css('width', `${progress}%` ); + $(`#result_progress_${activityId} done`).text(progress); + } + if(partialValue) { + if(!newPartialRes) { + newPartialRes = partialRes; + } + $(`#result_progress_${activityId} partialres`).text(newPartialRes); + var partialProgress = Math.round((newPartialRes / users) * 1000) / 10; + $(`#result_progress_${activityId} .partial`).css('width', `${partialProgress}%` ); + $(`#result_progress_${activityId} partial`).text(partialProgress); + } + }, + + updateActivityProgress: function(activityId, username, result) { + var prevValue= parseInt(document.querySelector(`#progress_${activityId}_${username} done`).textContent); + var users = parseInt(document.querySelector(`#progress_${activityId} total`).textContent); + var res= parseInt(document.querySelector(`#progress_${activityId} doneres`).textContent); + var partialres= parseInt(document.querySelector(`#progress_${activityId} partialres`).textContent); + + if(prevValue !== 100) { + var progress=result*100; + $(`#progress_${activityId}_${username} .done`).css('width', `${progress}%` ); + $(`#progress_${activityId}_${username} done`).text(progress); + if(prevValue == 0 && result !== 0) { + var newPartialProgressRes = partialres + 1; + $(`#progress_${activityId} partialres`).text(newPartialProgressRes); + var newPartialProgress = Math.round((newPartialProgressRes/users) * 1000)/10; + $(`#progress_${activityId} partial`).text(newPartialProgress); + $(`#progress_${activityId} .partial`).css('width', `${newPartialProgress}%` ); + } + if(result == 1) { + var newProgressRes = res + 1; + $(`#progress_${activityId} doneres`).text(newProgressRes); + var newProgress = Math.round((newProgressRes/ users) * 1000)/10; + $(`#progress_${activityId} done`).text(newProgress); + $(`#progress_${activityId} .done`).css('width', `${newProgress}%` ); + } + } else { + var progress=result*100; + $(`#progress_${activityId}_${username} .done`).css('width', `${progress}%` ); + $(`#progress_${activityId}_${username} done`).text(progress); + var newProgressRes = res - 1; + $(`#progress_${activityId} doneres`).text(newProgressRes); + var newProgress = Math.round((newProgressRes/ users) * 1000)/10; + $(`#progress_${activityId} done`).text(newProgress); + $(`#progress_${activityId} .done`).css('width', `${newProgress}%` ); + if(result == 0) { + var newPartialProgressRes = partialres - 1; + $(`#progress_${activityId} partialres`).text(newPartialProgressRes); + var newPartialProgress = Math.round((newPartialProgressRes/users) * 1000)/10; + $(`#progress_${activityId} partial`).text(newPartialProgress); + $(`#progress_${activityId} .partial`).css('width', `${newPartialProgress}%` ); + } + } }, openResults: function(activity, user){ @@ -158,14 +425,72 @@ var ActivityPainter = { icon: 'error', stack: false }); - }else{ - let content = `
${result[user]}
`; + } else { + // Extract and sanitize the string + const stringifyres = result[user] + .replace(//g, ">"); + // Create a pre-formatted text element with styling + let content = `
${stringifyres}
`; + let context = $('#iframe_floating iframe')[0].contentWindow.document; let body = $('body', context); + + // Set the content and ensure proper styling body.html(content); - toggleAddForm('iframe_floating'); + body.css({ + 'margin': '0', + 'padding': '0', + 'overflow': 'auto', + 'height': '100vh' + }); + Utils.toggleAddForm('iframe_floating'); } - }) + }); + }, + + downloadResults: function(activity, user){ + if(user) { + Simva.getActivityResultForUser(activity, user, function(error, result){ + if(error){ + $.toast({ + heading: 'Error loading the result', + text: error.message, + position: 'top-right', + icon: 'error', + stack: false + }); + } else { + var filename = `${activity}_${user}.json`; + Utils.download(filename, result[user]); + } + }); + } else { + Simva.getActivityResult(activity, (error, result) => { + if(error) { + toastParams.text = error.message; + $.toast(toastParams); + } else { + Utils.download(`activity_result_${activity}.json`, JSON.stringify(result, null, 2)); + } + }); + } + }, + + toggleCompletion: function(checkbox, activityId, username){ + let status = $(checkbox).is(":checked"); + + if(status){ + $(`#completion_${activityId}_${username}`).addClass('green'); + $(`#completion_${activityId}_${username}`).removeClass('red'); + }else{ + $(`#completion_${activityId}_${username}`).removeClass('green'); + $(`#completion_${activityId}_${username}`).addClass('red'); + } + + Simva.setActivityCompletion(activityId, username, status, function(){ + console.log('saved'); + }); } } diff --git a/public/activities/gameplaypainter.js b/public/activities/gameplaypainter.js old mode 100644 new mode 100755 index 072564c0..65fbfa90 --- a/public/activities/gameplaypainter.js +++ b/public/activities/gameplaypainter.js @@ -17,13 +17,31 @@ var GameplayActivityPainter = { }, getExtraForm: function () { - return `

+ return `

- Game URI can include tags: {authToken}, {username}, and {activityId}

`; + Game URI can include tags: {simvaResultUri}, {simvaHomePage}, {username}, {authToken}, {token_endpoint}, {userToken}, {studyId} and {activityId}

`; //

}, + getEditExtraForm: function () { + return `

+

+

+ Game URI can include tags: {simvaResultUri}, {simvaHomePage}, {username}, {authToken}, {token_endpoint}, {userToken}, {studyId} and {activityId}

`; + //

+ }, + + updateInputEditExtraForm(activity) { + var gameplay_trace_storage = document.getElementById('edit_gameplay_trace_storage'); + gameplay_trace_storage.checked = activity.extra_data.config.trace_storage; + var gameplay_backup = document.getElementById('edit_gameplay_backup'); + gameplay_backup.checked = activity.extra_data.config.backup; + var gameplay_game_uri = document.getElementById('edit_gameplay_game_uri'); + gameplay_game_uri.value = activity.extra_data.game_uri; + + }, + extractInformation: function(form, callback){ let activity = {}; @@ -43,39 +61,56 @@ var GameplayActivityPainter = { callback(null, activity); }, - fullyPaintActivity: function(activity){ - this.paintActivity(activity, participants); - let tmp = this; - - Simva.isActivityOpenable(activity._id, function(error, result){ + extractEditInformation: function(form, actualActivity, callback){ + let jform = $(form); + let formdata = Utils.getFormData(jform); + let activity = {}; - activity.isOpenable = result.openable; - if(activity.isOpenable){ - Simva.getActivityTarget(activity._id, function(error, result){ - activity.tmp.result = result; - tmp.paintActivityTargets(activity, result); - }); + if(actualActivity.name !== formdata.name) { + activity.name = formdata.name; + } + + let trace_storage = formdata.trace_storage === 'on'; + if(actualActivity.extra_data.config.trace_storage !== trace_storage) { + activity.trace_storage = trace_storage; + } + let realtime = formdata.realtime === 'on'; + if(actualActivity.extra_data.config.realtime !== realtime) { + activity.realtime = realtime; + } + let backup = formdata.backup === 'on'; + if(actualActivity.extra_data.config.backup !== backup) { + activity.backup = backup; + } + let game_uri=formdata.game_uri; + if(!(actualActivity.extra_data.game_uri == game_uri)) { + if(actualActivity.extra_data.game_uri) { + activity.game_uri = game_uri; + } else { + if(game_uri !== ''){ + activity.game_uri = game_uri; + } } + } + + callback(null, activity); + }, - tmp.updateParticipants(activity); - - }); + fullyPaintActivity: function(activity){ + this.paintActivity(activity, participants); + this.updateParticipants(activity); }, updateParticipants: function(activity){ - let tmp = this; - activity.tmp = {}; - - Simva.getActivityCompletion(activity._id, function(error, result){ - tmp.paintActivityCompletion(activity, result); - }); - - Simva.getActivityHasResult(activity._id, function(error, result){ - tmp.paintActivityResult(activity, result); - }); + if(activity.data.openable){ + PainterFactory.Painters["activity"].paintActivityTargets(activity, activity.data.target); + } + PainterFactory.Painters["activity"].paintActivityCompletion(activity, activity.data.completion, true); + PainterFactory.Painters["activity"].paintActivityProgress(activity, activity.data.progress); + PainterFactory.Painters["activity"].paintActivityResult(activity, activity.data.hasresult, false, "No Backup", null, null, true, "See Backup"); }, - downloadXasuConfig: function(activityId){ + downloadXasuConfig: function(activityId, studyId){ var content = JSON.stringify({ online: true, simva :true, @@ -86,7 +121,9 @@ var GameplayActivityPainter = { auth_endpoint : `${Simva.ssoUrl}/realms/${Simva.ssoRealm}/protocol/openid-connect/auth`, token_endpoint : `${Simva.ssoUrl}/realms/${Simva.ssoRealm}/protocol/openid-connect/token`, client_id : "simva-plugin", - code_challenge_method : "S256" + code_challenge_method : "S256", + simva_user_token:"true", + login_hint: studyId } }, null, 2); @@ -102,9 +139,9 @@ var GameplayActivityPainter = { paintActivity: function(activity, participants){ let activitybox = `

${activity.name}

-
+ +

${this.simpleName}

`; - /* activitybox += 'Realtime: '; @@ -120,7 +157,7 @@ var GameplayActivityPainter = { activitybox += `Download Data
XASU Config: - + `; } else { @@ -133,10 +170,13 @@ var GameplayActivityPainter = { } else { activitybox += 'Disabled'; } - activitybox += '

'; - activitybox += `
Completed: 0%
` + activitybox += '

'; + activitybox += `
Completed: 0% [ 0/0 ]
` + if(activity.extra_data.config.trace_storage){ + activitybox += `
GameProgress: 0 (0) % [ 0 (0) /0 ]
` + } if(activity.extra_data.config.backup){ - activitybox += `
Results: 0(0)%
` + activitybox += `
BackupResults: 0 (0) % [ 0 (0) /0 ]
` } activitybox += `${this.paintActivityParticipantsTable(activity, participants)}`; @@ -144,10 +184,8 @@ var GameplayActivityPainter = { }, paintActivityParticipantsTable: function(activity, participants){ - let toret = ''; - if(activity.extra_data.config.realtime){ - toret += ''; - } + let toret = '
UserCompletedProgressTraces
'; + //toret += ''; toret += ''; for (var i = 0; i < participants.length; i++) { @@ -156,25 +194,14 @@ var GameplayActivityPainter = { } toret += ''; + toret += ``; - if(activity.isOpenable || (activity.extra_data.game_uri && activity.extra_data.game_uri !== '') ){ - toret += ``; - }else{ - toret += ``; - } + toret += `${PainterFactory.Painters["activity"].paintCompletionRow(activity._id,participants[i].username, true)}`; - toret += ``; + toret += `` - if(activity.extra_data.config.realtime){ - toret += ` - `; - }else{ - toret += '' - } - if(activity.extra_data.config.backup){ - toret += ``; + toret += `${PainterFactory.Painters["activity"].paintResultRow(activity._id,participants[i].username)}`; }else{ toret += ''; } @@ -185,99 +212,18 @@ var GameplayActivityPainter = { return toret; }, - paintActivityCompletion: function(activity, status){ - let usernames = Object.keys(status); - - let done = 0; - - for (var i = 0; i < usernames.length; i++) { - if(status[usernames[i]]){ - done++; - } - - let completion = `${status[usernames[i]]}` - $(`#completion_${activity._id}_${usernames[i]}`).addClass(!status[usernames[i]] ? 'red' : 'green'); - $(`#completion_${activity._id}_${usernames[i]}`).empty(); - $(`#completion_${activity._id}_${usernames[i]}`).append(completion); - } - - let progress = Math.round((done / usernames.length) * 1000) / 10; - - if(isNaN(progress)){ - progress = 0; - } - - $(`#completion_progress_${activity._id} .done`).css('width', `${progress}%` ); - $(`#completion_progress_${activity._id} done`).text(progress); + updateActivityResult: function(activityId, username, backup) { + PainterFactory.Painters["activity"].updateActivityResult(activityId, username,backup); }, - paintActivityResult: function(activity, results){ - let usernames = Object.keys(results); - - let done = 0, partial = 0; - - for (var i = 0; i < usernames.length; i++) { - let status = results[usernames[i]]; - let traces = 'No traces'; - let backup = 'Disabled'; - if(activity.extra_data.config.backup){ - backup = 'No backup'; - } - - if(status){ - done++; - - let tmpprogress = 0; - if(status){ - if(activity.extra_data.config.backup && results[usernames[i]]){ - backup = ` - Download - `; - } - - } - /* - tmpprogress = (tmpprogress * 1000) / 10; - $(`#progress_${activity._id}_${usernames[i]} .done`).css('width', `${tmpprogress}%` ); - $(`#progress_${activity._id}_${usernames[i]} done`).text(tmpprogress);*/ - } - - - /*$(`traces_${activity._id}_${usernames[i]}`).addClass(status && status.realtime ? 'green' : 'red'); - $(`#traces_${activity._id}_${usernames[i]}`).empty(); - $(`#traces_${activity._id}_${usernames[i]}`).append(traces);*/ - - $(`#backup_${activity._id}_${usernames[i]}`).addClass(status ? 'green' : 'red'); - $(`#backup_${activity._id}_${usernames[i]}`).empty(); - $(`#backup_${activity._id}_${usernames[i]}`).append(backup); - } - - let progress = Math.round((done / usernames.length) * 1000) / 10; - if(isNaN(progress)){ - progress = 0; - } - $(`#result_progress_${activity._id} .done`).css('width', `${progress}%` ); - $(`#result_progress_${activity._id} done`).text(progress); - - /* - let partialprogress = Math.round((partial / usernames.length) * 1000) / 10; - if(isNaN(partialprogress)){ - partialprogress = 0; - } - $(`#result_progress_${activity._id} .partial`).css('width', `${partialprogress}%` ); - $(`#result_progress_${activity._id} partial`).text(partialprogress);*/ + updateActivityCompletion: function(activityId, username, completion) { + PainterFactory.Painters["activity"].updateActivityCompletion(activityId, username, completion, true); }, - paintActivityTargets: function(activity, results){ - let usernames = Object.keys(results); - - let done = 0, partial = 0; - - for (var i = 0; i < usernames.length; i++) { - $(`#${activity._id}_${usernames[i]}_target`).attr('href', results[usernames[i]]); - } + updateActivityProgress: function(activityId, username, result) { + PainterFactory.Painters["activity"].updateActivityProgress(activityId, username,result); }, + downloadBackup: function(activity, user){ var toastParams = { heading: 'Error loading the result', @@ -299,9 +245,15 @@ var GameplayActivityPainter = { } else { - Simva.downloadActivityResult(activity); + Simva.getActivityResult(activity, (error, result) => { + if(error) { + toastParams.text = error.message; + $.toast(toastParams); + } else { + Utils.download(`activity_result_${activity}.json`, JSON.stringify(result, null, 2)); + } + }); } - }, openTraces: function(activity, user){ @@ -339,16 +291,14 @@ var GameplayActivityPainter = { let context = $('#iframe_floating iframe')[0].contentWindow.document; let body = $('body', context); body.html(content); - toggleAddForm('iframe_floating'); + Utils.toggleAddForm('iframe_floating'); } }) }, getMinioData: function(activity){ Simva.getMinioDataUrl(activity, function(error, result){ - console.log("Callback triggered"); if(error){ - console.log("Error:", error); // Log the error object for better visibility $.toast({ heading: 'Error loading the result', text: error.message, @@ -357,7 +307,6 @@ var GameplayActivityPainter = { stack: false }); }else{ - console.log("Result:", result); // Log the entire result for debugging let url = result.url; // Open the generated URL in a new tab diff --git a/public/activities/imspackagepainter.js b/public/activities/imspackagepainter.js old mode 100644 new mode 100755 index 53eb445e..838cb2fa --- a/public/activities/imspackagepainter.js +++ b/public/activities/imspackagepainter.js @@ -23,6 +23,19 @@ var ImsPackagePainter = { //

` }, + getEditExtraForm: function () { + return `

+

`; + //

` + }, + + updateInputEditExtraForm(activity) { + var imspackage_trace_storage = document.getElementById('edit_imspackage_trace_storage'); + imspackage_trace_storage.checked = activity.extra_data.config.trace_storage; + var imspackage_backup = document.getElementById('edit_imspackage_backup'); + imspackage_backup.checked = activity.extra_data.config.backup; + }, + extractInformation: function(form, callback){ let activity = {}; @@ -41,46 +54,37 @@ var ImsPackagePainter = { callback(null, activity); }, + + extractEditInformation: function(form, actualActivity, callback){ + let jform = $(form); + let formdata = Utils.getFormData(jform); + let activity = {}; + + if(actualActivity.name !== formdata.name) { + activity.name = formdata.name; + } + + callback(null, activity); + }, fullyPaintActivity: function(activity){ this.paintActivity(activity, participants); - let tmp = this; - - Simva.isActivityOpenable(activity._id, function(error, result){ - - activity.isOpenable = result.openable; - if(activity.isOpenable){ - Simva.getActivityTarget(activity._id, function(error, result){ - activity.tmp.result = result; - tmp.paintActivityTargets(activity, result); - }); - } - - tmp.updateParticipants(activity); - - setInterval(function(){ - tmp.updateParticipants(activity); - }, 5000); - }); + this.updateParticipants(activity); }, updateParticipants: function(activity){ - let tmp = this; - activity.tmp = {}; - - Simva.getActivityCompletion(activity._id, function(error, result){ - tmp.paintActivityCompletion(activity, result); - }); - - Simva.getActivityResult(activity._id, function(error, result){ - tmp.paintActivityResult(activity, result); - }); + if(activity.data.openable){ + PainterFactory.Painters["activity"].paintActivityTargets(activity, activity.data.target); + } + PainterFactory.Painters["activity"].paintActivityCompletion(activity, activity.data.completion, true); + PainterFactory.Painters["activity"].paintActivityResult(activity, activity.data.result, false, "No Backup", null, null, true, "See Backup", "imspackage"); }, paintActivity: function(activity, participants){ let activitybox = `

${activity.name}

-
+ +

${this.simpleName}

`; /* @@ -117,12 +121,7 @@ var ImsPackagePainter = { toret += '
'; - if(activity.isOpenable || (activity.extra_data.game_uri && activity.extra_data.game_uri !== '') ){ - toret += ``; - }else{ - toret += ``; - } + toret += ``; toret += ``; @@ -246,16 +245,6 @@ var ImsPackagePainter = { $(`#result_progress_${activity._id} partial`).text(partialprogress); }, - paintActivityTargets: function(activity, results){ - let usernames = Object.keys(results); - - let done = 0, partial = 0; - - for (var i = 0; i < usernames.length; i++) { - $(`#${activity._id}_${usernames[i]}_target`).attr('href', results[usernames[i]]); - } - }, - downloadBackup: function(activity, user){ Simva.getActivityResultForUser(activity, user, function(error, result){ if(error){ @@ -310,7 +299,7 @@ var ImsPackagePainter = { let context = $('#iframe_floating iframe')[0].contentWindow.document; let body = $('body', context); body.html(content); - toggleAddForm('iframe_floating'); + Utils.toggleAddForm('iframe_floating'); } }) }, diff --git a/public/activities/limesurveypainter.js b/public/activities/limesurveypainter.js old mode 100644 new mode 100755 index ed569b63..3cb110a6 --- a/public/activities/limesurveypainter.js +++ b/public/activities/limesurveypainter.js @@ -37,9 +37,8 @@ var LimeSurveyPainter = { if(this.utils.surveys.length > 0){ form += ''; }else{ form += '

You don\'t have surveys.

' @@ -48,7 +47,7 @@ var LimeSurveyPainter = { form += `

Click to open LimeSurvey

-

LimeSurvey

+

LimeSurvey

Select LLS file

@@ -57,6 +56,67 @@ var LimeSurveyPainter = { return form; }, + + getEditExtraForm: function () { + let form="Survey"; + if(this.utils.surveys.length > 1){ + form += ''; + } else { + form += '

You don\'t have any other surveys.

' + } + form+="Survey Language"; + form += ''; + return form; + }, + + updateInputEditExtraForm(activity) { + Simva.setSurveyOwner(activity._id, (error, result) => { + // Step 1: Get the select element + var languageSelectElement = document.getElementById('language_list'); + // Step 2: Loop through the data and create options + if(activity.data.languages.list.length > 0){ + activity.data.languages.list.forEach((language) => { + // Step 3: Create a new option element + var option = document.createElement('option'); + + // Step 4: Set the value and text of the option + option.value = language; + option.text = language; + + // Step 5: Append the option to the select element + languageSelectElement.appendChild(option); + }); + + // Set a specific option as selected + if(activity.extra_data.language) { + languageSelectElement.value=activity.extra_data.language; + } + } + // Step 2: Loop through the data and create options + Simva.getSurveyList(activity._id, (error, result) => { + if(!error) { + // Step 1: Get the select element + var selectElement = document.getElementById('existing_survey_list'); + this.utils = result; + this.utils.surveys.forEach((survey) => { + // Step 3: Create a new option element + var option = document.createElement('option'); + + // Step 4: Set the value and text of the option + option.value = survey.sid; + option.text = `${survey.surveyls_title} - ${survey.sid}`; + + // Step 5: Append the option to the select element + selectElement.appendChild(option); + }); + + // Set a specific option as selected + selectElement.value=activity.extra_data.surveyId; + } + }); + }); + }, + downloadBackup: function(activity, type, user){ var toastParams = { heading: 'Error loading the result', @@ -77,6 +137,7 @@ var LimeSurveyPainter = { }); }, + extractInformation: function(form, callback){ let activity = {}; @@ -120,59 +181,86 @@ var LimeSurveyPainter = { } }, + extractEditInformation: function(form, actualActivity, callback){ + let jform = $(form); + let formdata = Utils.getFormData(jform); + let activity = {}; + + if(actualActivity.name !== formdata.name) { + activity.name = formdata.name; + } + let actualSurveyid=actualActivity.extra_data.surveyId; + if(typeof(actualSurveyid) == "string") { + actualSurveyid=Number(actualSurveyid); + } + let surveyid=formdata.existingid; + if(typeof(surveyid) == "string") { + surveyid=Number(surveyid); + } + if(actualSurveyid !== surveyid) { + activity.copysurvey = surveyid; + } + + if(actualActivity.extra_data.language !== formdata.language) { + activity.language = formdata.language; + } + + callback(null, activity); + }, + fullyPaintActivity: function(activity){ this.paintActivity(activity, participants); - let tmp = this; - this.updateParticipants(activity); - /*setInterval(function(){ - tmp.updateParticipants(activity); - }, 5000);*/ }, updateParticipants: function(activity){ - let tmp = this; - activity.tmp = {}; - - Simva.getActivityCompletion(activity._id, function(error, result){ - activity.tmp.completion = result; - tmp.paintActivityCompletion(activity, result); - }); - - Simva.getActivityResult(activity._id, function(error, result){ - activity.tmp.result = result; - tmp.paintActivityResult(activity, result); - }); - - Simva.getActivityTarget(activity._id, function(error, result){ - activity.tmp.result = result; - tmp.paintActivityTargets(activity, result); - }); + if(activity.data.openable){ + PainterFactory.Painters["activity"].paintActivityTargets(activity, activity.data.target); + } + PainterFactory.Painters["activity"].paintActivityCompletion(activity, activity.data.completion, false); + let usernames = Object.keys(activity.data.result); + let map= {}; + for (var i = 0; i < usernames.length; i++) { + let state = 'No Results'; + if(activity.data.result[usernames[i]]){ + if(activity.data.result[usernames[i]].submitdate){ + state = 'Completed'; + }else{ + state = 'Started'; + } + } + map[usernames[i]] = state; + } + PainterFactory.Painters["activity"].paintActivityResult(activity, map, "No Results", "No Results", "Started", "Started", "Completed","Completed","LimeSurveyPainter"); }, generateTinyURL: function(activityId, surveyId) { - //Simva.getTinyUrl(activityId, function(error, result){ - //}); let url=`${this.utils.url}${surveyId}`; - $.get(`https://tinyurl.com/api-create.php?url=${url}`, function(shorturl){ - // Copy the text inside the text field - navigator.clipboard.writeText(shorturl); - // Alert Short URL - alert(shorturl); + Simva.generateShlinkURL(url, "survey", `survey_${surveyId}`, null, (error, result) => { + if(!error) { + let shortUrl=result.shortUrl; + // Copy the text inside the text field + navigator.clipboard.writeText(shortUrl); + // Alert Short URL + alert(shortUrl); + } }); }, paintActivity: function(activity, participants){ $(`#test_${activity.test} .activities`).append(`

${activity.name}

-
+ +

${this.simpleName}

Survey ID: ${activity.extra_data.surveyId}

+

Survey Language: ${activity.extra_data.language}

+

Edit Survey

Generate Tiny URL

Full : ⬇️ Code : ⬇️

-
Completed: 0%
-
Results: 0(0)%
+
Completed: 0% [ 0 /0 ]
+
Results: 0 (0) % [ 0 (0) /0 ]
${this.paintActivityParticipantsTable(activity, participants)}
`); }, @@ -183,10 +271,8 @@ var LimeSurveyPainter = { if(!AllocatorFactory.Painters[allocator.type].isAllocatedToActivity(participants[i].username, activity)){ continue; } - - toret += ` - + toret += ``; + toret += ``; } @@ -195,96 +281,37 @@ var LimeSurveyPainter = { return toret; }, - paintActivityCompletion: function(activity, status){ - let usernames = Object.keys(status); - - let done = 0; - - for (var i = 0; i < usernames.length; i++) { - if(status[usernames[i]]){ - done++; - } - - let completion = `${status[usernames[i]]}` - $(`#completion_${activity._id}_${usernames[i]}`).removeClass(); - $(`#completion_${activity._id}_${usernames[i]}`).addClass(!status[usernames[i]] ? 'red' : 'green'); - $(`#completion_${activity._id}_${usernames[i]}`).empty(); - $(`#completion_${activity._id}_${usernames[i]}`).append(completion); - } - - let progress = Math.round((done / usernames.length) * 1000) / 10; - - if(isNaN(progress)){ - progress = 0; + updateActivityCompletion: function(activityId, username, completion) { + try { + PainterFactory.Painters["activity"].updateActivityCompletion(activityId, username, completion); + } catch(e) { } - - $(`#completion_progress_${activity._id} .done`).css('width', `${progress}%` ); - $(`#completion_progress_${activity._id} done`).text(progress); }, - paintActivityResult: function(activity, results){ - let usernames = Object.keys(results); - - let done = 0, partial = 0; - - for (var i = 0; i < usernames.length; i++) { - - let color = 'red'; - let state = 'No Results'; - - if(results[usernames[i]]){ - partial++; - if(results[usernames[i]].submitdate){ - color = 'green'; - state = 'Completed'; - done++; - }else{ - color = 'yellow'; - state = 'Started'; - } - - state =`${state}`; - } - - let completion = `${state}` - $(`#result_${activity._id}_${usernames[i]}`).removeClass(); - $(`#result_${activity._id}_${usernames[i]}`).addClass(color); - $(`#result_${activity._id}_${usernames[i]}`).empty(); - $(`#result_${activity._id}_${usernames[i]}`).append(completion); - } - - let progress = Math.round((done / usernames.length) * 1000) / 10; - let partialprogress = Math.round((partial / usernames.length) * 1000) / 10; - - if(isNaN(progress)){ - progress = 0; - } - if(isNaN(partialprogress)){ - partialprogress = 0; + updateActivityResult: function(activityId, username, result) { + try { + PainterFactory.Painters["activity"].updateActivityResult(activityId, username,result, "No Results","No Results", "Started", "Started", "Completed","Completed","LimeSurveyPainter"); + } catch(e) { } - - $(`#result_progress_${activity._id} .done`).css('width', `${progress}%` ); - $(`#result_progress_${activity._id} .partial`).css('width', `${partialprogress}%` ); - $(`#result_progress_${activity._id} done`).text(progress); - $(`#result_progress_${activity._id} partial`).text(partialprogress); }, - paintActivityTargets: function(activity, results){ - let usernames = Object.keys(results); - - let done = 0, partial = 0; - - for (var i = 0; i < usernames.length; i++) { - $(`#${activity._id}_${usernames[i]}_target`).attr('href', results[usernames[i]]); - } + openNewLimesurvey: function(){ + $('#iframe_floating iframe').prop('src', `${this.limesurveyurl}admin/survey/sa/newsurvey`); + Utils.toggleAddForm('iframe_floating'); }, - openLimesurvey: function(){ - $('#iframe_floating iframe').prop('src', `${this.limesurveyurl}/admin/survey/sa/newsurvey`); - toggleAddForm('iframe_floating'); + openEditLimesurvey: function(activityId, surveyid){ + $('#iframe_floating iframe').prop('src', `${this.limesurveyurl}admin/survey/sa/view/surveyid/${surveyid}`); + Simva.setSurveyOwner(activityId, function(error, result){ + if(!error) { + let currentSrc = $('#iframe_floating iframe').prop('src'); + $('#iframe_floating iframe').prop('src', `${currentSrc}`); + Utils.toggleAddForm('iframe_floating'); + } + }); }, - openResults: function(activity, type, user){ + openResults: function(activity, user, type){ Simva.getActivityResultWithTypeForUser(activity, type, user, function(error, result){ if(error){ $.toast({ @@ -311,7 +338,25 @@ var LimeSurveyPainter = { 'overflow': 'auto', 'height': '100vh' }); - toggleAddForm('iframe_floating'); + Utils.toggleAddForm('iframe_floating'); + } + }) + }, + + downloadResults: function(activity, user, type){ + Simva.getActivityResultWithTypeForUser(activity, type, user, function(error, result){ + if(error){ + $.toast({ + heading: 'Error loading the result', + text: error.message, + position: 'top-right', + icon: 'error', + stack: false + }); + }else{ + let stringifyres=JSON.stringify(result[user], null, 2); + var filename = `${activity}_${user}_${type}.json`; + Utils.download(filename, stringifyres); } }) } diff --git a/public/activities/ltitoolpainter.js b/public/activities/ltitoolpainter.js old mode 100644 new mode 100755 index 8ebd0a87..7df29f65 --- a/public/activities/ltitoolpainter.js +++ b/public/activities/ltitoolpainter.js @@ -43,6 +43,13 @@ var LTIToolPainter = { return form; }, + + getEditExtraForm: function () { + return ""; + }, + + updateInputEditExtraForm(activity) { + }, loadToolList: function(callback){ Simva.getLtiTools(function(error, result){ @@ -61,8 +68,6 @@ var LTIToolPainter = { form += '

No tools available. Create a new one.

' } - console.log(form); - $('#ltitool_byexisting').html(form); callback(); @@ -93,34 +98,28 @@ var LTIToolPainter = { } }, + extractEditInformation: function(form, actualActivity, callback){ + let jform = $(form); + let formdata = Utils.getFormData(jform); + let activity = {}; + + if(actualActivity.name !== formdata.name) { + activity.name = formdata.name; + } + callback(null, activity); + }, + fullyPaintActivity: function(activity){ this.paintActivity(activity, participants); - let tmp = this; - this.updateParticipants(activity); - setInterval(function(){ - tmp.updateParticipants(activity); - }, 5000); }, updateParticipants: function(activity){ - let tmp = this; - activity.tmp = {}; - - Simva.getActivityCompletion(activity._id, function(error, result){ - activity.tmp.completion = result; - tmp.paintActivityCompletion(activity, result); - }); - - Simva.getActivityResult(activity._id, function(error, result){ - activity.tmp.result = result; - tmp.paintActivityResult(activity, result); - }); - - Simva.getActivityTarget(activity._id, function(error, result){ - activity.tmp.result = result; - tmp.paintActivityTargets(activity, result); - }); + PainterFactory.Painters["activity"].paintActivityCompletion(activity, activity.data.completion, true); + PainterFactory.Painters["activity"].paintActivityResult(activity, activity.data.result); + if(activity.data.openable){ + PainterFactory.Painters["activity"].paintActivityTargets(activity, activity.data.target); + } }, paintActivity: function(activity, participants){ @@ -134,7 +133,8 @@ var LTIToolPainter = { $(`#test_${activity.test} .activities`).append(`

${activity.name}

-
+ +

${this.simpleName}

Tool ClientID: ${tool.client_id}

Completed: 0%
@@ -149,10 +149,8 @@ var LTIToolPainter = { if(!AllocatorFactory.Painters[allocator.type].isAllocatedToActivity(participants[i].username, activity)){ continue; } - - toret += ` - + toret += ``; + toret += ``; } @@ -235,16 +233,6 @@ var LTIToolPainter = { $(`#result_progress_${activity._id} partial`).text(partialprogress); }, - paintActivityTargets: function(activity, results){ - let usernames = Object.keys(results); - - let done = 0, partial = 0; - - for (var i = 0; i < usernames.length; i++) { - $(`#${activity._id}_${usernames[i]}_target`).attr('href', results[usernames[i]]); - } - }, - openResults: function(activity, user){ Simva.getActivityResultForUser(activity, user, function(error, result){ if(error){ @@ -260,7 +248,7 @@ var LTIToolPainter = { let context = $('#iframe_floating iframe')[0].contentWindow.document; let body = $('body', context); body.html(content); - toggleAddForm('iframe_floating'); + Utils.toggleAddForm('iframe_floating'); } }) }, diff --git a/public/activities/manualpainter.js b/public/activities/manualpainter.js old mode 100644 new mode 100755 index 47d530f3..8d635d03 --- a/public/activities/manualpainter.js +++ b/public/activities/manualpainter.js @@ -23,6 +23,21 @@ var ManualActivityPainter = { URI can include tags: {username}, and {activityId}

`; }, + getEditExtraForm: function () { + return `

+

+ URI can include tags: {username}, and {activityId}

`; + }, + + updateInputEditExtraForm(activity) { + var manual_user_managed = document.getElementById('edit_manual_user_managed'); + manual_user_managed.checked = activity.extra_data.user_managed; + var manual_uri = document.getElementById('edit_manual_uri'); + if(activity.extra_data.uri) { + manual_uri.value = activity.extra_data.uri; + } + }, + extractInformation: function(form, callback){ let activity = {}; @@ -40,38 +55,56 @@ var ManualActivityPainter = { callback(null, activity); }, + extractEditInformation: function(form, actualActivity, callback){ + let jform = $(form); + let formdata = Utils.getFormData(jform); + let activity = {}; + + if(actualActivity.name !== formdata.name) { + activity.name = formdata.name; + } + + let user_managed = formdata.user_managed === 'on'; + if(actualActivity.extra_data.user_managed !== user_managed) { + activity.user_managed = user_managed; + } + + if(!(actualActivity.extra_data.uri == formdata.uri)) { + if(actualActivity.extra_data.uri) { + activity.uri = formdata.uri; + } else { + if(formdata.uri !== ''){ + activity.uri = formdata.uri; + } + } + } + + callback(null, activity); + }, + fullyPaintActivity: function(activity){ this.paintActivity(activity, participants); - let tmp = this; - this.updateParticipants(activity); - setInterval(function(){ - tmp.updateParticipants(activity); - }, 5000); }, updateParticipants: function(activity){ - let tmp = this; - activity.tmp = {}; - - Simva.getActivityCompletion(activity._id, function(error, result){ - tmp.paintActivityCompletion(activity, result); - }); - - Simva.hasActivityResult(activity._id, function(error, result){ - tmp.paintActivityResult(activity, result); - }); + PainterFactory.Painters["activity"].paintActivityCompletion(activity, activity.data.completion, true); + PainterFactory.Painters["activity"].paintActivityResult(activity, activity.data.hasresult); + if(activity.data.openable){ + PainterFactory.Painters["activity"].paintActivityTargets(activity, activity.data.target); + } }, paintActivity: function(activity, participants){ let complete=activity.extra_data.user_managed ? 'can' : 'can\'t' $(`#test_${activity.test} .activities`).append(`

${activity.name}

-
+ +

${this.simpleName}

Students ${complete} complete

-
Completed: 0%
-
Results: 0(0)%
+
Completed: 0% [ 0 /0 ]
+
Results: 0 (0) % [ 0 (0) /0 ]
${this.paintActivityParticipantsTable(activity, participants)}`); }, @@ -82,12 +115,9 @@ var ManualActivityPainter = { if(!AllocatorFactory.Painters[allocator.type].isAllocatedToActivity(participants[i].username, activity)){ continue; } - - toret += `
- - `; + toret += ` + ${PainterFactory.Painters['activity'].paintCompletionRow(activity._id,participants[i].username, true)} + ${PainterFactory.Painters['activity'].paintResultRow(activity._id,participants[i].username)}`; } toret += '
UserCompletedProgressTracesBackup
${PainterFactory.Painters["activity"].paintUsernameOrToken(activity, participants[i], (activity.extra_data.game_uri && activity.extra_data.game_uri !== ''))} - ${participants[i].username}${participants[i].username}---
0%
0%
------
Disabled
${participants[i].username}${participants[i].username}${PainterFactory.Painters["activity"].paintUsernameOrToken(activity, participants[i])}---
${participants[i].username}---
${PainterFactory.Painters["activity"].paintUsernameOrToken(activity, participants[i], true)}--- ---
${participants[i].username}---
${PainterFactory.Painters["activity"].paintUsernameOrToken(activity, participants[i])}--- ---
${participants[i].username} - - ---
${PainterFactory.Painters["activity"].paintUsernameOrToken(activity, participants[i])}
'; @@ -95,106 +125,8 @@ var ManualActivityPainter = { return toret; }, - paintActivityCompletion: function(activity, status){ - let usernames = Object.keys(status); - - let done = 0; - - for (var i = 0; i < usernames.length; i++) { - if(status[usernames[i]]){ - done++; - } - - if(status[usernames[i]]){ - $(`#completion_${activity._id}_${usernames[i]}`).addClass('green'); - $(`#completion_${activity._id}_${usernames[i]}`).removeClass('red'); - }else{ - $(`#completion_${activity._id}_${usernames[i]}`).removeClass('green'); - $(`#completion_${activity._id}_${usernames[i]}`).addClass('red'); - } - - $(`#completion_${activity._id}_${usernames[i]}`).find('input[type="checkbox"]').prop('checked', status[usernames[i]]); - } - - let progress = Math.round((done / usernames.length) * 1000) / 10; - - if(isNaN(progress)){ - progress = 0; - } - - $(`#completion_progress_${activity._id} .done`).css('width', `${progress}%` ); - $(`#completion_progress_${activity._id} done`).text(progress); - }, - - paintActivityResult: function(activity, results){ - let usernames = Object.keys(results); - - let done = 0, partial = 0; - - for (var i = 0; i < usernames.length; i++) { - let status = results[usernames[i]]; - let result = 'No results' - - if(status){ - done++; - result = `See Results`; - } - - $(`#result_${activity._id}_${usernames[i]}`).addClass(status ? 'green' : 'red'); - $(`#result_${activity._id}_${usernames[i]}`).empty(); - $(`#result_${activity._id}_${usernames[i]}`).append(result); - } - - let progress = Math.round((done / usernames.length) * 1000) / 10; - let partialprogress = Math.round((partial / usernames.length) * 1000) / 10; - - if(isNaN(progress)){ - progress = 0; - } - if(isNaN(partialprogress)){ - partialprogress = 0; - } - - $(`#result_progress_${activity._id} .done`).css('width', `${progress}%` ); - $(`#result_progress_${activity._id} .partial`).css('width', `${partialprogress}%` ); - $(`#result_progress_${activity._id} done`).text(progress); - $(`#result_progress_${activity._id} partial`).text(partialprogress); - }, - - openResults: function(activity, user){ - Simva.getActivityResultForUser(activity, user, function(error, result){ - if(error){ - $.toast({ - heading: 'Error loading the result', - text: error.message, - position: 'top-right', - icon: 'error', - stack: false - }); - }else{ - let content = `
${result[user]}
`; - let context = $('#iframe_floating iframe')[0].contentWindow.document; - let body = $('body', context); - body.html(content); - toggleAddForm('iframe_floating'); - } - }) - }, - - toggleCompletion: function(checkbox, activityId, username){ - let status = $(checkbox).is(":checked"); - - if(status){ - $(`#completion_${activityId}_${username}`).addClass('green'); - $(`#completion_${activityId}_${username}`).removeClass('red'); - }else{ - $(`#completion_${activityId}_${username}`).removeClass('green'); - $(`#completion_${activityId}_${username}`).addClass('red'); - } - - Simva.setActivityCompletion(activityId, username, status, function(){ - console.log('saved'); - }); + updateActivityCompletion: function(activityId, username, completion) { + PainterFactory.Painters["activity"].updateActivityCompletion(activityId, username, completion, true); } } diff --git a/public/activities/miniopainter.js b/public/activities/miniopainter.js old mode 100644 new mode 100755 index c9672f8a..2ec36d2c --- a/public/activities/miniopainter.js +++ b/public/activities/miniopainter.js @@ -20,6 +20,13 @@ var ActivityPainter = { return ''; }, + getEditExtraForm: function () { + return this.getExtraForm(); + }, + + updateInputEditExtraForm(activity) { + }, + extractInformation: function(form, callback){ let activity = {}; @@ -32,33 +39,41 @@ var ActivityPainter = { callback(null, activity); }, + extractEditInformation: function(form, actualActivity, callback){ + let jform = $(form); + let formdata = Utils.getFormData(jform); + let activity = {}; + + if(actualActivity.name !== formdata.name) { + activity.name = formdata.name; + } + + callback(null, activity); + }, + fullyPaintActivity: function(activity){ this.paintActivity(activity, participants); let tmp = this; this.updateParticipants(activity); - setInterval(function(){ - tmp.updateParticipants(activity); - }, 5000); + //setInterval(function(){ + // tmp.updateParticipants(activity); + //}, 5000); }, updateParticipants: function(activity){ let tmp = this; activity.tmp = {}; - Simva.getActivityCompletion(activity._id, function(error, result){ - tmp.paintActivityCompletion(activity, result); - }); - - Simva.hasActivityResult(activity._id, function(error, result){ - tmp.paintActivityResult(activity, result); - }); + tmp.paintActivityCompletion(activity, activity.data.completion); + tmp.paintActivityResult(activity, activity.data.hasresult); }, paintActivity: function(activity, participants){ $(`#test_${activity.test} .activities`).append(`

${activity.name}

-
+ +

${this.simpleName}

Minio: Open minio

@@ -75,7 +90,7 @@ var ActivityPainter = { continue; } - toret += `${participants[i].username} + toret += `${PainterFactory.Painters["activity"].paintUsernameOrToken(activity, participants[i])} --- ---`; } @@ -122,7 +137,7 @@ var ActivityPainter = { if(status){ done++; - result = `See Results`; + result = `See Results`; } @@ -162,7 +177,7 @@ var ActivityPainter = { let context = $('#iframe_floating iframe')[0].contentWindow.document; let body = $('body', context); body.html(content); - toggleAddForm('iframe_floating'); + Utils.toggleAddForm('iframe_floating'); } }) } diff --git a/public/activities/rageanalyticspainter.js b/public/activities/rageanalyticspainter.js old mode 100644 new mode 100755 index a8662c68..8f27a43d --- a/public/activities/rageanalyticspainter.js +++ b/public/activities/rageanalyticspainter.js @@ -20,6 +20,13 @@ var RageAnalyticsActivityPainter = { return ''; }, + getEditExtraForm: function () { + return this.getExtraForm(); + }, + + updateInputEditExtraForm(activity) { + }, + extractInformation: function(form, callback){ let activity = {}; @@ -32,33 +39,40 @@ var RageAnalyticsActivityPainter = { callback(null, activity); }, + extractEditInformation: function(form, actualActivity, callback){ + let jform = $(form); + let formdata = Utils.getFormData(jform); + let activity = {}; + + if(actualActivity.name !== formdata.name) { + activity.name = formdata.name; + } + + callback(null, activity); + }, + fullyPaintActivity: function(activity){ this.paintActivity(activity, participants); let tmp = this; this.updateParticipants(activity); - setInterval(function(){ - tmp.updateParticipants(activity); - }, 5000); + //setInterval(function(){ + // tmp.updateParticipants(activity); + //}, 5000); }, updateParticipants: function(activity){ let tmp = this; activity.tmp = {}; - Simva.getActivityCompletion(activity._id, function(error, result){ - tmp.paintActivityCompletion(activity, result); - }); - - Simva.getActivityResult(activity._id, function(error, result){ - tmp.paintActivityResult(activity, result); - }); + tmp.paintActivityCompletion(activity, activity.data.completion); + tmp.paintActivityResult(activity, activity.data.result); }, paintActivity: function(activity, participants){ $(`#test_${activity.test} .activities`).append(`

${activity.name}

-
+

${this.simpleName}

Dashboard Link

Completed: 0%
@@ -74,7 +88,7 @@ var RageAnalyticsActivityPainter = { continue; } - toret += `${participants[i].username} + toret += `${PainterFactory.Painters["activity"].paintUsernameOrToken(activity, participants[i])} ---
0% ---`; @@ -179,15 +193,14 @@ var RageAnalyticsActivityPainter = { let context = $('#iframe_floating iframe')[0].contentWindow.document; let body = $('body', context); body.html(content); - toggleAddForm('iframe_floating'); + Utils.toggleAddForm('iframe_floating'); } }) }, openDashboard: function(activityId){ - console.log(`${this.utils.dashboard_url}${activityId}${this.utils.dashboard_query}`); $('#iframe_floating iframe').prop('src', `${this.utils.dashboard_url}${activityId}${this.utils.dashboard_query}`); - toggleAddForm('iframe_floating'); + Utils.toggleAddForm('iframe_floating'); }, } diff --git a/public/activities/rageminiopainter.js b/public/activities/rageminiopainter.js old mode 100644 new mode 100755 index 9abd9493..1b512d41 --- a/public/activities/rageminiopainter.js +++ b/public/activities/rageminiopainter.js @@ -20,6 +20,13 @@ var RageMinioActivityPainter = { return ''; }, + getEditExtraForm: function () { + return this.getExtraForm(); + }, + + updateInputEditExtraForm(activity) { + }, + extractInformation: function(form, callback){ let activity = {}; @@ -32,33 +39,41 @@ var RageMinioActivityPainter = { callback(null, activity); }, + extractEditInformation: function(form, actualActivity, callback){ + let jform = $(form); + let formdata = Utils.getFormData(jform); + + let activity = {}; + + if(actualActivity.name !== formdata.name) { + activity.name = formdata.name; + } + + callback(null, activity); + }, + fullyPaintActivity: function(activity){ this.paintActivity(activity, participants); let tmp = this; this.updateParticipants(activity); - setInterval(function(){ - tmp.updateParticipants(activity); - }, 5000); + //setInterval(function(){ + // tmp.updateParticipants(activity); + //}, 5000); }, updateParticipants: function(activity){ let tmp = this; activity.tmp = {}; - - Simva.getActivityCompletion(activity._id, function(error, result){ - tmp.paintActivityCompletion(activity, result); - }); - - Simva.getActivityResult(activity._id, function(error, result){ - tmp.paintActivityResult(activity, result); - }); + + tmp.paintActivityCompletion(activity, activity.data.completion); + tmp.paintActivityResult(activity, activity.data.result); }, paintActivity: function(activity, participants){ $(`#test_${activity.test} .activities`).append(`

${activity.name}

-
+

${this.simpleName}

Analytics: Dashboard - Minio: ${participants[i].username} + toret += `${PainterFactory.Painters["activity"].paintUsernameOrToken(activity, participants[i])} ---

0% --- @@ -233,7 +248,7 @@ var RageMinioActivityPainter = { let context = $('#iframe_floating iframe')[0].contentWindow.document; let body = $('body', context); body.html(content); - toggleAddForm('iframe_floating'); + Utils.toggleAddForm('iframe_floating'); } }) }, diff --git a/public/allocators/defaultallocatorpainter.js b/public/allocators/defaultallocatorpainter.js old mode 100644 new mode 100755 index 1c13ca77..fa684958 --- a/public/allocators/defaultallocatorpainter.js +++ b/public/allocators/defaultallocatorpainter.js @@ -139,10 +139,7 @@ var DefaultAllocatorPainter = { let participant = $('#edit_allocator_content select[name="username"]').val(); let test = $('#edit_allocator_content select[name="test"]').val(); - - console.log(participant); - console.log(test); - + if(!this.allocator.extra_data){ this.allocator.extra_data = {}; } diff --git a/public/allocators/groupallocatorpainter.js b/public/allocators/groupallocatorpainter.js old mode 100644 new mode 100755 index 8974ffd2..9a86d38f --- a/public/allocators/groupallocatorpainter.js +++ b/public/allocators/groupallocatorpainter.js @@ -137,9 +137,6 @@ var GroupAllocatorPainter = { let participant = $('#edit_allocator_content select[name="username"]').val(); let test = $('#edit_allocator_content select[name="test"]').val(); - console.log(participant); - console.log(test); - if(!this.allocator.extra_data){ this.allocator.extra_data = {}; } diff --git a/public/config.jpg b/public/config.jpg new file mode 100755 index 0000000000000000000000000000000000000000..cc18e9fcc28bb138353d9409d77a171d016f4bfa GIT binary patch literal 5389 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 L9E$<=?7sRxg)P=1 literal 0 HcmV?d00001 diff --git a/public/css/loader.css b/public/css/loader.css old mode 100644 new mode 100755 diff --git a/public/css/style.css b/public/css/style.css old mode 100644 new mode 100755 diff --git a/public/favicon.ico b/public/favicon.ico old mode 100644 new mode 100755 diff --git a/public/icon/back.png b/public/icon/back.png old mode 100644 new mode 100755 diff --git a/public/icon/check.png b/public/icon/check.png old mode 100644 new mode 100755 diff --git a/public/icon/editar.png b/public/icon/editar.png old mode 100644 new mode 100755 diff --git a/public/icon/gear.png b/public/icon/gear.png old mode 100644 new mode 100755 diff --git a/public/icon/group.png b/public/icon/group.png old mode 100644 new mode 100755 diff --git a/public/icon/home.png b/public/icon/home.png old mode 100644 new mode 100755 diff --git a/public/icon/info.png b/public/icon/info.png old mode 100644 new mode 100755 diff --git a/public/icon/pdf.png b/public/icon/pdf.png old mode 100644 new mode 100755 diff --git a/public/icon/power.png b/public/icon/power.png old mode 100644 new mode 100755 diff --git a/public/icon/refresh.png b/public/icon/refresh.png new file mode 100755 index 0000000000000000000000000000000000000000..7df6395abf4e48d4eb0c61c854068237dfa81ab4 GIT binary patch literal 94109 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 ydU_}rc?f<4!6PsN)W { + const data = JSON.parse(event.data); + if(data.type) { + this.triggerEvent(data.type, data); // Trigger custom listeners for other data.type + } else { + this.triggerEvent("message", data); // Trigger custom listeners for 'message' + } + console.log('Received message:', data); + }; + + // Handle errors and reconnection + this.eventSource.onerror = (event) => { + if (this.eventSource.readyState === EventSource.CLOSED) { + console.log('Connection closed. Attempting to reconnect...'); + this.isConnected = false; + this.reconnect(); + } else { + console.error('SSE Error:', JSON.stringify(event, null, 2)); + this.closeConnection(); + } + }; + + // Handle successful connection + this.eventSource.onopen = () => { + this.isConnected = true; + console.log('Connected to SSE server'); + this.triggerEvent('open'); // Trigger 'open' event + }; + } + + // Reconnect in case of connection failure + reconnect() { + if (!this.isConnected) { + setTimeout(() => { + console.log('Reconnecting to SSE server...'); + this.init(); // Attempt to reconnect + }, this.reconnectInterval); + } + } + + // Close the SSE connection manually + closeConnection() { + if (this.eventSource) { + this.eventSource.close(); + console.log('SSE connection closed'); + } + } + + // Add a custom event listener + on(eventType, callback) { + if (!this.listeners[eventType]) { + this.listeners[eventType] = []; + } + this.listeners[eventType].push(callback); + } + + // Trigger custom event listeners + triggerEvent(eventType, data = null) { + const eventListeners = this.listeners[eventType]; + if (eventListeners) { + eventListeners.forEach((callback) => callback(data)); + } + } + + // Remove all listeners for a specific event type + removeListeners(eventType) { + if (this.listeners[eventType]) { + delete this.listeners[eventType]; + } + } + + // Check if the client is connected + isConnected() { + return this.isConnected; + } +} + +//module.exports = SSEClientManager; +// Expose to the global scope +window.SSEClientManager = SSEClientManager; \ No newline at end of file diff --git a/public/libs/jsonTree/icons.svg b/public/libs/jsonTree/icons.svg old mode 100644 new mode 100755 diff --git a/public/libs/jsonTree/jsonTree.css b/public/libs/jsonTree/jsonTree.css old mode 100644 new mode 100755 diff --git a/public/libs/jsonTree/jsonTree.js b/public/libs/jsonTree/jsonTree.js old mode 100644 new mode 100755 diff --git a/public/libs/wsConnection.js b/public/libs/wsConnection.js new file mode 100755 index 00000000..5d587cf0 --- /dev/null +++ b/public/libs/wsConnection.js @@ -0,0 +1,62 @@ +// wsConnection.js +class WebSocketManager { + constructor(url) { + this.url = url; + this.ws = null; + this.listeners = []; + } + + // Initialize and connect to the WebSocket server + connect() { + this.ws = new WebSocket(this.url); + + // Handle connection open + this.ws.addEventListener('open', () => { + console.log(`Connected to WebSocket server at ${this.url}`); + }); + + // Handle incoming messages + this.ws.addEventListener('message', (message) => { + console.log('Received message:', message); + + // Notify listeners of the new message + this.listeners.forEach(listener => listener(message)); + }); + + // Handle connection close + this.ws.addEventListener('close', () => { + console.log('Disconnected from WebSocket server'); + }); + + // Handle errors + this.ws.addEventListener('error', (error) => { + console.error('WebSocket error:', error); + }); + } + + // Send a message to the WebSocket server + sendMessage(message) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(message)); + } else { + console.error('WebSocket is not open. Cannot send message.'); + } + } + + // Add a listener for incoming messages + addMessageListener(listener) { + this.listeners.push(listener); + } + + // Remove a listener for incoming messages + removeMessageListener(listener) { + this.listeners = this.listeners.filter(l => l !== listener); + } + + // Close the WebSocket connection + disconnect() { + if (this.ws) { + this.ws.close(); + } + } +} diff --git a/public/login.png b/public/login.png old mode 100644 new mode 100755 diff --git a/public/logo.png b/public/logo.png old mode 100644 new mode 100755 diff --git a/public/logo.psd b/public/logo.psd old mode 100644 new mode 100755 diff --git a/public/logo_text.png b/public/logo_text.png old mode 100644 new mode 100755 diff --git a/public/logo_text.psd b/public/logo_text.psd old mode 100644 new mode 100755 diff --git a/public/simva.js b/public/simva.js old mode 100644 new mode 100755 index 7d0c1a8f..9b708287 --- a/public/simva.js +++ b/public/simva.js @@ -6,11 +6,6 @@ var Simva = { ssoUrl:null, ssoRealm:null, - setAPIURL: function(apiurl){ - this.apiurl = apiurl; - }, - - setSSOURL: function(ssoUrl){ this.ssoUrl = ssoUrl; }, @@ -19,10 +14,8 @@ var Simva = { this.ssoRealm = ssoRealm; }, - setJWT: function(jwt){ - this.jwt = jwt; - this.expiration = parseInt(Utils.decodeJWT(this.jwt).exp); - + setAPIURL: function(apiUrl){ + this.apiurl = apiUrl; }, login: function(username, password, callback){ @@ -30,281 +23,283 @@ var Simva = { Utils.post('/users/login', body, callback); }, - checkAndUpdateAuth(callback){ - let current = Math.floor(Date.now() / 1000); + refreshAuth : function(callback){ + Utils.get(`/users/refresh_auth`, callback); + }, - if(current > this.expiration){ - this.refreshAuth(callback); - }else{ - callback(null); + //SHLINK URL + generateShlinkURL(url, tag, title, customSlug, length, callback){ + let body = { + url: url, + tag: tag, + title: title, + customSlug: customSlug, + length:length } + + Utils.post(`/bff/shlink`, body, callback); }, - refreshAuth: function(callback){ - Utils.get('/users/refresh_auth', function(error, result){ - if(!error){ - let body = JSON.parse(result); - Simva.setJWT(body.access_token); - callback(null); - }else{ - callback(error); - } - }); - }, - - // REQUEST CHECKING AUTH - - post: function(url, body, callback){ - this.checkAndUpdateAuth(function(error, result){ - if(!error){ - Utils.post(url, body, callback, Simva.jwt); - }else{ - console.log(error); - } - }) - }, - - patch: function(url, body, callback){ - this.checkAndUpdateAuth(function(error, result){ - if(!error){ - Utils.patch(url, body, callback, Simva.jwt); - }else{ - console.log(error); - } - }) - }, - - put: function(url, body, callback){ - this.checkAndUpdateAuth(function(error){ - if(!error){ - Utils.put(url, body, callback, Simva.jwt); - }else{ - console.log(error); - } - }) - }, - - get: function(url, callback){ - this.checkAndUpdateAuth(function(error){ - if(!error){ - Utils.get(url, callback, Simva.jwt); - }else{ - console.log(error); - } - }) - }, - - getPDF: function(url, callback){ - this.checkAndUpdateAuth(function(error){ - if(!error){ - Utils.getPDF(url, callback, Simva.jwt); - }else{ - console.log(error); - } - }) - }, - - delete: function(url, callback){ - this.checkAndUpdateAuth(function(error){ - if(!error){ - Utils.delete(url, callback, Simva.jwt); - }else{ - console.log(error); - } - }) + deleteShLink(shortCode, callback){ + Utils.delete(`/bff/shlink/${shortCode}`, callback); }, // USER - - register: function(username, email, password, role, callback){ - let body = { username: username, email: email, password: password, role: role }; - Utils.post(`${this.apiurl}/users/`, body, callback); + register: function(groupid, username, email, password, role, isToken, useNewGeneration, callback){ + let body = { + groupid : groupid, + username: username, + email: email, + password: password, + role: role, + isToken : isToken, + useNewGeneration : useNewGeneration + }; + Utils.post(`/bff/users`, body, callback); }, setRole: function(username, role, callback){ let body = { username: username, role: role }; - this.patch(`${this.apiurl}/users/${username}`, body, callback); + Utils.patch(`/bff/users/${username}`, body, callback); + }, + + getCurrentUser: function(callback){ + Utils.get(`/bff/users/me`, callback); }, // GROUPS getGroups: function(callback){ - this.get(`${this.apiurl}/groups`, callback); + Utils.get(`/bff/groups`, callback); }, - addGroup: function(name, callback){ - let body = { name: name }; - this.post(`${this.apiurl}/groups`, body, callback); + addGroup: function(name, newversion, callback){ + let body = { name: name }; + if(newversion) { + body.version = 1; + } else { + body.version = 0; + } + Utils.post(`/bff/groups`, body, callback); }, updateGroup: function(group, callback){ - this.put(`${this.apiurl}/groups/${group._id}`, group, callback); + Utils.put(`/bff/groups/${group._id}`, group, callback); }, getGroup: function(group_id, callback){ - this.get(`${this.apiurl}/groups/${group_id}`, callback); + Utils.get(`/bff/groups/${group_id}`, callback); + }, + + deleteGroup: function(group_id, callback){ + Utils.delete(`/bff/groups/${group_id}`, callback); }, getGroupParticipants: function(group_id, callback){ - this.get(`${this.apiurl}/groups/${group_id}/participants`, callback); + Utils.get(`/bff/groups/${group_id}/participants`, callback); }, // STUDIES getStudies: function(callback){ - this.get(`${this.apiurl}/studies`, callback); + Utils.get(`/bff/studies`, callback); }, addStudy: function(name, callback){ let body = { name: name }; - this.post(`${this.apiurl}/studies`, body, callback); + Utils.post(`/bff/studies`, body, callback); }, addTestToStudy: function(study_id, name, callback){ let body = { name: name }; - this.post(`${this.apiurl}/studies/${study_id}/tests`, body, callback); + Utils.post(`/bff/studies/${study_id}/tests`, body, callback); + }, + + getStudyEventsPresignedUrl: function(study_id, callback){ + Utils.get(`/studies/${study_id}/events/getPresignedUrl`, callback); + }, + + duplicateTestFromStudy: function(study_id, name, testId, callback){ + let body = { name: name, from : testId }; + Utils.post(`/bff/studies/${study_id}/tests`, body, callback); }, getStudy: function(study_id, callback){ - this.get(`${this.apiurl}/studies/${study_id}`, callback); + Utils.get(`/bff/studies/${study_id}`, callback); }, updateStudy: function(study, callback){ - this.put(`${this.apiurl}/studies/${study._id}`, study, callback); + Utils.put(`/bff/studies/${study._id}`, study, callback); + }, + + updateTest: function(studyId, test, callback){ + Utils.patch(`/bff/studies/${studyId}/tests/${test.id}`, test, callback); + }, + + updateActivity: function(activity, callback){ + Utils.patch(`/bff/activities/${activity.id}`, activity, callback); }, deleteStudy: function(study_id, callback){ - this.delete(`${this.apiurl}/studies/${study_id}`, callback); + Utils.delete(`/bff/studies/${study_id}`, callback); }, getAllocator: function(study_id, callback){ - this.get(`${this.apiurl}/studies/${study_id}/allocator`, callback); + Utils.get(`/bff/studies/${study_id}/allocator`, callback); }, updateAllocator: function(study_id, allocator, callback){ - this.put(`${this.apiurl}/studies/${study_id}/allocator`, allocator, callback); + Utils.put(`/bff/studies/${study_id}/allocator`, allocator, callback); }, getStudyTests: function(study_id, callback){ - this.get(`${this.apiurl}/studies/${study_id}/tests`, callback); + Utils.get(`/bff/studies/${study_id}/tests`, callback); + }, + + exportStudyConfig: function(study_id, callback){ + Utils.get(`/bff/studies/${study_id}/export`, callback); + }, + + importStudyConfig: function(newStudy, callback){ + Utils.post(`/bff/studies/import`, newStudy, callback); + }, + + getStudyTest: function(study_id,test_id, callback){ + Utils.get(`/bff/studies/${study_id}/tests/${test_id}`, callback); }, getStudyGroups: function(study_id, callback){ - this.get(`${this.apiurl}/studies/${study_id}/groups`, callback); + Utils.get(`/bff/studies/${study_id}/groups`, callback); }, getTestActivities: function(study_id, test_id, callback){ - this.get(`${this.apiurl}/studies/${study_id}/tests/${test_id}/activities`, callback); + Utils.get(`/bff/studies/${study_id}/tests/${test_id}/activities`, callback); }, getStudyParticipants: function(study_id, callback){ - this.get(`${this.apiurl}/studies/${study_id}/participants`, callback); + Utils.get(`/bff/studies/${study_id}/participants`, callback); }, getStudySchedule: function(study_id, callback){ - this.get(`${this.apiurl}/studies/${study_id}/schedule`, callback); + Utils.get(`/bff/studies/${study_id}/schedule`, callback); + }, + + + getScheduleEventsPresignedUrl: function(study_id, callback){ + Utils.get(`/studies/${study_id}/schedule/events/getPresignedUrl`, callback); }, + getEventsPresignedUrl: function(callback){ + Utils.get(`/events/getPresignedUrl`, callback); + }, + + // Activities addActivityToTest: function(study_id, test_id, activity, callback){ - this.post(`${this.apiurl}/studies/${study_id}/tests/${test_id}/activities`, activity, callback); + Utils.post(`/bff/studies/${study_id}/tests/${test_id}/activities`, activity, callback); + }, + + getActivity: function(activity_id, callback){ + Utils.get(`/bff/activities/${activity_id}`, callback); + }, + + setSurveyOwner: function(activity_id, callback){ + Utils.patch(`/bff/activities/${activity_id}/surveyowner`, {}, callback); + }, + + getSurveyList: function(activity_id, callback){ + Utils.get(`/bff/activities/${activity_id}/usersurveylist`, callback); + }, + + + getActivityProgress: function(activity_id, callback){ + Utils.get(`/bff/activities/${activity_id}/progress`, callback); }, getActivityCompletion: function(activity_id, callback){ - this.get(`${this.apiurl}/activities/${activity_id}/completion`, callback); + Utils.get(`/bff/activities/${activity_id}/completion`, callback); }, setActivityCompletion: function(activity_id, user, status, callback){ - this.post(`${this.apiurl}/activities/${activity_id}/completion?user=${user}`, { status: status }, callback); + Utils.post(`/bff/activities/${activity_id}/completion?user=${user}`, { status: status }, callback); }, getActivityResultForUser : function(activity_id, student, callback){ - this.get(`${this.apiurl}/activities/${activity_id}/result?users=${student}`, callback); + Utils.get(`/bff/activities/${activity_id}/result?users=${student}`, callback); }, getActivityResultWithTypeForUser : function(activity_id, type, student, callback){ - this.get(`${this.apiurl}/activities/${activity_id}/result?users=${student}&type=${type}`, callback); + Utils.get(`/bff/activities/${activity_id}/result?users=${student}&type=${type}`, callback); }, getActivityResult: function(activity_id, callback){ - this.get(`${this.apiurl}/activities/${activity_id}/result`, callback); + Utils.get(`/bff/activities/${activity_id}/result`, callback); }, getActivityResultWithType: function(activity_id, type, callback){ - this.get(`${this.apiurl}/activities/${activity_id}/result?type=${type}`, callback); + Utils.get(`/bff/activities/${activity_id}/result?type=${type}`, callback); }, getActivityHasResult: function(activity_id, callback){ - this.get(`${this.apiurl}/activities/${activity_id}/hasresult`, callback); - }, - - downloadActivityResult: async function(activity_id) { - try { - let url = `${this.apiurl}/activities/${activity_id}/result?token=${this.jwt}`; - window.location.href = url; // Redirige al usuario para iniciar la descarga - - } catch (error) { - console.error('Error al descargar el archivo:', error); - } + Utils.get(`/bff/activities/${activity_id}/hasresult`, callback); }, hasActivityResult: function(activity_id, callback){ - this.get(`${this.apiurl}/activities/${activity_id}/hasresult`, callback); + Utils.get(`/bff/activities/${activity_id}/hasresult`, callback); }, getActivityTarget: function(activity_id, callback){ - this.get(`${this.apiurl}/activities/${activity_id}/target`, callback); + Utils.get(`/bff/activities/${activity_id}/target`, callback); }, isActivityOpenable: function(activity_id, callback){ - this.get(`${this.apiurl}/activities/${activity_id}/openable`, callback); + Utils.get(`/bff/activities/${activity_id}/openable`, callback); }, getMinioDataUrl: function(activity_id, callback){ - this.get(`${this.apiurl}/activities/${activity_id}/presignedurl`, callback); + Utils.get(`/bff/activities/${activity_id}/presignedurl`, callback); }, deleteActivity: function(activity_id, callback){ - this.delete(`${this.apiurl}/activities/${activity_id}`, callback); + Utils.delete(`/bff/activities/${activity_id}`, callback); }, getActivityTypes: function(callback){ - this.get(`${this.apiurl}/activitytypes`, callback); + Utils.get(`/bff/activitytypes`, callback); }, getAllocatorTypes: function(callback){ - this.get(`${this.apiurl}/allocatortypes`, callback); + Utils.get(`/bff/allocatortypes`, callback); }, // LTI getLtiTools: function(callback){ - this.get(`${this.apiurl}/lti/tools`, callback); + Utils.get(`/bff/lti/tools`, callback); }, addLtiTool: function(tool, callback){ - this.post(`${this.apiurl}/lti/tools`, tool, callback); + Utils.post(`/bff/lti/tools`, tool, callback); }, deleteLtiTool: function(tool, callback){ - this.delete(`${this.apiurl}/lti/tools/${tool}`, callback); + Utils.delete(`/bff/lti/tools/${tool}`, callback); }, getLtiPlatforms: function(study, callback){ let query = ''; if(study){ - console.log(study); query = '?searchString=' + encodeURI(`{"studyId":"${study}"}`); } - this.get(`${this.apiurl}/lti/platforms${query}`, callback); + Utils.get(`/bff/lti/platforms${query}`, callback); }, addLtiPlatform: function(platform, callback){ - this.post(`${this.apiurl}/lti/platforms`, platform, callback); + Utils.post(`/bff/lti/platforms`, platform, callback); }, removePlatform: function(platform_id, callback){ - this.delete(`${this.apiurl}/lti/platforms/${platform_id}`, callback); + Utils.delete(`/bff/lti/platforms/${platform_id}`, callback); } } \ No newline at end of file diff --git a/public/toast/jquery.toast.css b/public/toast/jquery.toast.css old mode 100644 new mode 100755 diff --git a/public/toast/jquery.toast.js b/public/toast/jquery.toast.js old mode 100644 new mode 100755 diff --git a/public/traces/.gitignore b/public/traces/.gitignore old mode 100644 new mode 100755 diff --git a/public/ua.png b/public/ua.png old mode 100644 new mode 100755 diff --git a/public/ua.psd b/public/ua.psd old mode 100644 new mode 100755 diff --git a/public/utils.js b/public/utils.js old mode 100644 new mode 100755 index c96f7311..7679adfb --- a/public/utils.js +++ b/public/utils.js @@ -9,8 +9,17 @@ var Utils = { return indexed_array; }, + + toggleAddForm : function(id){ + $(`#${id}`).toggleClass('shown'); + }, + + toggleSubmit : function(form){ + $(form).find('input[type="submit"]').toggle(); + $(form).find('.loader').toggle(); + }, - post: function(url, body, callback, jwt){ + post: function(url, body, callback){ $.ajax({ type: 'POST', url: url, @@ -18,19 +27,16 @@ var Utils = { contentType: 'application/json', dataType: 'json', cache: false, - beforeSend: function (xhr) { - if(jwt){ - xhr.setRequestHeader("Authorization", `Bearer ${jwt}`); - } - }, success: function(data){ callback(null, data); }, - error: callback + error: function(error){ + callback(error); + }, }); }, - patch: function(url, body, callback, jwt){ + patch: function(url, body, callback){ $.ajax({ type: 'PATCH', url: url, @@ -38,19 +44,16 @@ var Utils = { contentType: 'application/json', dataType: 'json', cache: false, - beforeSend: function (xhr) { - if(jwt){ - xhr.setRequestHeader("Authorization", `Bearer ${jwt}`); - } - }, success: function(data){ callback(null, data); }, - error: callback + error: function(error){ + callback(error); + }, }); }, - put: function(url, body, callback, jwt){ + put: function(url, body, callback){ $.ajax({ type: 'PUT', url: url, @@ -58,19 +61,16 @@ var Utils = { contentType: 'application/json', dataType: 'json', cache: false, - beforeSend: function (xhr) { - if(jwt){ - xhr.setRequestHeader("Authorization", `Bearer ${jwt}`); - } - }, success: function(data){ callback(null, data); }, - error: callback + error: function(error){ + callback(error); + }, }); }, - get: function(url, callback, jwt){ + get: function(url, callback){ $.ajax({ type: 'GET', @@ -78,19 +78,16 @@ var Utils = { contentType: 'application/json', dataType: 'json', cache: false, - beforeSend: function (xhr) { - if(jwt){ - xhr.setRequestHeader("Authorization", `Bearer ${jwt}`); - } - }, success: function(data){ callback(null, data); }, - error: callback + error: function(error){ + callback(error); + }, }); }, - getPDF: function(url, callback, jwt){ + getPDF: function(url, callback){ var req = new XMLHttpRequest(); req.open("GET", url, true); @@ -105,23 +102,19 @@ var Utils = { req.send(); }, - delete: function(url, callback, jwt){ - + delete: function(url, callback){ $.ajax({ type: 'DELETE', url: url, contentType: 'application/json', dataType: 'json', cache: false, - beforeSend: function (xhr) { - if(jwt){ - xhr.setRequestHeader("Authorization", `Bearer ${jwt}`); - } - }, success: function(data){ callback(null, data); }, - error: callback + error: function(error){ + callback(error); + }, }); }, diff --git a/public/wallpaper.png b/public/wallpaper.png old mode 100644 new mode 100755 diff --git a/routes/index.js b/routes/index.js old mode 100644 new mode 100755 index 98690c49..33fdd92c --- a/routes/index.js +++ b/routes/index.js @@ -5,42 +5,20 @@ const session = require('express-session'); const bodyParser = require('body-parser'); const config = require('../config'); let usertools = require('./lib/usertools'); +const logger = require('../logger'); +const profiling = require('../profiling'); -const MongoStore = require("connect-mongo")(session); -const mongoose = require('mongoose'); - -var isTest = (process.env.NODE_ENV !== 'production'); -mongoose.connect( !isTest ? config.mongo.url : config.mongo.test, {useNewUrlParser: true}); -mongoose.connection.on('error', console.error.bind(console, 'connection error:')); -mongoose.connection.once('open', function() { - console.log('connected'); -}); - const app = express(); app.use(bodyParser.json({limit: '1mb'})); -/*app.use(session( + +app.use(session( { secret: 'simva app', - name: 'sessionID', - cookie: { - httpOnly: false, - sameSite: 'none', - secure: true + cookie: { + maxAge: config.simva.cookieMaxAgeInMin*60*1000 }, - resave: false, - saveUninitialized: false, - store: new MongoStore({ - mongooseConnection: mongoose.connection, - ttl: 60 * 60 * 24 * 1000, - }) - }) -);*/ -app.use(session( - { - secret: 'simva app', - cookie: {}, resave: false, // Set this to false saveUninitialized: false, // Set this to false } @@ -53,65 +31,22 @@ app.engine('ejs', require('express-ejs-extend')); app.set('views', path.join(__dirname, '/../views')); app.set('view engine', 'ejs'); -var auth = function(level){ - return function(req, res, next) { - if (req.session && req.session.user){ - usertools.authExpired(req, config, function(error, result){ - if(error){ - //res.status(error.status).send(error.data); - var pre = '/'; - for(var i = 0; i < level; i++){ - pre += '../'; - } - return res.redirect(`${pre}users/login`); - } else if(result) { - console.log("auth() - Refreshing token"); - let user = req.session.user; - console.log(`auth() - User:${JSON.stringify(user)}`); - console.log(`auth() - Access Token : ${result}`); - user.jwt = result; - usertools.setUser(req, user); - console.log("auth() - Refreshing token done"); - console.log(`auth() - User: ${JSON.stringify(user)}`); - return next(); - } else { - console.log("auth() - Token OK"); - return next(); - } - }); - }else if(req.query.jwt){ - console.log("auth() - New token"); - let user = {}; - let simvaToken = req.query.jwt; - let profile = usertools.getProfileFromJWT(simvaToken); - user.data = profile; - user.jwt = simvaToken; - usertools.setUser(req, user); - console.log("auth() - New token done"); - return next(); - }else{ - var pre = ''; - for(var i = 0; i < level; i++){ - pre += '../'; - } - return res.redirect(`${pre}users/login`); - } - }; -}; - router = express.Router(); app.use('/', router); -app.use('/users', require('./routes/users.js')(auth(1), config)); -app.use('/studies', require('./routes/studies.js')(auth(1), config)); -app.use('/groups', require('./routes/groups.js')(auth(1), config)); -app.use('/activities', require('./routes/activities.js')(auth(1), config)); -app.use('/scheduler', require('./routes/scheduler.js')(auth(1), config)); +app.use('/users', require('./routes/users.js')(usertools.auth(1), config)); +app.use('/bff', require('./routes/bff.js')(usertools.auth(1), config)); +app.use('/events', require('./routes/events.js')(usertools.auth(1), config)); +app.use('/studies', require('./routes/studies.js')(usertools.auth(1), config)); +app.use('/groups', require('./routes/groups.js')(usertools.auth(1), config)); +app.use('/previous-groups', require('./routes/previous-groups.js')(usertools.auth(1), config)); +app.use('/activities', require('./routes/activities.js')(usertools.auth(1), config)); +app.use('/scheduler', require('./routes/scheduler.js')(usertools.auth(1), config)); -router.get('/about', auth(0), function(req, res, next) { +router.get('/about', usertools.auth(0), function(req, res, next) { res.render('about', { config: config, user: req.session.user }); }); -router.get('/', auth(0), function(req, res, next) { +router.get('/', usertools.auth(0), function(req, res, next) { if(req.session.user.data.role == 'teacher'){ res.render('home', { config: config, user: req.session.user }); }else if(req.session.user.data.role == 'student'){ @@ -127,17 +62,16 @@ router.get('/', auth(0), function(req, res, next) { // catch 404 app.use((req, res, next) => { - console.log(`Error 404 on ${req.url}.`); - console.log(req.url); + logger.info(`Error 404 on ${req.url}.`); res.status(404).send({ message: 'Not found' }); }); // catch errors app.use((err, req, res, next) => { - console.log(err); + logger.info(err); const status = err.status || 500; const msg = err.error || err.message; - console.log(`Error ${status} (${msg}) on ${req.method} ${req.url} with payload ${req.body}.`); + logger.info(`Error ${status} (${msg}) on ${req.method} ${req.url} with payload ${req.body}.`); res.status(status).send({ message: msg }); }); diff --git a/routes/lib/activitiescontroler.js b/routes/lib/activitiescontroler.js new file mode 100755 index 00000000..a4127ba4 --- /dev/null +++ b/routes/lib/activitiescontroler.js @@ -0,0 +1,164 @@ +const logger = require('../../logger'); +const Simva = require('./simva'); + +module.exports = { + getActivities(studyid, testid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getTestActivities(studyid, testid, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + async getCompleteActivity(studyid, testid, activityid, sessionid) { + let act=await this.getActivity(activityid, sessionid); + act.data = {}; + act.data.completion=await this.getActivityCompletion(activityid, sessionid); + act.data.progress=await this.getActivityProgress(activityid, sessionid); + act.data.openable=await this.isActivityOpenable(activityid, sessionid); + if(act.data.openable) { + act.data.target=await this.getActivityTarget(activityid, sessionid); + } + if(act.type == "limesurvey") { + act.data.result=await this.getActivityResult(activityid, sessionid); + act.data.languages=await this.getSurveyLanguages(activityid, sessionid); + } else { + act.data.hasresult=await this.hasActivityResult(activityid, sessionid); + } + return act; + }, + + async exportCompleteActivity(studyid, testid, activityid, complete, sessionid) { + let act=await this.exportActivity(activityid, complete, sessionid); + return act; + }, + + async importActivity(studyid, testid, activity, sessionid) { + let act=await this.addActivityToTest(studyid, testid, activity, sessionid); + return act; + }, + + exportActivity(activityid, complete, sessionid) { + return new Promise((resolve, reject) => { + Simva.exportActivity(activityid, complete, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + addActivityToTest(studyid, testid, activity, sessionid) { + return new Promise((resolve, reject) => { + Simva.addActivityToTest(studyid, testid, activity, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + getActivity(activityid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getActivity(activityid, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + getSurveyLanguages(activityid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getSurveyLanguages(activityid, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + getActivityCompletion(activityid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getActivityCompletion(activityid, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + hasActivityResult(activityid, sessionid) { + return new Promise((resolve, reject) => { + Simva.hasActivityResult(activityid, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + getActivityResult(activityid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getActivityResult(activityid, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + getActivityProgress(activityid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getActivityProgress(activityid, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + getActivityTarget(activityid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getActivityTarget(activityid, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + isActivityOpenable(activityid, sessionid) { + return new Promise((resolve, reject) => { + Simva.isActivityOpenable(activityid, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result.openable); + } + }); + }); + }, +} \ No newline at end of file diff --git a/routes/lib/date.js b/routes/lib/date.js new file mode 100755 index 00000000..4124825f --- /dev/null +++ b/routes/lib/date.js @@ -0,0 +1,142 @@ +//const Duration = require('tinyduration'); + +/** + * + * @returns + */ +function now() { + return new Date(); +} + +/** + * + * @returns + */ +function epoch() { + return new Date(0); +} + +/** + * + * @param {string} strDate + * @returns + */ +function parseDate(strDate) { + const millis = Date.parse(strDate); + return new Date(millis); +} + +/** + * + * @param {Date} start + * @param {Date} end + * + * @returns {number} + */ +function duration(start, end) { + const endMillis = end.getTime(); + const startMillis = start.getTime(); + return endMillis - startMillis +1; +} + +const MILLIS_TO_SECONDS = 1000; +const MILLIS_TO_MINUTES = 60 * MILLIS_TO_SECONDS; +const MILLIS_TO_HOURS = 60 * MILLIS_TO_MINUTES; +const MILLIS_TO_DAYS = 24 * MILLIS_TO_HOURS; +const MILLIS_TO_MONTHS = 30 * MILLIS_TO_DAYS; +const MILLIS_TO_YEARS = 12 * MILLIS_TO_MONTHS; + +/** + * + * @param {number} durationMillis + * @returns {string} + */ +function formatDuration(durationMillis) { + const duration = {}; + + let value = Math.floor(durationMillis / MILLIS_TO_YEARS); + durationMillis = durationMillis % MILLIS_TO_YEARS; + duration.years = value; + + value = Math.floor(durationMillis / MILLIS_TO_MONTHS); + durationMillis = durationMillis % MILLIS_TO_MONTHS; + duration.months = value; + + value = Math.floor(durationMillis / MILLIS_TO_DAYS); + durationMillis = durationMillis % MILLIS_TO_DAYS; + duration.days = value; + + value = Math.floor(durationMillis / MILLIS_TO_HOURS); + durationMillis = durationMillis % MILLIS_TO_HOURS; + duration.hours = value; + + value = Math.floor(durationMillis / MILLIS_TO_MINUTES); + durationMillis = durationMillis % MILLIS_TO_MINUTES; + duration.minutes = value; + + value = Math.floor(durationMillis / MILLIS_TO_SECONDS); + durationMillis = durationMillis % MILLIS_TO_SECONDS; + duration.seconds = value; + + return Duration.serialize(duration); +} + +/** + * + * @param {number} durationMin + * @returns {string} + */ +function convertTimeToCron(durationMin) { + if (durationMin <= 0) { + throw new Error("Time should be greater than 0."); + } + + if (durationMin < 60) { + return `*/${durationMin} * * * *`; // Runs every N minutes + } + + const hours = Math.floor(durationMin / 60); + const extraMinutes = durationMin % 60; + + if (hours < 24) { + let cronExpressions = [`0 */${hours} * * *`]; // Runs every X hours + if (extraMinutes > 0) { + cronExpressions.push(`${extraMinutes} */${hours} * * *`); // Extra minute offset + } + return cronExpressions.join("\n"); + } + + const days = Math.floor(hours / 24); + const extraHours = hours % 24; + + if (days < 7) { + let cronExpressions = [`0 0 */${days} * *`]; // Runs every X days + if (extraHours > 0) { + cronExpressions.push(`0 */${extraHours} * * *`); // Extra hourly offset + } + return cronExpressions.join("\n"); + } + + const weeks = Math.floor(days / 7); + const extraDays = days % 7; + + if (weeks < 4) { + let cronExpressions = [`0 0 * */${weeks} *`]; // Runs every X weeks + if (extraDays > 0) { + cronExpressions.push(`0 0 */${extraDays} * *`); // Extra daily offset + } + return cronExpressions.join("\n"); + } + + const months = Math.floor(weeks / 4); + const extraWeeks = weeks % 4; + + let cronExpressions = [`0 0 1 */${months} *`]; // Runs every X months + if (extraWeeks > 0) { + cronExpressions.push(`0 0 * */${extraWeeks} *`); // Extra weekly offset + } + + return cronExpressions.join("\n"); +} + +module.exports = { now, convertTimeToCron }; \ No newline at end of file diff --git a/routes/lib/groupcontroler.js b/routes/lib/groupcontroler.js new file mode 100755 index 00000000..1f099dca --- /dev/null +++ b/routes/lib/groupcontroler.js @@ -0,0 +1,46 @@ +const logger = require('../../logger'); +const Simva = require('./simva'); + +module.exports = { + async getCompleteGroup(groupid, sessionid) { + let group=await this.getGroup(groupid, sessionid); + group.completeParticipants=await this.getGroupParticipants(groupid, sessionid); + return group; + }, + + getGroups(sessionid) { + return new Promise((resolve, reject) => { + Simva.getGroups(sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + getGroup(groupid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getGroup(groupid, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + getGroupParticipants(groupid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getGroupParticipants(groupid, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, +} \ No newline at end of file diff --git a/routes/lib/hMacKey/base58-universal/README.md b/routes/lib/hMacKey/base58-universal/README.md new file mode 100755 index 00000000..ba7ec0ce --- /dev/null +++ b/routes/lib/hMacKey/base58-universal/README.md @@ -0,0 +1 @@ +https://github.com/digitalbazaar/base58-universal \ No newline at end of file diff --git a/routes/lib/hMacKey/base58-universal/baseN.js b/routes/lib/hMacKey/base58-universal/baseN.js new file mode 100755 index 00000000..7be2aa7d --- /dev/null +++ b/routes/lib/hMacKey/base58-universal/baseN.js @@ -0,0 +1,157 @@ +/** + * Base-N/Base-X encoding/decoding functions. + * + * Original implementation from base-x: + * https://github.com/cryptocoinjs/base-x + * + * Which is MIT licensed: + * + * The MIT License (MIT) + * + * Copyright base-x contributors (c) 2016 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +// baseN alphabet indexes +const _reverseAlphabets = {}; + +/** + * BaseN-encodes a Uint8Array using the given alphabet. + * + * @param {Uint8Array} input - The bytes to encode in a Uint8Array. + * @param {string} alphabet - The alphabet to use for encoding. + * @param {number} maxline - The maximum number of encoded characters per line + * to use, defaults to none. + * + * @returns {string} The baseN-encoded output string. + */ +function encode(input, alphabet, maxline) { + if (!(input instanceof Uint8Array)) { + throw new TypeError('"input" must be a Uint8Array.'); + } + if (typeof alphabet !== 'string') { + throw new TypeError('"alphabet" must be a string.'); + } + if (maxline !== undefined && typeof maxline !== 'number') { + throw new TypeError('"maxline" must be a number.'); + } + if (input.length === 0) { + return ''; + } + + let output = ''; + + let i = 0; + const base = alphabet.length; + const first = alphabet.charAt(0); + const digits = [0]; + for (i = 0; i < input.length; ++i) { + let carry = input[i]; + for (let j = 0; j < digits.length; ++j) { + carry += digits[j] << 8; + digits[j] = carry % base; + carry = (carry / base) | 0; + } + + while (carry > 0) { + digits.push(carry % base); + carry = (carry / base) | 0; + } + } + + // deal with leading zeros + for (i = 0; input[i] === 0 && i < input.length - 1; ++i) { + output += first; + } + // convert digits to a string + for (i = digits.length - 1; i >= 0; --i) { + output += alphabet[digits[i]]; + } + + if (maxline) { + const regex = new RegExp('.{1,' + maxline + '}', 'g'); + output = output.match(regex).join('\r\n'); + } + + return output; +} + +/** + * Decodes a baseN-encoded (using the given alphabet) string to a + * Uint8Array. + * + * @param {string} input - The baseN-encoded input string. + * @param {string} alphabet - The alphabet to use for decoding. + * + * @returns {Uint8Array} The decoded bytes in a Uint8Array. + */ +function decode(input, alphabet) { + if (typeof input !== 'string') { + throw new TypeError('"input" must be a string.'); + } + if (typeof alphabet !== 'string') { + throw new TypeError('"alphabet" must be a string.'); + } + if (input.length === 0) { + return new Uint8Array(); + } + + let table = _reverseAlphabets[alphabet]; + if (!table) { + // compute reverse alphabet + table = _reverseAlphabets[alphabet] = []; + for (let i = 0; i < alphabet.length; ++i) { + table[alphabet.charCodeAt(i)] = i; + } + } + + // remove whitespace characters + input = input.replace(/\s/g, ''); + + const base = alphabet.length; + const first = alphabet.charAt(0); + const bytes = [0]; + for (let i = 0; i < input.length; i++) { + const value = table[input.charCodeAt(i)]; + if (value === undefined) { + return; + } + + let carry = value; + for (let j = 0; j < bytes.length; ++j) { + carry += bytes[j] * base; + bytes[j] = carry & 0xff; + carry >>= 8; + } + + while (carry > 0) { + bytes.push(carry & 0xff); + carry >>= 8; + } + } + + // deal with leading zeros + for (let k = 0; input[k] === first && k < input.length - 1; ++k) { + bytes.push(0); + } + + return new Uint8Array(bytes.reverse()); +} + +module.exports = { encode, decode }; \ No newline at end of file diff --git a/routes/lib/hMacKey/base58-universal/index.js b/routes/lib/hMacKey/base58-universal/index.js new file mode 100755 index 00000000..7220c548 --- /dev/null +++ b/routes/lib/hMacKey/base58-universal/index.js @@ -0,0 +1,31 @@ +/*! + * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. + */ +const { + encode : _encode, + decode : _decode +} = require('./baseN.js'); + +// base58 characters (Bitcoin alphabet) +const alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + +/** + * + * @param {Uint8Array} input + * @returns {string} + */ + +function encode(input, maxline) { + return _encode(input, alphabet, maxline); +} + +/** + * + * @param {string} input + * @returns {Uint8Array} + */ +function decode(input) { + return _decode(input, alphabet); +} + +module.exports = {decode, encode}; \ No newline at end of file diff --git a/routes/lib/hMacKey/crypto.js b/routes/lib/hMacKey/crypto.js new file mode 100755 index 00000000..a0bcbdec --- /dev/null +++ b/routes/lib/hMacKey/crypto.js @@ -0,0 +1,253 @@ +const { decode, encode } = require('./base58-universal/index.js'); + +/** + * + * @param {string} message + * @returns + */ +function getMessageEncoding(message) { + let enc = new TextEncoder(); + return enc.encode(message); +} + +/** + * + * @param {string} message + * @param {CryptoKey} key + * + * @returns {Promise} + */ +async function signMessage(message, key) { + const encoded = getMessageEncoding(message); + const signature = await crypto.subtle.sign( + "HMAC", + key, + encoded + ); + + const value = encode(new Uint8Array(signature)); + return value; +} + +/** + * + * @param {string} message + * @param {string} signature + * @param {CryptoKey} key + * + * @returns {Promise} + */ +async function verifyMessage(message, signature, key) { + const decodedSignature = decode(signature); + const encoded = getMessageEncoding(message); + const result = await crypto.subtle.verify( + "HMAC", + key, + decodedSignature, + encoded + ); + + return result; +} + +/** @type {HmacKeyGenParams} */ +const ALGORITHM = { + name: "HMAC", + hash: { name: "SHA-1" } +}; + +/** + * @returns {Promise} + */ +async function generateRandomHMACKey() { + const key = await crypto.subtle.generateKey( + ALGORITHM, + true, + ["sign", "verify"] + ); + return key; +} + +/** + * @param {string} textKey + * + * @returns {Promise} + */ +async function importKey(textKey) { + const encoded = getMessageEncoding(textKey); + + const key = await crypto.subtle.importKey( + "raw", // raw format of the key - should be Uint8Array + encoded, + ALGORITHM, + false, // = false + ["sign", "verify"] // what this key can do + ); + + return key; +} + +/** + * + * @param {Uint8Array | string} password + * @returns {Promise} + */ +async function getKeyMaterial(password) { + let encodedPassword; + + if (typeof password === 'string') { + encodedPassword = getMessageEncoding(password); + } else { + encodedPassword = password; + } + + const keyMaterial = await crypto.subtle.importKey( + "raw", + encodedPassword, + { name: "PBKDF2" }, + false, + ["deriveBits", "deriveKey"], + ); + + return keyMaterial; +} + +/** + * + * @param {CryptoKey} keyMaterial + * @param {Uint8Array} salt + * @returns + */ +async function getWrapKey(keyMaterial, salt) { + return crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt, + iterations: 100000, + hash: "SHA-256", + }, + keyMaterial, + { name: "AES-KW", length: 256 }, + true, + ["wrapKey", "unwrapKey"], + ); +} + +/** + * + * @param {CryptoKey} keyToWrap + * @param {CryptoKey} keyMaterial + * @param {Uint8Array} salt + * + * @returns {Promise} + */ +async function wrapCryptoKey(keyToWrap, keyMaterial, salt) { + const wrappingKey = await getWrapKey(keyMaterial, salt); + + const wrappedKey = await crypto.subtle.wrapKey("raw", keyToWrap, wrappingKey, "AES-KW"); + const encodedWrappedKey = encode(new Uint8Array(wrappedKey)); + return encodedWrappedKey; +} + +/** + * + * @param {number} size + * + * @returns {Uint8Array} + */ +function generateSalt(size = 16) { + return crypto.getRandomValues(new Uint8Array(size)); +} + +// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/wrapKey#examples +/* +window.crypto.subtle + .generateKey( + { + name: "AES-GCM", + length: 256, + }, + true, + ["encrypt", "decrypt"], + ) + .then((secretKey) => wrapCryptoKey(secretKey)) + .then((wrappedKey) => logger.debug(wrappedKey)); + */ + +/** + * + * @param {Uint8Array} wrappedKey + * @param {CryptoKey} keyMaterial + * @param {Uint8Array} salt + * @returns {Promise} + */ +async function unwrapHmacKey(wrappedKey, keyMaterial, salt) { + const unwrappingKey = await getWrapKey(keyMaterial, salt); + + const unwrappedKey = await crypto.subtle.unwrapKey( + "raw", // import format + wrappedKey, // ArrayBuffer representing key to unwrap + unwrappingKey, // CryptoKey representing key encryption key + "AES-KW", // algorithm identifier for key encryption key + ALGORITHM, // algorithm identifier for key to unwrap + true, // extractability of key to unwrap + ["sign", "verify"], // key usages for key to unwrap + ); + return unwrappedKey; +} + + +const DEFAULT_PASSWORD='12345'; + +/** + * @param {string} [encodedPassword] + * @param {HMACKey} [hmacKey] + * + * @returns {Promise} + */ +async function createHMACKey(encodedPassword = DEFAULT_PASSWORD, hmacKey) { + let encodedSalt = ''; + let encodedKey = ''; + if (hmacKey) { + encodedKey = hmacKey.encodedKey; + encodedSalt = hmacKey.encodedSalt; + } + + let salt; + if (encodedSalt && encodedSalt.length > 0) { + salt = decode(encodedSalt); + } else { + salt = generateSalt(); + encodedSalt = encode(salt) + } + + let keyMaterial; + if (encodedPassword && encodedPassword.length > 0) { + const password = decode(encodedPassword); + keyMaterial = await getKeyMaterial(password); + } else { + const password = generateSalt(); + encodedPassword = encode(password); + keyMaterial = await getKeyMaterial(password); + } + + let key; + if (encodedKey && encodedKey.length > 0) { + const keyBytes = decode(encodedKey); + key = await unwrapHmacKey(keyBytes, keyMaterial, salt); + } else { + key = await generateRandomHMACKey(); + encodedKey = await wrapCryptoKey(key, keyMaterial, salt); + } + + + return { + key, + salt, + encodedKey, + encodedSalt, + encodedPassword + }; +} + +module.exports = {createHMACKey, importKey,signMessage, verifyMessage }; \ No newline at end of file diff --git a/routes/lib/hMacKey/tokens.js b/routes/lib/hMacKey/tokens.js new file mode 100755 index 00000000..9b6152a8 --- /dev/null +++ b/routes/lib/hMacKey/tokens.js @@ -0,0 +1,55 @@ +const { signMessage, verifyMessage } = require("./crypto.js"); +const logger = require('../../../logger.js'); + +async function validateUrl(url, query, hmacKey) { + const signature = query.signature; + logger.debug(signature); + var toSign=Object.entries(query) + .filter(([key, value])=> key !== "signature") + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) // Sort by keys + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + toSign= url + '\n' + toSign; + logger.debug(toSign); + try { + if(await verifyMessage(toSign, signature, hmacKey)) { + logger.info("Valid signature !"); + return true; + } else { + logger.info("Invalid signature !"); + return false; + } + } catch(e) { + logger.info(e); + return false; + } +}; + + +async function createUrl(url, mapParameters, hmacKey) { + const buffer = new Uint8Array(8); + crypto.getRandomValues(buffer); + + mapParameters.ts = (new Date()).toISOString(); + var toSign=Object.entries(mapParameters) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) // Sort by keys + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + toSign=url + '\n' + toSign; + var signature; + try { + signature = await signMessage(toSign, hmacKey); + } catch(e) { + logger.info(e); + signature = "TODO"; + } + const queryString = Object.entries(mapParameters) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) // Sort by keys + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&'); + url = url + `?${queryString}&signature=${signature}`; + + return { data : {url : url} }; +} + +module.exports = {validateUrl, createUrl}; \ No newline at end of file diff --git a/routes/lib/kafka.js b/routes/lib/kafka.js new file mode 100755 index 00000000..b6e3adf8 --- /dev/null +++ b/routes/lib/kafka.js @@ -0,0 +1,79 @@ +var { Kafka, logLevel } = require('kafkajs'); +var logger = require("../../logger.js"); + +/** + * @typedef KafkaOpts + * @property {string} clientId + * @property {string[]} brokers + * @property {string} groupId + * @property {string} topic + */ + +class KafkaClient { + constructor({ clientId, brokers, groupId, topic }) { + logger.info("KAFKA CONFIG:"); + logger.info("Client Id : " + clientId + " | Brokers : "+ brokers + " | groupId : " + groupId + " | topic : "+ topic + " + "); + this.clientId = clientId; + this.brokers = brokers; + this.groupId = groupId; + this.topic = topic; + this.kafka = new Kafka({ logLevel: logLevel.INFO, clientId: this.clientId, brokers: this.brokers }); + this.consumer = this.kafka.consumer({ groupId: this.groupId }); + } + + // Connect and subscribe to a topic + async connect() { + try { + logger.info(`Connecting to Kafka brokers: ${this.brokers}`); + await this.consumer.connect(); + await this.consumer.subscribe({ topic: this.topic, fromBeginning: false }); + logger.info(`Subscribed to topic: ${this.topic}`); + } catch (error) { + console.error('Error connecting to Kafka:', error); + } + } + + // Run the consumer and process messages + async consumeLatestMessages(onMessage) { + try { + await this.connect(); + + logger.info(`Listening for messages from topic: ${this.topic}`); + + await this.consumer.run({ + // By default, eachMessage is invoked sequentially for each message in each partition. + // In order to concurrently process several messages per once, you can increase the partitionsConsumedConcurrently option. + partitionsConsumedConcurrently: 1, + eachMessage: async ({ topic, partition, message }) => { + const msgValue = message.value.toString(); + const msgOffset = message.offset; + const msgPartition = partition; + + const messageInfo = { + topic, + partition: msgPartition, + offset: msgOffset, + value: msgValue + }; + + // Call the provided onMessage callback with the message info + onMessage(messageInfo); + } + }); + } catch (error) { + console.error('Error consuming messages:', error); + } + } + + // Disconnect the consumer + async disconnect() { + try { + await this.consumer.disconnect(); + logger.info('Kafka consumer disconnected'); + } catch (error) { + console.error('Error disconnecting from Kafka:', error); + } + } +} + +module.exports = KafkaClient; \ No newline at end of file diff --git a/routes/lib/simva.js b/routes/lib/simva.js new file mode 100755 index 00000000..113a0bdc --- /dev/null +++ b/routes/lib/simva.js @@ -0,0 +1,378 @@ +const Utils = require('./utils'); +const config = require('../../config'); +const userClientsListManager = require('./userClientsListManager'); +const usertools = require('./usertools'); +class Simva { + apiurl; + ssoUrl; + ssoRealm; + shlinkapikey; + shlinkapidomain; + shlinkapiurl; + + constructor() { + this.apiurl= config.api.url; + this.ssoUrl = config.sso.ssoUrl; + this.ssoRealm = config.sso.ssoRealm; + this.shlinkapikey = config.shlink.apikey; + this.shlinkapidomain = config.shlink.apihost; + this.shlinkapiurl = config.shlink.apiurl; + } + + // REQUEST + post(url, body, sessionId, callback){ + usertools.authExpiredAndRefreshAuthWithCallback(userClientsListManager.getSession(sessionId), (error, result) => { + if(!error) { + Utils.post(url, body, callback, userClientsListManager.getJWT(sessionId)); + } + }); + + } + + patch(url, body, sessionId, callback){ + usertools.authExpiredAndRefreshAuthWithCallback(userClientsListManager.getSession(sessionId), (error, result) => { + if(!error) { + Utils.patch(url, body, callback, userClientsListManager.getJWT(sessionId)); + } + }); + } + + put(url, body, sessionId, callback){ + usertools.authExpiredAndRefreshAuthWithCallback(userClientsListManager.getSession(sessionId), (error, result) => { + if(!error) { + Utils.put(url, body, callback, userClientsListManager.getJWT(sessionId)); + } + }); + } + + get(url, sessionId, callback){ + usertools.authExpiredAndRefreshAuthWithCallback(userClientsListManager.getSession(sessionId), (error, result) => { + if(!error) { + Utils.get(url, callback, userClientsListManager.getJWT(sessionId)); + } + }); + } + + delete(url, sessionId, callback){ + usertools.authExpiredAndRefreshAuthWithCallback(userClientsListManager.getSession(sessionId), (error, result) => { + if(!error) { + Utils.delete(url, callback, userClientsListManager.getJWT(sessionId)); + } + }); + } + + //SHLINK URL + generateURL(url, tag, title, customSlug, length, callback){ + let body = { + "longUrl": url, + "tags": [ + tag + ], + //"validSince": "string", + //"validUntil": "string", + //"maxvisits": 0, + "title": title, + "crawlable": false, + "forwardQuery": true, + "findIfExists": true, + "domain": `${this.shlinkapidomain}`, + //"customSlug": null, + //"shortCodeLength": 0 + } + if(length) { + body.shortCodeLength = length; + } + if(customSlug) { + body.customSlug = customSlug; + } + + Utils.post(`${this.shlinkapiurl}/rest/v3/short-urls`, body, callback, null, this.shlinkapikey); + } + + //SHLINK URL + generateURL(url, tag, title, customSlug, length, callback){ + let body = { + "longUrl": url, + "tags": [ + tag + ], + //"validSince": "string", + //"validUntil": "string", + //"maxvisits": 0, + "title": title, + "crawlable": false, + "forwardQuery": true, + "findIfExists": true, + "domain": `${this.shlinkapidomain}`, + //"customSlug": null, + //"shortCodeLength": 0 + } + if(length) { + body.shortCodeLength = length; + } + if(customSlug) { + body.customSlug = customSlug; + } + + Utils.post(`${this.shlinkapiurl}/rest/v3/short-urls`, body, callback, null, this.shlinkapikey); + } + + //SHLINK URL + deleteShLink(shortCode, callback){ + Utils.delete(`${this.shlinkapiurl}/rest/v3/short-urls/${shortCode}?domain=${this.shlinkapidomain}`, callback, null, this.shlinkapikey); + } + + // USER + register(groupid, username, email, password, role, isToken, useNewGeneration, sessionId, callback){ + let body = { + groupid : groupid, + username: username, + email: email, + password: password, + role: role, + isToken : isToken, + useNewGeneration : useNewGeneration + }; + this.post(`${this.apiurl}/users`, body, sessionId, callback); + } + + setRole(username, role, sessionId, callback){ + let body = { username: username, role: role }; + this.patch(`${this.apiurl}/users/${username}`, body, sessionId, callback); + } + + getCurrentUser(sessionId, callback){ + this.get(`${this.apiurl}/users/me`, sessionId, callback); + } + + // GROUPS + getGroups(sessionId, callback){ + this.get(`${this.apiurl}/groups`, sessionId, callback); + } + + addGroup(name, newversion, sessionId, callback){ + let body = { name: name }; + if(newversion) { + body.version = 1; + } else { + body.version = 0; + } + this.post(`${this.apiurl}/groups`, body, sessionId, callback); + } + + updateGroup(group, sessionId, callback){ + this.put(`${this.apiurl}/groups/${group._id}`, group, sessionId, callback); + } + + getGroup(group_id, sessionId, callback){ + this.get(`${this.apiurl}/groups/${group_id}`, sessionId, callback); + } + + deleteGroup(group_id, sessionId, callback){ + this.delete(`${this.apiurl}/groups/${group_id}`, sessionId, callback); + } + + getGroupParticipants(group_id, sessionId, callback){ + this.get(`${this.apiurl}/groups/${group_id}/participants`, sessionId, callback); + } + + // STUDIES + + getStudies(sessionId, callback){ + this.get(`${this.apiurl}/studies`, sessionId, callback); + } + + addStudy(name, sessionId, callback){ + let body = { name: name }; + this.post(`${this.apiurl}/studies`, body, sessionId, callback); + } + + addTestToStudy(study_id, name, sessionId, callback){ + let body = { name: name }; + this.post(`${this.apiurl}/studies/${study_id}/tests`, body, sessionId, callback); + } + + duplicateTestFromStudy(study_id, name, testId, sessionId, callback){ + let body = { name: name, from : testId }; + this.post(`${this.apiurl}/studies/${study_id}/tests`, body, sessionId, callback); + } + + getStudy(study_id, sessionId, callback){ + this.get(`${this.apiurl}/studies/${study_id}`, sessionId, callback); + } + + updateStudy(study, sessionId, callback){ + this.put(`${this.apiurl}/studies/${study._id}`, study, sessionId, callback); + } + + updateTest(studyId, test, sessionId, callback){ + this.patch(`${this.apiurl}/studies/${studyId}/tests/${test.id}`, test, sessionId, callback); + } + + deleteStudy(study_id, sessionId, callback){ + this.delete(`${this.apiurl}/studies/${study_id}`, sessionId, callback); + } + + getAllocator(study_id, sessionId, callback){ + this.get(`${this.apiurl}/studies/${study_id}/allocator`, sessionId, callback); + } + + updateAllocator(study_id, allocator, sessionId, callback){ + this.put(`${this.apiurl}/studies/${study_id}/allocator`, allocator, sessionId, callback); + } + + getStudyTests(study_id, sessionId, callback){ + this.get(`${this.apiurl}/studies/${study_id}/tests`, sessionId, callback); + } + + exportStudyConfig(study_id, sessionId, callback){ + this.get(`${this.apiurl}/studies/${study_id}/export`, sessionId, callback); + } + + importStudyConfig(newStudy, sessionId, callback){ + this.post(`${this.apiurl}/studies/import`, newStudy, sessionId, callback); + } + + getStudyTest(study_id,test_id, sessionId, callback){ + this.get(`${this.apiurl}/studies/${study_id}/tests/${test_id}`, sessionId, callback); + } + + getStudyGroups(study_id, sessionId, callback){ + this.get(`${this.apiurl}/studies/${study_id}/groups`, sessionId, callback); + } + + getTestActivities(study_id, test_id, sessionId, callback){ + this.get(`${this.apiurl}/studies/${study_id}/tests/${test_id}/activities`, sessionId, callback); + } + + getStudyParticipants(study_id, sessionId, callback){ + this.get(`${this.apiurl}/studies/${study_id}/participants`, sessionId, callback); + } + + getStudySchedule(study_id, sessionId, callback){ + this.get(`${this.apiurl}/studies/${study_id}/schedule`, sessionId, callback); + } + + // Activities + + addActivityToTest(study_id, test_id, activity, sessionId, callback){ + this.post(`${this.apiurl}/studies/${study_id}/tests/${test_id}/activities`, activity, sessionId, callback); + } + + updateActivity(activity, sessionId, callback){ + this.patch(`${this.apiurl}/activities/${activity.id}`, activity, sessionId, callback); + } + + getActivity(activity_id, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}`, sessionId, callback); + } + + exportActivity(activity_id, complete, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/export?complete=${complete}`, sessionId, callback); + } + + setSurveyOwner(activity_id, sessionId, callback){ + this.patch(`${this.apiurl}/activities/${activity_id}/surveyowner`, {}, sessionId, callback); + } + + getSurveyList(activity_id, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/usersurveylist`, sessionId, callback); + } + + getSurveyLanguages(activity_id, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/surveylanguages`, sessionId, callback); + } + + getActivityProgress(activity_id, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/progress`, sessionId, callback); + } + + getActivityCompletion(activity_id, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/completion`, sessionId, callback); + } + + setActivityCompletion(activity_id, user, status, sessionId, callback){ + this.post(`${this.apiurl}/activities/${activity_id}/completion?user=${user}`, { status: status }, sessionId, callback); + } + + getActivityResultForUser (activity_id, student, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/result?users=${student}`, sessionId, callback); + } + + getActivityResultWithTypeForUser (activity_id, type, student, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/result?users=${student}&type=${type}`, sessionId, callback); + } + + getActivityResult(activity_id, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/result`, sessionId, callback); + } + + getActivityResultWithType(activity_id, type, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/result?type=${type}`, sessionId, callback); + } + + getActivityHasResult(activity_id, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/hasresult`, sessionId, callback); + } + + hasActivityResult(activity_id, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/hasresult`, sessionId, callback); + } + + getActivityTarget(activity_id, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/target`, sessionId, callback); + } + + isActivityOpenable(activity_id, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/openable`, sessionId, callback); + } + + getMinioDataUrl(activity_id, sessionId, callback){ + this.get(`${this.apiurl}/activities/${activity_id}/presignedurl`, sessionId, callback); + } + + deleteActivity(activity_id, sessionId, callback){ + this.delete(`${this.apiurl}/activities/${activity_id}`, sessionId, callback); + } + + getActivityTypes(sessionId, callback){ + this.get(`${this.apiurl}/activitytypes`, sessionId, callback); + } + + getAllocatorTypes(sessionId, callback){ + this.get(`${this.apiurl}/allocatortypes`, sessionId, callback); + } + + // LTI + + getLtiTools(sessionId, callback){ + this.get(`${this.apiurl}/lti/tools`, sessionId, callback); + } + + addLtiTool(tool, sessionId, callback){ + this.post(`${this.apiurl}/lti/tools`, tool, sessionId, callback); + } + + deleteLtiTool(tool, sessionId, callback){ + this.delete(`${this.apiurl}/lti/tools/${tool}`, sessionId, callback); + } + + getLtiPlatforms(study, sessionId, callback){ + let query = ''; + if(study){ + query = '?searchString=' + encodeURI(`{"studyId":"${study}"}`); + } + + this.get(`${this.apiurl}/lti/platforms${query}`, sessionId, callback); + } + + addLtiPlatform(platform, sessionId, callback){ + this.post(`${this.apiurl}/lti/platforms`, platform, sessionId, callback); + } + + removePlatform(platform_id, sessionId, callback) { + this.delete(`${this.apiurl}/lti/platforms/${platform_id}`, sessionId, callback); + } +} + +module.exports = new Simva(); \ No newline at end of file diff --git a/routes/lib/sseClientsListManager.js b/routes/lib/sseClientsListManager.js new file mode 100755 index 00000000..8640e718 --- /dev/null +++ b/routes/lib/sseClientsListManager.js @@ -0,0 +1,71 @@ +const logger = require("../../logger"); + +// sseManager.js +class SSEClientsListManager { + constructor() { + this.clients = new Map(); + } + + addActivityAndUserToMap(id, user, userRole, clientId) { + var obj = { user : user, userRole : userRole, id : id}; + obj.lastTime= Date.now(); + this.clients.set(clientId, obj); + this.displayClients(); + } + + displayClients() { + logger.info("{"); + for (let [clientId, clientData] of this.clients) { + logger.info(" " + clientId + ":" + JSON.stringify(clientData)+ ","); + } + logger.info("}"); + } + + getTimeSuperiorToXMinClientList(minutes) { + let clientsToSend = []; + for (let [clientId, clientData] of this.clients) { + let client = clientData; // Parse the stored client data + let fiveMinutesLater = new Date(client.lastTime).getTime() + minutes * 60 * 1000; // Add 5 minutes in milliseconds + + if (fiveMinutesLater <= Date.now()) { // Check if 5 minutes have passed + clientsToSend.push(clientId); + client.lastTime= Date.now(); + this.clients.set(clientId, client); + } + } + return clientsToSend; + } + + getClientList(message) { + let clientsToSend = []; + for (let [clientId, clientData] of this.clients) { + let client = clientData; // Parse the stored client data + if (client.userRole === 'teacher') { + // Check if the client's study includes the studyId + if (client.id == message.studyId) { + clientsToSend.push(clientId); // Add to the list if conditions are met + client.lastTime= Date.now(); + this.clients.set(clientId, client); + } + } else if (client.userRole === 'student') { + if(client.user == message.user) { + if (client.id == message.studyId) { + clientsToSend.push(clientId); // Add to the list if conditions are met + client.lastTime= Date.now(); + this.clients.set(clientId, client); + } + } + } else { + logger.info(`Client ${clientId} is not authorized.`); + } + } + return clientsToSend; + } + + removeClient(clientId) { + this.clients.delete(clientId); + this.displayClients(); + } +} + +module.exports = new SSEClientsListManager(); \ No newline at end of file diff --git a/routes/lib/sseManager.js b/routes/lib/sseManager.js new file mode 100755 index 00000000..0ea193be --- /dev/null +++ b/routes/lib/sseManager.js @@ -0,0 +1,86 @@ +const logger = require('../../logger'); +const sseClientManager = require('./sseClientsListManager'); +const userClientsListManager = require('./userClientsListManager'); + +// sseManager.js +class SSEManager { + constructor() { + this.clients = new Map(); // Track connected clients by a unique client ID + this.clientsOptions = new Map(); + } + + // Handle a new SSE client connection + addClient(req, res) { + const clientId = this.generateClientId(); + + // Set the required headers for SSE + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + // Add client to the client list + this.clients.set(clientId, res); + logger.info(`Client connected: ${clientId}`); + + // Handle client disconnect + req.on('close', () => { + logger.info(`Client disconnected: ${clientId}`); + this.clients.delete(clientId); + sseClientManager.removeClient(clientId); + userClientsListManager.removeClient(req.query.sessionID, clientId); + }); + + return clientId; // Return client ID for reference + } + + // Generate a unique client ID + generateClientId() { + return Math.random().toString(36).substr(2, 9); + } + + // Send a message to a specific client list + sendMessageToClientList(clientList, message, type) { + for(var i=0; i < clientList.length; i++) { + this.sendMessageToClient(clientList[i], message, type); + } + } + + // Send a message to a specific client + sendMessageToClient(clientId, message, type) { + const client = this.clients.get(clientId); + if (client) { + var msg=""; + if(type) { + msg=`event:${type}\n` + } + msg+=`data: ${JSON.stringify(message)}\n\n` + client.write(msg); + } else { + logger.error(`Cannot send message. Client ${clientId} is not connected.`); + } + } + + // Broadcast a message to all connected clients + broadcast(message, type) { + this.clients.forEach((client, clientId) => { + var msg=""; + if(type) { + msg=`event:${type}\n` + } + msg+=`data: ${JSON.stringify(message)}\n\n` + client.write(msg); + }); + } + + // Get the total number of connected clients (optional utility) + getClientCount() { + return this.clients.size; + } + + // Get the total number of connected clients (optional utility) + getClientConnected(clientId) { + return this.clients.has(clientId); + } +} + +module.exports = new SSEManager(); \ No newline at end of file diff --git a/routes/lib/studycontroler.js b/routes/lib/studycontroler.js new file mode 100755 index 00000000..116eddf1 --- /dev/null +++ b/routes/lib/studycontroler.js @@ -0,0 +1,136 @@ +const logger = require('../../logger'); +const Simva = require('./simva'); +const testcontroler = require('./testscontroler'); +const groupcontroler = require('./groupcontroler'); + +module.exports = { + async getCompleteStudy(studyid, sessionid) { + let study=await this.getStudy(studyid, sessionid); + study.participants = await this.getStudyParticipants(studyid, sessionid); + study.allgroups = await groupcontroler.getGroups(sessionid); + study.completeGroups = await this.getStudyGroups(studyid, sessionid); + study.completeAllocator = await this.getStudyAllocator(studyid, sessionid); + study.completeTests=[]; + for(let i=0;i { + Simva.getStudies(sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + refreshStudy(study, sessionid) { + return new Promise((resolve, reject) => { + Simva.updateStudy(study, sessionid, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }, + + + addStudy(studyname, sessionid) { + return new Promise((resolve, reject) => { + Simva.addStudy(studyname, sessionid, (error, study) => { + if(error) { + reject(error); + } else { + resolve(study); + } + }); + }); + }, + + getStudy(studyid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getStudy(studyid, sessionid, (error, study) => { + if(error) { + reject(error); + } else { + resolve(study); + } + }); + }); + }, + + getStudyParticipants(studyid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getStudyParticipants(studyid, sessionid, (error, participants) => { + if(error) { + reject(error); + } else { + resolve(participants); + } + }); + }); + }, + + getStudyGroups(studyid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getStudyGroups(studyid, sessionid, (error, groups) => { + if(error) { + reject(error); + } else { + resolve(groups); + } + }); + }); + }, + + getStudyAllocator(studyid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getAllocator(studyid, sessionid, (error, allocator) => { + if(error) { + reject(error); + } else { + resolve(allocator); + } + }); + }); + }, + + getStudyTests(studyid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getStudyTests(studyid, sessionid, (error, tests) => { + if(error) { + reject(error); + } else { + resolve(tests); + } + }); + }); + }, +} \ No newline at end of file diff --git a/routes/lib/testscontroler.js b/routes/lib/testscontroler.js new file mode 100755 index 00000000..fd997b3b --- /dev/null +++ b/routes/lib/testscontroler.js @@ -0,0 +1,69 @@ +const logger = require('../../logger'); +const Simva = require('./simva'); +const activitiescontroler = require('./activitiescontroler'); +module.exports = { + async getCompleteTest(studyid, testid, sessionid) { + let test=await this.getStudyTest(studyid, testid, sessionid); + test.completeActivities = []; + for(let i=0;i { + Simva.addTestToStudy(studyid, testname, sessionid, (error, test) => { + if(error) { + reject(error); + } else { + resolve(test); + } + }); + }); + }, + + getStudyTests(studyid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getStudyTests(studyid, sessionid, (error, tests) => { + if(error) { + reject(error); + } else { + resolve(tests); + } + }); + }); + }, + + getStudyTest(studyid, testid, sessionid) { + return new Promise((resolve, reject) => { + Simva.getStudyTest(studyid, testid, sessionid, (error, test) => { + if(error) { + reject(error); + } else { + resolve(test); + } + }); + }); + }, +} \ No newline at end of file diff --git a/routes/lib/userClientsListManager.js b/routes/lib/userClientsListManager.js new file mode 100755 index 00000000..fcb9ed8e --- /dev/null +++ b/routes/lib/userClientsListManager.js @@ -0,0 +1,104 @@ +const logger = require("../../logger"); + +var jwt = require('jsonwebtoken'); + +class UserClientsListManager { + constructor() { + this.sessionClients = new Map(); + this.sessions = new Map(); + } + + addUserSession(session) { + var obj = { session: session}; + if(session.user && session.user.jwt) { + obj.jwtdecoded = jwt.decode(session.user.jwt); + } + this.sessions.set(session.id, obj); + this.displaySessions(); + } + + displaySessions() { + logger.info("Sessions : {"); + for (let [sessionId, sessionData] of this.sessions) { + logger.info(" " + sessionId + ":" + JSON.stringify(sessionData, null)+ ","); + } + logger.info("}"); + } + + getJWT(sessionId) { + let result = this.sessions.get(sessionId); + if(result && result.session && result.session.user && result.session.user.jwt) { + return result.session.user.jwt; + } + } + + getSession(sessionId) { + let result = this.sessions.get(sessionId); + if(result && result.session) { + return result.session; + } else { + result = { id : sessionId }; + } + } + + refreshAuth(sessionId, access_token, refresh_token) { + var clientData = this.sessions.get(sessionId); + clientData.session.user.jwt = access_token; + clientData.jwtdecoded = jwt.decode(access_token); + //clientData.session.user.refreshToken = refresh_token; + this.sessions.set(sessionId, clientData); + } + + getRefreshClientList(sessions) { + let clientsToSend = []; + for (let i = 0; i < sessions.length; i++) { + let sessionId = sessions[i]; + var clients = this.sessionClients.get(sessionId); + for(let j = 0; j < clients.length; j++) { + clientsToSend.push(clients[j]); + } + } + return clientsToSend; + } + + removeSession(sessionId) { + this.sessionClients.delete(sessionId); + this.sessions.delete(sessionId); + this.displayClients(); + this.displaySessions(); + } + + addClient(sessionId, clientId) { + var clients = this.sessionClients.get(sessionId); + if(clients) { + clients.push(clientId); + this.sessionClients.set(sessionId, clients); + } else { + this.sessionClients.set(sessionId, [ clientId ]); + } + this.displayClients(); + } + + removeClient(sessionId, clientId) { + var clients = this.sessionClients.get(sessionId); + if(clients) { + var clientsFiltered = clients.filter(item => item !== clientId); + if(clientsFiltered.length == 0) { + this.sessionClients.delete(sessionId); + } else { + this.sessionClients.set(sessionId, clientsFiltered); + } + } + this.displayClients(); + } + + displayClients() { + logger.info("Clients : {"); + for (let [sessionId, clientId] of this.sessionClients) { + logger.info(" " + sessionId + ":" + clientId+ ","); + } + logger.info("}"); + } +} + +module.exports = new UserClientsListManager(); \ No newline at end of file diff --git a/routes/lib/usertools.js b/routes/lib/usertools.js old mode 100644 new mode 100755 index 47802c98..a23930c7 --- a/routes/lib/usertools.js +++ b/routes/lib/usertools.js @@ -4,28 +4,134 @@ var passport = require('passport'); let axios = require('axios'); -module.exports = { - setUser: function(req, user){ +const logger = require('../../logger'); +const config = require('../../config'); +const userClientsListManager = require("./userClientsListManager"); + + +class UserTools { + constructor() { + } + + redirectOpenId(level, req, res) { + var pre = '/'; + for(var i = 0; i < level; i++){ + pre += '../'; + } + req.session.intendedUrl=`${req.originalUrl}`; + if(req.session.intendedUrl.toLowerCase().includes("scheduler")) { + logger.info("scheduler"); + const keyword = "scheduler/"; + // Find the index of the keyword + const index = req.session.intendedUrl.indexOf(keyword); + var result; + if (index !== -1) { + // Extract everything after "scheduler/" + result = req.session.intendedUrl.substring(index + keyword.length); + } else { + result="" + } + return res.redirect(`${pre}users/openidscheduler?study=${result}`); + } else { + return res.redirect(`${pre}users/openid`); + } + } + + auth(level){ + var tmp=this; + return function(req, res, next) { + let simvaToken = userClientsListManager.getJWT(req.session.id); + if (req.session && req.session.user && req.session.user.jwt){ + tmp.authExpiredAndRefreshAuthWithCallback(userClientsListManager.getSession(req.session.id), (error, result) => { + if(error) { + tmp.redirectOpenId(level, req, res); + } else { + logger.debug("auth() - Token OK"); + return next(); + } + }); + } else if(simvaToken){ + logger.info("auth() - New token"); + let session = req.session; + let profile = tmp.getProfileFromJWT(simvaToken); + session.user.data = profile; + session.user.jwt = simvaToken; + userClientsListManager.addClient(session); + req.session.user.jwt = true; + logger.info("auth() - New token done"); + return next(); + }else{ + tmp.redirectOpenId(level, req, res); + } + }; + } + + async getRefreshSessionsList() { + let sessionsToSend = []; + for (let [sessionId, sessionData] of userClientsListManager.sessions) { + let ok = await this.isAuthExpiredPromise(sessionData.session); + if(ok) { + sessionsToSend.push(ok); + } + } + return sessionsToSend; + } + + async isAuthExpiredPromise(session) { + return new Promise((resolve, reject) => { + this.isAuthExpired(session, (error, result) => { + if(error) { + reject(error); + } else { + if(result.type == "expired") { + resolve(session.id); + } + } + }); + }); + } + + authExpiredAndRefreshAuthWithCallback(session, callback) { + this.authExpired(session, config, (error, result) => { + if(error) { + logger.info(JSON.stringify(error)); + if(session && session.id) { + userClientsListManager.removeSession(session.id); + } + callback(error); + } else { + if(result) { + logger.info(JSON.stringify(result)); + userClientsListManager.refreshAuth(session.id, result.access_token, result.refresh_token); + logger.info("Auth Refreshed"); + callback(null, {message:"Auth Refreshed"}); + } else { + callback(null, {message:"Auth OK"}); + } + } + }); + } + + setUser(req, user){ let decoded = jwt.decode(user.jwt); - console.log(`JWT : ${JSON.stringify(decoded)}`); - user.data.roles = decoded.realm_access.roles; - user.data.role = this.getRoleFromJWT(decoded); - req.session.user = user; - }, - - authExpired: function(req, config, callback){ - let current = Math.floor(Date.now() / 1000); - let jwtdecoded = this.decodeJWT(req.session.user.jwt); + logger.info(`JWT : ${JSON.stringify(decoded)}`); + req.session.user.data.roles = decoded.realm_access.roles; + req.session.user.data.role = this.getRoleFromJWT(decoded); + } + + isAuthExpired(session, callback){ try { + let current = Math.floor(Date.now() / 1000); + let jwtdecoded = this.decodeJWT(session.user.jwt); let expiration = parseInt(jwtdecoded.exp); if(current > expiration){ - console.log(`authExpired() - JWT: ${JSON.stringify(jwtdecoded)}`); - console.log(`authExpired() - Expiration: ${expiration}`); - console.log("authExpired() - Token Expired"); - this.refreshAuth(req, config, callback); + logger.info(`authExpired() - JWT: ${JSON.stringify(jwtdecoded)}`); + logger.info(`authExpired() - Expiration: ${expiration}`); + logger.info("authExpired() - Token Expired"); + callback(null, {type:"expired"}); }else{ - console.log("authExpired() - Token OK"); - callback(); + logger.debug("authExpired() - Token OK"); + callback(null, {type:"ok"}); } } catch(e) { callback({ @@ -36,16 +142,30 @@ module.exports = { } }); } - }, + } - decodeJWT: function(token){ + authExpired(session, config, callback) { + this.isAuthExpired(session, (error, result) => { + if(error) { + callback(error); + } else { + if(result.type == "expired") { + this.refreshAuth(session, config, callback); + } else { + callback(); + } + } + }); + } + + decodeJWT(token){ return jwt.decode(token); - }, + } - getProfileFromJWT: function(token){ + getProfileFromJWT(token){ let profile = {}; let simvaJwtToken = this.decodeJWT(token); - console.log(`getProfileFromJWT() : ${JSON.stringify(simvaJwtToken)}`); + logger.info(`getProfileFromJWT() : ${JSON.stringify(simvaJwtToken)}`); profile.provider = simvaJwtToken.iss; profile.id = simvaJwtToken.data.id; profile.username = simvaJwtToken.data.username; @@ -53,9 +173,9 @@ module.exports = { profile.roles = simvaJwtToken.realm_access.roles; profile.role = this.getRoleFromJWT(simvaJwtToken); return profile; - }, + } - getRoleFromJWT: function(decoded){ + getRoleFromJWT(decoded){ let role = 'norole'; if(decoded.realm_access.roles.includes('teacher') || decoded.realm_access.roles.includes('researcher')){ role = 'teacher'; @@ -63,15 +183,15 @@ module.exports = { role = 'student'; }; return role; - }, + } - refreshAuth: function(req, config, callback){ - if(req.session.user && req.session.user.refreshToken){ - console.log(`refreshAuth() - Refresh Token : ${req.session.user.refreshToken}`) - clientConfig= `${config.sso.clientId}:${config.sso.clientSecret}` + refreshAuth(session, config, callback){ + if(session.user && session.user.refreshToken){ + logger.info(`refreshAuth() - Refresh Token : ${session.user.refreshToken}`); + const clientConfig= `${config.sso.clientId}:${config.sso.clientSecret}`; const querystring = new URLSearchParams({ 'grant_type': 'refresh_token', - 'refresh_token': req.session.user.refreshToken + 'refresh_token': session.user.refreshToken }); axios.post(`${config.sso.url}/realms/${config.sso.realm}/protocol/openid-connect/token`, querystring, { headers: { @@ -80,10 +200,10 @@ module.exports = { } }).then(response => { try { - console.log(`refreshAuth() - Body : ${response.body}`); - let b = JSON.parse(response.body); - let simvaToken = b.access_token; - console.log(`refreshAuth() - Access Token : ${simvaToken}`); + let simvaToken = response.data.access_token; + let simvaRefreshToken = response.data.refresh_token; + logger.debug(`refreshAuth() - Access Token : ${simvaToken}`); + logger.debug(`refreshAuth() - Refresh Token : ${simvaRefreshToken}`); if(simvaToken == "undefined" || simvaToken == null) { callback({ status: 500, @@ -93,10 +213,10 @@ module.exports = { } }); } else { - callback(null, simvaToken); + callback(null, response.data); } } catch(e) { - console.log(e); + logger.info(e); callback({ status: 500, data: { @@ -124,4 +244,6 @@ module.exports = { }); } } -} \ No newline at end of file +} + +module.exports = new UserTools(); \ No newline at end of file diff --git a/routes/lib/utils.js b/routes/lib/utils.js new file mode 100755 index 00000000..46327f1c --- /dev/null +++ b/routes/lib/utils.js @@ -0,0 +1,78 @@ +var axios = require('axios'); + +module.exports = { + post: function (url, body, callback, jwt, apikey) { + const headers = {}; + if (jwt) { + headers['Authorization'] = `Bearer ${jwt}`; + } + if(apikey) { + headers['X-Api-Key'] = `${apikey}`; + } + + axios + .post(url, body, { headers }) + .then((response) => { + callback(null, response.data); + }) + .catch((error) => { + callback(error); + }); + }, + + patch: function (url, body, callback, jwt, apikey) { + const headers = {}; + if (jwt) { + headers['Authorization'] = `Bearer ${jwt}`; + } + if(apikey) { + headers['X-Api-Key'] = `${apikey}`; + } + axios + .patch(url, body, { headers }) + .then((response) => callback(null, response.data)) + .catch((error) => callback(error)); + }, + + put: function (url, body, callback, jwt, apikey) { + const headers = {}; + if (jwt) { + headers['Authorization'] = `Bearer ${jwt}`; + } + if(apikey) { + headers['X-Api-Key'] = `${apikey}`; + } + axios + .put(url, body, { headers }) + .then((response) => callback(null, response.data)) + .catch((error) => callback(error)); + }, + + get: function (url, callback, jwt, apikey) { + const headers = {}; + if (jwt) { + headers['Authorization'] = `Bearer ${jwt}`; + } + if(apikey) { + headers['X-Api-Key'] = `${apikey}`; + } + axios + .get(url, { headers }) + .then((response) => callback(null, response.data)) + .catch((error) => callback(error)); + }, + + delete: function (url, callback, jwt, apikey) { + const headers = {}; + if (jwt) { + headers['Authorization'] = `Bearer ${jwt}`; + } + if(apikey) { + headers['X-Api-Key'] = `${apikey}`; + } + axios + .delete(url, { headers }) + .then((response) => callback(null, response.data)) + .catch((error) => callback(error)); + }, +} diff --git a/routes/routes/activities.js b/routes/routes/activities.js old mode 100644 new mode 100755 diff --git a/routes/routes/bff.js b/routes/routes/bff.js new file mode 100755 index 00000000..ffb32edb --- /dev/null +++ b/routes/routes/bff.js @@ -0,0 +1,612 @@ +module.exports = function(auth, config){ + var express = require('express'), + router = express.Router(); + const logger = require('../../logger'); + const Simva = require('../lib/simva'); + const studycontroler = require('../lib/studycontroler'); + const groupcontroler = require('../lib/groupcontroler'); + const testscontroler = require('../lib/testscontroler'); + + /** + * USERS + * + */ + router.post('/shlink', auth, async (req, res, next) => { + Simva.generateURL(req.body.url, req.body.tag, req.body.title, req.body.customSlug, req.body.length, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.delete('/shlink/:shortcode', auth, async (req, res, next) => { + Simva.deleteShLink(req.params["shortcode"], (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send({ message : "Short Link deleted" }); + } + }); + }); + + /** + * USERS + * + */ + router.post('/users', auth, async (req, res, next) => { + Simva.register(req.body.groupid, req.body.username, req.body.email, req.body.password, req.body.role, req.body.isToken, req.body.useNewGeneration, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.patch('/users/:username', auth, async (req, res, next) => { + Simva.setRole(req.body.username, req.body.role, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/users/me', auth, async (req, res, next) => { + Simva.getCurrentUser(req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + /** + * GROUPS + * + */ + router.get('/groups', auth, async (req, res, next) => { + Simva.getGroups(req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.post('/groups', auth, async (req, res, next) => { + Simva.addGroup(req.body.name, req.body.version, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.put('/groups/:groupid', auth, async (req, res, next) => { + Simva.updateGroup(req.body, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/groups/:groupid', auth, async (req, res, next) => { + let groupid = req.params['groupid']; + let sessionid = req.session.id; + try { + let group = await groupcontroler.getCompleteGroup(groupid, sessionid); + res.status(200).send(group); + } catch(error) { + next(error.response.data); + } + }); + + router.delete('/groups/:groupid', auth, async (req, res, next) => { + Simva.deleteGroup(req.params['groupid'], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + /** + * GROUP PARTICIPANTS + * + */ + router.get('/groups/:groupid/participants', auth, async (req, res, next) => { + Simva.getGroupParticipants(req.params['groupid'], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + /** + * STUDIES + * + */ + router.get('/studies', auth, async (req, res, next) => { + Simva.getStudies(req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.post('/studies', auth, async (req, res, next) => { + Simva.addStudy(req.body.name, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/studies/:studyid', auth, async (req, res, next) => { + let studyId = req.params['studyid']; + let sessionid = req.session.id; + try { + let study = await studycontroler.getCompleteStudy(studyId, sessionid); + res.status(200).send(study); + } catch(error) { + next(error.response.data); + } + }); + + router.put('/studies/:studyid', auth, async (req, res, next) => { + Simva.updateStudy(req.body, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.delete('/studies/:studyid', auth, async (req, res, next) => { + Simva.deleteStudy(req.params['studyid'], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + /** + * TESTS + * + */ + router.post('/studies/:studyid/tests', auth, async (req, res, next) => { + if(req.body.from) { + try { + let testToDuplicate=await testscontroler.exportTest(req.params["studyid"], req.body.from, false, req.session.id); + testToDuplicate.name = req.body.name; + let newTest=await testscontroler.importTest(req.params["studyid"], testToDuplicate, req.session.id); + res.status(200).send(newTest); + } catch { + next(error.response.data); + } + } else { + Simva.addTestToStudy(req.params["studyid"], req.body.name, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + } + }); + + router.patch('/studies/:studyid/tests/:testid', auth, async (req, res, next) => { + Simva.updateTest(req.params["studyid"], req.body, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + + router.patch('/activities/:activityid', auth, async (req, res, next) => { + Simva.updateActivity(req.body, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.patch('/studies/:studyid', auth, async (req, res, next) => { + Simva.deleteStudy(req.params["studyid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + /** + * ALLOCATORS + * + */ + router.get('/studies/:studyid/allocator', auth, async (req, res, next) => { + Simva.getAllocator(req.params["studyid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.put('/studies/:studyid/allocator', auth, async (req, res, next) => { + Simva.updateAllocator(req.params["studyid"], req.body, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + /** + * TESTS + * + */ + router.get('/studies/:studyid/tests', auth, async (req, res, next) => { + Simva.getStudyTests(req.params["studyid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/studies/:studyid/export', auth, async (req, res, next) => { + let studyId = req.params['studyid']; + let sessionid = req.session.id; + try { + let study = await studycontroler.exportStudy(studyId, true, sessionid); + res.status(200).send(study); + } catch(error) { + next(error.response.data); + } + }); + + router.post('/studies/import', auth, async (req, res, next) => { + let newstudy = JSON.parse(atob(req.body.file)); + let sessionid = req.session.id; + try { + let study = await studycontroler.importStudy(newstudy, sessionid); + res.status(200).send(study); + } catch(error) { + next(error.response.data); + } + }); + + router.get('/studies/:studyid/tests/:testid', auth, async (req, res, next) => { + Simva.getStudyTest(req.params["studyid"], req.params["testid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/studies/:studyid/groups', auth, async (req, res, next) => { + Simva.getStudyGroups(req.params["studyid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/studies/:studyid/tests/:testid/activities', auth, async (req, res, next) => { + Simva.getTestActivities(req.params["studyid"], req.params["testid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/studies/:studyid/participants', auth, async (req, res, next) => { + Simva.getStudyParticipants(req.params["studyid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/studies/:studyid/schedule', auth, async (req, res, next) => { + Simva.getStudySchedule(req.params["studyid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + /** + * ACTIVITIES + * + */ + router.post('/studies/:studyid/tests/:testid/activities', auth, async (req, res, next) => { + Simva.addActivityToTest(req.params["studyid"], req.params["testid"], req.body, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/activities/:activityid', auth, async (req, res, next) => { + Simva.getActivity(req.params["activityid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.patch('/activities/:activityid/surveyowner', auth, async (req, res, next) => { + Simva.setSurveyOwner(req.params["activityid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/activities/:activityid/usersurveylist', auth, async (req, res, next) => { + Simva.getSurveyList(req.params["activityid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/activities/:activityid/progress', auth, async (req, res, next) => { + Simva.getActivityProgress(req.params["activityid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/activities/:activityid/completion', auth, async (req, res, next) => { + Simva.getActivityCompletion(req.params["activityid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.post('/activities/:activityid/completion', auth, async (req, res, next) => { + Simva.setActivityCompletion(req.params["activityid"], req.query.user, req.body.status, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/activities/:activityid/result', auth, async (req, res, next) => { + if(req.query.type) { + if(req.query.users) { + Simva.getActivityResultWithTypeForUser(req.params["activityid"], req.query.type, req.query.users, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + } else { + Simva.getActivityResultWithType(req.params["activityid"], req.query.type, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + } + } else { + if(req.query.users) { + Simva.getActivityResultForUser(req.params["activityid"], req.query.users, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + } else { + Simva.getActivityResult(req.params["activityid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + } + } + + }); + + router.get('/activities/:activityid/hasresult', auth, async (req, res, next) => { + Simva.getActivityHasResult(req.params["activityid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/activities/:activityid/target', auth, async (req, res, next) => { + Simva.getActivityTarget(req.params["activityid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/activities/:activityid/openable', auth, async (req, res, next) => { + Simva.isActivityOpenable(req.params["activityid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/activities/:activityid/presignedurl', auth, async (req, res, next) => { + Simva.getMinioDataUrl(req.params["activityid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.delete('/activities/:activityid', auth, async (req, res, next) => { + Simva.deleteActivity(req.params["activityid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/activitytypes', auth, async (req, res, next) => { + Simva.getActivityTypes(req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/allocatortypes', auth, async (req, res, next) => { + Simva.getAllocatorTypes(req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + /** + * LTI + * + */ + router.get('/lti/tools', auth, async (req, res, next) => { + Simva.getLtiTools(req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.post('/lti/tools', auth, async (req, res, next) => { + Simva.addLtiTool(req.body, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.delete('/lti/tools/:toolid', auth, async (req, res, next) => { + Simva.deleteLtiTool(req.params["toolid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.get('/lti/platforms', auth, async (req, res, next) => { + let studyid = null; + if(req.query.searchString) { + studyid = JSON.parse(req.query.searchString).studyId; + } + Simva.getLtiPlatforms(studyid, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.post('/lti/platforms', auth, async (req, res, next) => { + Simva.addLtiPlatform(req.body, req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + router.delete('/lti/platforms/:platformid', auth, async (req, res, next) => { + Simva.removePlatform(req.params["platformid"], req.session.id, (error, result) => { + if(error) { + next(error.response.data); + } else { + res.status(200).send(result); + } + }); + }); + + return router; + } \ No newline at end of file diff --git a/routes/routes/events.js b/routes/routes/events.js new file mode 100755 index 00000000..3163a949 --- /dev/null +++ b/routes/routes/events.js @@ -0,0 +1,71 @@ +module.exports = function(auth, config){ + var express = require('express'), + router = express.Router(); + const logger = require('../../logger'); + const { validateUrl , createUrl } = require("../lib/hMacKey/tokens.js"); + const sseManager = require('../lib/sseManager'); // Import SSE Manager + const sseClientsListManager = require('../lib/sseClientsListManager'); + const userClientsListManager = require('../lib/userClientsListManager'); + + /** + * To get presigned url for others page events + * + */ + router.get('/getPresignedUrl', auth, async (req, res, next) => { + const options = { + username: req.session.user.data.username, + sessionID: req.session.id + }; + + try { + const url = `${config.simva.url}/events`; + params={}; + const result = await createUrl(url, options, config.hmac.hmacKey); + res.status(200).send(result.data); + } catch (err) { + next(err); + } + }); + + router.get('/', async function(req, res, next) { + // Extract the token from the query parameters + const ts = req.query.ts; + const signature = req.query.signature; + if (!signature) { + return res.status(401).json({ message: 'No signature provided' }); + } + if (!ts) { + return res.status(401).json({ message: 'No timestamp provided' }); + } + + const url = config.simva.url + req.baseUrl; + const query = req.query; + try { + if(await validateUrl(url, query, config.hmac.hmacKey)) { + let studyid = req.query.studyId; + let user = req.query.username; + let userRole = req.query.userRole; + let sessionID = req.query.sessionID; + var clientId = sseManager.addClient(req, res); + userClientsListManager.addClient(sessionID, clientId); + if(studyid) { + const options = { + id: studyid, + user: user, + userRole: userRole, + clientId: clientId + }; + logger.debug(JSON.stringify(options)); + sseClientsListManager.addActivityAndUserToMap(options.id,options.user, options.userRole, options.clientId); + } + sseManager.sendMessageToClientList([clientId], {message:'ping',type:'ping'}); + } else { + res.status(401).send({ message: 'Signature not valid' }); + } + } catch (err) { + next(err); + } + }); + + return router; + } \ No newline at end of file diff --git a/routes/routes/groups.js b/routes/routes/groups.js old mode 100644 new mode 100755 index 9b3878d1..50b89891 --- a/routes/routes/groups.js +++ b/routes/routes/groups.js @@ -4,11 +4,11 @@ module.exports = function(auth, config){ router = express.Router(); router.get('/', auth, function(req, res, next) { - res.render('groups_list', { config: config, user: req.session.user }); + res.render('new_groups_list', { config: config, user: req.session.user }); }); router.get('/:groupid', auth, function(req, res, next) { - res.render('group_view', { config: config, user: req.session.user, group: req.params['groupid'] }); + res.render('new_group_view', { config: config, user: req.session.user, group: req.params['groupid'] }); }); return router; diff --git a/routes/routes/previous-groups.js b/routes/routes/previous-groups.js new file mode 100755 index 00000000..aac4f907 --- /dev/null +++ b/routes/routes/previous-groups.js @@ -0,0 +1,15 @@ +module.exports = function(auth, config){ + + var express = require('express'), + router = express.Router(); + + router.get('/', auth, function(req, res, next) { + res.render('previous_groups_list', { config: config, user: req.session.user }); + }); + + router.get('/:groupid', auth, function(req, res, next) { + res.render('previous_group_view', { config: config, user: req.session.user, group: req.params['groupid'] }); + }); + + return router; +} \ No newline at end of file diff --git a/routes/routes/scheduler.js b/routes/routes/scheduler.js old mode 100644 new mode 100755 diff --git a/routes/routes/studies.js b/routes/routes/studies.js old mode 100644 new mode 100755 index 22c97635..a1ce2851 --- a/routes/routes/studies.js +++ b/routes/routes/studies.js @@ -1,20 +1,120 @@ -module.exports = function(auth, config){ +const usertools = require('../lib/usertools.js'); +module.exports = function(auth, config){ var express = require('express'), - router = express.Router(); + router = express.Router(); + const logger = require('../../logger'); + const { createHMACKey } = require("../lib/hMacKey/crypto.js"); + const { createUrl } = require("../lib/hMacKey/tokens.js"); + const sseManager = require('../lib/sseManager'); // Import SSE Manager + const sseClientsListManager = require('../lib/sseClientsListManager'); + const userClientsListManager = require('../lib/userClientsListManager'); + const KafkaClient = require("../lib/kafka"); - router.get('/', auth, function(req, res, next) { - if(req.session.user.data.role === 'teacher'){ - res.render('studies_list', { config: config, user: req.session.user }); - }else{ - res.render('studies_play', { config: config, user: req.session.user }); - } + initHmacKey(); + kafka = new KafkaClient(config.kafka); + startKafkaConsumer(); + async function initHmacKey() { + config.hmac.hmacKey = (await createHMACKey(config.hmac.password + //, { + // encodedSalt: config.hmac.salt, + // encodedKey: config.hmac.key + //} + )).key; + logger.info("Initialized hmacKey"); + } + + // Method to start consuming messages using KafkaClient + async function startKafkaConsumer() { + try { + logger.info('Starting Kafka consumption...'); + // Start Kafka consumption and pass the processMessage as a callback + await kafka.consumeLatestMessages(processMessage); + } catch (error) { + logger.error('Error starting consumption: ' + error); + } + } + + const cron = require('node-cron'); + + // Schedule a task to run every 3 minutes + cron.schedule('*/3 * * * *', async () => { + logger.info('SSE Ping task is running every 3 minutes at ' + new Date()); + var clientsWithoutAction=sseClientsListManager.getTimeSuperiorToXMinClientList(5); + logger.debug(JSON.stringify(clientsWithoutAction)); + sseManager.sendMessageToClientList(clientsWithoutAction, {message:'ping',type:'ping'}); + }); + cron.schedule('*/30 * * * *', async () => { + logger.info('SSE auth expired task is running every 30 minutes at ' + new Date()) + var sessionsToRefresh=await usertools.getRefreshSessionsList(); + var clientsToRefresh=userClientsListManager.getRefreshClientList(sessionsToRefresh); + logger.debug(JSON.stringify(clientsToRefresh)); + sseManager.sendMessageToClientList(clientsToRefresh, {message:'auth expired',type:'refresh_auth'}); }); + async function processMessage(message) { + // Broadcast the message to client list + var msg = JSON.parse(message.value); + var clients=sseClientsListManager.getClientList(msg); + logger.info(JSON.stringify(clients)); + sseManager.sendMessageToClientList(clients, msg); + } + + + /** + * To get presigned url for schedule events + * + */ + router.get('/:studyid/schedule/events/getPresignedUrl', async (req, res, next) => { + const options = { + studyId: req.params['studyid'], + username: req.session.user.data.username, + userRole:"student", + sessionID: req.session.id + }; + + try { + const url = `${config.simva.url}/events`; + const result = await createUrl(url, options, config.hmac.hmacKey); + res.status(200).send(result.data); + } catch (err) { + next(err); + } + }); + + /** + * To get presigned url for schedule events + * + */ + router.get('/:studyid/events/getPresignedUrl', async (req, res, next) => { + const options = { + studyId: req.params['studyid'], + username: req.session.user.data.username, + userRole:"teacher", + sessionID: req.session.id + }; + + try { + const url = `${config.simva.url}/events`; + params={}; + const result = await createUrl(url, options, config.hmac.hmacKey); + res.status(200).send(result.data); + } catch (err) { + next(err); + } + }); + + router.get('/', auth, function(req, res, next) { + if(req.session.user.data.role === 'teacher'){ + res.render('studies_list', { config: config, user: req.session.user }); + }else{ + res.render('studies_play', { config: config, user: req.session.user }); + } + + }); router.get('/:studyid', auth, function(req, res, next) { res.render('study_view', { config: config, user: req.session.user, study: req.params['studyid'] }); }); - return router; } \ No newline at end of file diff --git a/routes/routes/users.js b/routes/routes/users.js old mode 100644 new mode 100755 index aca7a46f..b4c30375 --- a/routes/routes/users.js +++ b/routes/routes/users.js @@ -3,10 +3,13 @@ var jwt = require('jsonwebtoken'); var passport = require('passport'); let axios = require('axios'); - +const logger = require('../../logger'); +const Simva = require('../lib/simva'); process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +const userClientsListManager = require('../lib/userClientsListManager'); let usertools = require('../lib/usertools'); +const cron = require('node-cron'); module.exports = function(auth, config){ @@ -14,6 +17,19 @@ module.exports = function(auth, config){ // Using Keycloak openID var KeyCloakStrategy = require('passport-keycloak-oauth2-oidc').Strategy; + class SimvaKeyCloakStrategy extends KeyCloakStrategy { + authorizationParams(options) { + const params = super.authorizationParams(options); + if ('simva_user_token' in options) { + params.simva_user_token = options.simva_user_token; + } + if ('login_hint' in options) { + params.login_hint = options.login_hint; + } + return params; + } + } + let keycloakConfig = { clientID: config.sso.clientId, realm: config.sso.realm, @@ -25,11 +41,11 @@ module.exports = function(auth, config){ callbackURL: `${config.simva.url}/users/openid/return` } - console.log('--- SSO CONFIG ---'); - console.log(keycloakConfig); - console.log('------------------'); + logger.info('--- SSO CONFIG ---'); + logger.info(keycloakConfig); + logger.info('------------------'); - passport.use('openid', new KeyCloakStrategy( + passport.use('openid', new SimvaKeyCloakStrategy( keycloakConfig, function(accessToken, refreshToken, profile, done) { let user = {}; @@ -48,7 +64,7 @@ module.exports = function(auth, config){ router.get('/', auth, function(req, res, next) { res.redirect('../'); }); - + router.get('/login', function(req, res, next) { res.render('users_login', { config: config }); }); @@ -64,20 +80,37 @@ module.exports = function(auth, config){ router.get('/openid', passport.authenticate('openid')); + router.get('/openidscheduler', (req, res, next) => { + const options = { + login_hint : req.query.study, + simva_user_token: true + }; + passport.authenticate('openid', options)(req, res, next); + } +); + router.get('/openid/return', function (req, res, next) { passport.authenticate('openid', { failureRedirect: '/users/login' }, function(err, user) { - console.log('/openid/return: USER'); - + logger.info('/openid/return: USER'); if(err){ return res.redirect('../login'); } + req.session.user={}; + req.session.user.data = user.data; usertools.setUser(req, user); - console.log(user); - res.redirect('/'); + let session = req.session; + session.user = user; + logger.info(user); + const intendedUrl = req.session.intendedUrl || '/'; + delete req.session.intendedUrl; + userClientsListManager.addUserSession(session); + logger.info(req.session); + res.redirect(intendedUrl); })(req, res, next); }); router.get('/logout', auth, function(req, res, next){ + let sessionId = req.session.id; if(req.session.user.refreshToken){ clientConfig= `${config.sso.clientId}:${config.sso.clientSecret}` const querystring = new URLSearchParams({ @@ -91,6 +124,7 @@ module.exports = function(auth, config){ } }) .then(response => { + userClientsListManager.removeSession(sessionId); req.session.user = null; res.redirect('login'); }) @@ -98,17 +132,20 @@ module.exports = function(auth, config){ res.redirect('/'); }) }else{ + userClientsListManager.removeSession(sessionId); req.session.user = null; res.redirect('login'); } }); router.get('/refresh_auth', auth, function (req, res, next) { - usertools.refreshAuth(req, config, function(error, result){ - if(!error){ - res.send(result); + usertools.authExpiredAndRefreshAuthWithCallback(userClientsListManager.getSession(req.session.id), function(error, result){ + if(error){ + usertools.redirectOpenId(level, req, res); }else{ - res.status(error.status).send(error.data); + logger.debug("auth() - Token OK"); + res.send(result); + //return next(); } }); }); diff --git a/views/about.ejs b/views/about.ejs old mode 100644 new mode 100755 index 0a43f7be..f91aa3a4 --- a/views/about.ejs +++ b/views/about.ejs @@ -1,6 +1,6 @@ <%# views/about.ejs %> -<% extend('layout_with_menu') %> +<% extend('layout_with_menu_and_sse') %>

About

diff --git a/views/activities_list.ejs b/views/activities_list.ejs old mode 100644 new mode 100755 index a23dba02..9ca3f4ea --- a/views/activities_list.ejs +++ b/views/activities_list.ejs @@ -1,6 +1,6 @@ <%# views/activities_list.ejs %> -<% extend('layout_with_menu') %> +<% extend('layout_with_menu_and_sse') %>

Activities

diff --git a/views/css/css.css b/views/css/css.css old mode 100644 new mode 100755 diff --git a/views/groups_list.ejs b/views/groups_list.ejs deleted file mode 100644 index 12d4e8e8..00000000 --- a/views/groups_list.ejs +++ /dev/null @@ -1,78 +0,0 @@ -<%# views/groups_list.ejs %> - -<% extend('layout_with_menu') %> - - -

Groups

-
-
-
-
-
-
-
-
-

Add new

-
- - -
-
-
-
-
diff --git a/views/home.ejs b/views/home.ejs old mode 100644 new mode 100755 index feb99a21..e9f4e102 --- a/views/home.ejs +++ b/views/home.ejs @@ -1,6 +1,6 @@ <%# views/home.ejs %> -<% extend('layout_with_menu') %> +<% extend('layout_with_menu_and_sse') %>

Home

diff --git a/views/layout.ejs b/views/layout.ejs old mode 100644 new mode 100755 index bdb0a669..ea88e0fb --- a/views/layout.ejs +++ b/views/layout.ejs @@ -9,17 +9,30 @@ + + + + + + + + + + +
+
Simva - A Simple Validator
+
+ + Logout +
+
+ + + +
+ <%- content %> +
+ + diff --git a/views/new_group_view.ejs b/views/new_group_view.ejs new file mode 100755 index 00000000..ee03e79d --- /dev/null +++ b/views/new_group_view.ejs @@ -0,0 +1,443 @@ +<%# views/group_view.ejs %> + +<% extend('layout_with_menu_and_sse') %> + + +
+

Group:

+
+
+

Owners

+

+ +
+ +
+
+
+
+

Participants

+ +
+ +
+ +
+
+
+
+
+
+
+

Add owner

+
+ + + +
+
+
+
+
+ +
+
+
+
+

Edit Group

+
+ + + + + + +
+
+
+
+
+ +
+
+
+
+ Add existing + Add new + Add batch +
+
+
+ + + +
+
+
+
+ + + +
+ + +
+ + +
+
+
+
+

How many?

+ +

Participants will be added with random usernames created using:

+ +

And a length of:

+ + + + +
+
+
+
+
+
+ +
+ +
+
+ +
+ PDF +
+ + \ No newline at end of file diff --git a/views/new_groups_list.ejs b/views/new_groups_list.ejs new file mode 100755 index 00000000..433d51f1 --- /dev/null +++ b/views/new_groups_list.ejs @@ -0,0 +1,157 @@ +<%# views/groups_list.ejs %> + +<% extend('layout_with_menu_and_sse') %> + + +

Groups

+
+
+
+
+
+
+
+
+

Add new

+
+ + +
+
+
+
+
+
+
+
+

Edit Group

+
+ + + + + + +
+
+
+
+
+
+ + +
diff --git a/views/group_view.ejs b/views/previous_group_view.ejs old mode 100644 new mode 100755 similarity index 71% rename from views/group_view.ejs rename to views/previous_group_view.ejs index 4855eb4d..7f4b0719 --- a/views/group_view.ejs +++ b/views/previous_group_view.ejs @@ -1,25 +1,25 @@ <%# views/group_view.ejs %> -<% extend('layout_with_menu') %> +<% extend('layout_with_menu_and_sse') %>
-

Group:

+

Group:

Owners

- +

Participants

- +
@@ -42,6 +42,24 @@
+
+
+
+
+

Edit Group

+
+ + + + + + +
+
+
+
+
+
@@ -66,7 +84,7 @@
- +
@@ -126,13 +144,40 @@ if(error && error.responseJSON){ errorbox.text(error.responseJSON.message); }else{ - toggleAddForm('add_owner'); + Utils.toggleAddForm('add_owner'); } }); return false; }); + $("#edit_group_form").on('submit', function(event){ + event.preventDefault(); + let currentform = this; + let form = Utils.getFormData($("#edit_group_form")); + Utils.toggleSubmit(currentform); + let errorbox = $(this).find('.error'); + if(form.name == form.groupName) { + errorbox.text("No changes detected !"); + Utils.toggleSubmit(currentform); + } else { + Simva.getGroup(form.group, function(error, group){ + if(!error){ + group.name = form.name; + Simva.updateGroup(group, function(error, result){ + if(!error){ + Utils.toggleSubmit(currentform); + Utils.toggleAddForm('edit_group'); + reloadGroup(); + } + }); + } + + }); + } + return false; + }); + $("#existing_participant_form").on('submit', function(event){ event.preventDefault(); let form = Utils.getFormData($("#existing_participant_form")); @@ -153,7 +198,7 @@ if(error && error.responseJSON){ errorbox.text(error.responseJSON.message); }else{ - toggleAddForm('add_participant'); + Utils.toggleAddForm('add_participant'); } }); @@ -165,7 +210,7 @@ let form = Utils.getFormData($("#new_participant_form")); let errorbox = $(this).find('.error'); - Simva.register(form.username, form.email, form.password, form.role, function(error, result){ + Simva.register(groupid, form.username.toLowerCase(), form.email, form.password, form.role, false, false, function(error, result){ if(error && error.responseJSON){ errorbox.text(error.responseJSON.message); }else{ @@ -177,7 +222,7 @@ if(error && error.responseJSON){ errorbox.text(error.responseJSON.message); }else{ - toggleAddForm('add_participant'); + Utils.toggleAddForm('add_participant'); } }); } @@ -189,7 +234,7 @@ $("#batch_participant_form").on('submit', function(event){ event.preventDefault(); let currentform = this; - toggleSubmit(currentform); + Utils.toggleSubmit(currentform); let form = Utils.getFormData($("#batch_participant_form")); let errorbox = $(this).find('.error'); @@ -200,32 +245,30 @@ if(generated.length == form.amount){ group.participants = group.participants.concat(generated); Simva.updateGroup(group, function(error, result){ - toggleSubmit(currentform); + Utils.toggleSubmit(currentform); reloadGroup(); if(error && error.responseJSON){ errorbox.text(error.responseJSON.message); }else{ - toggleAddForm('add_participant'); + Utils.toggleAddForm('add_participant'); } }); } } let generateAndRegister = function(algorithm, length, callback){ - let username = generateUsername(algorithm, length); + let username = generateUsername(algorithm, length).toLowerCase(); - Simva.register(username, `${username}@dummy.dum`, username, 'student', function(error, result){ + Simva.register(groupid, username, `${username}@example.com`, username, 'student', true, false, function(error, result){ if(error && error.responseJSON){ generateAndRegister(algorithm, length, callback); }else{ - callback(username); + callback(result.username); } }); } - console.log(form); - for (var i = 0; i < form.amount; i++) { generateAndRegister(form.algorithm, form.length, completed); } @@ -237,54 +280,64 @@ reloadGroup(); }); - let toggleSubmit = function(form){ - $(form).find('input[type="submit"]').toggle(); - $(form).find('.loader').toggle(); + let openEditGroupForm = function(groupId){ + Simva.getGroup(groupId, function(error, group) { + document.getElementById("edit_group_error").innerHTML = ""; + var inputElement = document.getElementById('edit_group_name'); + inputElement.value = group.name; + $('#edit_group input[name="group"]').val(group._id); + $('#edit_group span[class="error"]').val(""); + $('#edit_group input[name="groupName"]').val(group.name); + Utils.toggleAddForm('edit_group'); + }); } let reloadGroup = function(){ - Simva.getGroup(groupid, function(error, result){ + Simva.getGroup(groupid, function(error, mygroup){ if(!error){ - group = result; - Simva.getGroupParticipants(groupid, function(error, result){ - if(!error){ - paintGroup(group, result); - } - }); + group=mygroup; + paintGroup(group, group.completeParticipants); + delete group.completeParticipants; + } else { + window.location.href="/groups"; } }); } let removeParticipant = function(participant){ - let toremove = -1; - for (var i = 0; i < group.participants.length; i++) { - if(group.participants[i] === participant){ - toremove = i; - break; + if(confirm(`Are you sure do you want to delete the participant '${participant}' of group '${group.name}'?`)){ + let toremove = -1; + for (var i = 0; i < group.participants.length; i++) { + if(group.participants[i] === participant){ + toremove = i; + break; + } } - } - group.participants.splice(i, 1); + group.participants.splice(i, 1); - Simva.updateGroup(group, function(error, result){ - reloadGroup(); - }); + Simva.updateGroup(group, function(error, result){ + reloadGroup(); + }); + } } - let removeOwner = function(owner){ - let toremove = -1; - for (var i = 0; i < group.owners.length; i++) { - if(group.owners[i] === owner){ - toremove = i; - break; + let deleteOwner = function(owner){ + if(confirm(`Are you sure do you want to delete the owner '${owner}' of group '${group.name}'?`)){ + let toremove = -1; + for (var i = 0; i < group.owners.length; i++) { + if(group.owners[i] === owner){ + toremove = i; + break; + } } - } - group.owners.splice(i, 1); + group.owners.splice(i, 1); - Simva.updateGroup(group, function(error, result){ - reloadGroup(); - }); + Simva.updateGroup(group, function(error, result){ + reloadGroup(); + }); + } } let paintGroup = function(group, participants){ @@ -315,7 +368,7 @@ if(owner == '<%= user.data.username %>'){ toret += ' disabled="disabled"'; }else{ - toret += ` onclick="removeOwner('${owner}')"` + toret += ` onclick="deleteOwner('${owner}')"` } toret += '>'; @@ -343,7 +396,11 @@ } let participantRowPDF = function(participant, position){ - return `${position}${participant.username}${participant.username}${participant.username}${participant.username}`; + let username = participant.username; + if(participant.isToken == "true") { + username = participant.token; + } + return `${position}${username}${username}${username}${username}`; } let changeTab = function(tab, form, subform){ @@ -352,11 +409,7 @@ $(tab).toggleClass('selected'); $(`#${subform}`).toggleClass('selected'); } - - let toggleAddForm = function(id){ - $(`#${id}`).toggleClass('shown'); - } - + let generateUsername = function(algorithm, length){ switch (algorithm) { case 'alphanumeric': diff --git a/views/previous_groups_list.ejs b/views/previous_groups_list.ejs new file mode 100755 index 00000000..0aee2f88 --- /dev/null +++ b/views/previous_groups_list.ejs @@ -0,0 +1,157 @@ +<%# views/groups_list.ejs %> + +<% extend('layout_with_menu_and_sse') %> + + +

Groups (deprecated) - This tab has been deprecated. Please prefer use the new group page version.

+
+
+
+
+
+
+
+
+

Add new

+
+ + +
+
+
+
+
+
+
+
+

Edit Group

+
+ + + + + + +
+
+
+
+
+
+ + +
diff --git a/views/scheduler.ejs b/views/scheduler.ejs old mode 100644 new mode 100755 index c8f69b1a..0b325f61 --- a/views/scheduler.ejs +++ b/views/scheduler.ejs @@ -1,4 +1,4 @@ -<%# views/layout.ejs %> +<%# views/scheduler.ejs %> @@ -9,13 +9,12 @@ + +

Studies

@@ -84,13 +164,43 @@

Add new

-
- - -
+
+ Add new Study + Import from existing +
+
+
+ + +
+
+
+
+
+
+
+

Edit Study

+
+ + + + + + +
+
+
+
+
- + + +
diff --git a/views/studies_play.ejs b/views/studies_play.ejs old mode 100644 new mode 100755 index 19bbf57c..b38923b1 --- a/views/studies_play.ejs +++ b/views/studies_play.ejs @@ -1,30 +1,9 @@ <%# views/studies_list.ejs %> -<% extend('layout_with_menu') %> +<% extend('layout_with_menu_and_sse') %> - + \ No newline at end of file diff --git a/views/users_contact_admin.ejs b/views/users_contact_admin.ejs old mode 100644 new mode 100755 diff --git a/views/users_login.ejs b/views/users_login.ejs old mode 100644 new mode 100755 diff --git a/views/users_role_edit.ejs b/views/users_role_edit.ejs old mode 100644 new mode 100755 index 14688609..67c5bd49 --- a/views/users_role_edit.ejs +++ b/views/users_role_edit.ejs @@ -5,8 +5,6 @@ \ No newline at end of file diff --git a/views/previous_group_view.ejs b/views/previous_group_view.ejs index 7f4b0719..8621db74 100755 --- a/views/previous_group_view.ejs +++ b/views/previous_group_view.ejs @@ -238,16 +238,12 @@ let form = Utils.getFormData($("#batch_participant_form")); let errorbox = $(this).find('.error'); - let generated = []; - let completed = function(username){ - generated.push(username); - - if(generated.length == form.amount){ + Simva.generateAndRegister(groupid, form.algorithm, form.length, form.amount, true, (error, generated) => { + if(!error) { group.participants = group.participants.concat(generated); Simva.updateGroup(group, function(error, result){ Utils.toggleSubmit(currentform); reloadGroup(); - if(error && error.responseJSON){ errorbox.text(error.responseJSON.message); }else{ @@ -255,28 +251,11 @@ } }); } - } - - let generateAndRegister = function(algorithm, length, callback){ - let username = generateUsername(algorithm, length).toLowerCase(); - - Simva.register(groupid, username, `${username}@example.com`, username, 'student', true, false, function(error, result){ - if(error && error.responseJSON){ - generateAndRegister(algorithm, length, callback); - }else{ - callback(result.username); - } - }); - } - - for (var i = 0; i < form.amount; i++) { - generateAndRegister(form.algorithm, form.length, completed); - } + }); return false; }); - //$('#studies_list').html(roller); reloadGroup(); }); @@ -409,33 +388,4 @@ $(tab).toggleClass('selected'); $(`#${subform}`).toggleClass('selected'); } - - let generateUsername = function(algorithm, length){ - switch (algorithm) { - case 'alphanumeric': - return randomString(length, 'a#'); - break; - case 'base58': - return randomString(length, 'b*'); - break; - case 'letters': - default: - return randomString(length, 'a'); - break; - } - } - - let randomString = function(length, chars) { - var mask = ''; - if (chars.indexOf('b') > -1) mask += 'abcdefghijkmnopqrstuvwxyz'; - if (chars.indexOf('B') > -1) mask += 'ABCDEFGHJKLMNPQRSTUVWXYZ'; - if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz'; - if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - if (chars.indexOf('*') > -1) mask += '123456789'; - if (chars.indexOf('#') > -1) mask += '0123456789'; - if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\'; - var result = ''; - for (var i = length; i > 0; i--) result += mask[Math.floor(Math.random() * mask.length)]; - return result; - } \ No newline at end of file From 71edc9f5876aeca9eeec8115e620fbfb50b7bd42 Mon Sep 17 00:00:00 2001 From: Julio SANTILARIO BERTHILIER Date: Wed, 2 Apr 2025 16:58:28 +0200 Subject: [PATCH 019/245] fix previous groups --- views/previous_group_view.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/previous_group_view.ejs b/views/previous_group_view.ejs index 8621db74..73295d75 100755 --- a/views/previous_group_view.ejs +++ b/views/previous_group_view.ejs @@ -238,7 +238,7 @@ let form = Utils.getFormData($("#batch_participant_form")); let errorbox = $(this).find('.error'); - Simva.generateAndRegister(groupid, form.algorithm, form.length, form.amount, true, (error, generated) => { + Simva.generateAndRegister(groupid, form.algorithm, form.length, form.amount, false, (error, generated) => { if(!error) { group.participants = group.participants.concat(generated); Simva.updateGroup(group, function(error, result){ From 22e390ec1d77c95c0096203685cd16a1434b90e3 Mon Sep 17 00:00:00 2001 From: Julio SANTILARIO BERTHILIER Date: Thu, 3 Apr 2025 08:56:56 +0200 Subject: [PATCH 020/245] fix get token or username only for current user --- routes/routes/bff.js | 6 +++++- views/layout.ejs | 2 +- views/layout_with_menu.ejs | 2 +- views/layout_with_menu_and_sse.ejs | 2 +- views/scheduler.ejs | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/routes/routes/bff.js b/routes/routes/bff.js index faf3560f..bbdd6695 100755 --- a/routes/routes/bff.js +++ b/routes/routes/bff.js @@ -91,7 +91,11 @@ module.exports = function(auth, config){ if(error) { next(error.response.data); } else { - res.status(200).send(result); + let name={username : result.username}; + if(result.isToken == 'true') { + name={token : result.token}; + } + res.status(200).send(name); } }); }); diff --git a/views/layout.ejs b/views/layout.ejs index ea88e0fb..da6f0aae 100755 --- a/views/layout.ejs +++ b/views/layout.ejs @@ -22,7 +22,7 @@ let getUserToken = function(){ Simva.getCurrentUser(function(error, result) { if(!error) { - if(result.isToken && result.isToken == "true") { + if(result.token) { document.getElementById('username').innerText = result.token; document.getElementById('account_username').removeAttribute('href'); } else { diff --git a/views/layout_with_menu.ejs b/views/layout_with_menu.ejs index f1c92f8b..303e4a86 100755 --- a/views/layout_with_menu.ejs +++ b/views/layout_with_menu.ejs @@ -24,7 +24,7 @@ let getUserToken = function(){ Simva.getCurrentUser(function(error, result) { if(!error) { - if(result.isToken && result.isToken == "true") { + if(result.token) { document.getElementById('username').innerText = result.token; document.getElementById('account_username').removeAttribute('href'); } else { diff --git a/views/layout_with_menu_and_sse.ejs b/views/layout_with_menu_and_sse.ejs index 27297f7c..648264a0 100755 --- a/views/layout_with_menu_and_sse.ejs +++ b/views/layout_with_menu_and_sse.ejs @@ -21,7 +21,7 @@ let getUserToken = function(){ Simva.getCurrentUser(function(error, result) { if(!error) { - if(result.isToken && result.isToken == "true") { + if(result.token) { document.getElementById('username').innerText = result.token; document.getElementById('account_username').removeAttribute('href'); } else { diff --git a/views/scheduler.ejs b/views/scheduler.ejs index 0b325f61..98ad5119 100755 --- a/views/scheduler.ejs +++ b/views/scheduler.ejs @@ -111,7 +111,7 @@ let getUserToken = function(){ Simva.getCurrentUser(function(error, result) { if(!error) { - if(result.isToken && result.isToken == "true") { + if(result.token) { document.getElementById('username').innerText = result.token; } else { document.getElementById('username').innerText = result.username; From 0106a7d73d0dad2d8c096932e3f4b74046b5c294 Mon Sep 17 00:00:00 2001 From: Julio SANTILARIO BERTHILIER Date: Thu, 3 Apr 2025 08:57:13 +0200 Subject: [PATCH 021/245] remove NODE_TLS_REJECT_UNAUTHORIZED = "0"; --- routes/routes/users.js | 1 - 1 file changed, 1 deletion(-) diff --git a/routes/routes/users.js b/routes/routes/users.js index b4c30375..f852bc83 100755 --- a/routes/routes/users.js +++ b/routes/routes/users.js @@ -5,7 +5,6 @@ var passport = require('passport'); let axios = require('axios'); const logger = require('../../logger'); const Simva = require('../lib/simva'); -process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; const userClientsListManager = require('../lib/userClientsListManager'); let usertools = require('../lib/usertools'); From 95c88b74e841c8f479d1c2fc66cc820d0bef432a Mon Sep 17 00:00:00 2001 From: Julio SANTILARIO BERTHILIER Date: Wed, 9 Apr 2025 08:54:17 +0200 Subject: [PATCH 022/245] adding url to Xasu Config and to uAdventure Simva Config --- public/activities/gameplaypainter.js | 1 + public/simva.js | 5 +++++ views/layout.ejs | 1 + views/layout_with_menu.ejs | 1 + views/layout_with_menu_and_sse.ejs | 1 + views/scheduler.ejs | 1 + views/study_view.ejs | 2 +- 7 files changed, 11 insertions(+), 1 deletion(-) diff --git a/public/activities/gameplaypainter.js b/public/activities/gameplaypainter.js index 4befc2b5..f83d1b6b 100755 --- a/public/activities/gameplaypainter.js +++ b/public/activities/gameplaypainter.js @@ -114,6 +114,7 @@ var GameplayActivityPainter = { var content = JSON.stringify({ online: true, simva :true, + homepage:`${Simva.url}`, lrs_endpoint : `${Simva.apiurl}/activities/${activityId}`, auth_protocol : "oauth2", auth_parameters : { diff --git a/public/simva.js b/public/simva.js index 54b70ef2..169d7420 100755 --- a/public/simva.js +++ b/public/simva.js @@ -5,6 +5,7 @@ var Simva = { expiration: null, ssoUrl:null, ssoRealm:null, + url:null, setSSOURL: function(ssoUrl){ this.ssoUrl = ssoUrl; @@ -18,6 +19,10 @@ var Simva = { this.apiurl = apiUrl; }, + setURL: function(url){ + this.url = url; + }, + login: function(username, password, callback){ let body = { username: username, password: password } Utils.post('/users/login', body, callback); diff --git a/views/layout.ejs b/views/layout.ejs index da6f0aae..969ebd8f 100755 --- a/views/layout.ejs +++ b/views/layout.ejs @@ -17,6 +17,7 @@
-
Simva - A Simple Validator
+
+ + +
+ +
+ <%= t('title') %> +
diff --git a/views/layout_logout.ejs b/views/layout_logout.ejs new file mode 100755 index 00000000..25d68009 --- /dev/null +++ b/views/layout_logout.ejs @@ -0,0 +1,152 @@ +<%# views/layout_with_menu.ejs %> + + + + + <%= t('title') %> + + + + + + + + + + + + + + + <%- content %> + + \ No newline at end of file diff --git a/views/layout_with_menu.ejs b/views/layout_with_menu.ejs index b924b7cf..0f5584f0 100755 --- a/views/layout_with_menu.ejs +++ b/views/layout_with_menu.ejs @@ -3,7 +3,7 @@ - Simva - A Simple Validator + <%= t('title') %> @@ -38,36 +38,146 @@ } getUserToken(); $(document).ready(function() { - $(".floater").on('click', function(event){ - if (event.target !== this) - return; - $(this).parent().toggleClass('shown'); + $(".floater").off('click').on('click', function(event) { + if (event.target !== this) return; + $(this).parent().removeClass('shown'); + }); + var currentlanguageCode; + // Get the container element and the button + const dropdown = document.getElementById('language-dropdown'); + const langBtn = document.getElementById('lang-btn'); + //const current = document.getElementById('current-lang'); + + function getFlagEmoji(languageCode) { + // Extract the country code (e.g., "BR" from "pt-BR") + const countryCode = languageCode.split('-')[1] || languageCode; + // Ensure the country code is uppercase + const codePoints = countryCode + .toUpperCase() + .split('') + .map(char => 127397 + char.charCodeAt(0)); // Offset for regional indicator symbols + return String.fromCodePoint(...codePoints); + } + + // Function to render the dropdown + function renderDropdown() { + const currentUrl = window.location.href; + Simva.getLanguage(function(error, result) { + if(error) { + console.log(error.message); + } else { + let updateLanguage; + const urlTemplate = result.flagTemplateUrl; + if(!result.current) { + const preferedLangs = navigator.languages; + console.log(`The Preferred languages are: ${preferedLangs}.`); + preferedLangs.forEach((lang) => { + if(!updateLanguage) { + if(result.languages.find(language => language.code === lang)) { + updateLanguage=lang; + } + } + }); + if(!updateLanguage) { + updateLanguage=result.default; + } + if(!currentUrl.includes("lng=") && updateLanguage) { + Simva.updateLanguage(updateLanguage, function(error, result) { + if(error) { + console.log(error.message); + } else { + console.log(result); + if(currentUrl.includes("?")) { + window.location.href=`${currentUrl}&lng=${updateLanguage}`; + } else { + window.location.href=`${currentUrl}?lng=${updateLanguage}`; + } + } + }); + return; + } + } + const currentlanguage = result.languages.find(language => language.code === result.current); + currentlanguageCode=currentlanguage.code; + langBtn.innerHTML=getFlagEmoji(currentlanguage.code) + " " + currentlanguage.name; + var dropdownHTML = '
'; + result.languages.forEach((lang) => { + dropdownHTML += ``; + }); + dropdownHTML += '
'; + dropdown.innerHTML = dropdownHTML; + dropdown.style.display = 'none'; + } + }) + } + + // Add event listener to the button + langBtn.addEventListener('click', () => { + // Show the dropdown when the button is clicked + dropdown.style.display = 'block'; + dropdown.style.top = '0'; // Position it on top of other elements }); + + // Hide the dropdown when an item is clicked or outside the dropdown is clicked + dropdown.addEventListener('click', (event) => { + if (event.target.tagName === 'BUTTON') { + const currentUrl = window.location.href; + const selectedLang = event.target.id; + // Do something with the selected language (e.g., update a variable or make an API call) + console.log(selectedLang); + Simva.updateLanguage(selectedLang, function(error, result) { + if(error) { + console.log(error.message); + } else { + console.log(result); + dropdown.style.display = 'none'; + if(currentUrl.includes("?")) { + window.location.href=currentUrl.replace(`lng=${currentlanguageCode}`,`lng=${selectedLang}`) + } else { + window.location.href=`${currentUrl}?lng=${selectedLang}`; + } + //window.location.reload(); + } + }); + } else if (event.target === dropdown) { + dropdown.style.display = 'none'; + } + }); + + // Initial render of the dropdown + renderDropdown(); });
-
Simva - A Simple Validator
+
+ + +
+ +
+ <%= t('title') %> +
diff --git a/views/layout_with_menu_and_sse.ejs b/views/layout_with_menu_and_sse.ejs index 798ee1e6..654a1352 100755 --- a/views/layout_with_menu_and_sse.ejs +++ b/views/layout_with_menu_and_sse.ejs @@ -2,7 +2,7 @@ - Simva - A Simple Validator + <%= t('title') %> @@ -33,10 +33,9 @@ } getUserToken(); $(document).ready(function() { - $(".floater").on('click', function(event){ - if (event.target !== this) - return; - $(this).parent().toggleClass('shown'); + $(".floater").off('click').on('click', function(event) { + if (event.target !== this) return; + $(this).parent().removeClass('shown'); }); }); @@ -70,30 +69,143 @@ } }); } + $(document).ready(function() { + var currentlanguageCode; + // Get the container element and the button + const dropdown = document.getElementById('language-dropdown'); + const langBtn = document.getElementById('lang-btn'); + //const current = document.getElementById('current-lang'); + + function getFlagEmoji(languageCode) { + // Extract the country code (e.g., "BR" from "pt-BR") + const countryCode = languageCode.split('-')[1] || languageCode; + // Ensure the country code is uppercase + const codePoints = countryCode + .toUpperCase() + .split('') + .map(char => 127397 + char.charCodeAt(0)); // Offset for regional indicator symbols + return String.fromCodePoint(...codePoints); + } + + // Function to render the dropdown + function renderDropdown() { + const currentUrl = window.location.href; + Simva.getLanguage(function(error, result) { + if(error) { + console.log(error.message); + } else { + let updateLanguage; + const urlTemplate = result.flagTemplateUrl; + if(!result.current) { + const preferedLangs = navigator.languages; + console.log(`The Preferred languages are: ${preferedLangs}.`); + preferedLangs.forEach((lang) => { + if(!updateLanguage) { + if(result.languages.find(language => language.code === lang)) { + updateLanguage=lang; + } + } + }); + if(!updateLanguage) { + updateLanguage=result.default; + } + if(!currentUrl.includes("lng=") && updateLanguage) { + Simva.updateLanguage(updateLanguage, function(error, result) { + if(error) { + console.log(error.message); + } else { + console.log(result); + if(currentUrl.includes("?")) { + window.location.href=`${currentUrl}&lng=${updateLanguage}`; + } else { + window.location.href=`${currentUrl}?lng=${updateLanguage}`; + } + } + }); + return; + } + } + const currentlanguage = result.languages.find(language => language.code === result.current); + currentlanguageCode=currentlanguage.code; + langBtn.innerHTML=getFlagEmoji(currentlanguage.code) + " " + currentlanguage.name; + var dropdownHTML = '
'; + result.languages.forEach((lang) => { + dropdownHTML += ``; + }); + dropdownHTML += '
'; + dropdown.innerHTML = dropdownHTML; + dropdown.style.display = 'none'; + } + }) + } + + // Add event listener to the button + langBtn.addEventListener('click', () => { + // Show the dropdown when the button is clicked + dropdown.style.display = 'block'; + dropdown.style.top = '0'; // Position it on top of other elements + }); + + // Hide the dropdown when an item is clicked or outside the dropdown is clicked + dropdown.addEventListener('click', (event) => { + if (event.target.tagName === 'BUTTON') { + const currentUrl = window.location.href; + const selectedLang = event.target.id; + // Do something with the selected language (e.g., update a variable or make an API call) + console.log(selectedLang); + Simva.updateLanguage(selectedLang, function(error, result) { + if(error) { + console.log(error.message); + } else { + console.log(result); + dropdown.style.display = 'none'; + if(currentUrl.includes("?")) { + window.location.href=currentUrl.replace(`lng=${currentlanguageCode}`,`lng=${selectedLang}`) + } else { + window.location.href=`${currentUrl}?lng=${selectedLang}`; + } + //window.location.reload(); + } + }); + } else if (event.target === dropdown) { + dropdown.style.display = 'none'; + } + }); + + // Initial render of the dropdown + renderDropdown(); + });
-
Simva - A Simple Validator
+
+ + +
+ +
+ <%= t('title') %> +
diff --git a/views/logout_about.ejs b/views/logout_about.ejs new file mode 100755 index 00000000..e9302944 --- /dev/null +++ b/views/logout_about.ejs @@ -0,0 +1,8 @@ +<%# views/about.ejs %> + +<% extend('layout_logout') %> + +
+

<%= t('title', { ns : 'about'}) %>

+

<%- t('text', { ns : 'about'}).replace(/\n/g, '

') %>

+
\ No newline at end of file diff --git a/views/new_group_view.ejs b/views/new_group_view.ejs old mode 100755 new mode 100644 index e8531ee1..b652aa60 --- a/views/new_group_view.ejs +++ b/views/new_group_view.ejs @@ -6,20 +6,20 @@
-->
-

Group:

+

<%= t('single.title' , { ns : 'groups' }) %>:

-

Owners

+

<%= t('owners.title' , { ns : 'groups' }) %>

- +
-

Participants

- +

<%= t('participants.title' , { ns : 'groups' }) %>

+
@@ -31,10 +31,10 @@
-

Add owner

+

<%= t('owners.add.title' , { ns : 'groups' }) %>

- - + +
@@ -46,12 +46,12 @@
-

Edit Group

+

<%= t('name.edit.title' , { ns : 'groups' }) %>

- - + +
@@ -64,43 +64,43 @@
- Add existing - Add new - Add batch + <%= t('participants.add.existing.title' , { ns : 'groups' }) %> + <%= t('participants.add.new.title' , { ns : 'groups' }) %> + <%= t('participants.add.batch.title' , { ns : 'groups' }) %>
- - + +
- - - + + +
- - + +
- +
-

How many?

+

<%= t('participants.add.batch.number.title' , { ns : 'groups' }) %>

-

Participants will be added with random usernames created using:

+

<%= t('participants.add.batch.algorithFormat' , { ns : 'groups' }) %>

-

And a length of:

+

<%= t('participants.add.batch.length.title' , { ns : 'groups' }) %>

- +
@@ -176,7 +176,7 @@ Utils.toggleSubmit(currentform); let errorbox = $(this).find('.error'); if(form.name == form.groupName) { - errorbox.text("No changes detected !"); + errorbox.text("<%= t('name.edit.nochange', { ns : 'groups' }) %>"); Utils.toggleSubmit(currentform); } else { Simva.getGroup(form.group, function(error, group){ @@ -190,7 +190,6 @@ } }); } - }); } return false; @@ -203,7 +202,7 @@ for (var i = 0; i < group.participants.length; i++) { if(group.participants[i] == form.name){ - errorbox.text('The participant is already in the group.'); + errorbox.text(`t("participants.errormessage.alreadyInGroup", { ns : "groups", group : "${group.name}", participant : "${form.name}" }) %>`); return false; } } @@ -272,6 +271,7 @@ }); return false; }); + reloadGroup(); }); @@ -302,7 +302,7 @@ } let removeParticipant = function(participant){ - if(confirm(`Are you sure do you want to delete the participant '${participant}' of group '${group.name}'?`)){ + if(confirm(`<%- t('participants.remove.confirm', { ns: 'groups', participant : '${participant}', group : '${group.name}' }) %>`)){ let toremove = -1; for (var i = 0; i < group.participants.length; i++) { if(group.participants[i] === participant){ @@ -320,7 +320,7 @@ } let deleteOwner = function(owner){ - if(confirm(`Are you sure do you want to delete the owner '${owner}' of group '${group.name}'?`)){ + if(confirm(`<%- t('owners.remove.confirm', { ns: 'groups', owner : '${owner}', group : '${group.name}' }) %>`)){ let toremove = -1; for (var i = 0; i < group.owners.length; i++) { if(group.owners[i] === owner){ @@ -347,9 +347,9 @@ $('#participants').empty(); $('#pdf_printable_table').empty(); if(group.participants.length == 0){ - $('#participants').append('

There are no participants in this group. Try adding some using the button on the right.

'); + $('#participants').append('

<%= t("participants.errormessage.zeroInGroup.title", { ns : "groups" }) %><%= t("participants.errormessage.zeroInGroup.message", { ns : "groups" }) %>

'); $('#pdf_printable_table').append(groupHeaderNoParticipantsPDF(group)); - $('#pdf_printable_table').append('

There are no participants in this group. Try adding some using the button on the right.

'); + $('#pdf_printable_table').append('

<%= t("participants.errormessage.zeroInGroup.title", { ns : "groups" }) %>

'); }else{ $('#participants').append(participantHeader()); $('#pdf_printable_table').append(headerRowPDF(group)); @@ -361,7 +361,7 @@ } let ownerRow = function(owner){ - let toret = `${owner}${owner}"`; if(owner == '<%= user.data.username %>'){ toret += ' disabled="disabled"'; }else{ @@ -373,25 +373,45 @@ } let groupHeaderNoParticipantsPDF = function(groupname) { - return `Group: ${group.name}`; + return `<%= t("single.title", { ns : "groups" }) %>: ${group.name}`; } let participantHeader = function(){ - return `PicUsernameIsTokenTokenEmailRole`; + return `<%= t("participants.username.title", { ns : "groups" }) %><%= t("participants.isToken.title", { ns : "groups" }) %><%= t("participants.token.title", { ns : "groups" }) %><%= t("participants.email.title", { ns : "groups" }) %><%= t("participants.role.title", { ns : "groups" }) %>`; } let participantRow = function(participant){ + let isToken=""; + let token=""; + let role=""; + if(participant.isToken) { + isToken = `<%= t("participants.isToken.ok", { ns : "groups" }) %>`; + token = participant.token; + } else { + isToken = `<%= t("participants.isToken.nok", { ns : "groups" }) %>`; + token = "-"; + } + switch(participant.role) { + case "student": + role = `<%= t("student", { ns : "roles" }) %>`; + break; + case "teacher": + role = `<%= t("teacher", { ns : "roles" }) %>`; + break; + default: + role = `<%= t("participants.role.notdefined", { ns : "groups" }) %>`; + } return ` ${participant.username} - ${participant.isToken} - ${participant.token} + ${isToken} + ${token} ${participant.email} - ${participant.role} - `; + ${role} + " onclick="removeParticipant('${participant.username}')">`; } let headerRowPDF = function(group){ - return `Group: ${group.name}No.NameCode`; + return `<%= t("single.title", { ns : "groups" }) %>: ${group.name}<%= t("participants.pdf.no.title", { ns : "groups" }) %><%= t("participants.pdf.name.title", { ns : "groups" }) %><%= t("participants.pdf.code.title", { ns : "groups" }) %>`; } let participantRowPDF = function(participant, position){ diff --git a/views/new_groups_list.ejs b/views/new_groups_list.ejs index 433d51f1..41a6dcc6 100755 --- a/views/new_groups_list.ejs +++ b/views/new_groups_list.ejs @@ -32,7 +32,7 @@ Utils.toggleSubmit(currentform); let errorbox = $(this).find('.error'); if(form.name == form.groupName) { - errorbox.text("No changes detected !"); + errorbox.text("<%= t('name.edit.nochange', { ns : 'groups' }) %>"); Utils.toggleSubmit(currentform); } else { Simva.getGroup(form.group, function(error, group){ @@ -75,7 +75,7 @@ } } }else{ - $('#groups_list').text('You have ot groups yet. Create one using the bottom right (+) button.'); + $('#groups_list').text('<%= t("zero.message.title", { ns : "groups" }) %>'); } } @@ -95,7 +95,7 @@ let deleteGroup = function(groupid, groupname){ event.preventDefault(); event.stopPropagation(); - if(confirm(`Are you sure do you want to delete the group '${groupname}'?\nGroup will be removed to all the studies, tests and activities.`)){ + if(confirm(`<%= t("remove.confirm", { ns : "groups", group : "${groupname}" }) %>`)){ Simva.deleteGroup(groupid, function(error, result){ if(!error){ Simva.getGroups(function(error, result){ @@ -112,13 +112,13 @@ return `
-

Group: ${group.name}

-

Participants: ${group.participants.length}

-

Owners: ${group.owners.length}

+

<%= t("single.title", { ns : "groups" }) %>: ${group.name}

+

<%= t("participants.title", { ns : "groups" }) %>: ${group.participants.length}

+

<%= t("owners.title", { ns : "groups" }) %>: ${group.owners.length}

`; } -

Groups

+

<%= t("title", { ns : "groups" }) %>

@@ -127,10 +127,10 @@
-

Add new

+

<%= t("new.title", { ns : "groups" }) %>

- - + +
@@ -139,12 +139,12 @@
-

Edit Group

+

<%= t("name.edit.title", { ns : "groups" }) %>

- - + +
diff --git a/views/previous_group_view.ejs b/views/previous_group_view.ejs index ca90b916..bf4d727f 100755 --- a/views/previous_group_view.ejs +++ b/views/previous_group_view.ejs @@ -6,20 +6,20 @@
-->
-

Group:

+

<%= t('single.title' , { ns : 'groups' }) %>:

-

Owners

+

<%= t('owners.title' , { ns : 'groups' }) %>

- +
-

Participants

- +

<%= t('participants.title' , { ns : 'groups' }) %>

+
@@ -31,10 +31,10 @@
-

Add owner

+

<%= t('owners.add.title' , { ns : 'groups' }) %>

- - + +
@@ -46,12 +46,12 @@
-

Edit Group

+

<%= t('name.edit.title' , { ns : 'groups' }) %>

- - + +
@@ -64,43 +64,43 @@
- Add existing - Add new - Add batch + <%= t('participants.add.existing.title' , { ns : 'groups' }) %> + <%= t('participants.add.new.title' , { ns : 'groups' }) %> + <%= t('participants.add.batch.title' , { ns : 'groups' }) %>
- - + +
- - - + + +
- - + +
- +
-

How many?

+

<%= t('participants.add.batch.number.title' , { ns : 'groups' }) %>

-

Participants will be added with random usernames created using:

+

<%= t('participants.add.batch.algorithFormat' , { ns : 'groups' }) %>

-

And a length of:

+

<%= t('participants.add.batch.length.title' , { ns : 'groups' }) %>

- +
@@ -176,7 +176,7 @@ Utils.toggleSubmit(currentform); let errorbox = $(this).find('.error'); if(form.name == form.groupName) { - errorbox.text("No changes detected !"); + errorbox.text("<%= t('name.edit.nochange', { ns : 'groups' }) %>"); Utils.toggleSubmit(currentform); } else { Simva.getGroup(form.group, function(error, group){ @@ -190,7 +190,6 @@ } }); } - }); } return false; @@ -203,7 +202,7 @@ for (var i = 0; i < group.participants.length; i++) { if(group.participants[i] == form.name){ - errorbox.text('The participant is already in the group.'); + errorbox.text(`t("participants.errormessage.alreadyInGroup", { ns : "groups", group : "${group.name}", participant : "${form.name}" }) %>`); return false; } } @@ -290,9 +289,9 @@ } let reloadGroup = function(){ - Simva.getGroup(groupid, function(error, mygroup){ + Simva.getGroup(groupid, function(error, result){ if(!error){ - group=mygroup; + group=result; paintGroup(group, group.completeParticipants); delete group.completeParticipants; loaded=true; @@ -303,7 +302,7 @@ } let removeParticipant = function(participant){ - if(confirm(`Are you sure do you want to delete the participant '${participant}' of group '${group.name}'?`)){ + if(confirm(`<%- t('participants.remove.confirm', { ns: 'groups', participant : '${participant}', group : '${group.name}' }) %>`)){ let toremove = -1; for (var i = 0; i < group.participants.length; i++) { if(group.participants[i] === participant){ @@ -321,7 +320,7 @@ } let deleteOwner = function(owner){ - if(confirm(`Are you sure do you want to delete the owner '${owner}' of group '${group.name}'?`)){ + if(confirm(`<%- t('owners.remove.confirm', { ns: 'groups', owner : '${owner}', group : '${group.name}' }) %>`)){ let toremove = -1; for (var i = 0; i < group.owners.length; i++) { if(group.owners[i] === owner){ @@ -348,9 +347,9 @@ $('#participants').empty(); $('#pdf_printable_table').empty(); if(group.participants.length == 0){ - $('#participants').append('

There are no participants in this group. Try adding some using the button on the right.

'); + $('#participants').append('

<%= t("participants.errormessage.zeroInGroup.title", { ns : "groups" }) %><%= t("participants.errormessage.zeroInGroup.message", { ns : "groups" }) %>

'); $('#pdf_printable_table').append(groupHeaderNoParticipantsPDF(group)); - $('#pdf_printable_table').append('

There are no participants in this group. Try adding some using the button on the right.

'); + $('#pdf_printable_table').append('

<%= t("participants.errormessage.zeroInGroup.title", { ns : "groups" }) %>

'); }else{ $('#participants').append(participantHeader()); $('#pdf_printable_table').append(headerRowPDF(group)); @@ -362,7 +361,7 @@ } let ownerRow = function(owner){ - let toret = `${owner}${owner}"`; if(owner == '<%= user.data.username %>'){ toret += ' disabled="disabled"'; }else{ @@ -374,23 +373,34 @@ } let groupHeaderNoParticipantsPDF = function(groupname) { - return `Group: ${group.name}`; + return `<%= t("single.title", { ns : "groups" }) %>: ${group.name}`; } let participantHeader = function(){ - return `PicUsernameEmailRole`; + return `<%= t("participants.username.title", { ns : "groups" }) %><%= t("participants.email.title", { ns : "groups" }) %><%= t("participants.role.title", { ns : "groups" }) %>`; } let participantRow = function(participant){ + let role=""; + switch(participant.role) { + case "student": + role = `<%= t("student", { ns : "roles" }) %>`; + break; + case "teacher": + role = `<%= t("teacher", { ns : "roles" }) %>`; + break; + default: + role = `<%= t("participants.role.notdefined", { ns : "groups" }) %>`; + } return ` ${participant.username} ${participant.email} - ${participant.role} - `; + ${role} + " onclick="removeParticipant('${participant.username}')">`; } let headerRowPDF = function(group){ - return `Group: ${group.name}No.NameCode`; + return `<%= t("single.title", { ns : "groups" }) %>: ${group.name}<%= t("participants.pdf.no.title", { ns : "groups" }) %><%= t("participants.pdf.name.title", { ns : "groups" }) %><%= t("participants.pdf.code.title", { ns : "groups" }) %>`; } let participantRowPDF = function(participant, position){ diff --git a/views/previous_groups_list.ejs b/views/previous_groups_list.ejs index 0aee2f88..98c5e295 100755 --- a/views/previous_groups_list.ejs +++ b/views/previous_groups_list.ejs @@ -32,7 +32,7 @@ Utils.toggleSubmit(currentform); let errorbox = $(this).find('.error'); if(form.name == form.groupName) { - errorbox.text("No changes detected !"); + errorbox.text("<%= t('name.edit.nochange', { ns : 'groups' }) %>"); Utils.toggleSubmit(currentform); } else { Simva.getGroup(form.group, function(error, group){ @@ -75,7 +75,7 @@ } } }else{ - $('#groups_list').text('You have ot groups yet. Create one using the bottom right (+) button.'); + $('#groups_list').text('<%= t("zero.message.title", { ns : "groups" }) %>'); } } @@ -95,7 +95,7 @@ let deleteGroup = function(groupid, groupname){ event.preventDefault(); event.stopPropagation(); - if(confirm(`Are you sure do you want to delete the group '${groupname}'?\nGroup will be removed to all the studies, tests and activities.`)){ + if(confirm(`<%= t("remove.confirm", { ns : "groups", group : "${groupname}" }) %>`)){ Simva.deleteGroup(groupid, function(error, result){ if(!error){ Simva.getGroups(function(error, result){ @@ -112,13 +112,13 @@ return `
-

Group: ${group.name}

-

Participants: ${group.participants.length}

-

Owners: ${group.owners.length}

+

<%= t("single.title", { ns : "groups" }) %>: ${group.name}

+

<%= t("participants.title", { ns : "groups" }) %>: ${group.participants.length}

+

<%= t("owners.title", { ns : "groups" }) %>: ${group.owners.length}

`; } -

Groups (deprecated) - This tab has been deprecated. Please prefer use the new group page version.

+

<%= t("deprecated.title", { ns : "groups" }) %> - <%= t("deprecated.message", { ns : "groups" }) %>

@@ -127,10 +127,10 @@
-

Add new

+

<%= t("new.title", { ns : "groups" }) %>

- - + +
@@ -139,12 +139,12 @@
-

Edit Group

+

<%= t("name.edit.title", { ns : "groups" }) %>

- - + +
diff --git a/views/scheduler.ejs b/views/scheduler.ejs old mode 100755 new mode 100644 index d9882513..40d04159 --- a/views/scheduler.ejs +++ b/views/scheduler.ejs @@ -3,7 +3,7 @@ - Simva - A Simple Validator + <%= t('title') %> @@ -21,10 +21,9 @@ var roller = '
'; $(document).ready(function() { - $(".floater").on('click', function(event){ - if (event.target !== this) - return; - $(this).parent().toggleClass('shown'); + $(".floater").off('click').on('click', function(event) { + if (event.target !== this) return; + $(this).parent().removeClass('shown'); }); }); @@ -192,17 +191,130 @@ reloadSchedule(); }); } + $(document).ready(function() { + var currentlanguageCode; + // Get the container element and the button + const dropdown = document.getElementById('language-dropdown'); + const langBtn = document.getElementById('lang-btn'); + //const current = document.getElementById('current-lang'); + + function getFlagEmoji(languageCode) { + // Extract the country code (e.g., "BR" from "pt-BR") + const countryCode = languageCode.split('-')[1] || languageCode; + // Ensure the country code is uppercase + const codePoints = countryCode + .toUpperCase() + .split('') + .map(char => 127397 + char.charCodeAt(0)); // Offset for regional indicator symbols + return String.fromCodePoint(...codePoints); + } + + // Function to render the dropdown + function renderDropdown() { + const currentUrl = window.location.href; + Simva.getLanguage(function(error, result) { + if(error) { + console.log(error.message); + } else { + let updateLanguage; + const urlTemplate = result.flagTemplateUrl; + if(!result.current) { + const preferedLangs = navigator.languages; + console.log(`The Preferred languages are: ${preferedLangs}.`); + preferedLangs.forEach((lang) => { + if(!updateLanguage) { + if(result.languages.find(language => language.code === lang)) { + updateLanguage=lang; + } + } + }); + if(!updateLanguage) { + updateLanguage=result.default; + } + if(!currentUrl.includes("lng=") && updateLanguage) { + Simva.updateLanguage(updateLanguage, function(error, result) { + if(error) { + console.log(error.message); + } else { + console.log(result); + if(currentUrl.includes("?")) { + window.location.href=`${currentUrl}&lng=${updateLanguage}`; + } else { + window.location.href=`${currentUrl}?lng=${updateLanguage}`; + } + } + }); + return; + } + } + const currentlanguage = result.languages.find(language => language.code === result.current); + currentlanguageCode=currentlanguage.code; + langBtn.innerHTML=getFlagEmoji(currentlanguage.code) + " " + currentlanguage.name; + var dropdownHTML = '
'; + result.languages.forEach((lang) => { + dropdownHTML += ``; + }); + dropdownHTML += '
'; + dropdown.innerHTML = dropdownHTML; + dropdown.style.display = 'none'; + } + }) + } + + // Add event listener to the button + langBtn.addEventListener('click', () => { + // Show the dropdown when the button is clicked + dropdown.style.display = 'block'; + dropdown.style.top = '0'; // Position it on top of other elements + }); + + // Hide the dropdown when an item is clicked or outside the dropdown is clicked + dropdown.addEventListener('click', (event) => { + if (event.target.tagName === 'BUTTON') { + const currentUrl = window.location.href; + const selectedLang = event.target.id; + // Do something with the selected language (e.g., update a variable or make an API call) + console.log(selectedLang); + Simva.updateLanguage(selectedLang, function(error, result) { + if(error) { + console.log(error.message); + } else { + console.log(result); + dropdown.style.display = 'none'; + if(currentUrl.includes("?")) { + window.location.href=currentUrl.replace(`lng=${currentlanguageCode}`,`lng=${selectedLang}`) + } else { + window.location.href=`${currentUrl}?lng=${selectedLang}`; + } + //window.location.reload(); + } + }); + } else if (event.target === dropdown) { + dropdown.style.display = 'none'; + } + }); + + // Initial render of the dropdown + renderDropdown(); + }); diff --git a/views/studenthome.ejs b/views/studenthome.ejs index fbc768be..947bce81 100755 --- a/views/studenthome.ejs +++ b/views/studenthome.ejs @@ -3,11 +3,8 @@ <% extend('layout_with_menu_and_sse') %>
-

Home

-

Welcome user!

-

This is the main view of Simva.

-

You can go to Activities to create survey activities, gameplay activities, and other types of custom activities required to build an study.

-

Use the Studies tab to create an study where a set of activities can be set up for an evaluation to be performed.

-

To manage Users and Groups, use the Groups tab where groups of users and users themselves can be created in order to allow them to participate in the studies.

+

<%= t('home.title') %>

+

<%- t('welcome.user.text', { username: user.data.username }) %> +

<%- t('welcome.student.text').replace(/\n/g, '

') %>

diff --git a/views/studies_list.ejs b/views/studies_list.ejs index 30d7441e..770248f6 100755 --- a/views/studies_list.ejs +++ b/views/studies_list.ejs @@ -57,7 +57,7 @@ }; reader.readAsDataURL($("#import_study_form").find('input[name="exportJson"]').get(0).files[0]); }else{ - $("#error").text('Select the file to upload first.'); + $("#error").text("<%- t('new.upload.errorMessage', { ns : 'studies' }) %>"); Utils.toggleSubmit(currentform); } @@ -72,7 +72,7 @@ Utils.toggleSubmit(currentform); let errorbox = $(this).find('.error'); if(form.name == form.studyName) { - errorbox.text("No changes detected !"); + errorbox.text("<%- t('name.edit.nochange', { ns : 'studies' }) %>"); Utils.toggleSubmit(currentform); } else { Simva.getStudy(form.study, function(error, study){ @@ -108,14 +108,14 @@ $('#studies_list').append(card); } }else{ - $('#studies_list').text('You have no studies yet. Create one using the bottom right (+) button.'); + $('#studies_list').text("<%- t('zero.message.title', { ns : 'studies' }) %>"); } } let deleteStudy = function(studyid, studyname){ event.preventDefault(); event.stopPropagation(); - if(confirm(`Are you sure do you want to delete the study '${studyname}'?\nAll the tests, activities and data will be deleted too.`)){ + if(confirm(`<%- t('remove.confirm', { ns : 'studies', study : '${studyname}' }) %>`)){ Simva.deleteStudy(studyid, function(error, result){ if(!error){ refreshStudies(); @@ -140,10 +140,10 @@ return `
🖍️ X -

Study: ${study.name}

-

Groups: ${study.groups.length}

-

Tests: ${study.tests.length}

-

owners: ${study.owners.length}

+

<%- t('single.title', { ns : 'studies' }) %>: ${study.name}

+

<%- t('groups.title', { ns : 'studies' }) %>: ${study.groups.length}

+

<%- t('tests.title', { ns : 'studies' }) %>: ${study.tests.length}

+

<%- t('owners.title', { ns : 'studies' }) %>: ${study.owners.length}

`; } @@ -154,7 +154,7 @@ $(`#${subform}`).toggleClass('selected'); } -

Studies

+

<%- t('title', { ns : 'studies' }) %>

@@ -163,22 +163,22 @@
-

Add new

+

<%- t('new.title', { ns : 'studies' }) %>

- Add new Study - Import from existing + <%- t('new.title', { ns : 'studies' }) %> + <%- t('new.upload.title', { ns : 'studies' }) %>
- - + +
@@ -188,11 +188,11 @@
-

Edit Study

+

"<%- t('name.edit.title', { ns : 'studies' }) %>

- - - + + + diff --git a/views/studies_play.ejs b/views/studies_play.ejs index b38923b1..6f6c9824 100755 --- a/views/studies_play.ejs +++ b/views/studies_play.ejs @@ -19,18 +19,18 @@ $('#studies_list').append(toCard(studies[i])); } }else{ - $('#studies_list').text('You have no studies yet to participate.'); + $('#studies_list').text("<%- t('scheduler.nostudy.title', { ns : 'studies' }) %>"); } } let toCard = function(study){ return `
-

Study: ${study.name}

-

Click here to start or continue your session in the study.

+

<%- t('single.title', { ns : 'studies' }) %>: ${study.name}

+

<%- t('scheduler.title', { ns : 'studies' }) %>

`; } -

Studies

+

<%- t('title', { ns : 'studies' }) %>

diff --git a/views/study_view.ejs b/views/study_view.ejs index 85fc083e..5b13674d 100644 --- a/views/study_view.ejs +++ b/views/study_view.ejs @@ -5,9 +5,9 @@ -

Study:

+

<%- t('single.title', { ns : 'studies' }) %>:

- +
@@ -18,23 +18,23 @@
-

Owners

+

<%- t('owners.title', { ns : 'studies' }) %>

- +
-

Groups

+

<%- t('groups.title', { ns : 'studies' }) %>

- +
-

Allocator

+

<%- t('allocator.title', { ns : 'studies' }) %>

@@ -42,18 +42,18 @@
<% if(config.lti.enabled == 'true'){ %>
-

LTI Platforms

+

<%- t('lti_platform.title', { ns : 'studies' }) %>

- +
<% } %>
-

Tests

- +

<%- t('tests.title', { ns : 'studies' }) %>

+
@@ -63,11 +63,11 @@
-

Add group

+

<%- t('groups.add.title', { ns : 'studies' }) %>

- + @@ -80,22 +80,22 @@
-

Add LTI Platform

+

<%- t('lti_platforms.add.title', { ns : 'studies' }) %>

-

Name:

+

<%- t('lti_platforms.add.name.title', { ns : 'studies' }) %>:

-

UTL:

+

<%- t('lti_platforms.add.url.title', { ns : 'studies' }) %>:

-

Client ID:

+

<%- t('lti_platforms.add.client_id.title', { ns : 'studies' }) %>:

-

Authentication Endpoint:

+

<%- t('lti_platforms.add.authentication_endpoint.title', { ns : 'studies' }) %>:

-

Accesstoken Endpoint:

+

<%- t('lti_platforms.add.accesstoken_endpoint.title', { ns : 'studies' }) %>Accesstoken Endpoint:

-

JWKS URL:

+

<%- t('lti_platforms.add.jkws_url.title', { ns : 'studies' }) %>:

- +
@@ -107,11 +107,11 @@
-

Select the new allocator

+

<%- t('allocator.change.title', { ns : 'studies' }) %>

- +
@@ -135,10 +135,10 @@
-

Add owner

+

<%- t('owners.add.title', { ns : 'studies' }) %>

- - + +
@@ -151,10 +151,10 @@
-

Add Test

+

<%- t('tests.add.title', { ns : 'studies' }) %>

- - + +
@@ -168,12 +168,12 @@
-

Edit Study

+

<%- t('name.edit.title', { ns : 'studies' }) %>

- - + +
@@ -185,27 +185,28 @@