From f19f5dca8e83c386021260c93f20820458cf105f Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+Berry-13@users.noreply.github.com> Date: Sat, 30 Dec 2023 03:42:19 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=F0=9F=9A=80=20feat:=20CDN=20(Fireb?= =?UTF-8?q?ase)=20&=20feat:=20account=20section=20(#1438)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * localization + api-endpoint * docs: added firebase documentation * chore: icons * chore: SettingsTabs * feat: account pannel; fix: gear icons * docs: position update * feat: firebase * feat: plugin support * route * fixed bugs with firebase and moved a lot of files * chore(DALLE3): using UUID v4 * feat: support for social strategies; moved '/images' path * fix: data ignored * gitignore update * docs: update firebase guide * refactor: Firebase - use singleton pattern for firebase initialization, initially on server start - reorganize imports, move firebase specific files to own service under Files - rename modules to remove 'avatar' redundancy - fix imports based on changes * ci(DALLE/DALLE3): fix tests to use logger and new expected outputs, add firebase tests * refactor(loadToolWithAuth): pass userId to tool as field * feat(images/parse): feat: Add URL Image Basename Extraction Implement a new module to extract the basename of an image from a given URL. This addition includes the function, which parses the URL and retrieves the basename using the Node.js 'url' and 'path' modules. The function is documented with JSDoc comments for better maintainability and understanding. This feature enhances the application's ability to handle and process image URLs efficiently. * refactor(addImages): function to use a more specific regular expression for observedImagePath based on the generated image markdown standard across the app * refactor(DALLE/DALLE3): utilize `getImageBasename` and `this.userId`; fix: pass correct image path to firebase url helper * fix(addImages): make more general to match any image markdown descriptor * fix(parse/getImageBasename): test result of this function for an actual image basename * ci(DALLE3): mock getImageBasename * refactor(AuthContext): use Recoil atom state for user * feat: useUploadAvatarMutation, react-query hook for avatar upload * fix(Toast): stack z-order of Toast over all components (1000) * refactor(showToast): add optional status field to avoid importing NotificationSeverity on each use of the function * refactor(routes/avatar): remove unnecessary get route, get userId from req.user.id, require auth on POST request * chore(uploadAvatar): TODO: remove direct use of Model, `User` * fix(client): fix Spinner imports * refactor(Avatar): use react-query hook, Toast, remove unnecessary states, add optimistic UI to upload * fix(avatar/localStrategy): correctly save local profile picture and cache bust for immediate rendering; fix: firebase init info message (only show once) * fix: use `includes` instead of `endsWith` for checking manual query of avatar image path in case more queries are appended (as is done in avatar/localStrategy) --------- Co-authored-by: Danny Avila --- .env.example | 11 + .gitignore | 4 +- api/app/clients/output_parsers/addImages.js | 6 +- api/app/clients/tools/DALL-E.js | 57 +- api/app/clients/tools/structured/DALLE3.js | 49 +- .../tools/structured/specs/DALLE3.spec.js | 74 +- api/app/clients/tools/util/handleTools.js | 6 +- api/package.json | 1 + api/server/index.js | 2 + api/server/routes/files/avatar.js | 34 + api/server/routes/files/index.js | 1 + api/server/services/Files/Firebase/images.js | 45 + api/server/services/Files/Firebase/index.js | 7 + .../services/Files/Firebase/initialize.js | 42 + .../Files/images/avatar/firebaseStrategy.js | 29 + .../Files/images/avatar/localStrategy.js | 32 + .../Files/images/avatar/uploadAvatar.js | 63 ++ api/server/services/Files/images/index.js | 4 + api/server/services/Files/images/parse.js | 27 + api/strategies/discordStrategy.js | 63 +- api/strategies/facebookStrategy.js | 57 +- api/strategies/githubStrategy.js | 53 +- api/strategies/googleStrategy.js | 53 +- client/src/App.jsx | 2 +- client/src/common/types.ts | 1 + client/src/components/Chat/ChatView.tsx | 2 +- client/src/components/Nav/Nav.tsx | 2 +- client/src/components/Nav/NavLinks.tsx | 2 +- client/src/components/Nav/Settings.tsx | 21 +- .../Nav/SettingsTabs/Account/Account.tsx | 18 + .../Nav/SettingsTabs/Account/Avatar.tsx | 145 +++ .../Nav/SettingsTabs/{ => Data}/Data.tsx | 2 +- .../{ => General}/AutoScrollSwitch.spec.tsx | 0 .../{ => General}/AutoScrollSwitch.tsx | 0 .../{ => General}/ClearChatsButton.spec.tsx | 0 .../SettingsTabs/{ => General}/General.tsx | 2 +- .../{ => General}/LangSelector.spec.tsx | 0 .../{ => General}/ThemeSelector.spec.tsx | 0 .../src/components/Nav/SettingsTabs/index.ts | 9 +- client/src/components/svg/GearIcon.tsx | 16 +- client/src/components/svg/UserIcon.tsx | 15 +- client/src/components/svg/index.ts | 1 + client/src/data-provider/mutations.ts | 17 + client/src/hooks/AuthContext.tsx | 10 +- client/src/hooks/useToast.ts | 8 +- client/src/localization/languages/Eng.tsx | 9 +- client/src/localization/languages/It.tsx | 7 +- client/src/store/user.ts | 6 +- docs/features/bing_jailbreak.md | 2 +- docs/features/firebase.md | 74 ++ docs/features/index.md | 1 + docs/features/logging_system.md | 2 +- docs/features/manage_your_database.md | 2 +- docs/features/pandoranext.md | 2 +- package-lock.json | 902 +++++++++++++++++- packages/data-provider/src/api-endpoints.ts | 2 + packages/data-provider/src/data-service.ts | 4 + packages/data-provider/src/keys.ts | 1 + packages/data-provider/src/types/files.ts | 10 + 59 files changed, 1850 insertions(+), 167 deletions(-) create mode 100644 api/server/routes/files/avatar.js create mode 100644 api/server/services/Files/Firebase/images.js create mode 100644 api/server/services/Files/Firebase/index.js create mode 100644 api/server/services/Files/Firebase/initialize.js create mode 100644 api/server/services/Files/images/avatar/firebaseStrategy.js create mode 100644 api/server/services/Files/images/avatar/localStrategy.js create mode 100644 api/server/services/Files/images/avatar/uploadAvatar.js create mode 100644 api/server/services/Files/images/parse.js create mode 100644 client/src/components/Nav/SettingsTabs/Account/Account.tsx create mode 100644 client/src/components/Nav/SettingsTabs/Account/Avatar.tsx rename client/src/components/Nav/SettingsTabs/{ => Data}/Data.tsx (98%) rename client/src/components/Nav/SettingsTabs/{ => General}/AutoScrollSwitch.spec.tsx (100%) rename client/src/components/Nav/SettingsTabs/{ => General}/AutoScrollSwitch.tsx (100%) rename client/src/components/Nav/SettingsTabs/{ => General}/ClearChatsButton.spec.tsx (100%) rename client/src/components/Nav/SettingsTabs/{ => General}/General.tsx (99%) rename client/src/components/Nav/SettingsTabs/{ => General}/LangSelector.spec.tsx (100%) rename client/src/components/Nav/SettingsTabs/{ => General}/ThemeSelector.spec.tsx (100%) create mode 100644 docs/features/firebase.md diff --git a/.env.example b/.env.example index c3c62becd53..0c9d303d6cc 100644 --- a/.env.example +++ b/.env.example @@ -281,6 +281,17 @@ EMAIL_PASSWORD= EMAIL_FROM_NAME= EMAIL_FROM=noreply@librechat.ai +#========================# +# Firebase CDN # +#========================# + +FIREBASE_API_KEY= +FIREBASE_AUTH_DOMAIN= +FIREBASE_PROJECT_ID= +FIREBASE_STORAGE_BUCKET= +FIREBASE_MESSAGING_SENDER_ID= +FIREBASE_APP_ID= + #==================================================# # Others # #==================================================# diff --git a/.gitignore b/.gitignore index af92a1f2daf..f360cbba0ac 100644 --- a/.gitignore +++ b/.gitignore @@ -82,4 +82,6 @@ data.ms/* auth.json /packages/ux-shared/ -/images \ No newline at end of file +/images + +!client/src/components/Nav/SettingsTabs/Data/ \ No newline at end of file diff --git a/api/app/clients/output_parsers/addImages.js b/api/app/clients/output_parsers/addImages.js index 38ceb9a6860..ec04bcac86c 100644 --- a/api/app/clients/output_parsers/addImages.js +++ b/api/app/clients/output_parsers/addImages.js @@ -60,12 +60,10 @@ function addImages(intermediateSteps, responseMessage) { if (!observation || !observation.includes('![')) { return; } - const observedImagePath = observation.match(/\(\/images\/.*\.\w*\)/g); + const observedImagePath = observation.match(/!\[.*\]\([^)]*\)/g); if (observedImagePath && !responseMessage.text.includes(observedImagePath[0])) { responseMessage.text += '\n' + observation; - if (process.env.DEBUG_PLUGINS) { - logger.debug('[addImages] added image from intermediateSteps:', observation); - } + logger.debug('[addImages] added image from intermediateSteps:', observation); } }); } diff --git a/api/app/clients/tools/DALL-E.js b/api/app/clients/tools/DALL-E.js index 88a7cf850ac..387294a1cbb 100644 --- a/api/app/clients/tools/DALL-E.js +++ b/api/app/clients/tools/DALL-E.js @@ -4,8 +4,15 @@ const fs = require('fs'); const path = require('path'); const OpenAI = require('openai'); // const { genAzureEndpoint } = require('~/utils/genAzureEndpoints'); +const { v4: uuidv4 } = require('uuid'); const { Tool } = require('langchain/tools'); const { HttpsProxyAgent } = require('https-proxy-agent'); +const { + saveImageToFirebaseStorage, + getFirebaseStorageImageUrl, + getFirebaseStorage, +} = require('~/server/services/Files/Firebase'); +const { getImageBasename } = require('~/server/services/Files/images'); const extractBaseURL = require('~/utils/extractBaseURL'); const saveImageFromUrl = require('./saveImageFromUrl'); const { logger } = require('~/config'); @@ -15,7 +22,9 @@ class OpenAICreateImage extends Tool { constructor(fields = {}) { super(); + this.userId = fields.userId; let apiKey = fields.DALLE_API_KEY || this.getApiKey(); + const config = { apiKey }; if (DALLE_REVERSE_PROXY) { config.baseURL = extractBaseURL(DALLE_REVERSE_PROXY); @@ -24,7 +33,6 @@ class OpenAICreateImage extends Tool { if (PROXY) { config.httpAgent = new HttpsProxyAgent(PROXY); } - // let azureKey = fields.AZURE_API_KEY || process.env.AZURE_API_KEY; // if (azureKey) { @@ -97,12 +105,11 @@ Guidelines: throw new Error('No image URL returned from OpenAI API.'); } - const regex = /img-[\w\d]+.png/; - const match = theImageUrl.match(regex); - let imageName = '1.png'; + const imageBasename = getImageBasename(theImageUrl); + let imageName = `image_${uuidv4()}.png`; - if (match) { - imageName = match[0]; + if (imageBasename) { + imageName = imageBasename; logger.debug('[DALL-E]', { imageName }); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png } else { logger.debug('[DALL-E] No image name found in the string.', { @@ -111,7 +118,18 @@ Guidelines: }); } - this.outputPath = path.resolve(__dirname, '..', '..', '..', '..', 'client', 'public', 'images'); + this.outputPath = path.resolve( + __dirname, + '..', + '..', + '..', + '..', + 'client', + 'public', + 'images', + this.userId, + ); + const appRoot = path.resolve(__dirname, '..', '..', '..', '..', 'client'); this.relativeImageUrl = path.relative(appRoot, this.outputPath); @@ -120,14 +138,25 @@ Guidelines: fs.mkdirSync(this.outputPath, { recursive: true }); } - try { - await saveImageFromUrl(theImageUrl, this.outputPath, imageName); - this.result = this.getMarkdownImageUrl(imageName); - } catch (error) { - logger.error('Error while saving the DALL-E image:', error); - this.result = theImageUrl; + const storage = getFirebaseStorage(); + if (storage) { + try { + await saveImageToFirebaseStorage(this.userId, theImageUrl, imageName); + this.result = await getFirebaseStorageImageUrl(`${this.userId}/${imageName}`); + logger.debug('[DALL-E] result: ' + this.result); + } catch (error) { + logger.error('Error while saving the image to Firebase Storage:', error); + this.result = `Failed to save the image to Firebase Storage. ${error.message}`; + } + } else { + try { + await saveImageFromUrl(theImageUrl, this.outputPath, imageName); + this.result = this.getMarkdownImageUrl(imageName); + } catch (error) { + logger.error('Error while saving the image locally:', error); + this.result = `Failed to save the image locally. ${error.message}`; + } } - return this.result; } } diff --git a/api/app/clients/tools/structured/DALLE3.js b/api/app/clients/tools/structured/DALLE3.js index dc5750a6892..17d0368f395 100644 --- a/api/app/clients/tools/structured/DALLE3.js +++ b/api/app/clients/tools/structured/DALLE3.js @@ -4,10 +4,17 @@ const fs = require('fs'); const path = require('path'); const { z } = require('zod'); const OpenAI = require('openai'); +const { v4: uuidv4 } = require('uuid'); const { Tool } = require('langchain/tools'); const { HttpsProxyAgent } = require('https-proxy-agent'); -const saveImageFromUrl = require('../saveImageFromUrl'); +const { + saveImageToFirebaseStorage, + getFirebaseStorageImageUrl, + getFirebaseStorage, +} = require('~/server/services/Files/Firebase'); +const { getImageBasename } = require('~/server/services/Files/images'); const extractBaseURL = require('~/utils/extractBaseURL'); +const saveImageFromUrl = require('../saveImageFromUrl'); const { logger } = require('~/config'); const { DALLE3_SYSTEM_PROMPT, DALLE_REVERSE_PROXY, PROXY } = process.env; @@ -15,6 +22,7 @@ class DALLE3 extends Tool { constructor(fields = {}) { super(); + this.userId = fields.userId; let apiKey = fields.DALLE_API_KEY || this.getApiKey(); const config = { apiKey }; if (DALLE_REVERSE_PROXY) { @@ -108,12 +116,12 @@ class DALLE3 extends Tool { n: 1, }); } catch (error) { - return `Something went wrong when trying to generate the image. The DALL-E API may unavailable: + return `Something went wrong when trying to generate the image. The DALL-E API may be unavailable: Error Message: ${error.message}`; } if (!resp) { - return 'Something went wrong when trying to generate the image. The DALL-E API may unavailable'; + return 'Something went wrong when trying to generate the image. The DALL-E API may be unavailable'; } const theImageUrl = resp.data[0].url; @@ -122,12 +130,11 @@ Error Message: ${error.message}`; return 'No image URL returned from OpenAI API. There may be a problem with the API or your configuration.'; } - const regex = /img-[\w\d]+.png/; - const match = theImageUrl.match(regex); - let imageName = '1.png'; + const imageBasename = getImageBasename(theImageUrl); + let imageName = `image_${uuidv4()}.png`; - if (match) { - imageName = match[0]; + if (imageBasename) { + imageName = imageBasename; logger.debug('[DALL-E-3]', { imageName }); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png } else { logger.debug('[DALL-E-3] No image name found in the string.', { @@ -146,6 +153,7 @@ Error Message: ${error.message}`; 'client', 'public', 'images', + this.userId, ); const appRoot = path.resolve(__dirname, '..', '..', '..', '..', '..', 'client'); this.relativeImageUrl = path.relative(appRoot, this.outputPath); @@ -154,13 +162,24 @@ Error Message: ${error.message}`; if (!fs.existsSync(this.outputPath)) { fs.mkdirSync(this.outputPath, { recursive: true }); } - - try { - await saveImageFromUrl(theImageUrl, this.outputPath, imageName); - this.result = this.getMarkdownImageUrl(imageName); - } catch (error) { - logger.error('Error while saving the image:', error); - this.result = theImageUrl; + const storage = getFirebaseStorage(); + if (storage) { + try { + await saveImageToFirebaseStorage(this.userId, theImageUrl, imageName); + this.result = await getFirebaseStorageImageUrl(`${this.userId}/${imageName}`); + logger.debug('[DALL-E-3] result: ' + this.result); + } catch (error) { + logger.error('Error while saving the image to Firebase Storage:', error); + this.result = `Failed to save the image to Firebase Storage. ${error.message}`; + } + } else { + try { + await saveImageFromUrl(theImageUrl, this.outputPath, imageName); + this.result = this.getMarkdownImageUrl(imageName); + } catch (error) { + logger.error('Error while saving the image locally:', error); + this.result = `Failed to save the image locally. ${error.message}`; + } } return this.result; diff --git a/api/app/clients/tools/structured/specs/DALLE3.spec.js b/api/app/clients/tools/structured/specs/DALLE3.spec.js index c61e1d35130..34fa3ebf00a 100644 --- a/api/app/clients/tools/structured/specs/DALLE3.spec.js +++ b/api/app/clients/tools/structured/specs/DALLE3.spec.js @@ -2,11 +2,40 @@ const fs = require('fs'); const path = require('path'); const OpenAI = require('openai'); const DALLE3 = require('../DALLE3'); +const { + getFirebaseStorage, + saveImageToFirebaseStorage, +} = require('~/server/services/Files/Firebase'); const saveImageFromUrl = require('../../saveImageFromUrl'); const { logger } = require('~/config'); jest.mock('openai'); +jest.mock('~/server/services/Files/Firebase', () => ({ + getFirebaseStorage: jest.fn(), + saveImageToFirebaseStorage: jest.fn(), + getFirebaseStorageImageUrl: jest.fn(), +})); + +jest.mock('~/server/services/Files/images', () => ({ + getImageBasename: jest.fn().mockImplementation((url) => { + // Split the URL by '/' + const parts = url.split('/'); + + // Get the last part of the URL + const lastPart = parts.pop(); + + // Check if the last part of the URL matches the image extension regex + const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i; + if (imageExtensionRegex.test(lastPart)) { + return lastPart; + } + + // If the regex test fails, return an empty string + return ''; + }), +})); + const generate = jest.fn(); OpenAI.mockImplementation(() => ({ images: { @@ -187,7 +216,48 @@ describe('DALLE3', () => { generate.mockResolvedValue(mockResponse); saveImageFromUrl.mockRejectedValue(error); const result = await dalle._call(mockData); - expect(logger.error).toHaveBeenCalledWith('Error while saving the image:', error); - expect(result).toBe(mockResponse.data[0].url); + expect(logger.error).toHaveBeenCalledWith('Error while saving the image locally:', error); + expect(result).toBe('Failed to save the image locally. Error while saving the image'); + }); + + it('should save image to Firebase Storage if Firebase is initialized', async () => { + const mockData = { + prompt: 'A test prompt', + }; + const mockImageUrl = 'http://example.com/img-test.png'; + const mockResponse = { data: [{ url: mockImageUrl }] }; + generate.mockResolvedValue(mockResponse); + getFirebaseStorage.mockReturnValue({}); // Simulate Firebase being initialized + + await dalle._call(mockData); + + expect(getFirebaseStorage).toHaveBeenCalled(); + expect(saveImageToFirebaseStorage).toHaveBeenCalledWith( + undefined, + mockImageUrl, + expect.any(String), + ); + }); + + it('should handle error when saving image to Firebase Storage fails', async () => { + const mockData = { + prompt: 'A test prompt', + }; + const mockImageUrl = 'http://example.com/img-test.png'; + const mockResponse = { data: [{ url: mockImageUrl }] }; + const error = new Error('Error while saving to Firebase'); + generate.mockResolvedValue(mockResponse); + getFirebaseStorage.mockReturnValue({}); // Simulate Firebase being initialized + saveImageToFirebaseStorage.mockRejectedValue(error); + + const result = await dalle._call(mockData); + + expect(logger.error).toHaveBeenCalledWith( + 'Error while saving the image to Firebase Storage:', + error, + ); + expect(result).toBe( + 'Failed to save the image to Firebase Storage. Error while saving to Firebase', + ); }); }); diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index 3afe2776729..352dd5dec74 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -67,19 +67,19 @@ const validateTools = async (user, tools = []) => { } }; -const loadToolWithAuth = async (user, authFields, ToolConstructor, options = {}) => { +const loadToolWithAuth = async (userId, authFields, ToolConstructor, options = {}) => { return async function () { let authValues = {}; for (const authField of authFields) { let authValue = process.env[authField]; if (!authValue) { - authValue = await getUserPluginAuthValue(user, authField); + authValue = await getUserPluginAuthValue(userId, authField); } authValues[authField] = authValue; } - return new ToolConstructor({ ...options, ...authValues }); + return new ToolConstructor({ ...options, ...authValues, userId }); }; }; diff --git a/api/package.json b/api/package.json index 684d1397894..fe5731cbd2f 100644 --- a/api/package.json +++ b/api/package.json @@ -44,6 +44,7 @@ "express-mongo-sanitize": "^2.2.0", "express-rate-limit": "^6.9.0", "express-session": "^1.17.3", + "firebase": "^10.6.0", "googleapis": "^126.0.1", "handlebars": "^4.7.7", "html": "^1.0.0", diff --git a/api/server/index.js b/api/server/index.js index 230f8e79d95..698620c56f3 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -4,6 +4,7 @@ const cors = require('cors'); const express = require('express'); const passport = require('passport'); const mongoSanitize = require('express-mongo-sanitize'); +const { initializeFirebase } = require('~/server/services/Files/Firebase/initialize'); const errorController = require('./controllers/ErrorController'); const configureSocialLogins = require('./socialLogins'); const { connectDb, indexSync } = require('~/lib/db'); @@ -23,6 +24,7 @@ const { jwtLogin, passportLogin } = require('~/strategies'); const startServer = async () => { await connectDb(); logger.info('Connected to MongoDB'); + initializeFirebase(); await indexSync(); const app = express(); diff --git a/api/server/routes/files/avatar.js b/api/server/routes/files/avatar.js new file mode 100644 index 00000000000..a7bb07c0f95 --- /dev/null +++ b/api/server/routes/files/avatar.js @@ -0,0 +1,34 @@ +const express = require('express'); +const multer = require('multer'); + +const uploadAvatar = require('~/server/services/Files/images/avatar/uploadAvatar'); +const { requireJwtAuth } = require('~/server/middleware/'); +const User = require('~/models/User'); + +const upload = multer(); +const router = express.Router(); + +router.post('/', requireJwtAuth, upload.single('input'), async (req, res) => { + try { + const userId = req.user.id; + const { manual } = req.body; + const input = req.file.buffer; + if (!userId) { + throw new Error('User ID is undefined'); + } + + // TODO: do not use Model directly, instead use a service method that uses the model + const user = await User.findById(userId).lean(); + + if (!user) { + throw new Error('User not found'); + } + const url = await uploadAvatar(userId, input, manual); + + res.json({ url }); + } catch (error) { + res.status(500).json({ message: 'An error occurred while uploading the profile picture' }); + } +}); + +module.exports = router; diff --git a/api/server/routes/files/index.js b/api/server/routes/files/index.js index 34c7dc62e3a..74b200c8066 100644 --- a/api/server/routes/files/index.js +++ b/api/server/routes/files/index.js @@ -18,5 +18,6 @@ router.use(uaParser); router.use('/', files); router.use('/images', images); +router.use('/images/avatar', require('./avatar')); module.exports = router; diff --git a/api/server/services/Files/Firebase/images.js b/api/server/services/Files/Firebase/images.js new file mode 100644 index 00000000000..e04902c02fe --- /dev/null +++ b/api/server/services/Files/Firebase/images.js @@ -0,0 +1,45 @@ +const fetch = require('node-fetch'); +const { ref, uploadBytes, getDownloadURL } = require('firebase/storage'); +const { getFirebaseStorage } = require('./initialize'); + +async function saveImageToFirebaseStorage(userId, imageUrl, imageName) { + const storage = getFirebaseStorage(); + if (!storage) { + console.error('Firebase is not initialized. Cannot save image to Firebase Storage.'); + return null; + } + + const storageRef = ref(storage, `images/${userId.toString()}/${imageName}`); + + try { + // Upload image to Firebase Storage using the image URL + await uploadBytes(storageRef, await fetch(imageUrl).then((response) => response.buffer())); + return imageName; + } catch (error) { + console.error('Error uploading image to Firebase Storage:', error.message); + return null; + } +} + +async function getFirebaseStorageImageUrl(imageName) { + const storage = getFirebaseStorage(); + if (!storage) { + console.error('Firebase is not initialized. Cannot get image URL from Firebase Storage.'); + return null; + } + + const storageRef = ref(storage, `images/${imageName}`); + + try { + // Get the download URL for the image from Firebase Storage + return `![generated image](${await getDownloadURL(storageRef)})`; + } catch (error) { + console.error('Error fetching image URL from Firebase Storage:', error.message); + return null; + } +} + +module.exports = { + saveImageToFirebaseStorage, + getFirebaseStorageImageUrl, +}; diff --git a/api/server/services/Files/Firebase/index.js b/api/server/services/Files/Firebase/index.js new file mode 100644 index 00000000000..905bf660d4f --- /dev/null +++ b/api/server/services/Files/Firebase/index.js @@ -0,0 +1,7 @@ +const images = require('./images'); +const initialize = require('./initialize'); + +module.exports = { + ...images, + ...initialize, +}; diff --git a/api/server/services/Files/Firebase/initialize.js b/api/server/services/Files/Firebase/initialize.js new file mode 100644 index 00000000000..5dc1f937915 --- /dev/null +++ b/api/server/services/Files/Firebase/initialize.js @@ -0,0 +1,42 @@ +const firebase = require('firebase/app'); +const { getStorage } = require('firebase/storage'); +const { logger } = require('~/config'); + +let i = 0; +let firebaseApp = null; + +const initializeFirebase = () => { + // Return existing instance if already initialized + if (firebaseApp) { + return firebaseApp; + } + + const firebaseConfig = { + apiKey: process.env.FIREBASE_API_KEY, + authDomain: process.env.FIREBASE_AUTH_DOMAIN, + projectId: process.env.FIREBASE_PROJECT_ID, + storageBucket: process.env.FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.FIREBASE_APP_ID, + }; + + if (Object.values(firebaseConfig).some((value) => !value)) { + i === 0 && + logger.info( + '[Optional] Firebase configuration missing or incomplete. Firebase will not be initialized.', + ); + i++; + return null; + } + + firebaseApp = firebase.initializeApp(firebaseConfig); + logger.info('Firebase initialized'); + return firebaseApp; +}; + +const getFirebaseStorage = () => { + const app = initializeFirebase(); + return app ? getStorage(app) : null; +}; + +module.exports = { initializeFirebase, getFirebaseStorage }; diff --git a/api/server/services/Files/images/avatar/firebaseStrategy.js b/api/server/services/Files/images/avatar/firebaseStrategy.js new file mode 100644 index 00000000000..9c000b43ecc --- /dev/null +++ b/api/server/services/Files/images/avatar/firebaseStrategy.js @@ -0,0 +1,29 @@ +const { ref, uploadBytes, getDownloadURL } = require('firebase/storage'); +const { getFirebaseStorage } = require('~/server/services/Files/Firebase/initialize'); +const { logger } = require('~/config'); + +async function firebaseStrategy(userId, webPBuffer, oldUser, manual) { + try { + const storage = getFirebaseStorage(); + if (!storage) { + throw new Error('Firebase is not initialized.'); + } + const avatarRef = ref(storage, `images/${userId.toString()}/avatar`); + + await uploadBytes(avatarRef, webPBuffer); + const urlFirebase = await getDownloadURL(avatarRef); + const isManual = manual === 'true'; + + const url = `${urlFirebase}?manual=${isManual}`; + if (isManual) { + oldUser.avatar = url; + await oldUser.save(); + } + return url; + } catch (error) { + logger.error('Error uploading profile picture:', error); + throw error; + } +} + +module.exports = firebaseStrategy; diff --git a/api/server/services/Files/images/avatar/localStrategy.js b/api/server/services/Files/images/avatar/localStrategy.js new file mode 100644 index 00000000000..021beda7d13 --- /dev/null +++ b/api/server/services/Files/images/avatar/localStrategy.js @@ -0,0 +1,32 @@ +const fs = require('fs').promises; +const path = require('path'); + +async function localStrategy(userId, webPBuffer, oldUser, manual) { + const userDir = path.resolve( + __dirname, + '..', + '..', + '..', + '..', + '..', + '..', + 'client', + 'public', + 'images', + userId, + ); + let avatarPath = path.join(userDir, 'avatar.png'); + const urlRoute = `/images/${userId}/avatar.png`; + await fs.mkdir(userDir, { recursive: true }); + await fs.writeFile(avatarPath, webPBuffer); + const isManual = manual === 'true'; + let url = `${urlRoute}?manual=${isManual}×tamp=${new Date().getTime()}`; + if (isManual) { + oldUser.avatar = url; + await oldUser.save(); + } + + return url; +} + +module.exports = localStrategy; diff --git a/api/server/services/Files/images/avatar/uploadAvatar.js b/api/server/services/Files/images/avatar/uploadAvatar.js new file mode 100644 index 00000000000..0726df9a4dd --- /dev/null +++ b/api/server/services/Files/images/avatar/uploadAvatar.js @@ -0,0 +1,63 @@ +const sharp = require('sharp'); +const fetch = require('node-fetch'); +const fs = require('fs').promises; +const User = require('~/models/User'); +const { getFirebaseStorage } = require('~/server/services/Files/Firebase/initialize'); +const firebaseStrategy = require('./firebaseStrategy'); +const localStrategy = require('./localStrategy'); +const { logger } = require('~/config'); + +async function convertToWebP(inputBuffer) { + return sharp(inputBuffer).resize({ width: 150 }).toFormat('webp').toBuffer(); +} + +async function uploadAvatar(userId, input, manual) { + try { + if (userId === undefined) { + throw new Error('User ID is undefined'); + } + const _id = userId; + // TODO: remove direct use of Model, `User` + const oldUser = await User.findOne({ _id }); + let imageBuffer; + if (typeof input === 'string') { + const response = await fetch(input); + + if (!response.ok) { + throw new Error(`Failed to fetch image from URL. Status: ${response.status}`); + } + imageBuffer = await response.buffer(); + } else if (input instanceof Buffer) { + imageBuffer = input; + } else if (typeof input === 'object' && input instanceof File) { + const fileContent = await fs.readFile(input.path); + imageBuffer = Buffer.from(fileContent); + } else { + throw new Error('Invalid input type. Expected URL, Buffer, or File.'); + } + const { width, height } = await sharp(imageBuffer).metadata(); + const minSize = Math.min(width, height); + const squaredBuffer = await sharp(imageBuffer) + .extract({ + left: Math.floor((width - minSize) / 2), + top: Math.floor((height - minSize) / 2), + width: minSize, + height: minSize, + }) + .toBuffer(); + const webPBuffer = await convertToWebP(squaredBuffer); + const storage = getFirebaseStorage(); + if (storage) { + const url = await firebaseStrategy(userId, webPBuffer, oldUser, manual); + return url; + } + + const url = await localStrategy(userId, webPBuffer, oldUser, manual); + return url; + } catch (error) { + logger.error('Error uploading the avatar:', error); + throw error; + } +} + +module.exports = uploadAvatar; diff --git a/api/server/services/Files/images/index.js b/api/server/services/Files/images/index.js index d5b818e937d..fa49eb95356 100644 --- a/api/server/services/Files/images/index.js +++ b/api/server/services/Files/images/index.js @@ -1,11 +1,15 @@ const convert = require('./convert'); const encode = require('./encode'); +const parse = require('./parse'); const resize = require('./resize'); const validate = require('./validate'); +const uploadAvatar = require('./avatar/uploadAvatar'); module.exports = { ...convert, ...encode, + ...parse, ...resize, ...validate, + uploadAvatar, }; diff --git a/api/server/services/Files/images/parse.js b/api/server/services/Files/images/parse.js new file mode 100644 index 00000000000..5a1113c97e4 --- /dev/null +++ b/api/server/services/Files/images/parse.js @@ -0,0 +1,27 @@ +const URL = require('url').URL; +const path = require('path'); + +const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i; + +/** + * Extracts the image basename from a given URL. + * + * @param {string} urlString - The URL string from which the image basename is to be extracted. + * @returns {string} The basename of the image file from the URL. + * Returns an empty string if the URL does not contain a valid image basename. + */ +function getImageBasename(urlString) { + try { + const url = new URL(urlString); + const basename = path.basename(url.pathname); + + return imageExtensionRegex.test(basename) ? basename : ''; + } catch (error) { + // If URL parsing fails, return an empty string + return ''; + } +} + +module.exports = { + getImageBasename, +}; diff --git a/api/strategies/discordStrategy.js b/api/strategies/discordStrategy.js index c6fdde6d8c3..994554200cd 100644 --- a/api/strategies/discordStrategy.js +++ b/api/strategies/discordStrategy.js @@ -1,51 +1,72 @@ const { Strategy: DiscordStrategy } = require('passport-discord'); const { logger } = require('~/config'); const User = require('~/models/User'); +const { useFirebase, uploadAvatar } = require('~/server/services/Files/images'); const discordLogin = async (accessToken, refreshToken, profile, cb) => { try { const email = profile.email; const discordId = profile.id; - const oldUser = await User.findOne({ - email, - }); + const oldUser = await User.findOne({ email }); const ALLOW_SOCIAL_REGISTRATION = process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; - let avatarURL; + let avatarUrl; + if (profile.avatar) { const format = profile.avatar.startsWith('a_') ? 'gif' : 'png'; - avatarURL = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`; + avatarUrl = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`; } else { const defaultAvatarNum = Number(profile.discriminator) % 5; - avatarURL = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNum}.png`; + avatarUrl = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNum}.png`; } if (oldUser) { - oldUser.avatar = avatarURL; - await oldUser.save(); + await handleExistingUser(oldUser, avatarUrl, useFirebase); return cb(null, oldUser); - } else if (ALLOW_SOCIAL_REGISTRATION) { - const newUser = await new User({ - provider: 'discord', - discordId, - username: profile.username, - email, - name: profile.global_name, - avatar: avatarURL, - }).save(); + } + if (ALLOW_SOCIAL_REGISTRATION) { + const newUser = await createNewUser(profile, discordId, email, avatarUrl, useFirebase); return cb(null, newUser); } - - return cb(null, false, { - message: 'User not found.', - }); } catch (err) { logger.error('[discordLogin]', err); return cb(err); } }; +const handleExistingUser = async (oldUser, avatarUrl, useFirebase) => { + if (!useFirebase && !oldUser.avatar.includes('?manual=true')) { + oldUser.avatar = avatarUrl; + await oldUser.save(); + } else if (useFirebase && !oldUser.avatar.includes('?manual=true')) { + const userId = oldUser._id; + const newavatarUrl = await uploadAvatar(userId, avatarUrl); + oldUser.avatar = newavatarUrl; + await oldUser.save(); + } +}; + +const createNewUser = async (profile, discordId, email, avatarUrl, useFirebase) => { + const newUser = await new User({ + provider: 'discord', + discordId, + username: profile.username, + email, + name: profile.global_name, + avatar: avatarUrl, + }).save(); + + if (useFirebase) { + const userId = newUser._id; + const newavatarUrl = await uploadAvatar(userId, avatarUrl); + newUser.avatar = newavatarUrl; + await newUser.save(); + } + + return newUser; +}; + module.exports = () => new DiscordStrategy( { diff --git a/api/strategies/facebookStrategy.js b/api/strategies/facebookStrategy.js index bb175a099cc..b8915b2cc4b 100644 --- a/api/strategies/facebookStrategy.js +++ b/api/strategies/facebookStrategy.js @@ -1,43 +1,64 @@ const FacebookStrategy = require('passport-facebook').Strategy; const { logger } = require('~/config'); const User = require('~/models/User'); +const { useFirebase, uploadAvatar } = require('~/server/services/Files/images'); const facebookLogin = async (accessToken, refreshToken, profile, cb) => { try { const email = profile.emails[0]?.value; const facebookId = profile.id; - const oldUser = await User.findOne({ - email, - }); + const oldUser = await User.findOne({ email }); const ALLOW_SOCIAL_REGISTRATION = process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; + const avatarUrl = profile.photos[0]?.value; if (oldUser) { - oldUser.avatar = profile.photo; - await oldUser.save(); + await handleExistingUser(oldUser, avatarUrl, useFirebase); return cb(null, oldUser); - } else if (ALLOW_SOCIAL_REGISTRATION) { - const newUser = await new User({ - provider: 'facebook', - facebookId, - username: profile.displayName, - email, - name: profile.name?.givenName + ' ' + profile.name?.familyName, - avatar: profile.photos[0]?.value, - }).save(); + } + if (ALLOW_SOCIAL_REGISTRATION) { + const newUser = await createNewUser(profile, facebookId, email, avatarUrl, useFirebase); return cb(null, newUser); } - - return cb(null, false, { - message: 'User not found.', - }); } catch (err) { logger.error('[facebookLogin]', err); return cb(err); } }; +const handleExistingUser = async (oldUser, avatarUrl, useFirebase) => { + if (!useFirebase && !oldUser.avatar.includes('?manual=true')) { + oldUser.avatar = avatarUrl; + await oldUser.save(); + } else if (useFirebase && !oldUser.avatar.includes('?manual=true')) { + const userId = oldUser._id; + const newavatarUrl = await uploadAvatar(userId, avatarUrl); + oldUser.avatar = newavatarUrl; + await oldUser.save(); + } +}; + +const createNewUser = async (profile, facebookId, email, avatarUrl, useFirebase) => { + const newUser = await new User({ + provider: 'facebook', + facebookId, + username: profile.displayName, + email, + name: profile.name?.givenName + ' ' + profile.name?.familyName, + avatar: avatarUrl, + }).save(); + + if (useFirebase) { + const userId = newUser._id; + const newavatarUrl = await uploadAvatar(userId, avatarUrl); + newUser.avatar = newavatarUrl; + await newUser.save(); + } + + return newUser; +}; + module.exports = () => new FacebookStrategy( { diff --git a/api/strategies/githubStrategy.js b/api/strategies/githubStrategy.js index 3962c58e50e..c8480d50c13 100644 --- a/api/strategies/githubStrategy.js +++ b/api/strategies/githubStrategy.js @@ -1,6 +1,7 @@ const { Strategy: GitHubStrategy } = require('passport-github2'); const { logger } = require('~/config'); const User = require('~/models/User'); +const { useFirebase, uploadAvatar } = require('~/server/services/Files/images'); const githubLogin = async (accessToken, refreshToken, profile, cb) => { try { @@ -9,32 +10,56 @@ const githubLogin = async (accessToken, refreshToken, profile, cb) => { const oldUser = await User.findOne({ email }); const ALLOW_SOCIAL_REGISTRATION = process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; + const avatarUrl = profile.photos[0].value; if (oldUser) { - oldUser.avatar = profile.photos[0].value; - await oldUser.save(); + await handleExistingUser(oldUser, avatarUrl, useFirebase); return cb(null, oldUser); - } else if (ALLOW_SOCIAL_REGISTRATION) { - const newUser = await new User({ - provider: 'github', - githubId, - username: profile.username, - email, - emailVerified: profile.emails[0].verified, - name: profile.displayName, - avatar: profile.photos[0].value, - }).save(); + } + if (ALLOW_SOCIAL_REGISTRATION) { + const newUser = await createNewUser(profile, githubId, email, avatarUrl, useFirebase); return cb(null, newUser); } - - return cb(null, false, { message: 'User not found.' }); } catch (err) { logger.error('[githubLogin]', err); return cb(err); } }; +const handleExistingUser = async (oldUser, avatarUrl, useFirebase) => { + if (!useFirebase && !oldUser.avatar.includes('?manual=true')) { + oldUser.avatar = avatarUrl; + await oldUser.save(); + } else if (useFirebase && !oldUser.avatar.includes('?manual=true')) { + const userId = oldUser._id; + const avatarURL = await uploadAvatar(userId, avatarUrl); + oldUser.avatar = avatarURL; + await oldUser.save(); + } +}; + +const createNewUser = async (profile, githubId, email, avatarUrl, useFirebase) => { + const newUser = await new User({ + provider: 'github', + githubId, + username: profile.username, + email, + emailVerified: profile.emails[0].verified, + name: profile.displayName, + avatar: avatarUrl, + }).save(); + + if (useFirebase) { + const userId = newUser._id; + const avatarURL = await uploadAvatar(userId, avatarUrl); + newUser.avatar = avatarURL; + await newUser.save(); + } + + return newUser; +}; + module.exports = () => new GitHubStrategy( { diff --git a/api/strategies/googleStrategy.js b/api/strategies/googleStrategy.js index e65c5403f4b..d013cc8e8fd 100644 --- a/api/strategies/googleStrategy.js +++ b/api/strategies/googleStrategy.js @@ -1,6 +1,7 @@ const { Strategy: GoogleStrategy } = require('passport-google-oauth20'); const { logger } = require('~/config'); const User = require('~/models/User'); +const { useFirebase, uploadAvatar } = require('~/server/services/Files/images'); const googleLogin = async (accessToken, refreshToken, profile, cb) => { try { @@ -9,32 +10,56 @@ const googleLogin = async (accessToken, refreshToken, profile, cb) => { const oldUser = await User.findOne({ email }); const ALLOW_SOCIAL_REGISTRATION = process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; + const avatarUrl = profile.photos[0].value; if (oldUser) { - oldUser.avatar = profile.photos[0].value; - await oldUser.save(); + await handleExistingUser(oldUser, avatarUrl, useFirebase); return cb(null, oldUser); - } else if (ALLOW_SOCIAL_REGISTRATION) { - const newUser = await new User({ - provider: 'google', - googleId, - username: profile.name.givenName, - email, - emailVerified: profile.emails[0].verified, - name: `${profile.name.givenName} ${profile.name.familyName}`, - avatar: profile.photos[0].value, - }).save(); + } + if (ALLOW_SOCIAL_REGISTRATION) { + const newUser = await createNewUser(profile, googleId, email, avatarUrl, useFirebase); return cb(null, newUser); } - - return cb(null, false, { message: 'User not found.' }); } catch (err) { logger.error('[googleLogin]', err); return cb(err); } }; +const handleExistingUser = async (oldUser, avatarUrl, useFirebase) => { + if ((!useFirebase && !oldUser.avatar.includes('?manual=true')) || oldUser.avatar === null) { + oldUser.avatar = avatarUrl; + await oldUser.save(); + } else if (useFirebase && !oldUser.avatar.includes('?manual=true')) { + const userId = oldUser._id; + const avatarURL = await uploadAvatar(userId, avatarUrl); + oldUser.avatar = avatarURL; + await oldUser.save(); + } +}; + +const createNewUser = async (profile, googleId, email, avatarUrl, useFirebase) => { + const newUser = await new User({ + provider: 'google', + googleId, + username: profile.name.givenName, + email, + emailVerified: profile.emails[0].verified, + name: `${profile.name.givenName} ${profile.name.familyName}`, + avatar: avatarUrl, + }).save(); + + if (useFirebase) { + const userId = newUser._id; + const avatarURL = await uploadAvatar(userId, avatarUrl); + newUser.avatar = avatarURL; + await newUser.save(); + } + + return newUser; +}; + module.exports = () => new GoogleStrategy( { diff --git a/client/src/App.jsx b/client/src/App.jsx index 72d07b1c96a..10c9ab9b509 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -34,7 +34,7 @@ const App = () => { - + diff --git a/client/src/common/types.ts b/client/src/common/types.ts index 807e2dc9424..2daf2d8ba0f 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -27,6 +27,7 @@ export type TShowToast = { severity?: NotificationSeverity; showIcon?: boolean; duration?: number; + status?: 'error' | 'success' | 'warning' | 'info'; }; export type TBaseSettingsProps = { diff --git a/client/src/components/Chat/ChatView.tsx b/client/src/components/Chat/ChatView.tsx index 5ce30951395..d582f4b3e9d 100644 --- a/client/src/components/Chat/ChatView.tsx +++ b/client/src/components/Chat/ChatView.tsx @@ -6,10 +6,10 @@ import { useChatHelpers, useSSE } from '~/hooks'; // import GenerationButtons from './Input/GenerationButtons'; import MessagesView from './Messages/MessagesView'; // import OptionsBar from './Input/OptionsBar'; +import { Spinner } from '~/components/svg'; import { ChatContext } from '~/Providers'; import Presentation from './Presentation'; import ChatForm from './Input/ChatForm'; -import { Spinner } from '~/components'; import { buildTree } from '~/utils'; import Landing from './Landing'; import Header from './Header'; diff --git a/client/src/components/Nav/Nav.tsx b/client/src/components/Nav/Nav.tsx index 552fd1d5257..86a3e335971 100644 --- a/client/src/components/Nav/Nav.tsx +++ b/client/src/components/Nav/Nav.tsx @@ -11,7 +11,7 @@ import { } from '~/hooks'; import { TooltipProvider, Tooltip } from '~/components/ui'; import { Conversations, Pages } from '../Conversations'; -import { Spinner } from '~/components'; +import { Spinner } from '~/components/svg'; import SearchBar from './SearchBar'; import NavToggle from './NavToggle'; import NavLinks from './NavLinks'; diff --git a/client/src/components/Nav/NavLinks.tsx b/client/src/components/Nav/NavLinks.tsx index ad1b9610cbb..2f5c0769bcb 100644 --- a/client/src/components/Nav/NavLinks.tsx +++ b/client/src/components/Nav/NavLinks.tsx @@ -119,7 +119,7 @@ function NavLinks() { } + svg={() => } text={localize('com_nav_settings')} clickHandler={() => setShowSettings(true)} /> diff --git a/client/src/components/Nav/Settings.tsx b/client/src/components/Nav/Settings.tsx index cce7ec27c20..da3fcefa7cf 100644 --- a/client/src/components/Nav/Settings.tsx +++ b/client/src/components/Nav/Settings.tsx @@ -1,9 +1,9 @@ import * as Tabs from '@radix-ui/react-tabs'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui'; -import { GearIcon, DataIcon } from '~/components/svg'; +import { GearIcon, DataIcon, UserIcon } from '~/components/svg'; import { useMediaQuery, useLocalize } from '~/hooks'; import type { TDialogProps } from '~/common'; -import { General, Data } from './SettingsTabs'; +import { General, Data, Account } from './SettingsTabs'; import { cn } from '~/utils'; export default function Settings({ open, onOpenChange }: TDialogProps) { @@ -39,7 +39,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) { > {localize('com_nav_setting_data')} + + + {localize('com_nav_setting_account')} + + diff --git a/client/src/components/Nav/SettingsTabs/Account/Account.tsx b/client/src/components/Nav/SettingsTabs/Account/Account.tsx new file mode 100644 index 00000000000..a7651ddca60 --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/Account.tsx @@ -0,0 +1,18 @@ +import * as Tabs from '@radix-ui/react-tabs'; +import Avatar from './Avatar'; +import React from 'react'; + +function Account() { + return ( + +
+
+ +
+
+
+
+ ); +} + +export default React.memo(Account); diff --git a/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx b/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx new file mode 100644 index 00000000000..64635f0a59d --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx @@ -0,0 +1,145 @@ +import { FileImage } from 'lucide-react'; +import { useSetRecoilState } from 'recoil'; +import { useState, useEffect } from 'react'; +import type { TUser } from 'librechat-data-provider'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui'; +import { useUploadAvatarMutation } from '~/data-provider'; +import { useToastContext } from '~/Providers'; +import { Spinner } from '~/components/svg'; +import { useLocalize } from '~/hooks'; +import { cn } from '~/utils/'; +import store from '~/store'; + +const sizeLimit = 2 * 1024 * 1024; // 2MB + +function Avatar() { + const setUser = useSetRecoilState(store.user); + const [input, setinput] = useState(null); + const [isDialogOpen, setDialogOpen] = useState(false); + const [previewUrl, setPreviewUrl] = useState(null); + + const localize = useLocalize(); + const { showToast } = useToastContext(); + + const { mutate: uploadAvatar, isLoading: isUploading } = useUploadAvatarMutation({ + onSuccess: (data) => { + showToast({ message: localize('com_ui_upload_success') }); + setDialogOpen(false); + + setUser((prev) => ({ ...prev, avatar: data.url } as TUser)); + }, + onError: (error) => { + console.error('Error:', error); + showToast({ message: localize('com_ui_upload_error'), status: 'error' }); + }, + }); + + useEffect(() => { + if (input) { + const reader = new FileReader(); + reader.onloadend = () => { + setPreviewUrl(reader.result as string); + }; + reader.readAsDataURL(input); + } else { + setPreviewUrl(null); + } + }, [input]); + + const handleFileChange = (event: React.ChangeEvent): void => { + const file = event.target.files?.[0]; + + if (file && file.size <= sizeLimit) { + setinput(file); + setDialogOpen(true); + } else { + showToast({ + message: localize('com_ui_upload_invalid'), + status: 'error', + }); + } + }; + + const handleUpload = () => { + if (!input) { + console.error('No file selected'); + return; + } + + const formData = new FormData(); + formData.append('input', input, input.name); + formData.append('manual', 'true'); + + uploadAvatar(formData); + }; + + return ( + <> +
+ {localize('com_nav_profile_picture')} + +
+ + setDialogOpen(false)}> + + + + {localize('com_ui_preview')} + + +
+ {previewUrl && ( + Preview + )} + +
+
+
+ + ); +} + +export default Avatar; diff --git a/client/src/components/Nav/SettingsTabs/Data.tsx b/client/src/components/Nav/SettingsTabs/Data/Data.tsx similarity index 98% rename from client/src/components/Nav/SettingsTabs/Data.tsx rename to client/src/components/Nav/SettingsTabs/Data/Data.tsx index 1786095eeda..d7db9969412 100644 --- a/client/src/components/Nav/SettingsTabs/Data.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/Data.tsx @@ -5,7 +5,7 @@ import { } from 'librechat-data-provider/react-query'; import React, { useState, useCallback, useRef } from 'react'; import { useOnClickOutside } from '~/hooks'; -import DangerButton from './DangerButton'; +import DangerButton from '../DangerButton'; export const RevokeKeysButton = ({ showText = true, diff --git a/client/src/components/Nav/SettingsTabs/AutoScrollSwitch.spec.tsx b/client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.spec.tsx similarity index 100% rename from client/src/components/Nav/SettingsTabs/AutoScrollSwitch.spec.tsx rename to client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.spec.tsx diff --git a/client/src/components/Nav/SettingsTabs/AutoScrollSwitch.tsx b/client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.tsx similarity index 100% rename from client/src/components/Nav/SettingsTabs/AutoScrollSwitch.tsx rename to client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.tsx diff --git a/client/src/components/Nav/SettingsTabs/ClearChatsButton.spec.tsx b/client/src/components/Nav/SettingsTabs/General/ClearChatsButton.spec.tsx similarity index 100% rename from client/src/components/Nav/SettingsTabs/ClearChatsButton.spec.tsx rename to client/src/components/Nav/SettingsTabs/General/ClearChatsButton.spec.tsx diff --git a/client/src/components/Nav/SettingsTabs/General.tsx b/client/src/components/Nav/SettingsTabs/General/General.tsx similarity index 99% rename from client/src/components/Nav/SettingsTabs/General.tsx rename to client/src/components/Nav/SettingsTabs/General/General.tsx index 343b36fbecd..48ecd6be006 100644 --- a/client/src/components/Nav/SettingsTabs/General.tsx +++ b/client/src/components/Nav/SettingsTabs/General/General.tsx @@ -12,7 +12,7 @@ import { } from '~/hooks'; import type { TDangerButtonProps } from '~/common'; import AutoScrollSwitch from './AutoScrollSwitch'; -import DangerButton from './DangerButton'; +import DangerButton from '../DangerButton'; import store from '~/store'; import { Dropdown } from '~/components/ui'; diff --git a/client/src/components/Nav/SettingsTabs/LangSelector.spec.tsx b/client/src/components/Nav/SettingsTabs/General/LangSelector.spec.tsx similarity index 100% rename from client/src/components/Nav/SettingsTabs/LangSelector.spec.tsx rename to client/src/components/Nav/SettingsTabs/General/LangSelector.spec.tsx diff --git a/client/src/components/Nav/SettingsTabs/ThemeSelector.spec.tsx b/client/src/components/Nav/SettingsTabs/General/ThemeSelector.spec.tsx similarity index 100% rename from client/src/components/Nav/SettingsTabs/ThemeSelector.spec.tsx rename to client/src/components/Nav/SettingsTabs/General/ThemeSelector.spec.tsx diff --git a/client/src/components/Nav/SettingsTabs/index.ts b/client/src/components/Nav/SettingsTabs/index.ts index 939c90f3b33..73174aa7984 100644 --- a/client/src/components/Nav/SettingsTabs/index.ts +++ b/client/src/components/Nav/SettingsTabs/index.ts @@ -1,4 +1,5 @@ -export { default as General } from './General'; -export { ClearChatsButton } from './General'; -export { default as Data } from './Data'; -export { RevokeKeysButton } from './Data'; +export { default as General } from './General/General'; +export { ClearChatsButton } from './General/General'; +export { default as Data } from './Data/Data'; +export { RevokeKeysButton } from './Data/Data'; +export { default as Account } from './Account/Account'; diff --git a/client/src/components/svg/GearIcon.tsx b/client/src/components/svg/GearIcon.tsx index e5ed475d521..98cc2fab982 100644 --- a/client/src/components/svg/GearIcon.tsx +++ b/client/src/components/svg/GearIcon.tsx @@ -1,14 +1,18 @@ import React from 'react'; -export default function GearIcon() { +interface GearIconProps { + className?: string; +} + +const GearIcon: React.FC = ({ className = '' }) => { return ( ); -} +}; + +export default GearIcon; diff --git a/client/src/components/svg/UserIcon.tsx b/client/src/components/svg/UserIcon.tsx index 8f15fadcaf6..e8535e45eb9 100644 --- a/client/src/components/svg/UserIcon.tsx +++ b/client/src/components/svg/UserIcon.tsx @@ -1,20 +1,17 @@ -import React from 'react'; - export default function UserIcon() { return ( - + ); diff --git a/client/src/components/svg/index.ts b/client/src/components/svg/index.ts index cc4f5a0148b..3ad62c93eea 100644 --- a/client/src/components/svg/index.ts +++ b/client/src/components/svg/index.ts @@ -40,3 +40,4 @@ export { default as GeminiIcon } from './GeminiIcon'; export { default as GoogleMinimalIcon } from './GoogleMinimalIcon'; export { default as AnthropicMinimalIcon } from './AnthropicMinimalIcon'; export { default as SendMessageIcon } from './SendMessageIcon'; +export { default as UserIcon } from './UserIcon'; diff --git a/client/src/data-provider/mutations.ts b/client/src/data-provider/mutations.ts index c38ca46c9e6..236c66a9479 100644 --- a/client/src/data-provider/mutations.ts +++ b/client/src/data-provider/mutations.ts @@ -12,6 +12,8 @@ import type { PresetDeleteResponse, LogoutOptions, TPreset, + UploadAvatarOptions, + AvatarUploadResponse, } from 'librechat-data-provider'; import { dataService, MutationKeys } from 'librechat-data-provider'; @@ -99,3 +101,18 @@ export const useLogoutUserMutation = ( }, }); }; + +/* Avatar upload */ +export const useUploadAvatarMutation = ( + options?: UploadAvatarOptions, +): UseMutationResult< + AvatarUploadResponse, // response data + unknown, // error + FormData, // request + unknown // context +> => { + return useMutation([MutationKeys.avatarUpload], { + mutationFn: (variables: FormData) => dataService.uploadAvatar(variables), + ...(options || {}), + }); +}; diff --git a/client/src/hooks/AuthContext.tsx b/client/src/hooks/AuthContext.tsx index 8cd63833bde..711f433ef2d 100644 --- a/client/src/hooks/AuthContext.tsx +++ b/client/src/hooks/AuthContext.tsx @@ -7,6 +7,7 @@ import { createContext, useContext, } from 'react'; +import { useRecoilState } from 'recoil'; import { TUser, TLoginResponse, setTokenHeader, TLoginUser } from 'librechat-data-provider'; import { useGetUserQuery, @@ -17,6 +18,7 @@ import { useNavigate } from 'react-router-dom'; import { TAuthConfig, TUserContext, TAuthContext, TResError } from '~/common'; import { useLogoutUserMutation } from '~/data-provider'; import useTimeout from './useTimeout'; +import store from '~/store'; const AuthContext = createContext(undefined); @@ -27,11 +29,13 @@ const AuthContextProvider = ({ authConfig?: TAuthConfig; children: ReactNode; }) => { - const navigate = useNavigate(); - const [user, setUser] = useState(undefined); + const [user, setUser] = useRecoilState(store.user); const [token, setToken] = useState(undefined); const [error, setError] = useState(undefined); const [isAuthenticated, setIsAuthenticated] = useState(false); + + const navigate = useNavigate(); + const setUserContext = useCallback( (userContext: TUserContext) => { const { token, isAuthenticated, user, redirect } = userContext; @@ -46,7 +50,7 @@ const AuthContextProvider = ({ navigate(redirect, { replace: true }); } }, - [navigate], + [navigate, setUser], ); const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) }); diff --git a/client/src/hooks/useToast.ts b/client/src/hooks/useToast.ts index 716526768d8..92f7bbfe17b 100644 --- a/client/src/hooks/useToast.ts +++ b/client/src/hooks/useToast.ts @@ -25,6 +25,7 @@ export default function useToast(showDelay = 100) { severity = NotificationSeverity.SUCCESS, showIcon = true, duration = 3000, // default duration for the toast to be visible + status, }: TShowToast) => { // Clear existing timeouts if (showTimerRef.current !== null) { @@ -36,7 +37,12 @@ export default function useToast(showDelay = 100) { // Timeout to show the toast showTimerRef.current = window.setTimeout(() => { - setToast({ open: true, message, severity, showIcon }); + setToast({ + open: true, + message, + severity: (status as NotificationSeverity) ?? severity, + showIcon, + }); // Hides the toast after the specified duration hideTimerRef.current = window.setTimeout(() => { setToast((prevToast) => ({ ...prevToast, open: false })); diff --git a/client/src/localization/languages/Eng.tsx b/client/src/localization/languages/Eng.tsx index 7acb936cf99..8a431707bcb 100644 --- a/client/src/localization/languages/Eng.tsx +++ b/client/src/localization/languages/Eng.tsx @@ -33,7 +33,8 @@ export default { com_ui_enter: 'Enter', com_ui_submit: 'Submit', com_ui_upload_success: 'Successfully uploaded file', - com_ui_upload_invalid: 'Invalid file for upload', + com_ui_upload_error: 'There was an error uploading your file', + com_ui_upload_invalid: 'Invalid file for upload. Must be an image not exceeding 2 MB', com_ui_cancel: 'Cancel', com_ui_save: 'Save', com_ui_copy_to_clipboard: 'Copy to clipboard', @@ -51,6 +52,9 @@ export default { com_ui_delete: 'Delete', com_ui_delete_conversation: 'Delete chat?', com_ui_delete_conversation_confirm: 'This will delete', + com_ui_preview: 'Preview', + com_ui_upload: 'Upload', + com_ui_connect: 'Connect', com_auth_error_login: 'Unable to login with the information provided. Please check your credentials and try again.', com_auth_error_login_rl: @@ -253,6 +257,8 @@ export default { 'Make sure to click \'Create and Continue\' to give at least the \'Vertex AI User\' role. Lastly, create a JSON key to import here.', com_nav_welcome_message: 'How can I help you today?', com_nav_auto_scroll: 'Auto-scroll to Newest on Open', + com_nav_profile_picture: 'Profile Picture', + com_nav_change_picture: 'Change picture', com_nav_plugin_store: 'Plugin store', com_nav_plugin_search: 'Search plugins', com_nav_plugin_auth_error: @@ -286,6 +292,7 @@ export default { com_nav_search_placeholder: 'Search messages', com_nav_setting_general: 'General', com_nav_setting_data: 'Data controls', + com_nav_setting_account: 'Account', com_nav_language: 'Language', com_nav_lang_auto: 'Auto detect', com_nav_lang_english: 'English', diff --git a/client/src/localization/languages/It.tsx b/client/src/localization/languages/It.tsx index d07eb798ab1..175b7820dbb 100644 --- a/client/src/localization/languages/It.tsx +++ b/client/src/localization/languages/It.tsx @@ -53,6 +53,9 @@ export default { com_ui_delete: 'Elimina', com_ui_delete_conversation: 'Eliminare la chat?', com_ui_delete_conversation_confirm: 'Questo eliminerà', + com_ui_preview: 'Anteprima', + com_ui_upload: 'Carica', + com_ui_connect: 'Connetti', com_auth_error_login: 'Impossibile accedere con le informazioni fornite. Per favore controlla le tue credenziali e riprova.', com_auth_error_login_rl: @@ -263,7 +266,9 @@ export default { 'Assicurati di fare clic su "Crea e continua" per dare almeno il ruolo "Vertex AI User". Infine, crea una chiave JSON da importare qui.', com_nav_welcome_message: 'Come posso aiutarti oggi?', com_nav_auto_scroll: 'Scorri automaticamente al Più recente all\'apertura', - com_nav_plugin_store: 'Negozio plugin', + com_nav_profile_picture: 'Immagine del profilo', + com_nav_change_picture: 'Cambia immagine', + com_nav_plugin_store: 'Negozio dei plugin', com_nav_plugin_search: 'Cerca plugin', com_nav_plugin_auth_error: 'Si è verificato un errore durante il tentativo di autenticare questo plugin. Per favore riprova.', diff --git a/client/src/store/user.ts b/client/src/store/user.ts index 04864d34263..d86bdc23730 100644 --- a/client/src/store/user.ts +++ b/client/src/store/user.ts @@ -1,9 +1,9 @@ import { atom } from 'recoil'; -import { TPlugin } from 'librechat-data-provider'; +import type { TUser, TPlugin } from 'librechat-data-provider'; -const user = atom({ +const user = atom({ key: 'user', - default: null, + default: undefined, }); const availableTools = atom({ diff --git a/docs/features/bing_jailbreak.md b/docs/features/bing_jailbreak.md index 17f4850186b..a014de476dc 100644 --- a/docs/features/bing_jailbreak.md +++ b/docs/features/bing_jailbreak.md @@ -1,7 +1,7 @@ --- title: 😈 Bing Jailbreak description: Quick overview of the Bing jailbreak and Sydney's system message -weight: -3 +weight: -2 --- # Bing Jailbreak diff --git a/docs/features/firebase.md b/docs/features/firebase.md new file mode 100644 index 00000000000..d6725044b24 --- /dev/null +++ b/docs/features/firebase.md @@ -0,0 +1,74 @@ +--- +title: 🔥 Firebase CDN Setup +description: This document provides instructions for setting up Firebase CDN for LibreChat +weight: -6 +--- + +# Firebase CDN Setup + +## Steps to Set Up Firebase + +1. Open the [Firebase website](https://firebase.google.com/). +2. Click on "Get started." +3. Sign in with your Google account. + +### Create a New Project + +- Name your project (you can use the same project as Google OAuth). + +![Project Name](https://github.com/danny-avila/LibreChat/assets/81851188/dccce3e0-b639-41ef-8142-19d24911c65c) + +- Optionally, you can disable Google Analytics. + +![Google Analytics](https://github.com/danny-avila/LibreChat/assets/81851188/5d4d58c5-451c-498b-97c0-f123fda79514) + +- Wait for 20/30 seconds for the project to be ready, then click on "Continue." + +![Continue](https://github.com/danny-avila/LibreChat/assets/81851188/6929802e-a30b-4b1e-b124-1d4b281d0403) + +- Click on "All Products." + +![All Products](https://github.com/danny-avila/LibreChat/assets/81851188/92866c82-2b03-4ebe-807e-73a0ccce695e) + +- Select "Storage." + +![Storage](https://github.com/danny-avila/LibreChat/assets/81851188/b22dcda1-256b-494b-a835-a05aeea02e89) + +- Click on "Get Started." + +![Get Started](https://github.com/danny-avila/LibreChat/assets/81851188/c3f0550f-8184-4c79-bb84-fa79655b7978) + +- Click on "Next." + +![Next](https://github.com/danny-avila/LibreChat/assets/81851188/2a65632d-fe22-4c71-b8f1-aac53ee74fb6) + +- Select your "Cloud Storage location." + +![Cloud Storage Location](https://github.com/danny-avila/LibreChat/assets/81851188/c094d4bc-8e5b-43c7-96d9-a05bcf4e2af6) + +- Return to the Project Overview. + +![Project Overview](https://github.com/danny-avila/LibreChat/assets/81851188/c425f4bb-a494-42f2-9fdc-ff2c8ce005e1) + +- Click on "+ Add app" under your project name, then click on "Web." + +![Web](https://github.com/danny-avila/LibreChat/assets/81851188/22dab877-93cb-4828-9436-10e14374e57e) + +- Register the app. + +![Register App](https://github.com/danny-avila/LibreChat/assets/81851188/0a1b0a75-7285-4f03-95cf-bf971bd7d874) + +- Save all this information in a text file. + +![Save Information](https://github.com/danny-avila/LibreChat/assets/81851188/056754ad-9d36-4662-888e-f189ddb38fd3) + +- Fill all the `firebaseConfig` variables in the `.env` file. + +```bash +FIREBASE_API_KEY=api_key #apiKey +FIREBASE_AUTH_DOMAIN=auth_domain #authDomain +FIREBASE_PROJECT_ID=project_id #projectId +FIREBASE_STORAGE_BUCKET=storage_bucket #storageBucket +FIREBASE_MESSAGING_SENDER_ID=messaging_sender_id #messagingSenderId +FIREBASE_APP_ID=1:your_app_id #appId +``` \ No newline at end of file diff --git a/docs/features/index.md b/docs/features/index.md index ab9e705369a..9bfda2be25e 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -22,6 +22,7 @@ weight: 2 * 🔨 [Automated Moderation](./mod_system.md) * 🪙 [Token Usage](./token_usage.md) +* 🔥 [Firebase CDN](./firebase.md) * 🍃 [Manage Your Database](./manage_your_database.md) * 🪵 [Logging System](./logging_system.md) * 📦 [PandoraNext](./pandoranext.md) diff --git a/docs/features/logging_system.md b/docs/features/logging_system.md index 76c3af88847..196849b787d 100644 --- a/docs/features/logging_system.md +++ b/docs/features/logging_system.md @@ -1,7 +1,7 @@ --- title: 🪵 Logging System +weight: -4 description: This doc explains how to use the logging feature of LibreChat, which saves error and debug logs in the `/api/logs` folder. You can use these logs to troubleshoot issues, monitor your server, and report bugs. You can also disable debug logs if you want to save space. -weight: -5 --- ### General diff --git a/docs/features/manage_your_database.md b/docs/features/manage_your_database.md index 23869b1047c..3de01e21cbc 100644 --- a/docs/features/manage_your_database.md +++ b/docs/features/manage_your_database.md @@ -1,7 +1,7 @@ --- title: 🍃 Manage Your Database description: How to install and configure Mongo Express to securely access and manage your MongoDB database in Docker. -weight: -6 +weight: -5 --- diff --git a/docs/features/pandoranext.md b/docs/features/pandoranext.md index 03daaec7c51..e639796e4d2 100644 --- a/docs/features/pandoranext.md +++ b/docs/features/pandoranext.md @@ -1,7 +1,7 @@ --- title: 📦 PandoraNext description: How to deploy PandoraNext to enable the `CHATGPT_REVERSE_PROXY` for use with LibreChat. -weight: -4 +weight: -3 --- # PandoraNext Deployment Guide diff --git a/package-lock.json b/package-lock.json index 748459ac504..1e3ff9e5d76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "express-mongo-sanitize": "^2.2.0", "express-rate-limit": "^6.9.0", "express-session": "^1.17.3", + "firebase": "^10.6.0", "googleapis": "^126.0.1", "handlebars": "^4.7.7", "html": "^1.0.0", @@ -4799,6 +4800,690 @@ "fast-json-stringify": "^5.7.0" } }, + "node_modules/@firebase/analytics": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", + "integrity": "sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz", + "integrity": "sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==" + }, + "node_modules/@firebase/app": { + "version": "0.9.23", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.23.tgz", + "integrity": "sha512-CA5pQ88We3FhyuesGKn1thaPBsJSGJGm6AlFToOmEJagWqBeDoNJqBkry/BsHnCs9xeYWWIprKxvuFmAFkdqoA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.0.tgz", + "integrity": "sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz", + "integrity": "sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==", + "dependencies": { + "@firebase/app-check": "0.8.0", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.23.tgz", + "integrity": "sha512-UCv0LEzcoqAgY+sLsau7aOZz0CJNLN2gESY68bHKmukNXEN6onLPxBKJzn68CsZZGcdiIEXwvrum1riWNPe9Gw==", + "dependencies": { + "@firebase/app": "0.9.23", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "node_modules/@firebase/auth": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.4.0.tgz", + "integrity": "sha512-SfFXZCHDbY+7oSR52NSwx0U7LjYiA+N8imloxphCf3/F+MFty/+mhdjSXGtrJYd0Gbud/qcyedfn2XnWJeIB/g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.4.9.tgz", + "integrity": "sha512-Fw03i7vduIciEBG4imLtA1duJbljgkfbxiBo/EuekcB+BnPxHp+e8OGMUfemPYeO7Munj6kUC9gr5DelsQkiNA==", + "dependencies": { + "@firebase/auth": "1.4.0", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@firebase/auth-compat/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@firebase/auth-compat/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/auth/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/auth/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@firebase/auth/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@firebase/auth/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.1.tgz", + "integrity": "sha512-VAhF7gYwunW4Lw/+RQZvW8dlsf2r0YYqV9W0Gi2Mz8+0TGg1mBJWoUtsHfOr8kPJXhcLsC4eP/z3x6L/Fvjk/A==", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.1.tgz", + "integrity": "sha512-ky82yLIboLxtAIWyW/52a6HLMVTzD2kpZlEilVDok73pNPLjkJYowj8iaIWK5nTy7+6Gxt7d00zfjL6zckGdXQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "1.0.1", + "@firebase/database-types": "1.0.0", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.0.tgz", + "integrity": "sha512-SjnXStoE0Q56HcFgNQ+9SsmJc0c8TqGARdI/T44KXy+Ets3r6x/ivhQozT66bMnCEjJRywYoxNurRTMlZF8VNg==", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.3.2.tgz", + "integrity": "sha512-K4TwMbgArWw+XAEUYX/vtk+TVy9n1uLeJKSrQeb89lwfkfyFINGLPME6YleaS0ovD1ziLM5/0WgL1CR4s53fDg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "@firebase/webchannel-wrapper": "0.10.3", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.22.tgz", + "integrity": "sha512-M166UvFvRri0CK/+5N0MIeXJVxR6BsX0/96xFT506DxRPIFezLjLcvfddtyFgfe0CtyQWoxBXt060uWUg3d/sw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/firestore": "4.3.2", + "@firebase/firestore-types": "3.0.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.0.tgz", + "integrity": "sha512-Meg4cIezHo9zLamw0ymFYBD4SMjLb+ZXIbuN7T7ddXN6MGoICmOTq3/ltdCGoDCS2u+H1XJs2u/cYp75jsX9Qw==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/firestore/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/firestore/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@firebase/firestore/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@firebase/firestore/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@firebase/functions": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.10.0.tgz", + "integrity": "sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.5.tgz", + "integrity": "sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/functions": "0.10.0", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==" + }, + "node_modules/@firebase/functions/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/functions/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@firebase/functions/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@firebase/functions/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@firebase/installations": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.4.tgz", + "integrity": "sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.4.tgz", + "integrity": "sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.4.tgz", + "integrity": "sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.4.tgz", + "integrity": "sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/messaging": "0.12.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==" + }, + "node_modules/@firebase/messaging/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" + }, + "node_modules/@firebase/performance": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", + "integrity": "sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.4.tgz", + "integrity": "sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.4", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==" + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz", + "integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz", + "integrity": "sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==" + }, + "node_modules/@firebase/storage": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.11.2.tgz", + "integrity": "sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.2.tgz", + "integrity": "sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-types": "0.8.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", + "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/storage/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/storage/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@firebase/storage/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@firebase/storage/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.3.tgz", + "integrity": "sha512-+ZplYUN3HOpgCfgInqgdDAbkGGVzES1cs32JJpeqoh87SkRobGXElJx+1GZSaDqzFL+bYiX18qEcBK76mYs8uA==" + }, "node_modules/@floating-ui/core": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", @@ -4841,6 +5526,35 @@ "node": ">=18.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.13", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.13.tgz", + "integrity": "sha512-OEZZu9v9AA+7/tghMDE8o5DAMD5THVnwSqDWuh7PPYO5287rTyqy0xEHT6/e4pbqSrhyLPdQFsam4TwFQVVIIw==", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@headlessui/react": { "version": "1.7.17", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz", @@ -5754,6 +6468,60 @@ "node": ">=4.2.0" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@radix-ui/number": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", @@ -10245,7 +11013,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -10258,14 +11025,12 @@ "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/cliui/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -10274,7 +11039,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -10288,7 +11052,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -11739,7 +12502,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -12691,6 +13453,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -12932,6 +13705,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-10.6.0.tgz", + "integrity": "sha512-bnYwHwZ6zB+dM6mGQPEXcFHtAT2WoVzG6H4SIR8HzURVGKJxBW+TqfP3qcJQjTZV3tDqDTo/XZkVmoU/SovV8A==", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-compat": "0.2.6", + "@firebase/app": "0.9.23", + "@firebase/app-check": "0.8.0", + "@firebase/app-check-compat": "0.3.7", + "@firebase/app-compat": "0.2.23", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "1.4.0", + "@firebase/auth-compat": "0.4.9", + "@firebase/database": "1.0.1", + "@firebase/database-compat": "1.0.1", + "@firebase/firestore": "4.3.2", + "@firebase/firestore-compat": "0.3.22", + "@firebase/functions": "0.10.0", + "@firebase/functions-compat": "0.3.5", + "@firebase/installations": "0.6.4", + "@firebase/installations-compat": "0.2.4", + "@firebase/messaging": "0.12.4", + "@firebase/messaging-compat": "0.2.4", + "@firebase/performance": "0.6.4", + "@firebase/performance-compat": "0.2.4", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-compat": "0.2.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-compat": "0.3.2", + "@firebase/util": "1.9.3" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -13246,7 +14052,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -14040,6 +14845,11 @@ "node": ">= 0.8" } }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -14113,6 +14923,11 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, "node_modules/identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", @@ -16534,6 +17349,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -16800,6 +17620,11 @@ "node": ">=0.8.0" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -21118,6 +21943,29 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -22038,7 +22886,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -24103,9 +24950,9 @@ "dev": true }, "node_modules/undici": { - "version": "5.26.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.3.tgz", - "integrity": "sha512-H7n2zmKEWgOllKkIUkLvFmsJQj062lSm3uA4EYApG8gLuiOM0/go9bIoC3HVaSnfg4xunowDE2i9p8drkXuvDw==", + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz", + "integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -24873,6 +25720,27 @@ "node": ">=12" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/webworkify": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/webworkify/-/webworkify-1.5.0.tgz", @@ -25383,7 +26251,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -25406,7 +26273,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -25424,7 +26290,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } @@ -25432,14 +26297,12 @@ "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/yargs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -25448,7 +26311,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -25497,7 +26359,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.3.2", + "version": "0.3.4", "license": "ISC", "dependencies": { "axios": "^1.3.4", diff --git a/packages/data-provider/src/api-endpoints.ts b/packages/data-provider/src/api-endpoints.ts index 380a0dafaf4..6c7f90c672c 100644 --- a/packages/data-provider/src/api-endpoints.ts +++ b/packages/data-provider/src/api-endpoints.ts @@ -69,3 +69,5 @@ export const assistants = (id?: string) => `/api/assistants${id ? `/${id}` : ''} export const files = () => '/api/files'; export const images = () => `${files()}/images`; + +export const avatar = () => `${images()}/avatar`; diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index b02565652bc..74ad766575e 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -197,6 +197,10 @@ export const uploadImage = (data: FormData): Promise => { return request.postMultiPart(endpoints.images(), data); }; +export const uploadAvatar = (data: FormData): Promise => { + return request.postMultiPart(endpoints.avatar(), data); +}; + export const deleteFiles = async (files: f.BatchFile[]): Promise => request.deleteWithOptions(endpoints.files(), { data: { files }, diff --git a/packages/data-provider/src/keys.ts b/packages/data-provider/src/keys.ts index 6da00f7fc4f..ec150d95873 100644 --- a/packages/data-provider/src/keys.ts +++ b/packages/data-provider/src/keys.ts @@ -24,4 +24,5 @@ export enum MutationKeys { updatePreset = 'updatePreset', deletePreset = 'deletePreset', logoutUser = 'logoutUser', + avatarUpload = 'avatarUpload', } diff --git a/packages/data-provider/src/types/files.ts b/packages/data-provider/src/types/files.ts index 242ad323ed5..95f329f6528 100644 --- a/packages/data-provider/src/types/files.ts +++ b/packages/data-provider/src/types/files.ts @@ -10,6 +10,10 @@ export type FileUploadResponse = { width: number; }; +export type AvatarUploadResponse = { + url: string; +}; + export type FileUploadBody = { formData: FormData; file_id: string; @@ -21,6 +25,12 @@ export type UploadMutationOptions = { onError?: (error: unknown, variables: FileUploadBody, context?: unknown) => void; }; +export type UploadAvatarOptions = { + onSuccess?: (data: AvatarUploadResponse, variables: FormData, context?: unknown) => void; + onMutate?: (variables: FormData) => void | Promise; + onError?: (error: unknown, variables: FormData, context?: unknown) => void; +}; + export type DeleteFilesResponse = { message: string; result: Record;