diff --git a/script/big-brother.js b/script/big-brother.js index 5978ed4..d577fcc 100644 --- a/script/big-brother.js +++ b/script/big-brother.js @@ -4,26 +4,23 @@ const fs = require('node:fs'); const path = require('path'); //#endregion -//#region VARIABLES +//#region CONSTANTS const commonDir = path.join(__dirname, '../common'); const spotifyRequestsLimit = 50; const thresholdLove = 0.6; +//#endregion -var albums = {}; +//#region STRUCTURE var albumDataStructure = { savedTracks: [], totalTracks: 0, name: "", artistsNames: [] }; - -var accessToken; -try { accessToken = fs.readFileSync(commonDir + '/spotify_access_token', 'utf8') } -catch (err) { console.error(err) } //#endregion //#region GET SAVED TRACKS -async function getOffsetSavedTracks(href=`https://api.spotify.com/v1/me/tracks?offset=0&limit=${spotifyRequestsLimit}`) { +async function getSavedTracks(accessToken, albums, href=`https://api.spotify.com/v1/me/tracks?offset=0&limit=${spotifyRequestsLimit}`) { try { const response = await axios.get(href, { headers: { 'Authorization': 'Bearer ' + accessToken, } }); response.data.items.forEach(t => { @@ -40,23 +37,19 @@ async function getOffsetSavedTracks(href=`https://api.spotify.com/v1/me/tracks?o albums[t.track.album.id].savedTracks.push(t.track.id); } }); - if(response.data.next) await getOffsetSavedTracks(response.data.next); + if(response.data.next) await getSavedTracks(accessToken, albums, response.data.next); } catch (error) { webError("Get saved tracks", error) } } - -async function getSavedTracks() { - await getOffsetSavedTracks(); -} //#endregion //#region TRESHOLD ALGORITHM -async function addAlbums(idsString) { +async function addAlbums(accessToken, idsString) { try { await axios.put(`https://api.spotify.com/v1/me/albums?ids=${idsString}`, { x: 'x' } , { headers: { 'Authorization': 'Bearer ' + accessToken, } }); } catch (error) { webError("Check and add album", error) } } -async function tresholdAlgorithm() { +async function tresholdAlgorithm(albums, accessToken) { var lovedAlbum = [] for(let album in albums) { if(albums[album].savedTracks.length >= albums[album].totalTracks * thresholdLove) { @@ -73,25 +66,25 @@ async function tresholdAlgorithm() { idsCounter = idsCounter+1; if(idsCounter == 20){ - await addAlbums(idsString); + await addAlbums(accessToken, idsString); idsString = ""; idsList = []; idsCounter = 0; } } if(idsCounter > 0) - await addAlbums(idsString); + await addAlbums(accessToken, idsString); } //#endregion //#region REMOVE SAVED TRACKS FROM SAVED ALBUMS -async function removeTracks(idsToDelete) { +async function removeTracks(accessToken, idsToDelete) { try { await axios.delete(`https://api.spotify.com/v1/me/tracks?ids=${idsToDelete}`, { headers: { 'Authorization': 'Bearer ' + accessToken, } }); } catch (error) { webError("Remove tracks", error) } } -async function checkAlbums(idsString, idsList) { +async function checkAlbums(accessToken, idsString, idsList, albums) { try { const response = await axios.get(`https://api.spotify.com/v1/me/albums/contains?ids=${idsString}`, { headers: { 'Authorization': 'Bearer ' + accessToken, } }); @@ -103,7 +96,7 @@ async function checkAlbums(idsString, idsList) { idsToDelete = idsToDelete.concat(track, ','); idsCounter = idsCounter+1; if(idsCounter == 50) { - await removeTracks(idsToDelete); + await removeTracks(accessToken, idsToDelete); var idsToDelete = ""; var idsCounter = 0; } @@ -111,11 +104,11 @@ async function checkAlbums(idsString, idsList) { } } if(idsCounter > 0) - await removeTracks(idsToDelete); + await removeTracks(accessToken, idsToDelete); } catch (error) { webError("Check albums", error) } } -async function removeTracksAlgorithm() { +async function removeTracksAlgorithm(albums, accessToken) { var idsString = ""; var idsList = []; var idsCounter = 0; @@ -126,7 +119,7 @@ async function removeTracksAlgorithm() { idsCounter = idsCounter+1; if(idsCounter == 20){ - await checkAlbums(idsString, idsList); + await checkAlbums(accessToken, idsString, idsList, albums); idsString = ""; idsList = []; @@ -134,7 +127,7 @@ async function removeTracksAlgorithm() { } } if(idsCounter > 0) - await checkAlbums(idsString, idsList); + await checkAlbums(accessToken, idsString, idsList, albums); } //#endregion @@ -152,23 +145,32 @@ function stepBeggining(step) { const sptor = "=".repeat(5); console.log(`\n${sptor} ${step} ${sptor}`); } - -async function stepExecution(stepName, stepFunc) { - stepBeggining(stepName); - await stepFunc() - stepSuccess(stepName); -} //#endregion //#region MAIN async function main() { + var albums = {}; + + var accessToken; + try { accessToken = fs.readFileSync(commonDir + '/spotify_access_token', 'utf8') } + catch (err) { console.error(err) } + + // ====================================================== const step1 = "Get liked tracks"; const step2 = "Apply treshold algorithm"; const step3 = "Remove saved tracks from saved albums"; try { - await stepExecution(step1, getSavedTracks); - await stepExecution(step2, tresholdAlgorithm); - await stepExecution(step3, removeTracksAlgorithm); + stepBeggining(step1); + await getSavedTracks(accessToken, albums); + stepSuccess(step1); + + stepBeggining(step2); + await tresholdAlgorithm(albums, accessToken); + stepSuccess(step2); + + stepBeggining(step3); + await removeTracksAlgorithm(albums, accessToken); + stepSuccess(step3); } catch (error) { } } //#endregion diff --git a/script/package-lock.json b/script/package-lock.json index e4f8228..5d960b7 100644 --- a/script/package-lock.json +++ b/script/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "axios": "^1.6.7" + "axios": "^1.6.7", + "pg": "^8.11.5" }, "devDependencies": { "jest": "^29.7.0" @@ -3141,6 +3142,87 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/pg": { + "version": "8.11.5", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.5.tgz", + "integrity": "sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==", + "dependencies": { + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -3180,6 +3262,41 @@ "node": ">=8" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -3372,6 +3489,14 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -3655,6 +3780,14 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/script/package.json b/script/package.json index 82dd4f7..cda1ab6 100644 --- a/script/package.json +++ b/script/package.json @@ -13,7 +13,8 @@ "author": "Félix Mielcarek", "license": "MIT", "dependencies": { - "axios": "^1.6.7" + "axios": "^1.6.7", + "pg": "^8.11.5" }, "devDependencies": { "jest": "^29.7.0" diff --git a/web/src/app.js b/web/src/app.js index e11f4c5..8444e0c 100644 --- a/web/src/app.js +++ b/web/src/app.js @@ -5,6 +5,7 @@ const cookieParser = require('cookie-parser'); const axios = require('axios'); const queryString = require('querystring'); const fs = require('node:fs'); +const pg = require('pg'); //#endregion //#region CONSTANTS @@ -15,6 +16,18 @@ const clientId = process.env.CLIENT_ID; const clientSecret = process.env.CLIENT_SECRET; const redirectUri = 'http://localhost:3000/callback'; const scope = 'user-read-private user-read-email user-library-read user-library-modify'; +const { Client } = pg +const client = new Client({ + user: process.env.DB_USER, + host: 'localhost', + database: 'bigbrother', + password: process.env.DB_PASSWORD, + port: 5432 +}) +//#endregion + +//#region VARIABLES +let state = ''; //#endregion //#region APP INIT @@ -44,7 +57,7 @@ function generateRandomString(length) { } app.get('/login', function (req, res) { - const state = generateRandomString(16); + state = generateRandomString(16); res.redirect('https://accounts.spotify.com/authorize?' + queryString.stringify({ @@ -60,8 +73,12 @@ app.get('/login', function (req, res) { //#region ACCESS TOKEN app.get('/callback', (req, res) => { res.cookie('account', 'true', { maxAge: 360000 }); + const code = req.query.code; - const state = req.query.state; + if(state != req.query.state) { + console.error("Spotify state error.") + return + } res.redirect('/'); @@ -81,14 +98,43 @@ app.get('/callback', (req, res) => { }; axios(authOptions) - .then(response => { - fs.writeFile(commonDir + '/spotify_access_token', response.data.access_token, err => { + .then(async response => { + /*fs.writeFile(commonDir + '/spotify_access_token', response.data.access_token, err => { if (err) { console.error(err); } else { console.log("Spotify access token recovered.") } - }); + });*/ + + const accessToken = response.data.access_token + + try { + const response = await axios.get(`https://api.spotify.com/v1/me`, { headers: { 'Authorization': 'Bearer ' + accessToken, } }); + + const data = { + SpotifyId: response.data.id, + AccessToken: accessToken + }; + + await client.connect() + + const sqlQuery = ` + INSERT INTO public.users (spotifyid,accesstoken) + VALUES ($1,$2) + ON CONFLICT (spotifyid) DO UPDATE SET + accesstoken = EXCLUDED.accesstoken + `; + + client.query(sqlQuery, [data.SpotifyId, data.AccessToken], (err, res) => { + if (err) { + console.error('Error executing query', err); + return; + } + console.log('Data inserted/updated successfully'); + client.end(); + }); + } catch (error) { console.log('Error getting user Spotify id') } }) .catch(error => { console.log('Error:', error); diff --git a/web/src/package-lock.json b/web/src/package-lock.json index 7997377..a8956c6 100644 --- a/web/src/package-lock.json +++ b/web/src/package-lock.json @@ -7,7 +7,8 @@ "dependencies": { "axios": "^1.6.7", "cookie-parser": "^1.4.6", - "express": "^4.18.3" + "express": "^4.18.3", + "pg": "^8.11.5" } }, "node_modules/accepts": { @@ -570,6 +571,122 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/pg": { + "version": "8.11.5", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.5.tgz", + "integrity": "sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==", + "dependencies": { + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -727,6 +844,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -778,6 +903,14 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } } } } diff --git a/web/src/package.json b/web/src/package.json index 8427237..5e12f1d 100644 --- a/web/src/package.json +++ b/web/src/package.json @@ -2,6 +2,7 @@ "dependencies": { "axios": "^1.6.7", "cookie-parser": "^1.4.6", - "express": "^4.18.3" + "express": "^4.18.3", + "pg": "^8.11.5" } }