feat: major UI overhaul with new components and enhanced UX

- Add comprehensive Company Wiki feature with complete state management
  - CompanyWikiManager, empty states, invite modals
- Implement new Chat system with enhanced layout and components
  - ChatLayout, ChatSidebar, MessageThread, FileUploadInput
- Create modern Login and OTP verification flows
  - LoginNew page, OTPVerification component
- Add new Employee Forms system with enhanced controller
- Introduce Figma-based design components and multiple choice inputs
- Add new font assets (NeueMontreal) and robot images for onboarding
- Enhance existing components with improved styling and functionality
- Update build configuration and dependencies
- Remove deprecated ModernLogin component
This commit is contained in:
Ra
2025-08-20 03:30:04 -07:00
parent 1a9e92d7bd
commit cf565df13e
47 changed files with 6654 additions and 2007 deletions

View File

@@ -4,8 +4,8 @@
"": {
"name": "auditly-functions",
"dependencies": {
"firebase-admin": "^13.4.0",
"firebase-functions": "^4.5.0",
"firebase-admin": "^12.1.1",
"firebase-functions": "^5.0.1",
"openai": "^5.12.2",
"stripe": "^18.4.0",
},
@@ -95,23 +95,23 @@
"@fastify/busboy": ["@fastify/busboy@3.2.0", "", {}, "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA=="],
"@firebase/app-check-interop-types": ["@firebase/app-check-interop-types@0.3.3", "", {}, "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A=="],
"@firebase/app-check-interop-types": ["@firebase/app-check-interop-types@0.3.2", "", {}, "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ=="],
"@firebase/app-types": ["@firebase/app-types@0.9.3", "", {}, "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw=="],
"@firebase/app-types": ["@firebase/app-types@0.9.2", "", {}, "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ=="],
"@firebase/auth-interop-types": ["@firebase/auth-interop-types@0.2.4", "", {}, "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA=="],
"@firebase/auth-interop-types": ["@firebase/auth-interop-types@0.2.3", "", {}, "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ=="],
"@firebase/component": ["@firebase/component@0.7.0", "", { "dependencies": { "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg=="],
"@firebase/component": ["@firebase/component@0.6.9", "", { "dependencies": { "@firebase/util": "1.10.0", "tslib": "^2.1.0" } }, "sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q=="],
"@firebase/database": ["@firebase/database@1.1.0", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "faye-websocket": "0.11.4", "tslib": "^2.1.0" } }, "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg=="],
"@firebase/database": ["@firebase/database@1.0.8", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.2", "@firebase/auth-interop-types": "0.2.3", "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "faye-websocket": "0.11.4", "tslib": "^2.1.0" } }, "sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg=="],
"@firebase/database-compat": ["@firebase/database-compat@2.1.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/database": "1.1.0", "@firebase/database-types": "1.0.16", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg=="],
"@firebase/database-compat": ["@firebase/database-compat@1.0.8", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/database": "1.0.8", "@firebase/database-types": "1.0.5", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" } }, "sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg=="],
"@firebase/database-types": ["@firebase/database-types@1.0.16", "", { "dependencies": { "@firebase/app-types": "0.9.3", "@firebase/util": "1.13.0" } }, "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw=="],
"@firebase/database-types": ["@firebase/database-types@1.0.5", "", { "dependencies": { "@firebase/app-types": "0.9.2", "@firebase/util": "1.10.0" } }, "sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ=="],
"@firebase/logger": ["@firebase/logger@0.5.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g=="],
"@firebase/logger": ["@firebase/logger@0.4.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A=="],
"@firebase/util": ["@firebase/util@1.13.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ=="],
"@firebase/util": ["@firebase/util@1.10.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ=="],
"@google-cloud/firestore": ["@google-cloud/firestore@7.11.3", "", { "dependencies": { "@opentelemetry/api": "^1.3.0", "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", "google-gax": "^4.3.3", "protobufjs": "^7.2.6" } }, "sha512-qsM3/WHpawF07SRVvEJJVRwhYzM7o9qtuksyuqnrMig6fxIrwWnsezECWsG/D5TyYru51Fv5c/RTqNDQ2yU+4w=="],
@@ -501,9 +501,9 @@
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"firebase-admin": ["firebase-admin@13.4.0", "", { "dependencies": { "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "^2.0.0", "@firebase/database-types": "^1.0.6", "@types/node": "^22.8.7", "farmhash-modern": "^1.1.0", "google-auth-library": "^9.14.2", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", "uuid": "^11.0.2" }, "optionalDependencies": { "@google-cloud/firestore": "^7.11.0", "@google-cloud/storage": "^7.14.0" } }, "sha512-Y8DcyKK+4pl4B93ooiy1G8qvdyRMkcNFfBSh+8rbVcw4cW8dgG0VXCCTp5NUwub8sn9vSPsOwpb9tE2OuFmcfQ=="],
"firebase-admin": ["firebase-admin@12.7.0", "", { "dependencies": { "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "1.0.8", "@firebase/database-types": "1.0.5", "@types/node": "^22.0.1", "farmhash-modern": "^1.1.0", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", "uuid": "^10.0.0" }, "optionalDependencies": { "@google-cloud/firestore": "^7.7.0", "@google-cloud/storage": "^7.7.0" } }, "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA=="],
"firebase-functions": ["firebase-functions@4.9.0", "", { "dependencies": { "@types/cors": "^2.8.5", "@types/express": "4.17.3", "cors": "^2.8.5", "express": "^4.17.1", "protobufjs": "^7.2.2" }, "peerDependencies": { "firebase-admin": "^10.0.0 || ^11.0.0 || ^12.0.0" }, "bin": { "firebase-functions": "lib/bin/firebase-functions.js" } }, "sha512-IqxOEsVAWGcRv9KRGzWQR5mOFuNsil3vsfkRPPiaV1U/ATC27/jbahh4z8I4rW8Xqa6cQE5xqnw0ueyMH7i7Ag=="],
"firebase-functions": ["firebase-functions@5.1.1", "", { "dependencies": { "@types/cors": "^2.8.5", "@types/express": "4.17.3", "cors": "^2.8.5", "express": "^4.17.1", "protobufjs": "^7.2.2" }, "peerDependencies": { "firebase-admin": "^11.10.0 || ^12.0.0" }, "bin": { "firebase-functions": "lib/bin/firebase-functions.js" } }, "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg=="],
"firebase-functions-test": ["firebase-functions-test@3.4.1", "", { "dependencies": { "@types/lodash": "^4.14.104", "lodash": "^4.17.5", "ts-deepmerge": "^2.0.1" }, "peerDependencies": { "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0", "firebase-functions": ">=4.9.0", "jest": ">=28.0.0" } }, "sha512-qAq0oszrBGdf4bnCF6t4FoSgMsepeIXh0Pi/FhikSE6e+TvKKGpfrfUP/5pFjJZxFcLsweoau88KydCql4xSeg=="],
@@ -677,11 +677,11 @@
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"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=="],
"jwks-rsa": ["jwks-rsa@3.2.0", "", { "dependencies": { "@types/express": "^4.17.20", "@types/jsonwebtoken": "^9.0.4", "debug": "^4.3.4", "jose": "^4.15.4", "limiter": "^1.1.5", "lru-memoizer": "^2.2.0" } }, "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww=="],
"jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
"jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
@@ -943,7 +943,7 @@
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
"v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="],
@@ -1007,14 +1007,16 @@
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"google-auth-library/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
"google-gax/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"gtoken/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
"http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"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/@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -1053,7 +1055,9 @@
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"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=="],
"google-auth-library/jws/jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"gtoken/jws/jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],

