From 9ec482567a8a6a9e096911238e1f45ed8a75d842 Mon Sep 17 00:00:00 2001 From: Jakob Stechow Date: Mon, 23 Dec 2024 21:36:13 +0100 Subject: [PATCH] feat: app notifications (#149) * wip * feat: subscribe to topic on client and send to topic on server * feat: fcm registration token table * feat: send registration token to backend every time and store it in database * deps: pnpm v9.15.1 * feat: send notifications when creating assignments in scheduler * fix: dont initialize app in firebaseMessagingBackgroundHandler * fix: initialize firebase admin app only if required * fix: dont send firebase messages on CI * fix: adjust messages * fix: only do firebase stuff if supported platform * fix: stuff from self-code-review * fix: remove unused import --- .github/workflows/ci.yml | 2 +- backend/.env.example | 3 + backend/package.json | 6 +- backend/pnpm-lock.yaml | 738 +++++++++++++++++- backend/src/app.module.ts | 5 + .../assignment-scheduler.service.ts | 25 +- backend/src/auth/auth.controller.ts | 6 - backend/src/db/functions/notification.ts | 13 + .../src/db/migrations/0013_loving_satana.sql | 13 + .../src/db/migrations/meta/0013_snapshot.json | 680 ++++++++++++++++ backend/src/db/migrations/meta/_journal.json | 7 + backend/src/db/schema.ts | 24 + backend/src/main.ts | 18 + .../notifications/notification.controller.ts | 44 ++ .../src/notifications/notification.module.ts | 7 + .../src/notifications/notification.service.ts | 24 + backend/src/notifications/notification.ts | 16 + flake.lock | 25 +- flake.nix | 6 + frontend/android/app/build.gradle | 3 + frontend/android/app/google-services.json | 29 + frontend/android/settings.gradle | 3 + frontend/firebase.json | 1 + frontend/lib/authenticated_navigation.dart | 2 +- frontend/lib/fetch/auth.dart | 28 +- frontend/lib/fetch/notification.dart | 15 + frontend/lib/fetch/task.dart | 30 +- frontend/lib/firebase_options.dart | 59 ++ frontend/lib/main.dart | 27 +- frontend/lib/models/assignment.dart | 34 +- frontend/lib/models/task.dart | 16 +- frontend/lib/models/task_group.dart | 23 +- .../models/task_with_maybe_task_group.dart | 25 + frontend/lib/models/user.dart | 14 +- frontend/lib/models/user_group.dart | 10 +- frontend/lib/notifications/handler.dart | 5 + frontend/lib/notifications/util.dart | 5 + .../assignments/assignments_widget.dart | 2 +- frontend/lib/widgets/screens/splash.dart | 2 +- frontend/lib/widgets/user/login_form.dart | 12 +- .../Flutter/GeneratedPluginRegistrant.swift | 4 + frontend/pubspec.lock | 56 ++ frontend/pubspec.yaml | 2 + frontend/test/widget_test.dart | 30 - 44 files changed, 1946 insertions(+), 153 deletions(-) create mode 100644 backend/src/db/functions/notification.ts create mode 100644 backend/src/db/migrations/0013_loving_satana.sql create mode 100644 backend/src/db/migrations/meta/0013_snapshot.json create mode 100644 backend/src/notifications/notification.controller.ts create mode 100644 backend/src/notifications/notification.module.ts create mode 100644 backend/src/notifications/notification.service.ts create mode 100644 backend/src/notifications/notification.ts create mode 100644 frontend/android/app/google-services.json create mode 100644 frontend/firebase.json create mode 100644 frontend/lib/fetch/notification.dart create mode 100644 frontend/lib/firebase_options.dart create mode 100644 frontend/lib/models/task_with_maybe_task_group.dart create mode 100644 frontend/lib/notifications/handler.dart create mode 100644 frontend/lib/notifications/util.dart delete mode 100644 frontend/test/widget_test.dart diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05a176f..5f2cfc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: - uses: pnpm/action-setup@v4 with: - version: 9 + version: 9.15.1 - name: Install deps run: pnpm i diff --git a/backend/.env.example b/backend/.env.example index 401137f..4acc568 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -4,3 +4,6 @@ JWT_SECRET=**** DATABASE_URL=postgres://postgres:changeme@localhost:5432/postgres NODE_ENV=development DB_PASSWORD=changeme + +# a oneline string containing valid json of the firebase service account (needed for firebase cloud messaging - app notifications) +FIREBASE_SERVICE_ACCOUNT_JSON_CONTENT= diff --git a/backend/package.json b/backend/package.json index 8ece47f..a791205 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,7 @@ "private": true, "license": "UNLICENSED", "engines": { - "pnpm": "~9" + "pnpm": "9.15.1" }, "scripts": { "build": "nest build", @@ -34,6 +34,7 @@ "@nestjs/serve-static": "4.0.2", "bcrypt": "5.1.1", "drizzle-orm": "0.37.0", + "firebase-admin": "^13.0.1", "pg": "8.11.5", "postgres": "3.4.4", "reflect-metadata": "0.2.0", @@ -92,6 +93,5 @@ ], "coverageDirectory": "../coverage", "testEnvironment": "node" - }, - "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1" + } } diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 2b22587..5730347 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -31,7 +31,10 @@ importers: version: 5.1.1 drizzle-orm: specifier: 0.37.0 - version: 0.37.0(pg@8.11.5)(postgres@3.4.4) + version: 0.37.0(@opentelemetry/api@1.9.0)(pg@8.11.5)(postgres@3.4.4) + firebase-admin: + specifier: ^13.0.1 + version: 13.0.1 pg: specifier: 8.11.5 version: 8.11.5 @@ -666,6 +669,70 @@ packages: resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/busboy@3.1.0': + resolution: {integrity: sha512-yHmUtGwEbW6HsKpPqT140/L6GpHtquHogRLgtanJFep3UAfDkE0fQfC49U+F9irCAoJVlv3M7VSp4rrtO4LnfA==} + + '@firebase/app-check-interop-types@0.3.3': + resolution: {integrity: sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==} + + '@firebase/app-types@0.9.3': + resolution: {integrity: sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==} + + '@firebase/auth-interop-types@0.2.4': + resolution: {integrity: sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==} + + '@firebase/component@0.6.11': + resolution: {integrity: sha512-eQbeCgPukLgsKD0Kw5wQgsMDX5LeoI1MIrziNDjmc6XDq5ZQnuUymANQgAb2wp1tSF9zDSXyxJmIUXaKgN58Ug==} + engines: {node: '>=18.0.0'} + + '@firebase/database-compat@2.0.1': + resolution: {integrity: sha512-IsFivOjdE1GrjTeKoBU/ZMenESKDXidFDzZzHBPQ/4P20ptGdrl3oLlWrV/QJqJ9lND4IidE3z4Xr5JyfUW1vg==} + engines: {node: '>=18.0.0'} + + '@firebase/database-types@1.0.7': + resolution: {integrity: sha512-I7zcLfJXrM0WM+ksFmFdAMdlq/DFmpeMNa+/GNsLyFo5u/lX5zzkPzGe3srVWqaBQBY5KprylDGxOsP6ETfL0A==} + + '@firebase/database@1.0.10': + resolution: {integrity: sha512-sWp2g92u7xT4BojGbTXZ80iaSIaL6GAL0pwvM0CO/hb0nHSnABAqsH7AhnWGsGvXuEvbPr7blZylPaR9J+GSuQ==} + engines: {node: '>=18.0.0'} + + '@firebase/logger@0.4.4': + resolution: {integrity: sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==} + engines: {node: '>=18.0.0'} + + '@firebase/util@1.10.2': + resolution: {integrity: sha512-qnSHIoE9FK+HYnNhTI8q14evyqbc/vHRivfB4TgCIUOl4tosmKSQlp7ltymOlMP4xVIJTg5wrkfcZ60X4nUf7Q==} + engines: {node: '>=18.0.0'} + + '@google-cloud/firestore@7.11.0': + resolution: {integrity: sha512-88uZ+jLsp1aVMj7gh3EKYH1aulTAMFAp8sH/v5a9w8q8iqSG27RiWLoxSAFr/XocZ9hGiWH1kEnBw+zl3xAgNA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/paginator@5.0.2': + resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} + engines: {node: '>=14.0.0'} + + '@google-cloud/projectify@4.0.0': + resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/promisify@4.0.0': + resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} + engines: {node: '>=14'} + + '@google-cloud/storage@7.14.0': + resolution: {integrity: sha512-H41bPL2cMfSi4EEnFzKvg7XSb7T67ocSXrmF7MPjfgFB0L6CKGzfIYJheAZi1iqXjz6XaCT1OBf6HCG5vDBTOQ==} + engines: {node: '>=14'} + + '@grpc/grpc-js@1.12.4': + resolution: {integrity: sha512-NBhrxEWnFh0FxeA0d//YP95lRFsSx2TNLEUQg4/W+5f/BMxcCjgOOIT24iD+ZB/tZw057j44DaIxja7w4XMrhg==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.13': + resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==} + engines: {node: '>=6'} + hasBin: true + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -784,6 +851,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -922,6 +992,10 @@ packages: engines: {node: '>=8.0.0', npm: '>=5.0.0'} hasBin: true + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@pkgr/core@0.1.1': resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -940,6 +1014,36 @@ packages: '@pm2/pm2-version-check@1.0.4': resolution: {integrity: sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -952,6 +1056,10 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} @@ -985,6 +1093,9 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -1039,6 +1150,9 @@ packages: '@types/jsonwebtoken@9.0.5': resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + '@types/luxon@3.4.2': resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} @@ -1051,6 +1165,9 @@ packages: '@types/node@20.3.1': resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==} + '@types/node@22.10.2': + resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1060,6 +1177,9 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/request@2.48.12': + resolution: {integrity: sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==} + '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -1078,6 +1198,9 @@ packages: '@types/supertest@6.0.0': resolution: {integrity: sha512-j3/Z2avY+H3yn+xp/ef//QyqqE+dg3rWh14Ewi/QZs6uVK+oOs7lFRXtjp2YHAqHJZ4OFGNmCxZO5vd7AuG/Dg==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1258,6 +1381,10 @@ packages: abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1378,6 +1505,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -1385,6 +1516,9 @@ packages: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + async@2.6.4: resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} @@ -1437,6 +1571,9 @@ packages: resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} engines: {node: '>= 10.0.0'} + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1930,6 +2067,9 @@ packages: sqlite3: optional: true + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -2101,6 +2241,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter2@0.4.14: resolution: {integrity: sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==} @@ -2138,6 +2282,9 @@ packages: resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} engines: {node: '>= 0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -2145,6 +2292,10 @@ packages: extrareqp2@1.0.0: resolution: {integrity: sha512-Gum0g1QYb6wpPJCVypWP3bbIuaibcFiJcpuPM10YSXp/tzqi84x9PJageob+eN4xVRIOto4wjSGNLyMD54D2xA==} + farmhash-modern@1.1.0: + resolution: {integrity: sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==} + engines: {node: '>=18.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2167,9 +2318,17 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-xml-parser@4.5.1: + resolution: {integrity: sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w==} + hasBin: true + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -2200,6 +2359,10 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + firebase-admin@13.0.1: + resolution: {integrity: sha512-sKQ/Yw8o/WdC9qTKvuLMBjTbdcBISIXW4+M9PXk0bNjxEbZf1Er7EVq47eRb5+bnKof10xlns6zAIbj4tmSexg==} + engines: {node: '>=18'} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2223,6 +2386,10 @@ packages: typescript: '>3.6.0' webpack: ^5.11.0 + form-data@2.5.2: + resolution: {integrity: sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==} + engines: {node: '>= 0.12'} + form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -2264,11 +2431,22 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} deprecated: This package is no longer supported. + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.0: + resolution: {integrity: sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==} + engines: {node: '>=14'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2342,6 +2520,14 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + google-auth-library@9.15.0: + resolution: {integrity: sha512-7ccSEJFDFO7exFbO6NRyC+xH8/mZ1GZGG2xxx9iHxZWcjUjJpjWxIMw3cofAKcueZ6DATiukmmprD7yavQHOyQ==} + engines: {node: '>=14'} + + google-gax@4.4.1: + resolution: {integrity: sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==} + engines: {node: '>=14'} + gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} @@ -2354,6 +2540,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -2388,6 +2578,9 @@ packages: resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} engines: {node: '>=8'} + html-entities@2.5.2: + resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2395,6 +2588,13 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-parser-js@0.5.8: + resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -2682,6 +2882,9 @@ packages: node-notifier: optional: true + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + js-git@0.7.8: resolution: {integrity: sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA==} @@ -2704,6 +2907,9 @@ packages: engines: {node: '>=4'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -2740,9 +2946,19 @@ packages: jwa@1.4.1: resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + jwa@2.0.0: + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + + jwks-rsa@3.1.0: + resolution: {integrity: sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==} + engines: {node: '>=14'} + jws@3.2.2: resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2762,6 +2978,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -2777,6 +2996,12 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -2811,6 +3036,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + lru-cache@10.2.2: resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} @@ -2826,6 +3054,9 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + lru-memoizer@2.3.0: + resolution: {integrity: sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==} + luxon@3.4.4: resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} engines: {node: '>=12'} @@ -2896,6 +3127,11 @@ packages: engines: {node: '>=4.0.0'} hasBin: true + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -3002,6 +3238,10 @@ packages: encoding: optional: true + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -3273,6 +3513,14 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proto3-json-serializer@2.0.2: + resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} + engines: {node: '>=14.0.0'} + + protobufjs@7.4.0: + resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3389,6 +3637,14 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + retry-request@7.0.2: + resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} + engines: {node: '>=14'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3559,6 +3815,12 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -3597,6 +3859,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + superagent@8.1.2: resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} engines: {node: '>=6.4.0 <13 || >=14'} @@ -3644,6 +3912,10 @@ packages: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + teeny-request@9.0.0: + resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} + engines: {node: '>=14'} + terser-webpack-plugin@5.3.10: resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} engines: {node: '>= 10.13.0'} @@ -3816,6 +4088,9 @@ packages: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -3840,6 +4115,14 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@11.0.3: + resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true @@ -3890,6 +4173,14 @@ packages: webpack-cli: optional: true + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4440,6 +4731,111 @@ snapshots: dependencies: levn: 0.4.1 + '@fastify/busboy@3.1.0': {} + + '@firebase/app-check-interop-types@0.3.3': {} + + '@firebase/app-types@0.9.3': {} + + '@firebase/auth-interop-types@0.2.4': {} + + '@firebase/component@0.6.11': + dependencies: + '@firebase/util': 1.10.2 + tslib: 2.6.3 + + '@firebase/database-compat@2.0.1': + dependencies: + '@firebase/component': 0.6.11 + '@firebase/database': 1.0.10 + '@firebase/database-types': 1.0.7 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + tslib: 2.6.3 + + '@firebase/database-types@1.0.7': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.10.2 + + '@firebase/database@1.0.10': + dependencies: + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.11 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + faye-websocket: 0.11.4 + tslib: 2.6.3 + + '@firebase/logger@0.4.4': + dependencies: + tslib: 2.6.3 + + '@firebase/util@1.10.2': + dependencies: + tslib: 2.6.3 + + '@google-cloud/firestore@7.11.0': + dependencies: + '@opentelemetry/api': 1.9.0 + fast-deep-equal: 3.1.3 + functional-red-black-tree: 1.0.1 + google-gax: 4.4.1 + protobufjs: 7.4.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@google-cloud/paginator@5.0.2': + dependencies: + arrify: 2.0.1 + extend: 3.0.2 + optional: true + + '@google-cloud/projectify@4.0.0': + optional: true + + '@google-cloud/promisify@4.0.0': + optional: true + + '@google-cloud/storage@7.14.0': + dependencies: + '@google-cloud/paginator': 5.0.2 + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.0.0 + abort-controller: 3.0.0 + async-retry: 1.3.3 + duplexify: 4.1.3 + fast-xml-parser: 4.5.1 + gaxios: 6.7.1 + google-auth-library: 9.15.0 + html-entities: 2.5.2 + mime: 3.0.0 + p-limit: 3.1.0 + retry-request: 7.0.2 + teeny-request: 9.0.0 + uuid: 8.3.2 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@grpc/grpc-js@1.12.4': + dependencies: + '@grpc/proto-loader': 0.7.13 + '@js-sdsl/ordered-map': 4.4.2 + optional: true + + '@grpc/proto-loader@0.7.13': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.2.3 + protobufjs: 7.4.0 + yargs: 17.7.2 + optional: true + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -4652,6 +5048,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + '@js-sdsl/ordered-map@4.4.2': + optional: true + '@lukeed/csprng@1.1.0': {} '@mapbox/node-pre-gyp@1.0.11': @@ -4819,6 +5218,9 @@ snapshots: transitivePeerDependencies: - encoding + '@opentelemetry/api@1.9.0': + optional: true + '@pkgr/core@0.1.1': {} '@pm2/agent@2.0.3': @@ -4872,6 +5274,39 @@ snapshots: transitivePeerDependencies: - supports-color + '@protobufjs/aspromise@1.1.2': + optional: true + + '@protobufjs/base64@1.1.2': + optional: true + + '@protobufjs/codegen@2.0.4': + optional: true + + '@protobufjs/eventemitter@1.1.0': + optional: true + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + optional: true + + '@protobufjs/float@1.0.2': + optional: true + + '@protobufjs/inquire@1.1.0': + optional: true + + '@protobufjs/path@1.1.2': + optional: true + + '@protobufjs/pool@1.1.0': + optional: true + + '@protobufjs/utf8@1.1.0': + optional: true + '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -4884,6 +5319,9 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@tootallnate/once@2.0.0': + optional: true + '@tootallnate/quickjs-emscripten@0.23.0': {} '@tsconfig/node10@1.0.11': {} @@ -4924,6 +5362,9 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 20.3.1 + '@types/caseless@0.12.5': + optional: true + '@types/connect@3.4.38': dependencies: '@types/node': 20.3.1 @@ -4991,6 +5432,9 @@ snapshots: dependencies: '@types/node': 20.3.1 + '@types/long@4.0.2': + optional: true + '@types/luxon@3.4.2': {} '@types/methods@1.1.4': {} @@ -4999,12 +5443,24 @@ snapshots: '@types/node@20.3.1': {} + '@types/node@22.10.2': + dependencies: + undici-types: 6.20.0 + '@types/parse-json@4.0.2': {} '@types/qs@6.9.15': {} '@types/range-parser@1.2.7': {} + '@types/request@2.48.12': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 20.3.1 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.2 + optional: true + '@types/semver@7.5.8': {} '@types/send@0.17.4': @@ -5032,6 +5488,9 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.7 + '@types/tough-cookie@4.0.5': + optional: true + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.32': @@ -5290,6 +5749,11 @@ snapshots: abbrev@1.1.1: {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + optional: true + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -5319,7 +5783,7 @@ snapshots: agent-base@7.1.1: dependencies: - debug: 4.3.5 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -5397,12 +5861,20 @@ snapshots: array-union@2.1.0: {} + arrify@2.0.1: + optional: true + asap@2.0.6: {} ast-types@0.13.4: dependencies: tslib: 2.6.3 + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + optional: true + async@2.6.4: dependencies: lodash: 4.17.21 @@ -5479,6 +5951,8 @@ snapshots: - encoding - supports-color + bignumber.js@9.1.2: {} + binary-extensions@2.3.0: {} bl@4.1.0: @@ -5876,11 +6350,20 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.37.0(pg@8.11.5)(postgres@3.4.4): + drizzle-orm@0.37.0(@opentelemetry/api@1.9.0)(pg@8.11.5)(postgres@3.4.4): optionalDependencies: + '@opentelemetry/api': 1.9.0 pg: 8.11.5 postgres: 3.4.4 + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + optional: true + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -6106,6 +6589,9 @@ snapshots: etag@1.8.1: {} + event-target-shim@5.0.1: + optional: true + eventemitter2@0.4.14: {} eventemitter2@5.0.1: {} @@ -6221,6 +6707,8 @@ snapshots: - supports-color optional: true + extend@3.0.2: {} + external-editor@3.1.0: dependencies: chardet: 0.7.0 @@ -6233,6 +6721,8 @@ snapshots: transitivePeerDependencies: - debug + farmhash-modern@1.1.0: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -6253,10 +6743,19 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-xml-parser@4.5.1: + dependencies: + strnum: 1.0.5 + optional: true + fastq@1.17.1: dependencies: reusify: 1.0.4 + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + fb-watchman@2.0.2: dependencies: bser: 2.1.1 @@ -6297,6 +6796,25 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + firebase-admin@13.0.1: + dependencies: + '@fastify/busboy': 3.1.0 + '@firebase/database-compat': 2.0.1 + '@firebase/database-types': 1.0.7 + '@types/node': 22.10.2 + farmhash-modern: 1.1.0 + google-auth-library: 9.15.0 + jsonwebtoken: 9.0.2 + jwks-rsa: 3.1.0 + node-forge: 1.3.1 + uuid: 11.0.3 + optionalDependencies: + '@google-cloud/firestore': 7.11.0 + '@google-cloud/storage': 7.14.0 + transitivePeerDependencies: + - encoding + - supports-color + flat-cache@4.0.1: dependencies: flatted: 3.3.2 @@ -6325,6 +6843,14 @@ snapshots: typescript: 5.1.3 webpack: 5.87.0(esbuild@0.19.12) + form-data@2.5.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + safe-buffer: 5.2.1 + optional: true + form-data@4.0.0: dependencies: asynckit: 0.4.0 @@ -6367,6 +6893,9 @@ snapshots: function-bind@1.1.2: {} + functional-red-black-tree@1.0.1: + optional: true + gauge@3.0.2: dependencies: aproba: 2.0.0 @@ -6379,6 +6908,25 @@ snapshots: strip-ansi: 6.0.1 wide-align: 1.1.5 + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.4 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.0: + dependencies: + gaxios: 6.7.1 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -6457,6 +7005,37 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + google-auth-library@9.15.0: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.0 + gtoken: 7.1.0 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + google-gax@4.4.1: + dependencies: + '@grpc/grpc-js': 1.12.4 + '@grpc/proto-loader': 0.7.13 + '@types/long': 4.0.2 + abort-controller: 3.0.0 + duplexify: 4.1.3 + google-auth-library: 9.15.0 + node-fetch: 2.7.0 + object-hash: 3.0.0 + proto3-json-serializer: 2.0.2 + protobufjs: 7.4.0 + retry-request: 7.0.2 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + gopd@1.0.1: dependencies: get-intrinsic: 1.2.4 @@ -6467,6 +7046,14 @@ snapshots: graphemer@1.4.0: {} + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + has-flag@3.0.0: {} has-flag@4.0.0: {} @@ -6489,6 +7076,9 @@ snapshots: hexoid@1.0.0: {} + html-entities@2.5.2: + optional: true + html-escaper@2.0.2: {} http-errors@2.0.0: @@ -6499,10 +7089,21 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-parser-js@0.5.8: {} + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + optional: true + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 - debug: 4.3.5 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -6516,7 +7117,7 @@ snapshots: https-proxy-agent@7.0.4: dependencies: agent-base: 7.1.1 - debug: 4.3.5 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -6990,6 +7591,8 @@ snapshots: - supports-color - ts-node + jose@4.15.9: {} + js-git@0.7.8: dependencies: bodec: 0.1.0 @@ -7012,6 +7615,10 @@ snapshots: jsesc@2.5.2: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.1.2 + json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -7054,11 +7661,33 @@ snapshots: ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 + jwa@2.0.0: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jwks-rsa@3.1.0: + dependencies: + '@types/express': 4.17.17 + '@types/jsonwebtoken': 9.0.5 + debug: 4.4.0 + jose: 4.15.9 + limiter: 1.1.5 + lru-memoizer: 2.3.0 + transitivePeerDependencies: + - supports-color + jws@3.2.2: dependencies: jwa: 1.4.1 safe-buffer: 5.2.1 + jws@4.0.0: + dependencies: + jwa: 2.0.0 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -7074,6 +7703,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + limiter@1.1.5: {} + lines-and-columns@1.2.4: {} loader-runner@4.3.0: {} @@ -7086,6 +7717,11 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: + optional: true + + lodash.clonedeep@4.5.0: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} @@ -7111,6 +7747,9 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + long@5.2.3: + optional: true + lru-cache@10.2.2: {} lru-cache@5.1.1: @@ -7123,6 +7762,11 @@ snapshots: lru-cache@7.18.3: {} + lru-memoizer@2.3.0: + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 6.0.0 + luxon@3.4.4: {} macos-release@2.5.1: {} @@ -7174,6 +7818,9 @@ snapshots: mime@2.6.0: {} + mime@3.0.0: + optional: true + mimic-fn@2.1.0: {} minimatch@3.1.2: @@ -7261,6 +7908,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-forge@1.3.1: {} + node-int64@0.4.0: {} node-releases@2.0.14: {} @@ -7355,7 +8004,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.1 - debug: 4.3.5 + debug: 4.4.0 get-uri: 6.0.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.4 @@ -7571,6 +8220,27 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + proto3-json-serializer@2.0.2: + dependencies: + protobufjs: 7.4.0 + optional: true + + protobufjs@7.4.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.3.1 + long: 5.2.3 + optional: true + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -7699,6 +8369,19 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + retry-request@7.0.2: + dependencies: + '@types/request': 2.48.12 + extend: 3.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + retry@0.13.1: + optional: true + reusify@1.0.4: {} rimraf@3.0.2: @@ -7851,7 +8534,7 @@ snapshots: socks-proxy-agent@8.0.3: dependencies: agent-base: 7.1.1 - debug: 4.3.5 + debug: 4.4.0 socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -7889,6 +8572,14 @@ snapshots: statuses@2.0.1: {} + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + optional: true + + stream-shift@1.0.3: + optional: true + streamsearch@1.1.0: {} string-length@4.0.2: @@ -7922,6 +8613,12 @@ snapshots: strip-json-comments@3.1.1: {} + strnum@1.0.5: + optional: true + + stubs@3.0.0: + optional: true + superagent@8.1.2: dependencies: component-emitter: 1.3.1 @@ -7979,6 +8676,18 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 + teeny-request@9.0.0: + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + stream-events: 1.0.5 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + terser-webpack-plugin@5.3.10(esbuild@0.19.12)(webpack@5.87.0(esbuild@0.19.12)): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -8138,6 +8847,8 @@ snapshots: dependencies: '@lukeed/csprng': 1.1.0 + undici-types@6.20.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -8156,6 +8867,11 @@ snapshots: utils-merge@1.0.1: {} + uuid@11.0.3: {} + + uuid@8.3.2: + optional: true + uuid@9.0.1: {} v8-compile-cache-lib@3.0.1: {} @@ -8225,6 +8941,14 @@ snapshots: - esbuild - uglify-js + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.8 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index b3aa016..9ea979a 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -11,14 +11,18 @@ import { AssignmentController } from './assignment/assignment.controller'; import { TaskGroupController } from './tasks/task-group.controller'; import { EventsGateway } from './shopping-list/events.gateway'; import { ShoppingListController } from './shopping-list/shopping-list.controller'; +import { NotificationController } from './notifications/notification.controller'; +import { NotificationModule } from './notifications/notification.module'; const rootPathStatic = join(__dirname, '../../src/client/public/'); @Module({ imports: [ AuthModule, + // needed to active job scheduling from @nestjs/schedule ScheduleModule.forRoot(), AssignmentsModule, + NotificationModule, ServeStaticModule.forRoot({ rootPath: rootPathStatic, exclude: ['/api/(.*)'], @@ -31,6 +35,7 @@ const rootPathStatic = join(__dirname, '../../src/client/public/'); TaskGroupController, UserGroupController, ShoppingListController, + NotificationController, ], providers: [EventsGateway], }) diff --git a/backend/src/assignment/assignment-scheduler.service.ts b/backend/src/assignment/assignment-scheduler.service.ts index 65e1990..4d1adc9 100644 --- a/backend/src/assignment/assignment-scheduler.service.ts +++ b/backend/src/assignment/assignment-scheduler.service.ts @@ -3,10 +3,14 @@ import { Cron, CronExpression } from '@nestjs/schedule'; import { dbInsertAssignments } from 'src/db/functions/assignment'; import { hydrateTaskGroupsToAssignToAssignments } from './util'; import { dbGetTaskGroupsToAssignForCurrentInterval } from 'src/db/functions/task-group'; +import { dbGetFCMRegistrationTokensByUserIds } from 'src/db/functions/notification'; +import { sendFirebaseMessages } from 'src/notifications/notification'; +import { Message } from 'firebase-admin/messaging'; @Injectable() export class AssignmentSchedulerService { - @Cron(CronExpression.EVERY_10_SECONDS) + // TODO: optimally, this is not run this often, but just once in a day, and on task creation we immediately create assignments as needed. + @Cron(CronExpression.EVERY_30_SECONDS) async handleCreateAssignmentsCron() { const taskGroupsToAssign = await dbGetTaskGroupsToAssignForCurrentInterval({ overrideNow: undefined, @@ -29,6 +33,25 @@ export class AssignmentSchedulerService { 2, )}`, ); + + if (process.env.CI === 'true') { + return; + } + + const userIds = assignmentsToCreate.map( + (assignment) => assignment.userId, + ); + const tokens = await dbGetFCMRegistrationTokensByUserIds(userIds); + const messages = tokens.map((token) => { + return { + token, + notification: { + title: '🚀 New Assignments Waiting!', + body: "You have new assignments waiting for you! Click to learn more. Let's get them done!", + }, + } satisfies Message; + }); + await sendFirebaseMessages({ messages }); } } } diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index b127cd7..d6ac4fe 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -15,7 +15,6 @@ import { userTable } from 'src/db/schema'; import { AuthService } from './auth.service'; import { dbAddUserToUserGroup, - dbGetUserGroupOfUser, dbGetUserGroupByInviteCode, } from 'src/db/functions/user-group'; import { eq } from 'drizzle-orm'; @@ -115,13 +114,8 @@ export class AuthController { 'The access token seemed valid, but the user id included in the jwt token could not be found in the database.', ); } - const userGroup = await dbGetUserGroupOfUser(req.user.sub); return { userId: req.user.sub, - userGroup: { - id: userGroup?.userGroup.name, - name: userGroup?.userGroup.name, - }, email: user.email, username: user.username, }; diff --git a/backend/src/db/functions/notification.ts b/backend/src/db/functions/notification.ts new file mode 100644 index 0000000..9a6eb02 --- /dev/null +++ b/backend/src/db/functions/notification.ts @@ -0,0 +1,13 @@ +import { inArray } from 'drizzle-orm'; +import { db } from '..'; +import { userFcmRegistrationTokenMappingTable } from '../schema'; + +export async function dbGetFCMRegistrationTokensByUserIds(userIds: number[]) { + const result = await db + .select({ + token: userFcmRegistrationTokenMappingTable.fcmRegistrationToken, + }) + .from(userFcmRegistrationTokenMappingTable) + .where(inArray(userFcmRegistrationTokenMappingTable.userId, userIds)); + return result.map((row) => row.token); +} diff --git a/backend/src/db/migrations/0013_loving_satana.sql b/backend/src/db/migrations/0013_loving_satana.sql new file mode 100644 index 0000000..e660d3c --- /dev/null +++ b/backend/src/db/migrations/0013_loving_satana.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS "user_fcm_registration_token_mapping" ( + "user_id" integer NOT NULL, + "fcm_registration_token" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "user_fcm_registration_token_mapping_user_id_fcm_registration_token_pk" PRIMARY KEY("user_id","fcm_registration_token") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_fcm_registration_token_mapping" ADD CONSTRAINT "user_fcm_registration_token_mapping_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/backend/src/db/migrations/meta/0013_snapshot.json b/backend/src/db/migrations/meta/0013_snapshot.json new file mode 100644 index 0000000..9288aa2 --- /dev/null +++ b/backend/src/db/migrations/meta/0013_snapshot.json @@ -0,0 +1,680 @@ +{ + "id": "e8c76911-4055-4a4e-8164-0163712a823b", + "prevId": "d69d0003-16de-4727-aa9b-5be75fae46be", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.assignment": { + "name": "assignment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "assignment_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "assignment_task_id_task_id_fk": { + "name": "assignment_task_id_task_id_fk", + "tableFrom": "assignment", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assignment_user_id_user_id_fk": { + "name": "assignment_user_id_user_id_fk", + "tableFrom": "assignment", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shopping_list_item": { + "name": "shopping_list_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_group_id": { + "name": "user_group_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "shopping_list_item_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shopping_list_item_user_group_id_user_group_id_fk": { + "name": "shopping_list_item_user_group_id_user_group_id_fk", + "tableFrom": "shopping_list_item", + "tableTo": "user_group", + "columnsFrom": [ + "user_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_group": { + "name": "task_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interval": { + "name": "interval", + "type": "interval", + "primaryKey": false, + "notNull": true + }, + "initial_start_date": { + "name": "initial_start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "user_group_id": { + "name": "user_group_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_group_user_mapping": { + "name": "task_group_user_mapping", + "schema": "", + "columns": { + "task_group_id": { + "name": "task_group_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assignment_ordinal": { + "name": "assignment_ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "task_group_user_mapping_task_group_id_task_group_id_fk": { + "name": "task_group_user_mapping_task_group_id_task_group_id_fk", + "tableFrom": "task_group_user_mapping", + "tableTo": "task_group", + "columnsFrom": [ + "task_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "task_group_user_mapping_user_id_user_id_fk": { + "name": "task_group_user_mapping_user_id_user_id_fk", + "tableFrom": "task_group_user_mapping", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_group_user_mapping_task_group_id_user_id_assignment_ordinal_pk": { + "name": "task_group_user_mapping_task_group_id_user_id_assignment_ordinal_pk", + "columns": [ + "task_group_id", + "user_id", + "assignment_ordinal" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_group_id": { + "name": "task_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "task_task_group_id_task_group_id_fk": { + "name": "task_task_group_id_task_group_id_fk", + "tableFrom": "task", + "tableTo": "task_group", + "columnsFrom": [ + "task_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_user_group_mapping": { + "name": "task_user_group_mapping", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_group_id": { + "name": "user_group_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "task_user_group_mapping_task_id_task_id_fk": { + "name": "task_user_group_mapping_task_id_task_id_fk", + "tableFrom": "task_user_group_mapping", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "task_user_group_mapping_user_group_id_user_group_id_fk": { + "name": "task_user_group_mapping_user_group_id_user_group_id_fk", + "tableFrom": "task_user_group_mapping", + "tableTo": "user_group", + "columnsFrom": [ + "user_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_fcm_registration_token_mapping": { + "name": "user_fcm_registration_token_mapping", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fcm_registration_token": { + "name": "fcm_registration_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_fcm_registration_token_mapping_user_id_user_id_fk": { + "name": "user_fcm_registration_token_mapping_user_id_user_id_fk", + "tableFrom": "user_fcm_registration_token_mapping", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_fcm_registration_token_mapping_user_id_fcm_registration_token_pk": { + "name": "user_fcm_registration_token_mapping_user_id_fcm_registration_token_pk", + "columns": [ + "user_id", + "fcm_registration_token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_group_invite": { + "name": "user_group_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_group_id": { + "name": "user_group_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_group_invite_user_group_id_user_group_id_fk": { + "name": "user_group_invite_user_group_id_user_group_id_fk", + "tableFrom": "user_group_invite", + "tableTo": "user_group", + "columnsFrom": [ + "user_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_group": { + "name": "user_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_user_group_mapping": { + "name": "user_user_group_mapping", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_group_id": { + "name": "user_group_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_user_group_mapping_user_id_user_id_fk": { + "name": "user_user_group_mapping_user_id_user_id_fk", + "tableFrom": "user_user_group_mapping", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_user_group_mapping_user_group_id_user_group_id_fk": { + "name": "user_user_group_mapping_user_group_id_user_group_id_fk", + "tableFrom": "user_user_group_mapping", + "tableTo": "user_group", + "columnsFrom": [ + "user_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_user_group_mapping_user_id_user_group_id_pk": { + "name": "user_user_group_mapping_user_id_user_group_id_pk", + "columns": [ + "user_id", + "user_group_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.assignment_state": { + "name": "assignment_state", + "schema": "public", + "values": [ + "pending", + "completed" + ] + }, + "public.shopping_list_item_state": { + "name": "shopping_list_item_state", + "schema": "public", + "values": [ + "pending", + "purchased", + "deleted" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/backend/src/db/migrations/meta/_journal.json b/backend/src/db/migrations/meta/_journal.json index 45b6823..2eb6114 100644 --- a/backend/src/db/migrations/meta/_journal.json +++ b/backend/src/db/migrations/meta/_journal.json @@ -92,6 +92,13 @@ "when": 1733517749342, "tag": "0012_shallow_preak", "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1734793036836, + "tag": "0013_loving_satana", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 12a8d98..e527932 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -199,3 +199,27 @@ export const shoppingListItemTable = pgTable('shopping_list_item', { state: shoppingListItemStateEnum('state').notNull().default('pending'), createdAt: timestamp('created_at').notNull().defaultNow(), }); + +/** + * This table stores FCM (Firebase Cloud Messaging) registration tokens for users. + * Note that we allow multiple fcm registration tokens to exist per user, + * as they may be logged in on multiple devices, and we still would want to + */ +export const userFcmRegistrationTokenMappingTable = pgTable( + 'user_fcm_registration_token_mapping', + { + userId: integer('user_id') + .references(() => userTable.id) + .notNull(), + fcmRegistrationToken: text('fcm_registration_token').notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => { + return { + pk: primaryKey({ + columns: [table.userId, table.fcmRegistrationToken], + }), + }; + }, +); diff --git a/backend/src/main.ts b/backend/src/main.ts index 39f21a6..02a25a4 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,5 +1,6 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import firebaseAdmin from 'firebase-admin'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -7,6 +8,23 @@ async function bootstrap() { app.enableCors(); await app.listen(3000, '0.0.0.0'); + if (firebaseAdmin.apps.length === 0) { + const firebaseServiceAccountJsonContent = + process.env.FIREBASE_SERVICE_ACCOUNT_JSON_CONTENT; + + if (firebaseServiceAccountJsonContent === undefined) { + throw new Error( + 'FIREBASE_SERVICE_ACCOUNT_JSON_CONTENT environment variable must be set.', + ); + } + + firebaseAdmin.initializeApp({ + credential: firebaseAdmin.credential.cert( + JSON.parse(firebaseServiceAccountJsonContent), + ), + }); + } + const appUrl = await app.getUrl(); console.log(`Flatshare Backend is running on: ${appUrl}`); } diff --git a/backend/src/notifications/notification.controller.ts b/backend/src/notifications/notification.controller.ts new file mode 100644 index 0000000..c8fa36d --- /dev/null +++ b/backend/src/notifications/notification.controller.ts @@ -0,0 +1,44 @@ +import { + Body, + Controller, + HttpException, + HttpStatus, + Post, +} from '@nestjs/common'; +import { sql } from 'drizzle-orm'; +import { db } from 'src/db'; +import { userFcmRegistrationTokenMappingTable } from 'src/db/schema'; + +@Controller('notifications') +export class NotificationController { + @Post('registration-token') + async postRegistrationToken( + @Body() + { + userId, + registrationToken, + }: { + userId: number; + registrationToken: string; + }, + ) { + try { + await db + .insert(userFcmRegistrationTokenMappingTable) + .values({ userId, fcmRegistrationToken: registrationToken }) + .onConflictDoUpdate({ + target: [ + userFcmRegistrationTokenMappingTable.userId, + userFcmRegistrationTokenMappingTable.fcmRegistrationToken, + ], + set: { updatedAt: sql`now()` }, + }); + } catch (error) { + console.error({ error }); + throw new HttpException( + `Failed to post registration token: ${error}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/backend/src/notifications/notification.module.ts b/backend/src/notifications/notification.module.ts new file mode 100644 index 0000000..6487b4a --- /dev/null +++ b/backend/src/notifications/notification.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { NotificationSchedulerService } from './notification.service'; + +@Module({ + providers: [NotificationSchedulerService], +}) +export class NotificationModule {} diff --git a/backend/src/notifications/notification.service.ts b/backend/src/notifications/notification.service.ts new file mode 100644 index 0000000..b5ef78e --- /dev/null +++ b/backend/src/notifications/notification.service.ts @@ -0,0 +1,24 @@ +// here we should notify users that still havent completed assignments and only 1/2/3 days are left or something like that +export class NotificationSchedulerService { + // @Cron('0 8 * * 1') + // async handleSendNotificationStartOfWeek() {} + // + // @Cron(CronExpression.EVERY_30_SECONDS) + // async handleDebugNotifications() { + // console.log('EVERY 30 seconds'); + // + // const messaging = getMessaging(); + // + // const tokens = await dbGetFCMRegistrationTokens(); + // for (const token of tokens) { + // // messaging.send({ + // // notification: { + // // title: 'Reminder - Assignment', + // // body: "Don't forget to vacuum!", + // // }, + // // token: token.fcmRegistrationToken, + // // }); + // console.log({ token }); + // } + // } +} diff --git a/backend/src/notifications/notification.ts b/backend/src/notifications/notification.ts new file mode 100644 index 0000000..637865d --- /dev/null +++ b/backend/src/notifications/notification.ts @@ -0,0 +1,16 @@ +import { Message, getMessaging } from 'firebase-admin/messaging'; + +export async function sendFirebaseMessages({ + messages, +}: { + messages: Message[]; +}) { + if (messages.length === 0) { + return; + } + + const messaging = getMessaging(); + + // TODO: all tokens that error because they are no longer registered should be removed from our table. + await messaging.sendEach(messages); +} diff --git a/flake.lock b/flake.lock index 8545d80..07a117f 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1733588544, - "narHash": "sha256-FCE/q0JA3VYtrtKy+fFxwxu1EB6HektN8LLewsDm5yg=", + "lastModified": 1734797770, + "narHash": "sha256-a5imzZoh5h3Dhnum9+zSQb2QUWdSPO1+qwxBNuO6YPg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b5879c104e9e424af2c6153870c84ef22a9804e2", + "rev": "48b8a5d743cb569ddd71b2ad31eef12b7139d0d0", "type": "github" }, "original": { @@ -65,12 +65,29 @@ "type": "github" } }, + "nixpkgs-pnpm": { + "locked": { + "lastModified": 1734795326, + "narHash": "sha256-hY5UNECd6xXimwZoamTyP0hb1Jq21tc2xk2I8k7z9U4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3f5c1fc2affdb6c70b44ca624b4710843b7e3059", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3f5c1fc2affdb6c70b44ca624b4710843b7e3059", + "type": "github" + } + }, "root": { "inputs": { "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "nixpkgs-gradle": "nixpkgs-gradle", - "nixpkgs-jdk": "nixpkgs-jdk" + "nixpkgs-jdk": "nixpkgs-jdk", + "nixpkgs-pnpm": "nixpkgs-pnpm" } }, "systems": { diff --git a/flake.nix b/flake.nix index e8423db..5c09f4b 100644 --- a/flake.nix +++ b/flake.nix @@ -5,6 +5,9 @@ flake-utils.url = "github:numtide/flake-utils"; nixpkgs.url = "github:NixOS/nixpkgs"; + # pnpm 9.15.1 + nixpkgs-pnpm.url = "github:NixOS/nixpkgs/3f5c1fc2affdb6c70b44ca624b4710843b7e3059"; + # gradle v7.6.3 nixpkgs-gradle.url = "github:NixOS/nixpkgs/68bb040a9617ec704cb453cc921f7516d5b36cae"; # jdk19 @@ -14,6 +17,7 @@ outputs = { self, nixpkgs, + nixpkgs-pnpm, nixpkgs-gradle, nixpkgs-jdk, flake-utils, @@ -29,6 +33,7 @@ }; pkgs-gradle = import nixpkgs-gradle {inherit system;}; pkgs-jdk = import nixpkgs-jdk {inherit system;}; + pkgs-pnpm = import nixpkgs-pnpm {inherit system;}; androidEnv = pkgs.androidenv.override {licenseAccepted = true;}; @@ -61,6 +66,7 @@ pcre2 pkg-config firebase-tools + pkgs-pnpm.pnpm ]; CMAKE_PREFIX_PATH = "${pkgs.lib.makeLibraryPath [libsecret.dev gtk3.dev]}"; diff --git a/frontend/android/app/build.gradle b/frontend/android/app/build.gradle index 47841c0..c7fd24d 100644 --- a/frontend/android/app/build.gradle +++ b/frontend/android/app/build.gradle @@ -1,5 +1,8 @@ plugins { id "com.android.application" + // START: FlutterFire Configuration + id 'com.google.gms.google-services' + // END: FlutterFire Configuration id "kotlin-android" // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" diff --git a/frontend/android/app/google-services.json b/frontend/android/app/google-services.json new file mode 100644 index 0000000..d98c62a --- /dev/null +++ b/frontend/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "66431485841", + "project_id": "flatshare-223fd", + "storage_bucket": "flatshare-223fd.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:66431485841:android:f9a7a5dd12273c2f0fbc30", + "android_client_info": { + "package_name": "com.invertedecho.flatshare" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyBak-z8ENI9BDldZAwlcvNTSMpn5AGJuL0" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/frontend/android/settings.gradle b/frontend/android/settings.gradle index 536165d..7fb86d7 100644 --- a/frontend/android/settings.gradle +++ b/frontend/android/settings.gradle @@ -19,6 +19,9 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.0" apply false + // START: FlutterFire Configuration + id "com.google.gms.google-services" version "4.3.15" apply false + // END: FlutterFire Configuration id "org.jetbrains.kotlin.android" version "1.7.10" apply false } diff --git a/frontend/firebase.json b/frontend/firebase.json new file mode 100644 index 0000000..88a988f --- /dev/null +++ b/frontend/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"flatshare-223fd","appId":"1:66431485841:android:f9a7a5dd12273c2f0fbc30","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"flatshare-223fd","configurations":{"android":"1:66431485841:android:f9a7a5dd12273c2f0fbc30","ios":"1:66431485841:ios:c9ffa9634ee6fb680fbc30"}}}}}} diff --git a/frontend/lib/authenticated_navigation.dart b/frontend/lib/authenticated_navigation.dart index 5ef56d2..b5303a3 100644 --- a/frontend/lib/authenticated_navigation.dart +++ b/frontend/lib/authenticated_navigation.dart @@ -155,7 +155,7 @@ class _AuthenticatedNavigationState extends State { ), Text("Invite new user to your group") ], - )) + )), ]; }) ], diff --git a/frontend/lib/fetch/auth.dart b/frontend/lib/fetch/auth.dart index 12ad266..e38cc4f 100644 --- a/frontend/lib/fetch/auth.dart +++ b/frontend/lib/fetch/auth.dart @@ -1,9 +1,12 @@ import 'dart:convert'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flatshare/fetch/notification.dart'; import 'package:flatshare/fetch/user_group.dart'; import 'package:flatshare/main.dart'; import 'package:flatshare/models/user.dart'; import 'package:flatshare/models/user_group.dart'; +import 'package:flatshare/notifications/util.dart'; import 'package:flatshare/utils/env.dart'; import 'package:http/http.dart' as http; @@ -55,20 +58,33 @@ Future register( } } -Future getProfile() async { +Future fetchProfile() async { var apiBaseUrl = getApiBaseUrl(); var profileRes = await authenticatedClient.get(Uri.parse('$apiBaseUrl/auth/profile')); + if (profileRes.statusCode == 401) { + return null; + } return User.fromJson(jsonDecode(profileRes.body)); } -Future<(User?, UserGroup?)> getUserInfo() async { +Future<(User?, UserGroup?)> fetchProfileAndUserGroup() async { try { - User userProfile = await getProfile(); - UserGroup? userGroup = - await fetchUserGroupForUser(userId: userProfile.userId); - return (userProfile, userGroup); + User? user = await fetchProfile(); + if (user == null) { + return (null, null); + } + UserGroup? userGroup = await fetchUserGroupForUser(userId: user.userId); + + if (getIsSupportedPlatformFirebase()) { + String? registrationToken = await FirebaseMessaging.instance.getToken(); + if (registrationToken != null) { + await sendFCMToken(user.userId, registrationToken); + } + } + return (user, userGroup); } catch (err) { + print(err); return (null, null); } } diff --git a/frontend/lib/fetch/notification.dart b/frontend/lib/fetch/notification.dart new file mode 100644 index 0000000..63a4748 --- /dev/null +++ b/frontend/lib/fetch/notification.dart @@ -0,0 +1,15 @@ +import 'dart:convert'; + +import 'package:flatshare/main.dart'; +import 'package:flatshare/utils/env.dart'; + +sendFCMToken(int userId, String registrationToken) async { + var apiBaseUrl = getApiBaseUrl(); + final response = await authenticatedClient.post( + Uri.parse('$apiBaseUrl/notifications/registration-token'), + body: jsonEncode( + {'userId': userId, 'registrationToken': registrationToken})); + if (response.statusCode != 201) { + throw Exception("Failed to post firebase registration token to backend"); + } +} diff --git a/frontend/lib/fetch/task.dart b/frontend/lib/fetch/task.dart index c6394e6..637cd7e 100644 --- a/frontend/lib/fetch/task.dart +++ b/frontend/lib/fetch/task.dart @@ -2,38 +2,10 @@ import 'dart:convert'; import 'package:flatshare/main.dart'; import 'package:flatshare/models/task.dart'; -import 'package:flatshare/models/task_group.dart'; +import 'package:flatshare/models/task_with_maybe_task_group.dart'; import 'package:flatshare/utils/env.dart'; import 'package:flatshare/widgets/tasks/create_task.dart'; -// TODO: https://github.com/invertedEcho/flatshare/issues/121 -class TaskWithMaybeTaskGroup extends Task { - TaskGroup? taskGroup; - - TaskWithMaybeTaskGroup( - {required super.id, - required super.title, - super.description, - super.taskGroupId, - this.taskGroup}); - - factory TaskWithMaybeTaskGroup.fromJson(Map json) { - try { - return TaskWithMaybeTaskGroup( - id: json['id'] as int, - title: json['title'] as String, - description: json['description'] as String?, - taskGroupId: json['taskGroupId'] as int?, - taskGroup: json['maybeCreatedTaskGroup'] != null - ? TaskGroup.fromJson(json['maybeCreatedTaskGroup']) - : null); - } catch (e) { - throw FormatException( - "Failed to parse task with maybe task group: ${e.toString()}"); - } - } -} - Future> fetchTasks({required int userGroupId}) async { var apiBaseUrl = getApiBaseUrl(); final response = await authenticatedClient diff --git a/frontend/lib/firebase_options.dart b/frontend/lib/firebase_options.dart new file mode 100644 index 0000000..9d5eb5e --- /dev/null +++ b/frontend/lib/firebase_options.dart @@ -0,0 +1,59 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyBak-z8ENI9BDldZAwlcvNTSMpn5AGJuL0', + appId: '1:66431485841:android:f9a7a5dd12273c2f0fbc30', + messagingSenderId: '66431485841', + projectId: 'flatshare-223fd', + storageBucket: 'flatshare-223fd.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyBEQXiTYEd-q0_XxY_RMfwuwmFxdZxnjpk', + appId: '1:66431485841:ios:c9ffa9634ee6fb680fbc30', + messagingSenderId: '66431485841', + projectId: 'flatshare-223fd', + storageBucket: 'flatshare-223fd.firebasestorage.app', + iosBundleId: 'com.invertedecho.flaatshare', + ); +} + diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 45afab7..e8fdef5 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,18 +1,43 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flatshare/authenticated_navigation.dart'; import 'package:flatshare/fetch/authenticated_client.dart'; +import 'package:flatshare/notifications/handler.dart'; +import 'package:flatshare/notifications/util.dart'; import 'package:flatshare/providers/task.dart'; import 'package:flatshare/providers/task_group.dart'; import 'package:flatshare/providers/user.dart'; import 'package:flatshare/unauthenticated_navigation.dart'; import 'package:flatshare/widgets/screens/splash.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; + +import 'firebase_options.dart'; Future main() async { await dotenv.load(fileName: '.env'); + if (getIsSupportedPlatformFirebase()) { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + FirebaseMessaging messaging = FirebaseMessaging.instance; + + await messaging.requestPermission( + alert: true, + announcement: false, + badge: true, + carPlay: false, + criticalAlert: false, + provisional: false, + sound: true, + ); + + FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); + } runApp(MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => UserProvider()), diff --git a/frontend/lib/models/assignment.dart b/frontend/lib/models/assignment.dart index e5cc04f..5dad259 100644 --- a/frontend/lib/models/assignment.dart +++ b/frontend/lib/models/assignment.dart @@ -1,3 +1,6 @@ +// TODO: this model seems to have way too many properties... +// for example, the task group title really doesnt have to be in here, we also store task groups. + class Assignment { final int id; final String title; @@ -25,23 +28,20 @@ class Assignment { this.dueDate, }); - factory Assignment.fromJson(Map json) { - return Assignment( - id: json['id'] as int, - title: json['title'] as String, - isCompleted: json['isCompleted'] as bool, - assigneeId: json['assigneeId'] as int, - assigneeName: json['assigneeName'] as String, - createdAt: DateTime.parse(json['createdAt'] as String), - isOneOff: json['isOneOff'] as bool, - description: json['description'] as String?, - dueDate: json['dueDate'] != null - ? DateTime.parse(json['dueDate'] as String) - : null, - taskGroupId: json['taskGroupId'] as int?, - taskGroupTitle: json['taskGroupTitle'] as String?, - ); - } + Assignment.fromJson(Map json) + : id = json['id'] as int, + title = json['title'] as String, + isCompleted = json['isCompleted'] as bool, + assigneeId = json['assigneeId'] as int, + assigneeName = json['assigneeName'] as String, + createdAt = DateTime.parse(json['createdAt'] as String), + isOneOff = json['isOneOff'] as bool, + description = json['description'] as String?, + dueDate = json['dueDate'] != null + ? DateTime.parse(json['dueDate'] as String) + : null, + taskGroupId = json['taskGroupId'] as int?, + taskGroupTitle = json['taskGroupTitle'] as String?; @override String toString() { diff --git a/frontend/lib/models/task.dart b/frontend/lib/models/task.dart index 01847e1..4e88a69 100644 --- a/frontend/lib/models/task.dart +++ b/frontend/lib/models/task.dart @@ -10,17 +10,11 @@ class Task { this.description, this.taskGroupId}); - factory Task.fromJson(Map json) { - try { - return Task( - id: json['id'] as int, - title: json['title'] as String, - description: json['description'] as String?, - taskGroupId: json['taskGroupId'] as int?); - } catch (e) { - throw FormatException("Failed to parse task: ${e.toString()}"); - } - } + Task.fromJson(Map json) + : id = json['id'] as int, + title = json['title'] as String, + description = json['description'] as String?, + taskGroupId = json['taskGroupId'] as int?; } enum TaskType { oneOff, recurring } diff --git a/frontend/lib/models/task_group.dart b/frontend/lib/models/task_group.dart index 6eda4ff..45d17b3 100644 --- a/frontend/lib/models/task_group.dart +++ b/frontend/lib/models/task_group.dart @@ -11,23 +11,12 @@ class TaskGroup { required this.interval, }); - factory TaskGroup.fromJson(Map json) { - return switch (json) { - { - 'id': int id, - 'title': String title, - 'description': String? description, - 'interval': String interval, - } => - TaskGroup( - id: id, - title: title, - description: description, - interval: interval, - ), - _ => throw const FormatException("Failed to parse task groups.") - }; - } + TaskGroup.fromJson(Map json) + : id = json['id'] as int, + title = json['title'] as String, + description = json['description'] as String?, + interval = json['interval'] as String; + @override String toString() { return title; diff --git a/frontend/lib/models/task_with_maybe_task_group.dart b/frontend/lib/models/task_with_maybe_task_group.dart new file mode 100644 index 0000000..c382686 --- /dev/null +++ b/frontend/lib/models/task_with_maybe_task_group.dart @@ -0,0 +1,25 @@ +// TODO: i hateee this so much... +// TODO: https://github.com/invertedEcho/flatshare/issues/121 +import 'package:flatshare/models/task.dart'; +import 'package:flatshare/models/task_group.dart'; + +class TaskWithMaybeTaskGroup extends Task { + TaskGroup? taskGroup; + + TaskWithMaybeTaskGroup( + {required super.id, + required super.title, + super.description, + super.taskGroupId, + this.taskGroup}); + + TaskWithMaybeTaskGroup.fromJson(Map json) + : taskGroup = json['maybeCreatedTaskGroup'] != null + ? TaskGroup.fromJson(json['maybeCreatedTaskGroup']) + : null, + super( + id: json['id'] as int, + title: json['title'] as String, + description: json['description'] as String?, + taskGroupId: json['taskGroupId'] as int?); +} diff --git a/frontend/lib/models/user.dart b/frontend/lib/models/user.dart index 23bb3a3..2bde863 100644 --- a/frontend/lib/models/user.dart +++ b/frontend/lib/models/user.dart @@ -3,15 +3,13 @@ class User { final String email; final String username; - const User( - {required this.userId, required this.email, required this.username}); + User({required this.userId, required this.email, required this.username}); + + User.fromJson(Map json) + : userId = json['userId'] as int, + email = json['email'] as String, + username = json['username'] as String; - factory User.fromJson(Map json) { - return User( - userId: json['userId'] as int, - email: json['email'] as String, - username: json['username'] as String); - } @override String toString() { return username; diff --git a/frontend/lib/models/user_group.dart b/frontend/lib/models/user_group.dart index 533a427..affe594 100644 --- a/frontend/lib/models/user_group.dart +++ b/frontend/lib/models/user_group.dart @@ -4,12 +4,10 @@ class UserGroup { const UserGroup({required this.id, required this.name}); - factory UserGroup.fromJson(Map json) { - return UserGroup( - id: json['id'] as int, - name: json['name'] as String, - ); - } + UserGroup.fromJson(Map json) + : id = json['id'] as int, + name = json['name'] as String; + @override String toString() { return name; diff --git a/frontend/lib/notifications/handler.dart b/frontend/lib/notifications/handler.dart new file mode 100644 index 0000000..ac2c05a --- /dev/null +++ b/frontend/lib/notifications/handler.dart @@ -0,0 +1,5 @@ +import 'package:firebase_messaging/firebase_messaging.dart'; + +Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { + print("Handled a background message: ${message.messageId}"); +} diff --git a/frontend/lib/notifications/util.dart b/frontend/lib/notifications/util.dart new file mode 100644 index 0000000..83470ee --- /dev/null +++ b/frontend/lib/notifications/util.dart @@ -0,0 +1,5 @@ +import 'dart:io'; + +bool getIsSupportedPlatformFirebase() { + return Platform.isAndroid || Platform.isIOS; +} diff --git a/frontend/lib/widgets/assignments/assignments_widget.dart b/frontend/lib/widgets/assignments/assignments_widget.dart index fc0a745..f2a248c 100644 --- a/frontend/lib/widgets/assignments/assignments_widget.dart +++ b/frontend/lib/widgets/assignments/assignments_widget.dart @@ -102,7 +102,7 @@ class AssignmentsWidgetState extends State { return const Padding( padding: EdgeInsets.all(16.0), child: Text( - "Currently, there are no assignments. To get started, use the + Action Button on the bottom right."), + "Currently, there are no assignments. To get started, use the + Action Button in the Tasks Page, on the bottom right."), ); } List> sortedAssignmentGroups = []; diff --git a/frontend/lib/widgets/screens/splash.dart b/frontend/lib/widgets/screens/splash.dart index 38655b4..baff853 100644 --- a/frontend/lib/widgets/screens/splash.dart +++ b/frontend/lib/widgets/screens/splash.dart @@ -22,7 +22,7 @@ class SplashScreenState extends State { @override void initState() { super.initState(); - userInfoFuture = getUserInfo(); + userInfoFuture = fetchProfileAndUserGroup(); } @override diff --git a/frontend/lib/widgets/user/login_form.dart b/frontend/lib/widgets/user/login_form.dart index 24aba8e..32e6389 100644 --- a/frontend/lib/widgets/user/login_form.dart +++ b/frontend/lib/widgets/user/login_form.dart @@ -1,5 +1,4 @@ import 'package:flatshare/fetch/auth.dart'; -import 'package:flatshare/fetch/user_group.dart'; import 'package:flatshare/main.dart'; import 'package:flatshare/models/user.dart'; import 'package:flatshare/models/user_group.dart'; @@ -46,13 +45,20 @@ class LoginFormState extends State { if (!mounted) return; - User user = await getProfile(); + (User?, UserGroup?) userInfo = await fetchProfileAndUserGroup(); + User? user = userInfo.$1; - UserGroup? userGroup = await fetchUserGroupForUser(userId: user.userId); + if (user == null) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text( + 'Failed to get user information directly after logging in. Very weird...'))); + return; + } var userProvider = Provider.of(context, listen: false); userProvider.setUser(user); + UserGroup? userGroup = userInfo.$2; if (userGroup != null) { userProvider.setUserGroup(userGroup); diff --git a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift index ecc2cee..7cafb92 100644 --- a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,15 @@ import FlutterMacOS import Foundation +import firebase_core +import firebase_messaging import flutter_secure_storage_macos import path_provider_foundation import share_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index a0b673e..c10f747 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "9371d13b8ee442e3bfc08a24e3a1b3742c839abbfaf5eef11b79c4b862c89bf7" + url: "https://pub.dev" + source: hosted + version: "1.3.41" animated_custom_dropdown: dependency: "direct main" description: @@ -97,6 +105,54 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "06537da27db981947fa535bb91ca120b4e9cb59cb87278dbdde718558cafc9ff" + url: "https://pub.dev" + source: hosted + version: "3.4.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: b94b217e3ad745e784960603d33d99471621ecca151c99c670869b76e50ad2a6 + url: "https://pub.dev" + source: hosted + version: "5.3.1" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88" + url: "https://pub.dev" + source: hosted + version: "2.17.5" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "29941ba5a3204d80656c0e52103369aa9a53edfd9ceae05a2bb3376f24fda453" + url: "https://pub.dev" + source: hosted + version: "15.1.0" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "26c5370d3a79b15c8032724a68a4741e28f63e1f1a45699c4f0a8ae740aadd72" + url: "https://pub.dev" + source: hosted + version: "4.5.43" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "58276cd5d9e22a9320ef9e5bc358628920f770f93c91221f8b638e8346ed5df4" + url: "https://pub.dev" + source: hosted + version: "3.8.13" fixnum: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 01de95c..2e4e9b6 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -23,6 +23,8 @@ dependencies: go_router: 14.2.1 socket_io_client: 2.0.3+1 flutter_glow: 0.3.2 + firebase_core: 3.4.0 + firebase_messaging: 15.1.0 dev_dependencies: flutter_test: diff --git a/frontend/test/widget_test.dart b/frontend/test/widget_test.dart deleted file mode 100644 index 812c978..0000000 --- a/frontend/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:frontend/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}