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 `})`;
+ } 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')}
+
+
+
+
+ >
+ );
+}
+
+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).
+
+
+
+- Optionally, you can disable Google Analytics.
+
+
+
+- Wait for 20/30 seconds for the project to be ready, then click on "Continue."
+
+
+
+- Click on "All Products."
+
+
+
+- Select "Storage."
+
+
+
+- Click on "Get Started."
+
+
+
+- Click on "Next."
+
+
+
+- Select your "Cloud Storage location."
+
+
+
+- Return to the Project Overview.
+
+
+
+- Click on "+ Add app" under your project name, then click on "Web."
+
+
+
+- Register the app.
+
+
+
+- Save all this information in a text file.
+
+
+
+- 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;