View File

@@ -1,10 +1,10 @@
const { onRequest } = require("firebase-functions/v2/https");
const admin = require("firebase-admin");
const serviceAccount = require("./auditly-c0027-firebase-adminsdk-fbsvc-1db7c58141.json");
const functions = require("firebase-functions");
const OpenAI = require("openai");
const Stripe = require("stripe");
const serviceAccount = require("./auditly-c0027-firebase-adminsdk-fbsvc-1db7c58141.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
@@ -177,206 +177,179 @@ const generateOTP = () => {
return Math.floor(100000 + Math.random() * 900000).toString();
};
// CORS middleware
const cors = (req, res, next) => {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.set('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.set('Access-Control-Max-Age', '3600');
if (req.method === 'OPTIONS') {
res.status(204).send('');
return;
}
next();
};
// Helper function to set CORS headers
const setCorsHeaders = (res) => {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.set('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.set('Access-Control-Max-Age', '3600');
};
// Send OTP Function
exports.sendOTP = functions.https.onRequest((req, res) => {
cors(req, res, async () => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
exports.sendOTP = onRequest({ cors: true }, async (req, res) => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const { email, inviteCode } = req.body;
const { email, inviteCode } = req.body;
if (!email) {
return res.status(400).json({ error: "Email is required" });
}
if (!email) {
return res.status(400).json({ error: "Email is required" });
}
try {
// Generate OTP
const otp = generateOTP();
const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes expiry
try {
// Generate OTP
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 Firestore
await db.collection("otps").doc(email).set({
otp,
expiresAt,
attempts: 0,
inviteCode: inviteCode || null,
createdAt: Date.now(),
});
// In production, send actual email
console.log(`📧 OTP for ${email}: ${otp} (expires in 5 minutes)`);
// In production, send actual email
console.log(`📧 OTP for ${email}: ${otp} (expires in 5 minutes)`);
res.json({
success: true,
message: "Verification code sent to your email",
// Always include OTP in emulator mode for testing
otp,
});
} catch (error) {
console.error("Send OTP error:", error);
res.status(500).json({ error: "Failed to send verification code" });
}
});
res.json({
success: true,
message: "Verification code sent to your email",
// Always include OTP in emulator mode for testing
otp,
});
} catch (error) {
console.error("Send OTP error:", error);
res.status(500).json({ error: "Failed to send verification code" });
}
});
// Verify OTP Function
exports.verifyOTP = functions.https.onRequest((req, res) => {
cors(req, res, async () => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
exports.verifyOTP = onRequest({ cors: true }, async (req, res) => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const { email, otp } = req.body;
if (!email || !otp) {
return res.status(400).json({ error: "Email and OTP are required" });
}
try {
// Retrieve OTP document
const otpDoc = await db.collection("otps").doc(email).get();
if (!otpDoc.exists) {
return res.status(400).json({ error: "Invalid verification code" });
}
const { email, otp } = req.body;
const otpData = otpDoc.data();
if (!email || !otp) {
return res.status(400).json({ error: "Email and OTP are required" });
}
try {
// Retrieve OTP document
const otpDoc = await db.collection("otps").doc(email).get();
if (!otpDoc.exists) {
return res.status(400).json({ error: "Invalid verification code" });
}
const otpData = otpDoc.data();
// Check if OTP is expired
if (Date.now() > otpData.expiresAt) {
await otpDoc.ref.delete();
return res.status(400).json({ error: "Verification code has expired" });
}
// Check if too many attempts
if (otpData.attempts >= 5) {
await otpDoc.ref.delete();
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,
});
return res.status(400).json({ error: "Invalid verification code" });
}
// OTP is valid - clean up and create/find user
// Check if OTP is expired
if (Date.now() > otpData.expiresAt) {
await otpDoc.ref.delete();
// Generate a unique user ID for this email if it doesn't exist
let userId;
let userDoc;
// Check if user already exists by email
const existingUserQuery = await db.collection("users")
.where("email", "==", email)
.limit(1)
.get();
if (!existingUserQuery.empty) {
// User exists, get their ID
userDoc = existingUserQuery.docs[0];
userId = userDoc.id;
} else {
// Create new user
userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
userDoc = null;
}
// Prepare user object for response
const user = {
uid: userId,
email: email,
displayName: email.split("@")[0],
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 new user document
userData.createdAt = Date.now();
await userRef.set(userData);
} else {
// Update existing user with latest login info
await userRef.update({
lastLoginAt: Date.now(),
});
}
// Generate a simple session token (in production, use proper JWT)
const customToken = `session_${userId}_${Date.now()}`;
// Handle invitation if present
let inviteData = null;
if (otpData.inviteCode) {
try {
const inviteDoc = await db
.collectionGroup("invites")
.where("code", "==", otpData.inviteCode)
.where("status", "==", "pending")
.limit(1)
.get();
if (!inviteDoc.empty) {
inviteData = inviteDoc.docs[0].data();
}
} catch (error) {
console.error("Error fetching invite:", error);
}
}
res.json({
success: true,
user,
token: customToken,
invite: inviteData,
});
} catch (error) {
console.error("Verify OTP error:", error);
res.status(500).json({ error: "Failed to verify code" });
return res.status(400).json({ error: "Verification code has expired" });
}
});
// Check if too many attempts
if (otpData.attempts >= 5) {
await otpDoc.ref.delete();
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,
});
return res.status(400).json({ error: "Invalid verification code" });
}
// OTP is valid - clean up and create/find user
await otpDoc.ref.delete();
// Generate a unique user ID for this email if it doesn't exist
let userId;
let userDoc;
// Check if user already exists by email
const existingUserQuery = await db.collection("users")
.where("email", "==", email)
.limit(1)
.get();
if (!existingUserQuery.empty) {
// User exists, get their ID
userDoc = existingUserQuery.docs[0];
userId = userDoc.id;
} else {
// Create new user
userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
userDoc = null;
}
// Prepare user object for response
const user = {
uid: userId,
email: email,
displayName: email.split("@")[0],
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 new user document
userData.createdAt = Date.now();
await userRef.set(userData);
} else {
// Update existing user with latest login info
await userRef.update({
lastLoginAt: Date.now(),
});
}
// Generate a simple session token (in production, use proper JWT)
const customToken = `session_${userId}_${Date.now()}`;
// Handle invitation if present
let inviteData = null;
if (otpData.inviteCode) {
try {
const inviteDoc = await db
.collectionGroup("invites")
.where("code", "==", otpData.inviteCode)
.where("status", "==", "pending")
.limit(1)
.get();
if (!inviteDoc.empty) {
inviteData = inviteDoc.docs[0].data();
}
} catch (error) {
console.error("Error fetching invite:", error);
}
}
res.json({
success: true,
user,
token: customToken,
invite: inviteData,
});
} catch (error) {
console.error("Verify OTP error:", error);
res.status(500).json({ error: "Failed to verify code" });
}
});
// Create Invitation Function
exports.createInvitation = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.createInvitation = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
@@ -396,7 +369,7 @@ exports.createInvitation = functions.https.onRequest(async (req, res) => {
try {
// 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)}`;
@@ -429,7 +402,7 @@ exports.createInvitation = functions.https.onRequest(async (req, res) => {
});
// Generate invite links
const baseUrl = process.env.CLIENT_URL || 'http://localhost:5174';
const baseUrl = process.env.CLIENT_URL || 'http://localhost:5173';
const inviteLink = `${baseUrl}/#/employee-form/${code}`;
const emailLink = `mailto:${email}?subject=You're invited to join our organization&body=Hi ${name},%0A%0AYou've been invited to complete a questionnaire for our organization. Please click the link below to get started:%0A%0A${inviteLink}%0A%0AThis link will expire in 7 days.%0A%0AThank you!`;
@@ -452,9 +425,7 @@ exports.createInvitation = functions.https.onRequest(async (req, res) => {
});
// Get Invitation Status Function
exports.getInvitationStatus = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.getInvitationStatus = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
return;
@@ -501,9 +472,7 @@ exports.getInvitationStatus = functions.https.onRequest(async (req, res) => {
});
// Consume Invitation Function
exports.consumeInvitation = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.consumeInvitation = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
return;
@@ -581,8 +550,7 @@ exports.consumeInvitation = functions.https.onRequest(async (req, res) => {
});
// Submit Employee Answers Function
exports.submitEmployeeAnswers = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
@@ -659,8 +627,7 @@ exports.submitEmployeeAnswers = functions.https.onRequest(async (req, res) => {
});
// Generate Employee Report Function
exports.generateEmployeeReport = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.generateEmployeeReport = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
@@ -794,9 +761,7 @@ Return ONLY valid JSON that matches this structure. Be thorough but professional
});
// Generate Company Wiki Function
exports.generateCompanyWiki = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
return;
@@ -840,6 +805,8 @@ exports.generateCompanyWiki = functions.https.onRequest(async (req, res) => {
});
// content is guaranteed to be schema-conformant JSON
console.log(completion.choices[0].message);
console.log(completion.choices[0].message.content);
const parsed = JSON.parse(completion.choices[0].message.content);
const report = {
@@ -852,6 +819,9 @@ exports.generateCompanyWiki = functions.https.onRequest(async (req, res) => {
generatedAt: Date.now(),
...parsed.wiki,
};
console.log(report);
console.log(wiki);
} else {
// Fallback to mock data when OpenAI is not available
report = {
@@ -921,8 +891,8 @@ exports.generateCompanyWiki = functions.https.onRequest(async (req, res) => {
res.json({
success: true,
report,
wiki,
...report,
...wiki,
});
} catch (error) {
console.error("Generate company wiki error:", error);
@@ -931,8 +901,7 @@ exports.generateCompanyWiki = functions.https.onRequest(async (req, res) => {
});
// Chat Function
exports.chat = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.chat = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
@@ -1007,9 +976,7 @@ Provide helpful, actionable insights while maintaining professional confidential
});
// Create Organization Function
exports.createOrganization = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.createOrganization = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
return;
@@ -1121,8 +1088,7 @@ exports.createOrganization = functions.https.onRequest(async (req, res) => {
});
// Get User Organizations Function
exports.getUserOrganizations = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.getUserOrganizations = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
@@ -1166,9 +1132,7 @@ exports.getUserOrganizations = functions.https.onRequest(async (req, res) => {
});
// Join Organization Function (via invite)
exports.joinOrganization = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.joinOrganization = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
return;
@@ -1282,9 +1246,7 @@ exports.joinOrganization = functions.https.onRequest(async (req, res) => {
});
// Create Stripe Checkout Session Function
exports.createCheckoutSession = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.createCheckoutSession = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
return;
@@ -1374,7 +1336,7 @@ exports.createCheckoutSession = functions.https.onRequest(async (req, res) => {
});
// Handle Stripe Webhook Function
exports.stripeWebhook = functions.https.onRequest(async (req, res) => {
exports.stripeWebhook = onRequest(async (req, res) => {
if (!stripe) {
return res.status(500).send('Stripe not configured');
}
@@ -1385,7 +1347,7 @@ exports.stripeWebhook = functions.https.onRequest(async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
@@ -1424,9 +1386,7 @@ exports.stripeWebhook = functions.https.onRequest(async (req, res) => {
});
// Get Subscription Status Function
exports.getSubscriptionStatus = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.getSubscriptionStatus = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
return;
@@ -1652,9 +1612,7 @@ async function handlePaymentFailed(invoice) {
// });
// Save Company Report Function
exports.saveCompanyReport = functions.https.onRequest(async (req, res) => {
setCorsHeaders(res);
exports.saveCompanyReport = onRequest({ cors: true }, async (req, res) => {
if (req.method === 'OPTIONS') {
res.status(204).send('');
return;

View File

@@ -13,8 +13,8 @@
},
"main": "index.js",
"dependencies": {
"firebase-admin": "^13.4.0",
"firebase-functions": "^4.5.0",
"firebase-admin": "^12.1.1",
"firebase-functions": "^5.0.1",
"openai": "^5.12.2",
"stripe": "^18.4.0"
},