update 9/20/25
This commit is contained in:
1
functions/.tool-versions
Normal file
1
functions/.tool-versions
Normal file
@@ -0,0 +1 @@
|
||||
direnv 2.37.1
|
||||
@@ -5,11 +5,13 @@
|
||||
"name": "auditly-functions",
|
||||
"dependencies": {
|
||||
"@google-cloud/vertexai": "^1.10.0",
|
||||
"firebase-admin": "^12.7.0",
|
||||
"@neondatabase/serverless": "^0.9.5",
|
||||
"firebase-functions": "^6.4.0",
|
||||
"pg": "^8.12.0",
|
||||
"stripe": "^18.5.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.11.6",
|
||||
"firebase-functions-test": "^3.4.1",
|
||||
},
|
||||
},
|
||||
@@ -183,6 +185,8 @@
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@neondatabase/serverless": ["@neondatabase/serverless@0.9.5", "", { "dependencies": { "@types/pg": "8.11.6" } }, "sha512-siFas6gItqv6wD/pZnvdu34wEqgG3nSE6zWZdq5j2DEsa+VvX8i/5HXJOo06qrw5axPXn+lGCxeR+NLaSPIXug=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
@@ -259,6 +263,8 @@
|
||||
|
||||
"@types/node": ["@types/node@22.17.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w=="],
|
||||
|
||||
"@types/pg": ["@types/pg@8.15.5", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
@@ -419,7 +425,7 @@
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
|
||||
|
||||
@@ -773,6 +779,8 @@
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
@@ -801,6 +809,24 @@
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
|
||||
|
||||
"pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="],
|
||||
|
||||
"pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
"pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="],
|
||||
|
||||
"pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="],
|
||||
|
||||
"pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
|
||||
|
||||
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||
|
||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
@@ -809,6 +835,16 @@
|
||||
|
||||
"pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="],
|
||||
|
||||
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||
|
||||
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
||||
|
||||
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||
|
||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
|
||||
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
|
||||
|
||||
"pretty-format": ["pretty-format@30.0.5", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw=="],
|
||||
|
||||
"proto3-json-serializer": ["proto3-json-serializer@2.0.2", "", { "dependencies": { "protobufjs": "^7.2.5" } }, "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ=="],
|
||||
@@ -871,6 +907,8 @@
|
||||
|
||||
"source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
|
||||
@@ -969,6 +1007,8 @@
|
||||
|
||||
"write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||
@@ -979,12 +1019,16 @@
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"@babel/core/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/traverse/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"@google-cloud/storage/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
@@ -995,26 +1039,32 @@
|
||||
|
||||
"@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||
|
||||
"@neondatabase/serverless/@types/pg": ["@types/pg@8.11.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
"debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"google-gax/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"http-proxy-agent/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"https-proxy-agent/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"istanbul-lib-source-maps/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
||||
|
||||
"jwks-rsa/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
@@ -1023,8 +1073,6 @@
|
||||
|
||||
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||
@@ -1045,18 +1093,22 @@
|
||||
|
||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
"@neondatabase/serverless/@types/pg/pg-types": ["pg-types@4.1.0", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg=="],
|
||||
|
||||
"jsonwebtoken/jws/jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
||||
|
||||
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"teeny-request/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"teeny-request/https-proxy-agent/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"@neondatabase/serverless/@types/pg/pg-types/postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
|
||||
|
||||
"@neondatabase/serverless/@types/pg/pg-types/postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
|
||||
|
||||
"@neondatabase/serverless/@types/pg/pg-types/postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
|
||||
|
||||
"@neondatabase/serverless/@types/pg/pg-types/postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
|
||||
}
|
||||
}
|
||||
|
||||
98
functions/database.js
Normal file
98
functions/database.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const { Pool } = require('pg');
|
||||
const { neon } = require('@neondatabase/serverless');
|
||||
|
||||
// Database configuration
|
||||
const DB_CONFIG = {
|
||||
// Use Neon serverless for edge deployments
|
||||
connectionString: process.env.DATABASE_URL || process.env.NEON_DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||
};
|
||||
|
||||
// Create connection pool for traditional deployment
|
||||
const pool = new Pool(DB_CONFIG);
|
||||
|
||||
// Create Neon serverless client for edge/serverless deployment
|
||||
let neonClient = null;
|
||||
if (DB_CONFIG.connectionString) {
|
||||
neonClient = neon(DB_CONFIG.connectionString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database client - either pool connection or Neon serverless
|
||||
* @returns {Promise<any>} Database client
|
||||
*/
|
||||
async function getDbClient() {
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true' && neonClient) {
|
||||
return neonClient;
|
||||
}
|
||||
|
||||
return pool.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query using the appropriate client
|
||||
* @param {string} query - SQL query
|
||||
* @param {any[]} params - Query parameters
|
||||
* @returns {Promise<any>} Query result
|
||||
*/
|
||||
async function executeQuery(query, params = []) {
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true' && neonClient) {
|
||||
// Neon serverless client
|
||||
return await neonClient(query, params);
|
||||
} else {
|
||||
// Traditional pool connection
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const result = await client.query(query, params);
|
||||
return result.rows;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a transaction
|
||||
* @param {Function} callback - Function to execute within transaction
|
||||
* @returns {Promise<any>} Transaction result
|
||||
*/
|
||||
async function executeTransaction(callback) {
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true' && neonClient) {
|
||||
// For Neon serverless, we'll need to handle this differently
|
||||
// Note: Neon serverless doesn't support traditional transactions
|
||||
// Each query is automatically committed
|
||||
return await callback(neonClient);
|
||||
} else {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const result = await callback(client);
|
||||
await client.query('COMMIT');
|
||||
return result;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all database connections
|
||||
*/
|
||||
async function closeConnections() {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pool,
|
||||
neonClient,
|
||||
getDbClient,
|
||||
executeQuery,
|
||||
executeTransaction,
|
||||
closeConnections,
|
||||
DB_CONFIG
|
||||
};
|
||||
@@ -1,23 +1,12 @@
|
||||
const { onRequest } = require("firebase-functions/v2/https");
|
||||
const { setGlobalOptions, logger } = require("firebase-functions/v2");
|
||||
const admin = require("firebase-admin");
|
||||
const { VertexAI } = require('@google-cloud/vertexai');
|
||||
const Stripe = require("stripe");
|
||||
const { executeQuery, executeTransaction } = require('./database');
|
||||
|
||||
// Set global options for all functions to use us-central1 region
|
||||
|
||||
setGlobalOptions({ region: "us-central1" });
|
||||
|
||||
// const serviceAccount = require("./auditly-consulting-firebase-adminsdk-fbsvc-e4b51ef5cf.json");
|
||||
const serviceAccount = require("./auditly-c0027-firebase-adminsdk-fbsvc-1db7c58141.json")
|
||||
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount)
|
||||
});
|
||||
|
||||
//region Interface Clients
|
||||
const db = admin.firestore();
|
||||
|
||||
// Initialize Vertex AI with your project ID
|
||||
// This automatically uses IAM authentication from the service account
|
||||
const vertexAI = new VertexAI({
|
||||
@@ -441,59 +430,78 @@ const RESPONSE_FORMAT_COMPANY = {
|
||||
//endregion Constants
|
||||
|
||||
//region Helper Functions
|
||||
const validateAuthAndGetContext = async (req, res) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
async function validateAuthAndGetContext(req, res) {
|
||||
// Set CORS headers for all requests
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
||||
.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
|
||||
|
||||
if (req.method == "OPTIONS") {
|
||||
res.headers['Access-Control-Allow-Origin'] = '*';
|
||||
res.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS';
|
||||
res.headers['Access-Control-Allow-Headers'] = 'Authorization, Content-Type';
|
||||
if (req.method === "OPTIONS") {
|
||||
res.status(204).send('');
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new Error('Missing or invalid authorization header');
|
||||
res.status(401).json({ error: 'Missing or invalid authorization header' });
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
// Validate token format (should start with 'session_')
|
||||
if (!token.startsWith('session_')) {
|
||||
throw new Error('Invalid token format');
|
||||
// Validate token format more thoroughly
|
||||
if (!token.startsWith('session_') || token.length < 20) {
|
||||
res.status(401).json({ error: 'Invalid token format' });
|
||||
return null;
|
||||
}
|
||||
|
||||
// Look up token in Firestore
|
||||
const tokenDoc = await db.collection("authTokens").doc(token).get();
|
||||
try {
|
||||
// Look up token in PostgreSQL
|
||||
const tokenRows = await executeQuery(
|
||||
'SELECT * FROM auth_tokens WHERE token = $1 AND is_active = true',
|
||||
[token]
|
||||
);
|
||||
|
||||
if (!tokenDoc.exists) {
|
||||
throw new Error('Token not found');
|
||||
if (tokenRows.length === 0) {
|
||||
res.status(401).json({ error: 'Token not found' });
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenData = tokenRows[0];
|
||||
|
||||
if (Date.now() > tokenData.expires_at) {
|
||||
res.status(401).json({ error: 'Token has expired' });
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update last used timestamp (don't await to avoid blocking)
|
||||
executeQuery(
|
||||
'UPDATE auth_tokens SET last_used_at = $1 WHERE token = $2',
|
||||
[Date.now(), token]
|
||||
).catch(error => {
|
||||
console.warn('Failed to update token lastUsedAt:', error);
|
||||
});
|
||||
|
||||
// Get user's organizations
|
||||
const userOrgsRows = await executeQuery(
|
||||
'SELECT organization_id FROM user_organizations WHERE user_id = $1',
|
||||
[tokenData.user_id]
|
||||
);
|
||||
|
||||
const orgIds = userOrgsRows.map(row => row.organization_id);
|
||||
|
||||
return {
|
||||
userId: tokenData.user_id,
|
||||
orgIds: orgIds,
|
||||
orgId: orgIds[0] || null,
|
||||
token: token
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Auth validation error:', error);
|
||||
res.status(500).json({ error: 'Authentication validation failed' });
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenData = tokenDoc.data();
|
||||
|
||||
if (!tokenData.isActive) {
|
||||
throw new Error('Token is inactive');
|
||||
}
|
||||
|
||||
if (Date.now() > tokenData.expiresAt) {
|
||||
throw new Error('Token has expired');
|
||||
}
|
||||
|
||||
// Update last used timestamp
|
||||
await tokenDoc.ref.update({ lastUsedAt: Date.now() });
|
||||
|
||||
// Get user's organizations
|
||||
const userOrgsSnapshot = await db.collection("users").doc(tokenData.userId).collection("organizations").get();
|
||||
const orgIds = userOrgsSnapshot.docs.map(doc => doc.id);
|
||||
|
||||
return {
|
||||
userId: tokenData.userId,
|
||||
orgIds: orgIds,
|
||||
// For backward compatibility, use first org as default
|
||||
orgId: orgIds[0] || null,
|
||||
token: token
|
||||
};
|
||||
};
|
||||
|
||||
const generateOTP = () => {
|
||||
@@ -640,9 +648,18 @@ const verifyUserAuthorization = async (userId, orgId) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if user exists in the organization's employees collection
|
||||
const employeeDoc = await db.collection("orgs").doc(orgId).collection("employees").doc(userId).get();
|
||||
return employeeDoc.exists;
|
||||
// Check if user exists in the organization's employees or as an organization member
|
||||
const employeeRows = await executeQuery(
|
||||
'SELECT id FROM employees WHERE organization_id = $1 AND (id = $2 OR user_id = $2)',
|
||||
[orgId, userId]
|
||||
);
|
||||
|
||||
const userOrgRows = await executeQuery(
|
||||
'SELECT user_id FROM user_organizations WHERE user_id = $1 AND organization_id = $2',
|
||||
[userId, orgId]
|
||||
);
|
||||
|
||||
return employeeRows.length > 0 || userOrgRows.length > 0;
|
||||
} catch (error) {
|
||||
console.error("Authorization check error:", error);
|
||||
return false;
|
||||
@@ -667,14 +684,19 @@ exports.sendOTP = onRequest({cors: true}, async (req, res) => {
|
||||
const otp = generateOTP();
|
||||
const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes expiry
|
||||
|
||||
// Store OTP in Firestore
|
||||
await db.collection("otps").doc(email).set({
|
||||
otp,
|
||||
expiresAt,
|
||||
attempts: 0,
|
||||
inviteCode: inviteCode || null,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
// Store OTP in PostgreSQL
|
||||
await executeQuery(
|
||||
`INSERT INTO otps (email, otp, expires_at, attempts, invite_code, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (email)
|
||||
DO UPDATE SET
|
||||
otp = EXCLUDED.otp,
|
||||
expires_at = EXCLUDED.expires_at,
|
||||
attempts = 0,
|
||||
invite_code = EXCLUDED.invite_code,
|
||||
created_at = EXCLUDED.created_at`,
|
||||
[email, otp, expiresAt, 0, inviteCode || null, Date.now()]
|
||||
);
|
||||
|
||||
// In production, send actual email
|
||||
console.log(`📧 OTP for ${email}: ${otp} (expires in 5 minutes)`);
|
||||
@@ -705,56 +727,59 @@ exports.verifyOTP = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Retrieve OTP document
|
||||
const otpDoc = await db.collection("otps").doc(email).get();
|
||||
// Retrieve OTP from PostgreSQL
|
||||
const otpRows = await executeQuery(
|
||||
'SELECT * FROM otps WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (!otpDoc.exists) {
|
||||
if (otpRows.length === 0) {
|
||||
return res.status(400).json({ error: "Invalid verification code" });
|
||||
}
|
||||
|
||||
const otpData = otpDoc.data();
|
||||
const otpData = otpRows[0];
|
||||
|
||||
// Check if OTP is expired
|
||||
if (Date.now() > otpData.expiresAt) {
|
||||
await otpDoc.ref.delete();
|
||||
if (Date.now() > otpData.expires_at) {
|
||||
await executeQuery('DELETE FROM otps WHERE email = $1', [email]);
|
||||
return res.status(400).json({ error: "Verification code has expired" });
|
||||
}
|
||||
|
||||
// Check if too many attempts
|
||||
if (otpData.attempts >= 5) {
|
||||
await otpDoc.ref.delete();
|
||||
await executeQuery('DELETE FROM otps WHERE email = $1', [email]);
|
||||
return res.status(400).json({ error: "Too many failed attempts" });
|
||||
}
|
||||
|
||||
// Verify OTP
|
||||
if (otpData.otp !== otp) {
|
||||
await otpDoc.ref.update({
|
||||
attempts: (otpData.attempts || 0) + 1,
|
||||
});
|
||||
await executeQuery(
|
||||
'UPDATE otps SET attempts = $1 WHERE email = $2',
|
||||
[(otpData.attempts || 0) + 1, email]
|
||||
);
|
||||
return res.status(400).json({ error: "Invalid verification code" });
|
||||
}
|
||||
|
||||
// OTP is valid - clean up and create/find user
|
||||
await otpDoc.ref.delete();
|
||||
await executeQuery('DELETE FROM otps WHERE email = $1', [email]);
|
||||
|
||||
// Generate a unique user ID for this email if it doesn't exist
|
||||
let userId;
|
||||
let userDoc;
|
||||
let userExists = false;
|
||||
|
||||
// Check if user already exists by email
|
||||
const existingUserQuery = await db.collection("users")
|
||||
.where("email", "==", email)
|
||||
.limit(1)
|
||||
.get();
|
||||
const existingUserRows = await executeQuery(
|
||||
'SELECT id FROM users WHERE email = $1 LIMIT 1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (!existingUserQuery.empty) {
|
||||
if (existingUserRows.length > 0) {
|
||||
// User exists, get their ID
|
||||
userDoc = existingUserQuery.docs[0];
|
||||
userId = userDoc.id;
|
||||
userId = existingUserRows[0].id;
|
||||
userExists = true;
|
||||
} else {
|
||||
// Create new user
|
||||
userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
userDoc = null;
|
||||
}
|
||||
|
||||
// Prepare user object for response
|
||||
@@ -765,53 +790,52 @@ exports.verifyOTP = onRequest({cors: true}, async (req, res) => {
|
||||
emailVerified: true,
|
||||
};
|
||||
|
||||
// Create or update user document in Firestore
|
||||
const userRef = db.collection("users").doc(userId);
|
||||
|
||||
const userData = {
|
||||
id: userId,
|
||||
email: email,
|
||||
displayName: email.split("@")[0],
|
||||
emailVerified: true,
|
||||
lastLoginAt: Date.now(),
|
||||
};
|
||||
|
||||
if (!userDoc) {
|
||||
// Create or update user document in PostgreSQL
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (!userExists) {
|
||||
// Create new user document
|
||||
userData.createdAt = Date.now();
|
||||
await userRef.set(userData);
|
||||
await executeQuery(
|
||||
`INSERT INTO users (id, email, display_name, email_verified, created_at, last_login_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[userId, email, email.split("@")[0], true, currentTime, currentTime]
|
||||
);
|
||||
} else {
|
||||
// Update existing user with latest login info
|
||||
await userRef.update({
|
||||
lastLoginAt: Date.now(),
|
||||
});
|
||||
await executeQuery(
|
||||
'UPDATE users SET last_login_at = $1 WHERE id = $2',
|
||||
[currentTime, userId]
|
||||
);
|
||||
}
|
||||
|
||||
// Generate a simple session token (in production, use proper JWT)
|
||||
const customToken = `session_${userId}_${Date.now()}`;
|
||||
|
||||
// Store auth token in Firestore for validation
|
||||
await db.collection("authTokens").doc(customToken).set({
|
||||
userId: userId,
|
||||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + (30 * 24 * 60 * 60 * 1000), // 30 days
|
||||
lastUsedAt: Date.now(),
|
||||
isActive: true
|
||||
});
|
||||
// Store auth token in PostgreSQL for validation
|
||||
await executeQuery(
|
||||
`INSERT INTO auth_tokens (token, user_id, created_at, expires_at, last_used_at, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[
|
||||
customToken,
|
||||
userId,
|
||||
currentTime,
|
||||
currentTime + (30 * 24 * 60 * 60 * 1000), // 30 days
|
||||
currentTime,
|
||||
true
|
||||
]
|
||||
);
|
||||
|
||||
// Handle invitation if present
|
||||
let inviteData = null;
|
||||
if (otpData.inviteCode) {
|
||||
if (otpData.invite_code) {
|
||||
try {
|
||||
const inviteDoc = await db
|
||||
.collectionGroup("invites")
|
||||
.where("code", "==", otpData.inviteCode)
|
||||
.where("status", "==", "pending")
|
||||
.limit(1)
|
||||
.get();
|
||||
const inviteRows = await executeQuery(
|
||||
'SELECT * FROM invites WHERE code = $1 AND status = $2 LIMIT 1',
|
||||
[otpData.invite_code, 'pending']
|
||||
);
|
||||
|
||||
if (!inviteDoc.empty) {
|
||||
inviteData = inviteDoc.docs[0].data();
|
||||
if (inviteRows.length > 0) {
|
||||
inviteData = inviteRows[0];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching invite:", error);
|
||||
@@ -833,9 +857,8 @@ exports.verifyOTP = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Create Invitation
|
||||
exports.createInvitation = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -844,8 +867,6 @@ exports.createInvitation = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
const { name, email, role = "employee", department } = req.body;
|
||||
|
||||
if (!email || !name) {
|
||||
@@ -925,7 +946,7 @@ exports.createInvitation = onRequest({cors: true}, async (req, res) => {
|
||||
},
|
||||
},
|
||||
],
|
||||
from: { email: 'no-reply@auditly.com', name: 'Auditly' },
|
||||
from: { email: 'no-reply@orbitly.com', name: 'Orbitly' },
|
||||
template_id: process.env.SENDGRID_TEMPLATE_ID,
|
||||
}),
|
||||
});
|
||||
@@ -1196,6 +1217,9 @@ exports.submitEmployeeAnswers = onRequest({cors: true}, async (req, res) => {
|
||||
} else {
|
||||
// Authenticated submission
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!employeeId || !answers) {
|
||||
return res.status(400).json({ error: "Employee ID and answers are required for authenticated submissions" });
|
||||
@@ -1391,9 +1415,8 @@ Be thorough, professional, and focus on actionable insights.
|
||||
|
||||
//region Generate Employee Report
|
||||
exports.generateEmployeeReport = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1511,8 +1534,8 @@ Be thorough, professional, and focus on actionable insights.
|
||||
|
||||
//region Generate Company Report
|
||||
exports.generateCompanyReport = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1520,8 +1543,6 @@ exports.generateCompanyReport = onRequest({cors: true}, async (req, res) => {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
@@ -1646,9 +1667,8 @@ Be thorough, professional, and focus on actionable insights.`;
|
||||
|
||||
//region Chat
|
||||
exports.chat = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1739,8 +1759,8 @@ Instructions:
|
||||
|
||||
//region Create Organization
|
||||
exports.createOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1749,90 +1769,63 @@ exports.createOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
const { name } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: "Organization name is required" });
|
||||
}
|
||||
|
||||
// Generate unique organization ID
|
||||
const orgId = `org_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Create comprehensive organization document
|
||||
const orgData = {
|
||||
name,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
onboardingCompleted: false,
|
||||
ownerId: authContext.userId,
|
||||
// Subscription fields (will be populated after Stripe setup)
|
||||
subscription: {
|
||||
status: 'trial', // trial, active, past_due, canceled
|
||||
stripeCustomerId: null,
|
||||
stripeSubscriptionId: null,
|
||||
currentPeriodStart: null,
|
||||
currentPeriodEnd: null,
|
||||
trialEnd: Date.now() + (14 * 24 * 60 * 60 * 1000), // 14 day trial
|
||||
},
|
||||
// Usage tracking
|
||||
usage: {
|
||||
employeeCount: 0,
|
||||
reportsGenerated: 0,
|
||||
lastReportGeneration: null,
|
||||
},
|
||||
// Organization settings
|
||||
settings: {
|
||||
allowedEmployeeCount: 50, // Default limit
|
||||
featuresEnabled: {
|
||||
aiReports: true,
|
||||
chat: true,
|
||||
analytics: true,
|
||||
}
|
||||
await executeTransaction(async (client) => {
|
||||
// Create comprehensive organization document
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true') {
|
||||
await client(
|
||||
`INSERT INTO organizations (
|
||||
id, name, owner_id, onboarding_completed,
|
||||
subscription_status, trial_end, employee_count, reports_generated,
|
||||
allowed_employee_count, features_enabled, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
|
||||
[
|
||||
orgId, name, authContext.userId, false,
|
||||
'trial', currentTime + (14 * 24 * 60 * 60 * 1000), // 14 day trial
|
||||
0, 0, 50,
|
||||
JSON.stringify({"aiReports": true, "chat": true, "analytics": true}),
|
||||
currentTime, currentTime
|
||||
]
|
||||
);
|
||||
|
||||
// Add organization to user's organizations (for multi-org support)
|
||||
await client(
|
||||
`INSERT INTO user_organizations (user_id, organization_id, role, onboarding_completed, joined_at)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[authContext.userId, orgId, 'owner', false, currentTime]
|
||||
);
|
||||
} else {
|
||||
await client.query(
|
||||
`INSERT INTO organizations (
|
||||
id, name, owner_id, onboarding_completed,
|
||||
subscription_status, trial_end, employee_count, reports_generated,
|
||||
allowed_employee_count, features_enabled, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
|
||||
[
|
||||
orgId, name, authContext.userId, false,
|
||||
'trial', currentTime + (14 * 24 * 60 * 60 * 1000), // 14 day trial
|
||||
0, 0, 50,
|
||||
JSON.stringify({"aiReports": true, "chat": true, "analytics": true}),
|
||||
currentTime, currentTime
|
||||
]
|
||||
);
|
||||
|
||||
// Add organization to user's organizations (for multi-org support)
|
||||
await client.query(
|
||||
`INSERT INTO user_organizations (user_id, organization_id, role, onboarding_completed, joined_at)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[authContext.userId, orgId, 'owner', false, currentTime]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const orgRef = db.collection("orgs").doc(orgId);
|
||||
await orgRef.set(orgData);
|
||||
|
||||
// Get user information from Firestore (since we don't use Firebase Auth)
|
||||
const userRef = db.collection("users").doc(authContext.userId);
|
||||
const userDoc = await userRef.get();
|
||||
|
||||
if (!userDoc.exists) {
|
||||
console.error("User document not found:", authContext.userId);
|
||||
return res.status(400).json({ error: "User not found" });
|
||||
}
|
||||
|
||||
const userData = userDoc.data();
|
||||
|
||||
// Add owner info to organization document (owners are NOT employees)
|
||||
const ownerInfo = {
|
||||
id: authContext.userId,
|
||||
name: userData.displayName || userData.email.split("@")[0],
|
||||
email: userData.email,
|
||||
joinedAt: Date.now()
|
||||
};
|
||||
|
||||
// Update org document with owner info
|
||||
await orgRef.update({
|
||||
ownerInfo: ownerInfo,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// Add organization to user's organizations (for multi-org support)
|
||||
const userOrgRef = db.collection("users").doc(authContext.userId).collection("organizations").doc(orgId);
|
||||
await userOrgRef.set({
|
||||
orgId,
|
||||
name,
|
||||
role: "owner",
|
||||
onboardingCompleted: false,
|
||||
joinedAt: Date.now(),
|
||||
});
|
||||
|
||||
// Update user document with latest activity
|
||||
await userRef.update({
|
||||
lastLoginAt: Date.now(),
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -1841,9 +1834,12 @@ exports.createOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
name,
|
||||
role: "owner",
|
||||
onboardingCompleted: false,
|
||||
joinedAt: Date.now(),
|
||||
subscription: orgData.subscription,
|
||||
requiresSubscription: true, // Signal frontend to show subscription flow
|
||||
joinedAt: currentTime,
|
||||
subscription: {
|
||||
status: 'trial',
|
||||
trialEnd: currentTime + (14 * 24 * 60 * 60 * 1000)
|
||||
},
|
||||
requiresSubscription: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Create organization error:", error);
|
||||
@@ -1853,17 +1849,9 @@ exports.createOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
//endregion Create Organization
|
||||
|
||||
//region Get Organizations
|
||||
exports.getUserOrganizations = onRequest(async (req, res) => {
|
||||
let authContext;
|
||||
try {
|
||||
authContext = await validateAuthAndGetContext(req, res);
|
||||
} catch (error) {
|
||||
logger.debug("Auth validation failed:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
exports.getUserOrganizations = onRequest({cors: true}, async (req, res) => {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1872,8 +1860,6 @@ exports.getUserOrganizations = onRequest(async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
|
||||
// Get user's organizations
|
||||
const userOrgsSnapshot = await db
|
||||
.collection("users")
|
||||
@@ -1906,8 +1892,8 @@ exports.getUserOrganizations = onRequest(async (req, res) => {
|
||||
|
||||
//region Join Organization
|
||||
exports.joinOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1916,8 +1902,6 @@ exports.joinOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
const { inviteCode } = req.body;
|
||||
|
||||
if (!inviteCode) {
|
||||
@@ -2167,7 +2151,7 @@ exports.joinOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
// res.status(500).json({ error: 'Webhook handler failed' });
|
||||
// }
|
||||
// });
|
||||
//endregion Stripe Webhook
|
||||
//#endregion Stripe Webhook
|
||||
|
||||
//region Get Sub Status
|
||||
// exports.getSubscriptionStatus = onRequest(async (req, res) => {
|
||||
@@ -2228,7 +2212,7 @@ exports.joinOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
// res.status(500).json({ error: "Failed to get subscription status" });
|
||||
// }
|
||||
// });
|
||||
//endregion Get Sub Status
|
||||
//#endregion Get Sub Status
|
||||
|
||||
//region Save Company Report
|
||||
// exports.saveCompanyReport = onRequest(async (req, res) => {
|
||||
@@ -2272,12 +2256,12 @@ exports.joinOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
// res.status(500).json({ error: "Failed to save company report" });
|
||||
// }
|
||||
// });
|
||||
//endregion Save Company Report
|
||||
//#endregion Save Company Report
|
||||
|
||||
//region Get Org Data
|
||||
exports.getOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2286,21 +2270,34 @@ exports.getOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
}
|
||||
|
||||
// Get organization data
|
||||
const orgDoc = await db.collection("orgs").doc(orgId).get();
|
||||
if (!orgDoc.exists) {
|
||||
const orgRows = await executeQuery(
|
||||
'SELECT * FROM organizations WHERE id = $1',
|
||||
[orgId]
|
||||
);
|
||||
|
||||
if (orgRows.length === 0) {
|
||||
return res.status(404).json({ error: "Organization not found" });
|
||||
}
|
||||
|
||||
const orgData = { id: orgId, ...orgDoc.data() };
|
||||
const orgData = {
|
||||
id: orgRows[0].id,
|
||||
name: orgRows[0].name,
|
||||
industry: orgRows[0].industry,
|
||||
description: orgRows[0].description,
|
||||
mission: orgRows[0].mission,
|
||||
vision: orgRows[0].vision,
|
||||
values: orgRows[0].values,
|
||||
onboardingCompleted: orgRows[0].onboarding_completed,
|
||||
onboardingData: orgRows[0].onboarding_data,
|
||||
createdAt: orgRows[0].created_at,
|
||||
updatedAt: orgRows[0].updated_at
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -2308,10 +2305,6 @@ exports.getOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Get org data error:", error);
|
||||
if (error.message.includes('Missing or invalid authorization') ||
|
||||
error.message.includes('Token')) {
|
||||
return res.status(401).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: "Failed to get organization data" });
|
||||
}
|
||||
});
|
||||
@@ -2319,8 +2312,8 @@ exports.getOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Update Organization Data
|
||||
exports.updateOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2329,8 +2322,6 @@ exports.updateOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
const { data } = req.body;
|
||||
|
||||
if (!data) {
|
||||
@@ -2366,8 +2357,8 @@ exports.updateOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Get Employees
|
||||
exports.getEmployees = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2376,9 +2367,6 @@ exports.getEmployees = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
@@ -2413,8 +2401,8 @@ exports.getEmployees = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Get Submissions
|
||||
exports.getSubmissions = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2423,9 +2411,6 @@ exports.getSubmissions = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
@@ -2456,8 +2441,8 @@ exports.getSubmissions = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Get Reports
|
||||
exports.getReports = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2466,9 +2451,6 @@ exports.getReports = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
@@ -2552,8 +2534,8 @@ exports.getReports = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Save Report
|
||||
exports.saveReport = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2568,12 +2550,6 @@ exports.saveReport = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify user authorization
|
||||
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
||||
if (!isAuthorized) {
|
||||
return res.status(403).json({ error: "Unauthorized access to organization" });
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
const currentTime = Date.now();
|
||||
if (!reportData.id) {
|
||||
@@ -2603,8 +2579,8 @@ exports.saveReport = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Get Company Reports
|
||||
exports.getCompanyReports = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2613,8 +2589,6 @@ exports.getCompanyReports = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
@@ -2646,8 +2620,8 @@ exports.getCompanyReports = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Upload Image
|
||||
exports.uploadImage = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2655,19 +2629,14 @@ exports.uploadImage = onRequest({cors: true}, async (req, res) => {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const { orgId, userId, imageData } = req.body;
|
||||
const { orgId, userId } = authContext;
|
||||
const { imageData } = req.body;
|
||||
|
||||
if (!orgId || !userId || !imageData) {
|
||||
return res.status(400).json({ error: "Organization ID, user ID, and image data are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify user authorization
|
||||
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
||||
if (!isAuthorized) {
|
||||
return res.status(403).json({ error: "Unauthorized access to organization" });
|
||||
}
|
||||
|
||||
// Validate image data
|
||||
const { collectionName, documentId, dataUrl, filename, originalSize, compressedSize, width, height } = imageData;
|
||||
|
||||
@@ -2764,7 +2733,7 @@ exports.uploadImage = onRequest({cors: true}, async (req, res) => {
|
||||
// res.status(500).json({ error: "Failed to get image" });
|
||||
// }
|
||||
// });
|
||||
//endregion Get Image
|
||||
//#endregion Get Image
|
||||
|
||||
//region Delete Image
|
||||
// exports.deleteImage = onRequest(async (req, res) => {
|
||||
@@ -2809,4 +2778,4 @@ exports.uploadImage = onRequest({cors: true}, async (req, res) => {
|
||||
// res.status(500).json({ error: "Failed to delete image" });
|
||||
// }
|
||||
// });
|
||||
//endregion Delete Image
|
||||
//#endregion Delete Image
|
||||
|
||||
358
functions/migrated_functions.js
Normal file
358
functions/migrated_functions.js
Normal file
@@ -0,0 +1,358 @@
|
||||
// Complete Migration Helper Script for remaining functions
|
||||
// This script contains the migrated versions of key functions
|
||||
|
||||
const { executeQuery, executeTransaction } = require('./database');
|
||||
|
||||
/**
|
||||
* Get User Organizations - Migrated to PostgreSQL
|
||||
*/
|
||||
exports.getUserOrganizations = onRequest({cors: true}, async (req, res) => {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Get user's organizations
|
||||
const userOrgsRows = await executeQuery(
|
||||
`SELECT uo.organization_id as orgId, uo.role, uo.joined_at as joinedAt,
|
||||
uo.onboarding_completed as onboardingCompleted, o.name
|
||||
FROM user_organizations uo
|
||||
JOIN organizations o ON uo.organization_id = o.id
|
||||
WHERE uo.user_id = $1`,
|
||||
[authContext.userId]
|
||||
);
|
||||
|
||||
const organizations = userOrgsRows.map(row => ({
|
||||
orgId: row.orgid,
|
||||
name: row.name,
|
||||
role: row.role,
|
||||
onboardingCompleted: row.onboardingcompleted,
|
||||
joinedAt: row.joinedat
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
organizations,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Get user organizations error:", error);
|
||||
res.status(500).json({ error: "Failed to get user organizations" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get Organization Data - Migrated to PostgreSQL
|
||||
*/
|
||||
exports.getOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
}
|
||||
|
||||
// Get organization data
|
||||
const orgRows = await executeQuery(
|
||||
'SELECT * FROM organizations WHERE id = $1',
|
||||
[orgId]
|
||||
);
|
||||
|
||||
if (orgRows.length === 0) {
|
||||
return res.status(404).json({ error: "Organization not found" });
|
||||
}
|
||||
|
||||
const orgData = {
|
||||
id: orgRows[0].id,
|
||||
name: orgRows[0].name,
|
||||
industry: orgRows[0].industry,
|
||||
description: orgRows[0].description,
|
||||
mission: orgRows[0].mission,
|
||||
vision: orgRows[0].vision,
|
||||
values: orgRows[0].values,
|
||||
onboardingCompleted: orgRows[0].onboarding_completed,
|
||||
onboardingData: orgRows[0].onboarding_data,
|
||||
createdAt: orgRows[0].created_at,
|
||||
updatedAt: orgRows[0].updated_at
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
org: orgData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Get org data error:", error);
|
||||
res.status(500).json({ error: "Failed to get organization data" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update Organization Data - Migrated to PostgreSQL
|
||||
*/
|
||||
exports.updateOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "PUT") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = req.body;
|
||||
|
||||
if (!data) {
|
||||
return res.status(400).json({ error: "Data is required" });
|
||||
}
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
}
|
||||
|
||||
// Build dynamic update query
|
||||
const updateFields = [];
|
||||
const values = [];
|
||||
let paramCount = 1;
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
// Convert camelCase to snake_case for database columns
|
||||
const dbKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
|
||||
updateFields.push(`${dbKey} = $${paramCount}`);
|
||||
values.push(typeof value === 'object' ? JSON.stringify(value) : value);
|
||||
paramCount++;
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).json({ error: "No valid fields to update" });
|
||||
}
|
||||
|
||||
updateFields.push(`updated_at = $${paramCount}`);
|
||||
values.push(Date.now());
|
||||
values.push(orgId); // for WHERE clause
|
||||
|
||||
const query = `UPDATE organizations SET ${updateFields.join(', ')} WHERE id = $${paramCount + 1}`;
|
||||
await executeQuery(query, values);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Organization data updated successfully"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Update org data error:", error);
|
||||
res.status(500).json({ error: "Failed to update organization data" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get Employees - Migrated to PostgreSQL
|
||||
*/
|
||||
exports.getEmployees = onRequest({cors: true}, async (req, res) => {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
}
|
||||
|
||||
// Get all employees
|
||||
const employeesRows = await executeQuery(
|
||||
'SELECT * FROM employees WHERE organization_id = $1 AND status != $2 ORDER BY name',
|
||||
[orgId, 'deleted']
|
||||
);
|
||||
|
||||
const employees = employeesRows.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
role: row.role,
|
||||
department: row.department,
|
||||
status: row.status,
|
||||
joinedAt: row.joined_at,
|
||||
createdAt: row.created_at
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
employees
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Get employees error:", error);
|
||||
res.status(500).json({ error: "Failed to get employees" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Create Invitation - Migrated to PostgreSQL
|
||||
*/
|
||||
exports.createInvitation = onRequest({cors: true}, async (req, res) => {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { name, email, role = "employee", department } = req.body;
|
||||
|
||||
if (!email || !name) {
|
||||
return res.status(400).json({ error: "Name and email are required" });
|
||||
}
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
}
|
||||
|
||||
// Generate invite code
|
||||
const code = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
// Generate employee ID
|
||||
const employeeId = `emp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Create employee data
|
||||
const employeeData = {
|
||||
id: employeeId,
|
||||
name: name.trim(),
|
||||
email: email.trim(),
|
||||
role: role?.trim() || "employee",
|
||||
department: department?.trim() || "General",
|
||||
status: "invited",
|
||||
inviteCode: code
|
||||
};
|
||||
|
||||
await executeTransaction(async (client) => {
|
||||
// Store invitation
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true') {
|
||||
await client(
|
||||
`INSERT INTO invites (code, organization_id, email, employee_data, status, created_at, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
code, orgId, email, JSON.stringify(employeeData), 'pending',
|
||||
currentTime, currentTime + (7 * 24 * 60 * 60 * 1000) // 7 days
|
||||
]
|
||||
);
|
||||
|
||||
// Create employee record
|
||||
await client(
|
||||
`INSERT INTO employees (id, organization_id, name, email, role, department, status, invite_code, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[employeeId, orgId, name.trim(), email.trim(), role || 'employee', department || 'General', 'invited', code, currentTime]
|
||||
);
|
||||
} else {
|
||||
await client.query(
|
||||
`INSERT INTO invites (code, organization_id, email, employee_data, status, created_at, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
code, orgId, email, JSON.stringify(employeeData), 'pending',
|
||||
currentTime, currentTime + (7 * 24 * 60 * 60 * 1000) // 7 days
|
||||
]
|
||||
);
|
||||
|
||||
// Create employee record
|
||||
await client.query(
|
||||
`INSERT INTO employees (id, organization_id, name, email, role, department, status, invite_code, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[employeeId, orgId, name.trim(), email.trim(), role || 'employee', department || 'General', 'invited', code, currentTime]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate invite links
|
||||
const baseUrl = process.env.CLIENT_URL || 'https://auditly-one.vercel.app';
|
||||
const inviteLink = `${baseUrl}/#/employee-form/${code}`;
|
||||
|
||||
console.log(`📧 Invite link: ${inviteLink}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code,
|
||||
employee: employeeData,
|
||||
inviteLink,
|
||||
message: "Invitation sent successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Create invitation error:", error);
|
||||
res.status(500).json({ error: "Failed to create invitation" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get Invitation Status - Migrated to PostgreSQL
|
||||
*/
|
||||
exports.getInvitationStatus = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const { code } = req.query;
|
||||
|
||||
if (!code) {
|
||||
return res.status(400).json({ error: "Invitation code is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const inviteRows = await executeQuery(
|
||||
'SELECT * FROM invites WHERE code = $1',
|
||||
[code]
|
||||
);
|
||||
|
||||
if (inviteRows.length === 0) {
|
||||
return res.status(404).json({ error: "Invitation not found" });
|
||||
}
|
||||
|
||||
const invite = inviteRows[0];
|
||||
|
||||
// Check if expired
|
||||
if (Date.now() > invite.expires_at) {
|
||||
return res.status(400).json({ error: "Invitation has expired" });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
used: invite.status !== 'pending',
|
||||
employee: invite.employee_data,
|
||||
invite: {
|
||||
...invite,
|
||||
employee: invite.employee_data
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Get invitation status error:", error);
|
||||
res.status(500).json({ error: "Failed to get invitation status" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
// Export these functions so they can be used to replace the existing ones
|
||||
};
|
||||
227
functions/migrations/001_create_initial_schema.sql
Normal file
227
functions/migrations/001_create_initial_schema.sql
Normal file
@@ -0,0 +1,227 @@
|
||||
-- Initial migration to create all tables for the Auditly application
|
||||
-- Migration: 001_create_initial_schema.sql
|
||||
|
||||
-- Create users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
display_name VARCHAR(255),
|
||||
email_verified BOOLEAN DEFAULT false,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
last_login_at BIGINT,
|
||||
updated_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000
|
||||
);
|
||||
|
||||
-- Create organizations table
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
owner_id VARCHAR(255) NOT NULL REFERENCES users(id),
|
||||
industry VARCHAR(255),
|
||||
size VARCHAR(255),
|
||||
description TEXT,
|
||||
mission TEXT,
|
||||
vision TEXT,
|
||||
values TEXT,
|
||||
founding_year VARCHAR(255),
|
||||
evolution TEXT,
|
||||
major_milestones TEXT,
|
||||
advantages TEXT,
|
||||
vulnerabilities TEXT,
|
||||
competitors TEXT,
|
||||
market_position TEXT,
|
||||
current_challenges TEXT,
|
||||
short_term_goals TEXT,
|
||||
long_term_goals TEXT,
|
||||
key_metrics TEXT,
|
||||
culture_description TEXT,
|
||||
work_environment TEXT,
|
||||
leadership_style TEXT,
|
||||
communication_style TEXT,
|
||||
additional_context TEXT,
|
||||
onboarding_completed BOOLEAN DEFAULT false,
|
||||
onboarding_data JSONB,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
updated_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
|
||||
-- Subscription fields
|
||||
subscription_status VARCHAR(50) DEFAULT 'trial',
|
||||
stripe_customer_id VARCHAR(255),
|
||||
stripe_subscription_id VARCHAR(255),
|
||||
current_period_start BIGINT,
|
||||
current_period_end BIGINT,
|
||||
trial_end BIGINT,
|
||||
|
||||
-- Usage tracking
|
||||
employee_count INTEGER DEFAULT 0,
|
||||
reports_generated INTEGER DEFAULT 0,
|
||||
last_report_generation BIGINT,
|
||||
|
||||
-- Settings
|
||||
allowed_employee_count INTEGER DEFAULT 50,
|
||||
features_enabled JSONB DEFAULT '{
|
||||
"aiReports": true,
|
||||
"chat": true,
|
||||
"analytics": true
|
||||
}'::jsonb
|
||||
);
|
||||
|
||||
-- Create user_organizations junction table for multi-org support
|
||||
CREATE TABLE IF NOT EXISTS user_organizations (
|
||||
user_id VARCHAR(255) REFERENCES users(id) ON DELETE CASCADE,
|
||||
organization_id VARCHAR(255) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'employee', -- owner, admin, employee
|
||||
joined_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
onboarding_completed BOOLEAN DEFAULT false,
|
||||
PRIMARY KEY (user_id, organization_id)
|
||||
);
|
||||
|
||||
-- Create employees table
|
||||
CREATE TABLE IF NOT EXISTS employees (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
organization_id VARCHAR(255) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
user_id VARCHAR(255) REFERENCES users(id) ON DELETE SET NULL, -- nullable for invite-only employees
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(255) DEFAULT 'employee',
|
||||
department VARCHAR(255) DEFAULT 'General',
|
||||
status VARCHAR(50) DEFAULT 'invited', -- invited, active, inactive
|
||||
invite_code VARCHAR(255),
|
||||
joined_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
updated_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
|
||||
UNIQUE(organization_id, email)
|
||||
);
|
||||
|
||||
-- Create auth_tokens table
|
||||
CREATE TABLE IF NOT EXISTS auth_tokens (
|
||||
token VARCHAR(255) PRIMARY KEY,
|
||||
user_id VARCHAR(255) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
expires_at BIGINT NOT NULL,
|
||||
last_used_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
is_active BOOLEAN DEFAULT true
|
||||
);
|
||||
|
||||
-- Create otps table for email verification
|
||||
CREATE TABLE IF NOT EXISTS otps (
|
||||
email VARCHAR(255) PRIMARY KEY,
|
||||
otp VARCHAR(6) NOT NULL,
|
||||
expires_at BIGINT NOT NULL,
|
||||
attempts INTEGER DEFAULT 0,
|
||||
invite_code VARCHAR(255),
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000
|
||||
);
|
||||
|
||||
-- Create invites table
|
||||
CREATE TABLE IF NOT EXISTS invites (
|
||||
code VARCHAR(255) PRIMARY KEY,
|
||||
organization_id VARCHAR(255) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
employee_data JSONB NOT NULL, -- Contains employee info like name, role, department
|
||||
status VARCHAR(50) DEFAULT 'pending', -- pending, consumed, expired
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
expires_at BIGINT NOT NULL,
|
||||
consumed_at BIGINT,
|
||||
consumed_by VARCHAR(255)
|
||||
);
|
||||
|
||||
-- Create submissions table
|
||||
CREATE TABLE IF NOT EXISTS submissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id VARCHAR(255) NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
|
||||
organization_id VARCHAR(255) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
answers JSONB NOT NULL,
|
||||
status VARCHAR(50) DEFAULT 'completed',
|
||||
submission_type VARCHAR(50) DEFAULT 'regular', -- regular, invite
|
||||
invite_code VARCHAR(255),
|
||||
submitted_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
|
||||
UNIQUE(employee_id) -- One submission per employee
|
||||
);
|
||||
|
||||
-- Create employee_reports table
|
||||
CREATE TABLE IF NOT EXISTS employee_reports (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id VARCHAR(255) NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
|
||||
organization_id VARCHAR(255) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
employee_name VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
summary TEXT,
|
||||
submission_id INTEGER REFERENCES submissions(id),
|
||||
company_context JSONB,
|
||||
|
||||
-- Report sections
|
||||
role_and_output JSONB,
|
||||
insights JSONB,
|
||||
strengths TEXT[],
|
||||
weaknesses TEXT[],
|
||||
opportunities JSONB[],
|
||||
risks TEXT[],
|
||||
recommendations TEXT[],
|
||||
grading_overview JSONB,
|
||||
|
||||
generated_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
updated_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
|
||||
UNIQUE(employee_id) -- One report per employee
|
||||
);
|
||||
|
||||
-- Create company_reports table
|
||||
CREATE TABLE IF NOT EXISTS company_reports (
|
||||
id SERIAL PRIMARY KEY,
|
||||
organization_id VARCHAR(255) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
report_data JSONB NOT NULL, -- Contains the full company report structure
|
||||
executive_summary TEXT,
|
||||
generated_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
updated_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_organizations_owner ON organizations(owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_organizations_user ON user_organizations(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_organizations_org ON user_organizations(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_org ON employees(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_user ON employees(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_email ON employees(organization_id, email);
|
||||
CREATE INDEX IF NOT EXISTS idx_auth_tokens_user ON auth_tokens(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_auth_tokens_expires ON auth_tokens(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_invites_org ON invites(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invites_email ON invites(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_invites_status ON invites(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_submissions_employee ON submissions(employee_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_submissions_org ON submissions(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employee_reports_employee ON employee_reports(employee_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employee_reports_org ON employee_reports(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_company_reports_org ON company_reports(organization_id);
|
||||
|
||||
-- Create trigger function to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = EXTRACT(epoch FROM NOW()) * 1000;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Create triggers to automatically update updated_at
|
||||
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_organizations_updated_at BEFORE UPDATE ON organizations
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_employees_updated_at BEFORE UPDATE ON employees
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_employee_reports_updated_at BEFORE UPDATE ON employee_reports
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_company_reports_updated_at BEFORE UPDATE ON company_reports
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
106
functions/migrations/002_fix_company_report.sql
Normal file
106
functions/migrations/002_fix_company_report.sql
Normal file
@@ -0,0 +1,106 @@
|
||||
DROP TABLE IF EXISTS company_reports; -- it's empty
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
DO $a$
|
||||
BEGIN
|
||||
CREATE TYPE department_breakdown_item AS (
|
||||
"department" VARCHAR,
|
||||
"count" int
|
||||
);
|
||||
CREATE TYPE company_report_overview AS (
|
||||
"total_employees" int,
|
||||
"department_breakdown" department_breakdown_item[],
|
||||
"submissionRate" double precision,
|
||||
"last_updated" TIMESTAMP,
|
||||
"average_performance_score" double precision,
|
||||
"risk_level" VARCHAR
|
||||
);
|
||||
CREATE TYPE weaknesses AS (
|
||||
"title" VARCHAR,
|
||||
"description" TEXT
|
||||
);
|
||||
CREATE TYPE new_hire AS (
|
||||
"name" VARCHAR,
|
||||
"department" VARCHAR,
|
||||
"role" VARCHAR,
|
||||
"impact" TEXT
|
||||
);
|
||||
CREATE TYPE promotion AS (
|
||||
"name" VARCHAR,
|
||||
"from_role" VARCHAR,
|
||||
"to_role" VARCHAR,
|
||||
"impact" TEXT
|
||||
);
|
||||
CREATE TYPE departure AS (
|
||||
"name" VARCHAR,
|
||||
"department" VARCHAR,
|
||||
"reason" TEXT,
|
||||
"impact" TEXT
|
||||
|
||||
);
|
||||
CREATE TYPE personnel_changes AS (
|
||||
"new_hires" new_hire[],
|
||||
"promotions" promotion[],
|
||||
"departures" departure[]
|
||||
);
|
||||
CREATE TYPE immediate_hiring_needs AS (
|
||||
"department" VARCHAR,
|
||||
"role" VARCHAR,
|
||||
"priority" VARCHAR,
|
||||
"reasoning" TEXT
|
||||
);
|
||||
CREATE TYPE forward_operating_plan AS (
|
||||
"title" VARCHAR,
|
||||
"details" TEXT
|
||||
);
|
||||
CREATE TYPE organizational_impact_summary_employee AS (
|
||||
"employee_name" VARCHAR,
|
||||
"impact" TEXT,
|
||||
"description" TEXT,
|
||||
"suggested_pay" double precision
|
||||
);
|
||||
CREATE TYPE organizational_impact_summary AS (
|
||||
"category" VARCHAR,
|
||||
"employees" organizational_impact_summary_employee[]
|
||||
);
|
||||
CREATE TYPE team_score AS (
|
||||
"employee_name" VARCHAR,
|
||||
"grade" VARCHAR,
|
||||
"reliability" double precision,
|
||||
"role_fit" double precision,
|
||||
"scalability" double precision,
|
||||
"output" double precision,
|
||||
"initiative" double precision
|
||||
);
|
||||
CREATE TYPE grading_breakdown AS (
|
||||
"department_name_short" VARCHAR,
|
||||
"department_name" VARCHAR,
|
||||
"lead" VARCHAR,
|
||||
"support" VARCHAR,
|
||||
"department_grade" VARCHAR,
|
||||
"executive_summary" TEXT,
|
||||
"team_scores" team_score[]
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN RAISE NOTICE 'Type department_breakdown_item or company_report_overview already exists, skipping creation.';
|
||||
END
|
||||
$a$ LANGUAGE plpgsql;
|
||||
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS company_reports (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"organization_id" VARCHAR NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
"prompt_used" TEXT,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"overview" company_report_overview,
|
||||
"strengths" TEXT[],
|
||||
"weaknesses" weaknesses[],
|
||||
"personnel_changes" personnel_changes,
|
||||
"immediate_hiring_needs" immediate_hiring_needs[],
|
||||
"forward_operating_plan" forward_operating_plan[],
|
||||
"organizational_impact_summary" organizational_impact_summary[],
|
||||
"grading_breakdown" grading_breakdown[]
|
||||
);
|
||||
131
functions/migrations/migrate.js
Normal file
131
functions/migrations/migrate.js
Normal file
@@ -0,0 +1,131 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { executeQuery, executeTransaction } = require('../database');
|
||||
|
||||
/**
|
||||
* Run all database migrations
|
||||
*/
|
||||
async function runMigrations() {
|
||||
console.log('🚀 Starting database migrations...');
|
||||
|
||||
try {
|
||||
// Create migrations table to track what's been run
|
||||
await executeQuery(`
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
filename VARCHAR(255) NOT NULL UNIQUE,
|
||||
executed_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000
|
||||
);
|
||||
`);
|
||||
|
||||
// Get list of executed migrations
|
||||
const executedMigrations = await executeQuery(
|
||||
'SELECT filename FROM migrations ORDER BY executed_at'
|
||||
);
|
||||
const executedFiles = new Set(executedMigrations.map(m => m.filename));
|
||||
|
||||
// Read migration files
|
||||
const migrationsDir = path.join(__dirname);
|
||||
const migrationFiles = fs.readdirSync(migrationsDir)
|
||||
.filter(file => file.endsWith('.sql'))
|
||||
.sort();
|
||||
|
||||
console.log(`📁 Found ${migrationFiles.length} migration files`);
|
||||
|
||||
for (const file of migrationFiles) {
|
||||
if (executedFiles.has(file)) {
|
||||
console.log(`⏭️ Skipping ${file} (already executed)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`🔄 Executing ${file}...`);
|
||||
|
||||
const filePath = path.join(migrationsDir, file);
|
||||
const sql = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
await executeTransaction(async (client) => {
|
||||
// Execute the migration SQL
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true') {
|
||||
// For Neon serverless, split by statements and execute individually
|
||||
const statements = sql
|
||||
.split(';')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
|
||||
for (const statement of statements) {
|
||||
if (statement.trim()) {
|
||||
await client(statement);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await client.query(sql);
|
||||
}
|
||||
|
||||
// Record that this migration was executed
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true') {
|
||||
await client('INSERT INTO migrations (filename) VALUES ($1)', [file]);
|
||||
} else {
|
||||
await client.query('INSERT INTO migrations (filename) VALUES ($1)', [file]);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Completed ${file}`);
|
||||
}
|
||||
|
||||
console.log('🎉 All migrations completed successfully!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new migration file
|
||||
* @param {string} name - Migration name
|
||||
*/
|
||||
function createMigration(name) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
|
||||
const filename = `${timestamp}_${name}.sql`;
|
||||
const filepath = path.join(__dirname, filename);
|
||||
|
||||
const template = `-- Migration: ${filename}
|
||||
-- Description: ${name}
|
||||
|
||||
-- Add your SQL here
|
||||
|
||||
`;
|
||||
|
||||
fs.writeFileSync(filepath, template);
|
||||
console.log(`📝 Created migration: ${filename}`);
|
||||
return filename;
|
||||
}
|
||||
|
||||
// Run migrations if this file is executed directly
|
||||
if (require.main === module) {
|
||||
const command = process.argv[2];
|
||||
|
||||
if (command === 'create') {
|
||||
const name = process.argv[3];
|
||||
if (!name) {
|
||||
console.error('❌ Please provide a migration name: npm run db:migrate create migration_name');
|
||||
process.exit(1);
|
||||
}
|
||||
createMigration(name);
|
||||
} else {
|
||||
runMigrations()
|
||||
.then(() => {
|
||||
console.log('🏁 Migration process completed');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('💥 Migration process failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runMigrations,
|
||||
createMigration
|
||||
};
|
||||
@@ -1,25 +1,29 @@
|
||||
{
|
||||
"name": "functions",
|
||||
"description": "Cloud Functions for Firebase",
|
||||
"scripts": {
|
||||
"serve": "firebase emulators:start --only functions",
|
||||
"shell": "firebase functions:shell",
|
||||
"start": "npm run shell",
|
||||
"deploy": "firebase deploy --only functions",
|
||||
"logs": "firebase functions:log"
|
||||
},
|
||||
"engines": {
|
||||
"node": "22"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@google-cloud/vertexai": "^1.10.0",
|
||||
"firebase-admin": "^12.7.0",
|
||||
"firebase-functions": "^6.4.0",
|
||||
"stripe": "^18.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"firebase-functions-test": "^3.4.1"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
"name": "functions",
|
||||
"description": "NeonDB Backend Functions",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"node": "22"
|
||||
},
|
||||
"scripts": {
|
||||
"deploy": "firebase deploy --only functions",
|
||||
"logs": "firebase functions:log",
|
||||
"serve": "firebase emulators:start --only functions",
|
||||
"shell": "firebase functions:shell",
|
||||
"start": "npm run shell",
|
||||
"db:migrate": "node migrations/migrate.js",
|
||||
"db:seed": "node migrations/seed.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/vertexai": "^1.10.0",
|
||||
"@neondatabase/serverless": "^0.9.5",
|
||||
"pg": "^8.12.0",
|
||||
"stripe": "^18.5.0",
|
||||
"firebase-functions": "^6.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.11.6",
|
||||
"firebase-functions-test": "^3.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user