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