diff --git a/PROFILE_PICTURES_FEATURE.md b/PROFILE_PICTURES_FEATURE.md
new file mode 100644
index 0000000..ec8d018
--- /dev/null
+++ b/PROFILE_PICTURES_FEATURE.md
@@ -0,0 +1,92 @@
+# Profile Picture Management Feature
+
+This implementation adds the ability for users to manage their profile pictures from multiple social login sources.
+
+## Features Implemented
+
+### 1. Profile Picture Array
+- Added `profilePictures` array to User model to store multiple profile pictures
+- Each picture includes:
+ - `source`: The provider (gravatar, facebook, google, github, etc.)
+ - `url`: The picture URL
+ - `isSelected`: Boolean indicating if this is the currently selected picture
+
+### 2. Automatic Picture Collection
+- When users link social accounts, their profile pictures are automatically added to the array
+- Gravatar is always available as a fallback option
+- The first linked account's picture becomes the default selection
+
+### 3. Profile Picture Selection Interface
+- Users can view all available profile pictures in their account settings
+- Radio button interface allows selecting which picture to use
+- Pictures are displayed as thumbnails with the source provider name
+
+### 4. Account Unlinking Integration
+- When users unlink a social account, the corresponding profile picture is removed
+- If the removed picture was selected, the system automatically selects another available picture
+- Gravatar is used as the ultimate fallback
+
+## Files Modified
+
+### Models
+- `models/User.js`: Added profilePictures schema and helper methods
+
+### Controllers
+- `controllers/user.js`: Added profile picture selection handler and updated account view
+
+### Views
+- `views/account/profile.pug`: Added profile picture selection interface
+
+### Configuration
+- `config/passport.js`: Updated all OAuth strategies to use new picture management
+- `app.js`: Added route for profile picture updates
+
+## API Endpoints
+
+### POST /account/picture
+Updates the selected profile picture for the authenticated user.
+
+**Parameters:**
+- `pictureSource`: The source of the picture to select (gravatar, facebook, google, etc.)
+
+**Response:**
+- Redirects to `/account` with success/error flash message
+
+## User Model Methods
+
+### addProfilePicture(source, url)
+Adds or updates a profile picture from a specific source.
+
+### selectProfilePicture(source)
+Selects a profile picture as the active one.
+
+### removeProfilePicture(source)
+Removes a profile picture from a specific source and handles fallback selection.
+
+## Usage Example
+
+```javascript
+// Add a profile picture
+user.addProfilePicture('facebook', 'https://graph.facebook.com/123/picture');
+
+// Select a different picture
+user.selectProfilePicture('google');
+
+// Remove a picture
+user.removeProfilePicture('facebook');
+
+await user.save();
+```
+
+## Migration Notes
+
+- Existing users will automatically get gravatar added to their profile pictures array via middleware
+- The current `profile.picture` field is maintained for backward compatibility
+- For existing deployments, run the migration script: `node scripts/migrate-profile-pictures.js`
+- No database schema migration is required as the new fields have default values
+
+## Security Considerations
+
+- Profile picture URLs are validated to come from trusted OAuth providers
+- CSRF protection is maintained for the picture selection form
+- User authentication is required for all profile picture operations
\ No newline at end of file
diff --git a/README.md b/README.md
index 51528c8..3591d99 100644
--- a/README.md
+++ b/README.md
@@ -90,7 +90,7 @@ I also tried to make it as **generic** and **reusable** as possible to cover mos
- Support for a range of foundational and embedding models (DeepSeek, Llama, Mistral, Sentence Transformers, etc.) via LangChain, Together.AI, and Hugging Face
- **API Examples**
- **Backoffice:** Lob (USPS Mail), Paypal, Quickbooks, Stripe, Twilio (text messaging)
- - **Data, Media & Entertainment:** Alpha Vantage (stocks and finance info) with ChartJS, Github, Foursquare, Last.fm, New York Times, PubChem (chemical information), Trakt.tv (movies/TV), Twitch, Tumblr (OAuth 1.0a example), Web Scraping
+ - **Data, Media & Entertainment:** Alpha Vantage (stocks and finance info) with ChartJS, Github, Foursquare, Last.fm, New York Times, PubChem (chemical information), Trakt.tv (movies/TV), Twitch, Tumblr (OAuth 1.0a example), Web Scraping, Wikipedia
- **Maps and Location:** Google Maps, HERE Maps
- **Productivity:** Google Drive, Google Sheets
@@ -459,6 +459,19 @@ The OpenAI moderation API for checking harmful inputs is free to use as long as
+
+
+**Wikipedia API** uses the public REST API which doesn't require any API keys or authentication. The Wikipedia API provides access to Wikipedia content and metadata.
+
+- **No setup required** - The Wikipedia REST API is completely free and open
+- **API Documentation**: Visit Wikipedia REST API for full documentation
+- **Rate Limits**: Please be respectful of Wikipedia's servers and don't make excessive requests
+- **Example endpoints used**:
+ - Search: `https://en.wikipedia.org/api/rest_v1/page/search/{query}`
+ - Page summary: `https://en.wikipedia.org/api/rest_v1/page/summary/{title}`
+
+
+
## Web Analytics
This project supports integrating web analytics tools such as Google Analytics 4 and Facebook Pixel, along with Open Graph metadata for social sharing. Below are instructions to help you set up these features in your application.
diff --git a/app.js b/app.js
index 97150d8..7a0aa13 100644
--- a/app.js
+++ b/app.js
@@ -197,6 +197,7 @@ app.get('/account/verify', passportConfig.isAuthenticated, userController.getVer
app.get('/account/verify/:token', passportConfig.isAuthenticated, userController.getVerifyEmailToken);
app.get('/account', passportConfig.isAuthenticated, userController.getAccount);
app.post('/account/profile', passportConfig.isAuthenticated, userController.postUpdateProfile);
+app.post('/account/picture', passportConfig.isAuthenticated, userController.postUpdatePicture);
app.post('/account/password', passportConfig.isAuthenticated, userController.postUpdatePassword);
app.post('/account/delete', passportConfig.isAuthenticated, userController.postDeleteAccount);
app.post('/account/logout-everywhere', passportConfig.isAuthenticated, userController.postLogoutEverywhere);
@@ -232,6 +233,7 @@ app.get('/api/chart', apiController.getChart);
app.get('/api/google/sheets', passportConfig.isAuthenticated, passportConfig.isAuthorized, apiController.getGoogleSheets);
app.get('/api/quickbooks', passportConfig.isAuthenticated, passportConfig.isAuthorized, apiController.getQuickbooks);
app.get('/api/trakt', apiController.getTrakt);
+app.get('/api/wikipedia', apiController.getWikipedia);
app.get('/api/pubchem', apiController.getPubChem);
/**
diff --git a/config/passport.js b/config/passport.js
index e72b044..1f5094e 100644
--- a/config/passport.js
+++ b/config/passport.js
@@ -175,7 +175,7 @@ passport.use(
user.facebook = profile.id;
user.profile.name = user.profile.name || `${profile.name.givenName} ${profile.name.familyName}`;
user.profile.gender = user.profile.gender || profile._json.gender;
- user.profile.picture = user.profile.picture || `https://graph.facebook.com/${profile.id}/picture?type=large`;
+ user.addProfilePicture('facebook', `https://graph.facebook.com/${profile.id}/picture?type=large`);
await user.save();
req.flash('info', { msg: 'Facebook account has been linked.' });
return done(null, user);
@@ -204,8 +204,8 @@ passport.use(
await saveOAuth2UserTokens(req, accessToken, null, params.expires_in, null, 'facebook');
user.profile.name = `${profile.name.givenName} ${profile.name.familyName}`;
user.profile.gender = profile._json.gender;
- user.profile.picture = `https://graph.facebook.com/${profile.id}/picture?type=large`;
user.profile.location = profile._json.location ? profile._json.location.name : '';
+ user.addProfilePicture('facebook', `https://graph.facebook.com/${profile.id}/picture?type=large`);
await user.save();
return done(null, user);
} catch (err) {
@@ -245,9 +245,11 @@ passport.use(
const user = await saveOAuth2UserTokens(req, accessToken, null, null, null, 'github');
user.github = profile.id;
user.profile.name = user.profile.name || profile.displayName;
- user.profile.picture = user.profile.picture || profile._json.avatar_url;
user.profile.location = user.profile.location || profile._json.location;
user.profile.website = user.profile.website || profile._json.blog;
+ if (profile._json.avatar_url) {
+ user.addProfilePicture('github', profile._json.avatar_url);
+ }
await user.save();
req.flash('info', { msg: 'GitHub account has been linked.' });
return done(null, user);
@@ -282,9 +284,11 @@ passport.use(
req.user = user;
await saveOAuth2UserTokens(req, accessToken, null, null, null, 'github');
user.profile.name = profile.displayName;
- user.profile.picture = profile._json.avatar_url;
user.profile.location = profile._json.location;
user.profile.website = profile._json.blog;
+ if (profile._json.avatar_url) {
+ user.addProfilePicture('github', profile._json.avatar_url);
+ }
await user.save();
return done(null, user);
} catch (err) {
@@ -322,7 +326,9 @@ passport.use(
user.tokens.push({ kind: 'x', accessToken, tokenSecret });
user.profile.name = user.profile.name || profile.displayName;
user.profile.location = user.profile.location || profile._json.location;
- user.profile.picture = user.profile.picture || profile._json.profile_image_url_https;
+ if (profile._json.profile_image_url_https) {
+ user.addProfilePicture('x', profile._json.profile_image_url_https);
+ }
await user.save();
req.flash('info', { msg: 'X account has been linked.' });
return done(null, user);
@@ -340,7 +346,9 @@ passport.use(
user.tokens.push({ kind: 'x', accessToken, tokenSecret });
user.profile.name = profile.displayName;
user.profile.location = profile._json.location;
- user.profile.picture = profile._json.profile_image_url_https;
+ if (profile._json.profile_image_url_https) {
+ user.addProfilePicture('x', profile._json.profile_image_url_https);
+ }
await user.save();
return done(null, user);
} catch (err) {
@@ -381,7 +389,9 @@ const googleStrategyConfig = new GoogleStrategy(
user.google = profile.id;
user.profile.name = user.profile.name || profile.displayName;
user.profile.gender = user.profile.gender || profile._json.gender;
- user.profile.picture = user.profile.picture || profile._json.picture;
+ if (profile._json.picture) {
+ user.addProfilePicture('google', profile._json.picture);
+ }
await user.save();
req.flash('info', { msg: 'Google account has been linked.' });
return done(null, user);
@@ -408,7 +418,9 @@ const googleStrategyConfig = new GoogleStrategy(
await saveOAuth2UserTokens(req, accessToken, refreshToken, params.expires_in, null, 'google');
user.profile.name = profile.displayName;
user.profile.gender = profile._json.gender;
- user.profile.picture = profile._json.picture;
+ if (profile._json.picture) {
+ user.addProfilePicture('google', profile._json.picture);
+ }
await user.save();
return done(null, user);
} catch (err) {
@@ -458,7 +470,9 @@ passport.use(
user.linkedin = profile.id;
user.tokens.push({ kind: 'linkedin', accessToken: null }); // null for now since passport-openidconnect isn't returning it yet; will update when it supports it
user.profile.name = user.profile.name || profile.displayName;
- user.profile.picture = user.profile.picture || profile.photos;
+ if (profile.photos) {
+ user.addProfilePicture('linkedin', profile.photos);
+ }
await user.save();
req.flash('info', { msg: 'LinkedIn account has been linked.' });
return done(null, user);
@@ -483,7 +497,9 @@ passport.use(
user.tokens.push({ kind: 'linkedin', accessToken: null });
user.email = normalizedEmail;
user.profile.name = profile.displayName;
- user.profile.picture = profile.photos || '';
+ if (profile.photos) {
+ user.addProfilePicture('linkedin', profile.photos);
+ }
await user.save();
return done(null, user);
} catch (err) {
@@ -521,7 +537,9 @@ const twitchStrategyConfig = new TwitchStrategy(
const user = await saveOAuth2UserTokens(req, accessToken, refreshToken, params.expires_in, null, 'twitch');
user.twitch = profile.id;
user.profile.name = user.profile.name || profile.displayName;
- user.profile.picture = user.profile.picture || profile.profile_image_url;
+ if (profile.profile_image_url) {
+ user.addProfilePicture('twitch', profile.profile_image_url);
+ }
await user.save();
req.flash('info', { msg: 'Twitch account has been linked.' });
return done(null, user);
@@ -548,7 +566,9 @@ const twitchStrategyConfig = new TwitchStrategy(
await saveOAuth2UserTokens(req, accessToken, refreshToken, params.expires_in, null, 'twitch');
user.profile.name = profile.display_name;
user.profile.email = profile.email;
- user.profile.picture = profile.profile_image_url;
+ if (profile.profile_image_url) {
+ user.addProfilePicture('twitch', profile.profile_image_url);
+ }
await user.save();
return done(null, user);
} catch (err) {
@@ -665,7 +685,9 @@ passport.use(
const data = await response.json();
const profileData = data.response.players[0];
user.profile.name = user.profile.name || profileData.personaname;
- user.profile.picture = user.profile.picture || profileData.avatarmedium;
+ if (profileData.avatarmedium) {
+ user.addProfilePicture('steam', profileData.avatarmedium);
+ }
await user.save();
return done(null, user);
} catch (err) {
@@ -686,7 +708,9 @@ passport.use(
user.email = `${steamId}@steam.com`; // steam does not disclose emails, prevent duplicate keys
user.tokens.push({ kind: 'steam', accessToken: steamId });
user.profile.name = profileData.personaname;
- user.profile.picture = profileData.avatarmedium;
+ if (profileData.avatarmedium) {
+ user.addProfilePicture('steam', profileData.avatarmedium);
+ }
await user.save();
return done(null, user);
} catch (err) {
@@ -809,7 +833,9 @@ const discordStrategyConfig = new OAuth2Strategy(
const user = await saveOAuth2UserTokens(req, accessToken, refreshToken, params.expires_in, null, 'discord');
user.discord = discordProfile.id;
user.profile.name = user.profile.name || discordProfile.username;
- user.profile.picture = user.profile.picture || (discordProfile.avatar ? `https://cdn.discordapp.com/avatars/${discordProfile.id}/${discordProfile.avatar}.png` : undefined);
+ if (discordProfile.avatar) {
+ user.addProfilePicture('discord', `https://cdn.discordapp.com/avatars/${discordProfile.id}/${discordProfile.avatar}.png`);
+ }
await user.save();
req.flash('info', { msg: 'Discord account has been linked.' });
return done(null, user);
@@ -835,7 +861,9 @@ const discordStrategyConfig = new OAuth2Strategy(
req.user = user;
await saveOAuth2UserTokens(req, accessToken, refreshToken, params.expires_in, null, 'discord');
user.profile.name = discordProfile.username;
- user.profile.picture = discordProfile.avatar ? `https://cdn.discordapp.com/avatars/${discordProfile.id}/${discordProfile.avatar}.png` : undefined;
+ if (discordProfile.avatar) {
+ user.addProfilePicture('discord', `https://cdn.discordapp.com/avatars/${discordProfile.id}/${discordProfile.avatar}.png`);
+ }
await user.save();
return done(null, user);
} catch (err) {
diff --git a/controllers/ai.js b/controllers/ai.js
index b2e02e0..d936486 100644
--- a/controllers/ai.js
+++ b/controllers/ai.js
@@ -11,8 +11,14 @@ const { ChatTogetherAI } = require('@langchain/community/chat_models/togetherai'
const { HumanMessage } = require('@langchain/core/messages');
const { CacheBackedEmbeddings } = require('langchain/embeddings/cache_backed');
const { MongoClient } = require('mongodb');
-// eslint-disable-next-line import/extensions
-const pdfjsLib = require('pdfjs-dist/legacy/build/pdf.mjs');
+// Using dynamic import for pdfjs-dist due to ESM compatibility
+let pdfjsLib;
+const getPdfjsLib = async () => {
+ if (!pdfjsLib) {
+ pdfjsLib = await import('pdfjs-dist/legacy/build/pdf.mjs');
+ }
+ return pdfjsLib;
+};
/**
* GET /ai
@@ -243,8 +249,9 @@ exports.postRagIngest = async (req, res) => {
// Process the PDF file
try {
+ const pdfjs = await getPdfjsLib();
const loader = new PDFLoader(filePath, {
- pdfjs: () => Promise.resolve(pdfjsLib),
+ pdfjs: () => Promise.resolve(pdfjs),
});
const docs = await loader.load();
// Split the document into chunks
diff --git a/controllers/api.js b/controllers/api.js
index 02c6f46..520ec69 100644
--- a/controllers/api.js
+++ b/controllers/api.js
@@ -4,11 +4,16 @@ const cheerio = require('cheerio');
const { LastFmNode } = require('lastfm');
const multer = require('multer');
const { OAuth } = require('oauth');
-// Disable eslint rule for @octakit/rest until the following github issue is resolved
-// github npm package bug: https://github.com/octokit/rest.js/issues/446
-// eslint-disable-next-line import/no-unresolved
-const { Octokit } = require('@octokit/rest');
-const stripe = require('stripe')(process.env.STRIPE_SKEY);
+// Using dynamic import for @octokit/rest due to ESM compatibility
+let OctokitClass;
+const getOctokit = async () => {
+ if (!OctokitClass) {
+ const { Octokit } = await import('@octokit/rest');
+ OctokitClass = Octokit;
+ }
+ return OctokitClass;
+};
+const stripe = process.env.STRIPE_SKEY ? require('stripe')(process.env.STRIPE_SKEY) : null;
const twilioClient = require('twilio')(process.env.TWILIO_SID, process.env.TWILIO_TOKEN);
const googledrive = require('@googleapis/drive');
const googlesheets = require('@googleapis/sheets');
@@ -168,6 +173,7 @@ exports.getGithub = async (req, res, next) => {
let userInfo;
let userRepos;
let userEvents;
+ const Octokit = await getOctokit();
if (githubToken) {
github = new Octokit({
auth: req.user.tokens.find((token) => token.kind === 'github').accessToken,
@@ -481,6 +487,10 @@ exports.getStripe = (req, res) => {
* Make a payment.
*/
exports.postStripe = (req, res) => {
+ if (!stripe) {
+ req.flash('errors', { msg: 'Stripe API key not configured.' });
+ return res.redirect('/api/stripe');
+ }
const { stripeToken, stripeEmail } = req.body;
stripe.charges.create(
{
@@ -1444,6 +1454,40 @@ exports.getTrakt = async (req, res, next) => {
}
};
+/**
+ * GET /api/wikipedia
+ * Wikipedia API example.
+ */
+exports.getWikipedia = async (req, res, next) => {
+ const searchTerm = req.query.search || 'Node.js';
+
+ try {
+ // Search for articles
+ const searchResponse = await fetch(`https://en.wikipedia.org/api/rest_v1/page/search/${encodeURIComponent(searchTerm)}?limit=5`);
+ if (!searchResponse.ok) throw new Error('Wikipedia search failed');
+ const searchResults = await searchResponse.json();
+
+ // Get summary for the first result if available
+ let summary = null;
+ if (searchResults.pages && searchResults.pages.length > 0) {
+ const firstResult = searchResults.pages[0];
+ const summaryResponse = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(firstResult.key)}`);
+ if (summaryResponse.ok) {
+ summary = await summaryResponse.json();
+ }
+ }
+
+ res.render('api/wikipedia', {
+ title: 'Wikipedia API',
+ searchTerm,
+ searchResults: searchResults.pages || [],
+ summary
+ });
+ } catch (error) {
+ next(error);
+ }
+};
+
/**
* GET /api/pubchem
* PubChem API example - Chemical information for Aspirin.
diff --git a/controllers/user.js b/controllers/user.js
index a0fd98d..b3c9e18 100644
--- a/controllers/user.js
+++ b/controllers/user.js
@@ -328,6 +328,28 @@ exports.postUpdateProfile = async (req, res, next) => {
}
};
+/**
+ * POST /account/picture
+ * Update selected profile picture.
+ */
+exports.postUpdatePicture = async (req, res, next) => {
+ try {
+ const user = await User.findById(req.user.id);
+ const { pictureSource } = req.body;
+
+ if (user.selectProfilePicture(pictureSource)) {
+ await user.save();
+ req.flash('success', { msg: 'Profile picture has been updated.' });
+ } else {
+ req.flash('errors', { msg: 'Invalid profile picture selection.' });
+ }
+
+ res.redirect('/account');
+ } catch (err) {
+ next(err);
+ }
+};
+
/**
* POST /account/password
* Update current password.
@@ -393,6 +415,10 @@ exports.getOauthUnlink = async (req, res, next) => {
return res.redirect('/account');
}
user.tokens = tokensWithoutProviderToUnlink;
+
+ // Remove profile picture from the unlinked provider
+ user.removeProfilePicture(provider.toLowerCase());
+
await user.save();
req.flash('info', {
msg: `${provider.charAt(0).toUpperCase() + provider.slice(1).toLowerCase()} account has been unlinked.`,
diff --git a/models/User.js b/models/User.js
index 468d61a..3491761 100644
--- a/models/User.js
+++ b/models/User.js
@@ -41,6 +41,12 @@ const userSchema = new mongoose.Schema(
website: String,
picture: String,
},
+
+ profilePictures: [{
+ source: { type: String, required: true }, // 'gravatar', 'facebook', 'google', etc.
+ url: { type: String, required: true },
+ isSelected: { type: Boolean, default: false }
+ }],
},
{ timestamps: true },
);
@@ -63,6 +69,28 @@ userSchema.virtual('isLoginExpired').get(function checkLoginTokenExpiration() {
return Date.now() > this.loginExpires;
});
+// Middleware to ensure gravatar is always available
+userSchema.pre('save', function ensureGravatar(next) {
+ // Ensure gravatar is always available as an option
+ const hasGravatar = this.profilePictures.some(pic => pic.source === 'gravatar');
+
+ if (!hasGravatar) {
+ const gravatarUrl = this.gravatar();
+ this.profilePictures.push({
+ source: 'gravatar',
+ url: gravatarUrl,
+ isSelected: this.profilePictures.length === 0 || !this.profile.picture
+ });
+
+ // If no picture is set, use gravatar
+ if (!this.profile.picture) {
+ this.profile.picture = gravatarUrl;
+ }
+ }
+
+ next();
+});
+
// Middleware to clear expired tokens on save
userSchema.pre('save', function clearExpiredTokens(next) {
const now = Date.now();
@@ -135,6 +163,50 @@ userSchema.statics.generateToken = function generateToken() {
return crypto.randomBytes(32).toString('hex');
};
+// Helper method for managing profile pictures
+userSchema.methods.addProfilePicture = function addProfilePicture(source, url) {
+ // Remove existing picture from same source
+ this.profilePictures = this.profilePictures.filter(pic => pic.source !== source);
+
+ // Add new picture
+ this.profilePictures.push({ source, url, isSelected: false });
+
+ // If this is the first picture or no picture is selected, make it the selected one
+ if (this.profilePictures.length === 1 || !this.profilePictures.some(pic => pic.isSelected)) {
+ this.profilePictures[this.profilePictures.length - 1].isSelected = true;
+ this.profile.picture = url;
+ }
+};
+
+userSchema.methods.selectProfilePicture = function selectProfilePicture(source) {
+ // Deselect all pictures
+ this.profilePictures.forEach(pic => { pic.isSelected = false; });
+
+ // Select the specified picture
+ const selectedPic = this.profilePictures.find(pic => pic.source === source);
+ if (selectedPic) {
+ selectedPic.isSelected = true;
+ this.profile.picture = selectedPic.url;
+ return true;
+ }
+ return false;
+};
+
+userSchema.methods.removeProfilePicture = function removeProfilePicture(source) {
+ const wasSelected = this.profilePictures.find(pic => pic.source === source && pic.isSelected);
+ this.profilePictures = this.profilePictures.filter(pic => pic.source !== source);
+
+ // If the removed picture was selected, select another one or fallback to gravatar
+ if (wasSelected) {
+ if (this.profilePictures.length > 0) {
+ this.profilePictures[0].isSelected = true;
+ this.profile.picture = this.profilePictures[0].url;
+ } else {
+ this.profile.picture = this.gravatar();
+ }
+ }
+};
+
// Helper methods for token verification
userSchema.methods.verifyTokenAndIp = function verifyTokenAndIp(token, ip, tokenType) {
const hashedIp = this.constructor.hashIP(ip);
diff --git a/package-lock.json b/package-lock.json
index 1e77b4d..3b6b313 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -95,6 +95,7 @@
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.27.3.tgz",
"integrity": "sha512-IjLt0gd3L4jlOfilxVXTifn42FnVffMgDC04RJK1KDZpmkBWLv0XC92MVVmkxrFZNS/7l3xWgP/I3nqtX1sQHw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
@@ -166,6 +167,7 @@
"resolved": "https://registry.npmjs.org/@browserbasehq/sdk/-/sdk-2.6.0.tgz",
"integrity": "sha512-83iXP5D7xMm8Wyn66TUaUrgoByCmAJuoMoZQI3sGg3JAiMlTfnCIMqyVBoNSaItaPIkaCnrsj6LiusmXV2X9YA==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
@@ -489,7 +491,6 @@
"resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.11.0.tgz",
"integrity": "sha512-pgtNIYt0jkDBq+sbOX8un3W3NulRrRBkSJEo6Rvze9rjvXT5k71XeSm7QsG2bz6jOcJ1KOqoNDPPSSXT4dw34Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@huggingface/jinja": "^0.5.1",
"@huggingface/tasks": "^0.19.49"
@@ -1179,7 +1180,6 @@
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.78.tgz",
"integrity": "sha512-Nn0x9erQlK3zgtRU1Z8NUjLuyW0gzdclMsvLQ6wwLeDqV91pE+YKl6uQb+L2NUDs4F0N7c2Zncgz46HxrvPzuA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@cfworker/json-schema": "^4.0.2",
"ansi-styles": "^5.0.0",
@@ -1845,7 +1845,6 @@
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.5.tgz",
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.2",
@@ -2336,7 +2335,6 @@
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0.tgz",
"integrity": "sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"playwright": "1.56.0"
},
@@ -2352,7 +2350,6 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -2501,7 +2498,8 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
@@ -2518,6 +2516,7 @@
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/ms": "*"
}
@@ -2554,7 +2553,8 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@types/node": {
"version": "18.19.129",
@@ -2570,6 +2570,7 @@
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
"integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.4"
@@ -2585,7 +2586,8 @@
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@types/uuid": {
"version": "10.0.0",
@@ -2628,6 +2630,7 @@
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"event-target-shim": "^5.0.0"
},
@@ -2668,7 +2671,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2700,6 +2702,7 @@
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"humanize-ms": "^1.2.1"
},
@@ -2954,7 +2957,6 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
@@ -3196,6 +3198,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
@@ -3413,7 +3416,6 @@
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz",
"integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
@@ -4114,7 +4116,6 @@
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -4391,7 +4392,6 @@
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -4688,6 +4688,7 @@
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6"
}
@@ -4703,6 +4704,7 @@
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.8.x"
}
@@ -4799,7 +4801,6 @@
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
"integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.7",
@@ -4989,6 +4990,7 @@
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
"integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"readable-web-to-node-stream": "^3.0.0",
"strtok3": "^6.2.4",
@@ -5205,13 +5207,15 @@
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/formdata-node": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz",
"integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">= 18"
}
@@ -5370,6 +5374,7 @@
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
"license": "Apache-2.0",
"optional": true,
+ "peer": true,
"dependencies": {
"gaxios": "^5.0.0",
"json-bigint": "^1.0.0"
@@ -5384,6 +5389,7 @@
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"debug": "4"
},
@@ -5397,6 +5403,7 @@
"integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==",
"license": "Apache-2.0",
"optional": true,
+ "peer": true,
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^5.0.0",
@@ -5413,6 +5420,7 @@
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"agent-base": "6",
"debug": "4"
@@ -5665,7 +5673,6 @@
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
"integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
@@ -5876,6 +5883,7 @@
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"ms": "^2.0.0"
}
@@ -5953,7 +5961,8 @@
"url": "https://feross.org/support"
}
],
- "license": "BSD-3-Clause"
+ "license": "BSD-3-Clause",
+ "peer": true
},
"node_modules/ignore": {
"version": "5.3.2",
@@ -5961,7 +5970,6 @@
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">= 4"
}
@@ -6406,6 +6414,7 @@
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=8"
},
@@ -6551,7 +6560,8 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
@@ -7403,7 +7413,6 @@
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
"integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@mongodb-js/saslprep": "^1.3.0",
"bson": "^6.10.4",
@@ -8449,6 +8458,7 @@
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
"integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=8"
},
@@ -8576,7 +8586,6 @@
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz",
"integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"playwright-core": "1.56.0"
},
@@ -8628,7 +8637,6 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -8644,6 +8652,7 @@
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">= 0.6.0"
}
@@ -8705,6 +8714,7 @@
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"punycode": "^2.3.1"
},
@@ -8864,7 +8874,8 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/queue-microtask": {
"version": "1.2.3",
@@ -8964,6 +8975,7 @@
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz",
"integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"readable-stream": "^4.7.0"
},
@@ -8980,6 +8992,7 @@
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
@@ -9061,7 +9074,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/resolve": {
"version": "1.22.10",
@@ -9107,6 +9121,7 @@
"resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.6.0.tgz",
"integrity": "sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==",
"license": "Apache-2.0",
+ "peer": true,
"engines": {
"node": ">=10.7.0"
},
@@ -9827,6 +9842,7 @@
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
"integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@tokenizer/token": "^0.3.0",
"peek-readable": "^4.1.0"
@@ -10002,6 +10018,7 @@
"resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
"integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
@@ -10019,6 +10036,7 @@
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"license": "BSD-3-Clause",
+ "peer": true,
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
@@ -10034,6 +10052,7 @@
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">= 4.0.0"
}
@@ -10375,6 +10394,7 @@
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
@@ -10789,6 +10809,7 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -10973,7 +10994,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/scripts/migrate-profile-pictures.js b/scripts/migrate-profile-pictures.js
new file mode 100644
index 0000000..eafbeab
--- /dev/null
+++ b/scripts/migrate-profile-pictures.js
@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+
+/**
+ * Migration script to add gravatar profile pictures to existing users
+ * Run this script after deploying the profile picture feature
+ */
+
+const mongoose = require('mongoose');
+const User = require('../models/User');
+
+async function migrateProfilePictures() {
+ try {
+ // Connect to MongoDB
+ await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/hackathon-starter');
+ console.log('Connected to MongoDB');
+
+ // Find all users without profilePictures array or with empty array
+ const users = await User.find({
+ $or: [
+ { profilePictures: { $exists: false } },
+ { profilePictures: { $size: 0 } }
+ ]
+ });
+
+ console.log(`Found ${users.length} users to migrate`);
+
+ let migratedCount = 0;
+ for (const user of users) {
+ try {
+ // Initialize profilePictures array if it doesn't exist
+ if (!user.profilePictures) {
+ user.profilePictures = [];
+ }
+
+ // Add gravatar if not present
+ const hasGravatar = user.profilePictures.some(pic => pic.source === 'gravatar');
+ if (!hasGravatar) {
+ const gravatarUrl = user.gravatar();
+ user.profilePictures.push({
+ source: 'gravatar',
+ url: gravatarUrl,
+ isSelected: true
+ });
+
+ // Set profile picture if not set
+ if (!user.profile.picture) {
+ user.profile.picture = gravatarUrl;
+ }
+
+ await user.save();
+ migratedCount++;
+ console.log(`Migrated user: ${user.email}`);
+ }
+ } catch (error) {
+ console.error(`Error migrating user ${user.email}:`, error.message);
+ }
+ }
+
+ console.log(`Migration completed. ${migratedCount} users migrated.`);
+ } catch (error) {
+ console.error('Migration failed:', error);
+ process.exit(1);
+ } finally {
+ await mongoose.disconnect();
+ console.log('Disconnected from MongoDB');
+ }
+}
+
+// Run the migration
+if (require.main === module) {
+ migrateProfilePictures();
+}
+
+module.exports = migrateProfilePictures;
\ No newline at end of file
diff --git a/views/account/profile.pug b/views/account/profile.pug
index 8f568e0..459cb8a 100644
--- a/views/account/profile.pug
+++ b/views/account/profile.pug
@@ -48,6 +48,20 @@ block content
label.col-md-3.col-form-label.font-weight-bold.text-right Profile Picture
.col-sm-4.mb-2
img.profile(src=user.profile.picture ? user.profile.picture : user.gravatar(), width='100', height='100')
+ if user.profilePictures && user.profilePictures.length > 1
+ .mt-3
+ p.small Change your profile picture:
+ form(action='/account/picture', method='POST')
+ input(type='hidden', name='_csrf', value=_csrf)
+ .row
+ each picture in user.profilePictures
+ .col-md-6.mb-2
+ .form-check
+ input.form-check-input(type='radio', name='pictureSource', value=picture.source, checked=picture.isSelected, id=`pic-${picture.source}`)
+ label.form-check-label(for=`pic-${picture.source}`)
+ img.me-2(src=picture.url, width='40', height='40', style='border-radius: 50%;')
+ = picture.source.charAt(0).toUpperCase() + picture.source.slice(1)
+ button.btn.btn-sm.btn-outline-primary.mt-2(type='submit') Update Picture
.form-group
.offset-sm-3.col-md-7.pl-2
button.btn.btn.btn-primary(type='submit')
diff --git a/views/api/index.pug b/views/api/index.pug
index fcaa666..8fb42fc 100644
--- a/views/api/index.pug
+++ b/views/api/index.pug
@@ -131,6 +131,12 @@ block content
.card-body
img(src='https://i.imgur.com/Adtl9qg.png', height=40, style='padding: 0px 10px 0px 0px')
| trakt.tv
+ .col-md-4
+ a(href='/api/wikipedia', style='color: #000')
+ .card.mb-3(style='background-color: #f8f9fa')
+ .card-body
+ img(src='https://upload.wikimedia.org/wikipedia/commons/8/80/Wikipedia-logo-v2.svg', height=40, style='padding: 0px 10px 0px 0px')
+ | Wikipedia
.col-md-4
a(href='/api/pubchem', style='color: #fff')
.card.text-white.mb-3(style='background-color: rgba(128, 200, 255, 1)')
diff --git a/views/api/wikipedia.pug b/views/api/wikipedia.pug
new file mode 100644
index 0000000..5414311
--- /dev/null
+++ b/views/api/wikipedia.pug
@@ -0,0 +1,47 @@
+extends ../layout
+
+block content
+ .pb-2.mt-2.mb-4.border-bottom
+ h3 Wikipedia API
+ p.lead Search and retrieve information from Wikipedia
+
+ .row
+ .col-md-12
+ form(method='GET', action='/api/wikipedia')
+ .input-group.mb-3
+ input.form-control(type='text', name='search', placeholder='Search Wikipedia...', value=searchTerm)
+ button.btn.btn-primary(type='submit') Search
+
+ if summary
+ .row
+ .col-md-12
+ .card.mb-4
+ .card-header
+ h4= summary.title
+ .card-body
+ .row
+ if summary.thumbnail
+ .col-md-3
+ img.img-fluid.rounded(src=summary.thumbnail.source, alt=summary.title)
+ .col-md-9
+ p= summary.extract
+ if summary.content_urls && summary.content_urls.desktop
+ a.btn.btn-outline-primary(href=summary.content_urls.desktop.page, target='_blank') Read Full Article
+
+ if searchResults && searchResults.length > 0
+ .row
+ .col-md-12
+ h4 Search Results
+ .list-group
+ each result in searchResults
+ .list-group-item
+ .d-flex.w-100.justify-content-between
+ h5.mb-1= result.title
+ small= result.key
+ p.mb-1= result.excerpt
+ if result.thumbnail
+ img.img-thumbnail(src=result.thumbnail.url, alt=result.title, style='max-width: 100px; height: auto;')
+ else if searchTerm
+ .row
+ .col-md-12
+ .alert.alert-info No results found for "#{searchTerm}"
\ No newline at end of file