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 27548468ec3..fd1131092e8 100644 --- a/api/package.json +++ b/api/package.json @@ -45,6 +45,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 9112d62f807..1f0236207d5 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'); @@ -26,6 +27,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 22685abaa71..cbe16b9e210 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,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", @@ -5146,6 +5147,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", @@ -5188,6 +5873,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", @@ -6155,6 +6869,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", @@ -10899,7 +11667,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", @@ -10912,14 +11679,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" } @@ -10928,7 +11693,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", @@ -10942,7 +11706,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", @@ -13606,7 +14369,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" } @@ -14605,6 +15367,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", @@ -14885,6 +15658,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", @@ -15241,7 +16047,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.*" } @@ -16224,6 +17029,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", @@ -16302,6 +17112,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", @@ -18899,10 +19714,10 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + "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", @@ -19180,6 +19995,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", @@ -24167,12 +24987,27 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/protoduck": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-4.0.0.tgz", - "integrity": "sha512-9sxuz0YTU/68O98xuDn8NBxTVH9EuMhrBTxZdiBL0/qxRmWhB/5a8MagAebDa+98vluAZTs8kMZibCdezbRCeQ==", + "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": { - "genfun": "^4.0.1" + "@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": { @@ -25182,7 +26017,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" } @@ -27578,9 +28412,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" }, @@ -28586,6 +29420,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", @@ -29104,7 +29959,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" } @@ -29127,7 +29981,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", @@ -29145,7 +29998,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" } @@ -29153,14 +30005,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" } @@ -29169,7 +30019,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", 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;