From 4dae3adb1eb50e3c4ad36bf45edf1b79254fceea Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 31 Jul 2025 11:42:45 -0400 Subject: [PATCH 01/70] added user models --- database/user.js | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/database/user.js b/database/user.js index 755c757..8418144 100644 --- a/database/user.js +++ b/database/user.js @@ -3,14 +3,27 @@ const db = require("./db"); const bcrypt = require("bcrypt"); const User = db.define("user", { - username: { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + + name: { type: DataTypes.STRING, allowNull: false, - unique: true, - validate: { - len: [3, 20], - }, }, + + bio: { + type: DataTypes.TEXT, + allowNull: true + }, + + avatar_url: { + type: DataTypes.ENUM("image", "gif"), + allowNull: true, + }, + email: { type: DataTypes.STRING, allowNull: true, @@ -19,15 +32,23 @@ const User = db.define("user", { isEmail: true, }, }, + auth0Id: { type: DataTypes.STRING, allowNull: true, unique: true, }, - passwordHash: { + + password_hash: { type: DataTypes.STRING, allowNull: true, + comment: "Securely hashed password", }, + + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW + } }); // Instance method to check password From fc7abb9bf0d9b73b29bba724db60d0931bb450fb Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 31 Jul 2025 14:17:33 -0400 Subject: [PATCH 02/70] added echoes model --- database/echoes.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 database/echoes.js diff --git a/database/echoes.js b/database/echoes.js new file mode 100644 index 0000000..1a166ac --- /dev/null +++ b/database/echoes.js @@ -0,0 +1,58 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Echoes = db.define("user", { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + + sender_id: { + type: DataTypes.INTEGER, + allowNull: false + }, + + type: { + type: DataTypes.ENUM("self", "friend", "public"), + allowNull: false, + }, + + text: { + type: DataTypes.STRING, + allowNull: true, + }, + + unlock_datetime: { + type: DataTypes.DATE, + allowNull: false + }, + + is_unlocked: { + type: DataTypes.VIRTUAL, + get() { + const now = new Date(); + return now >= this.unlock_datetime; + } + }, + + is_archived: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false, + }, + + show_sender_name: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false, + } + +}, { + tableName: 'Echoes', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', +}); + +module.exports = Echoes; From 1eb36d2fc7d382729debe37771914d05eebee59e Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 31 Jul 2025 14:29:03 -0400 Subject: [PATCH 03/70] created multiple users in seed for testing purposes and updated table name in Users model --- database/seed.js | 7 ++++--- database/user.js | 18 +++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/database/seed.js b/database/seed.js index e58b595..d23ee7e 100644 --- a/database/seed.js +++ b/database/seed.js @@ -7,9 +7,10 @@ const seed = async () => { await db.sync({ force: true }); // Drop and recreate tables const users = await User.bulkCreate([ - { username: "admin", passwordHash: User.hashPassword("admin123") }, - { username: "user1", passwordHash: User.hashPassword("user111") }, - { username: "user2", passwordHash: User.hashPassword("user222") }, + { name: "jeramy", password_hash: User.hashPassword("123"), email: "jeramy@gmail.com" }, + { name: "aiyanna", password_hash: User.hashPassword("456"), email: "aiyanna@gmail.com" }, + { name: "emmanuel", password_hash: User.hashPassword("789"), email: "emmanuel@gmail.com" }, + { name: "olivia", password_hash: User.hashPassword("101112"), email: "olivia@gmail.com" }, ]); console.log(`👤 Created ${users.length} users`); diff --git a/database/user.js b/database/user.js index 8418144..665ca55 100644 --- a/database/user.js +++ b/database/user.js @@ -26,7 +26,7 @@ const User = db.define("user", { email: { type: DataTypes.STRING, - allowNull: true, + allowNull: false, unique: true, validate: { isEmail: true, @@ -43,20 +43,20 @@ const User = db.define("user", { type: DataTypes.STRING, allowNull: true, comment: "Securely hashed password", - }, - - created_at: { - type: DataTypes.DATE, - defaultValue: DataTypes.NOW - } + }, +},{ + tableName: 'users', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', }); // Instance method to check password User.prototype.checkPassword = function (password) { - if (!this.passwordHash) { + if (!this.password_hash) { return false; // Auth0 users don't have passwords } - return bcrypt.compareSync(password, this.passwordHash); + return bcrypt.compareSync(password, this.password_hash); }; // Class method to hash password From d361c72f4df2c0a808c66adb98c0c2bd0f667193 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 31 Jul 2025 14:46:02 -0400 Subject: [PATCH 04/70] issues connecting to database resolved --- .env.example | 3 --- package-lock.json | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 848ad61..0000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -DATABASE_URL=postgres://hostname:5432/database-name -JWT_SECRET=super-secret-jwt-key -FRONTEND_URL=http://localhost:3000 diff --git a/package-lock.json b/package-lock.json index af0cf82..1b30289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "license": "ISC", "dependencies": { From 7cc241bc225a540c2835f37bf907e01be0fce558 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 31 Jul 2025 15:14:09 -0400 Subject: [PATCH 05/70] changed name attribute to username --- api/index.js | 3 ++- api/users.js | 16 ++++++++++++++++ database/seed.js | 8 ++++---- database/user.js | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 api/users.js diff --git a/api/index.js b/api/index.js index f08162e..25fea5f 100644 --- a/api/index.js +++ b/api/index.js @@ -1,7 +1,8 @@ const express = require("express"); const router = express.Router(); const testDbRouter = require("./test-db"); +const usersRouter = require("./users") router.use("/test-db", testDbRouter); - +router.use("/users", usersRouter); module.exports = router; diff --git a/api/users.js b/api/users.js new file mode 100644 index 0000000..9d8192c --- /dev/null +++ b/api/users.js @@ -0,0 +1,16 @@ +const express = require("express") +const router = express.Router(); +const { User } = require("../database"); + +router.get("/", async (req, res) => { + try { + const users = await User.findAll({ + attributes: { exclude: ["password_hash"] } + }); + res.json(users); + } catch (err) { + res.status(500).json({ error: "Failed to fetch users" }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/database/seed.js b/database/seed.js index d23ee7e..a61cbcd 100644 --- a/database/seed.js +++ b/database/seed.js @@ -7,10 +7,10 @@ const seed = async () => { await db.sync({ force: true }); // Drop and recreate tables const users = await User.bulkCreate([ - { name: "jeramy", password_hash: User.hashPassword("123"), email: "jeramy@gmail.com" }, - { name: "aiyanna", password_hash: User.hashPassword("456"), email: "aiyanna@gmail.com" }, - { name: "emmanuel", password_hash: User.hashPassword("789"), email: "emmanuel@gmail.com" }, - { name: "olivia", password_hash: User.hashPassword("101112"), email: "olivia@gmail.com" }, + { username: "jeramy", password_hash: User.hashPassword("123456"), email: "jeramy@gmail.com" }, + { username: "aiyanna", password_hash: User.hashPassword("456"), email: "aiyanna@gmail.com" }, + { username: "emmanuel", password_hash: User.hashPassword("789"), email: "emmanuel@gmail.com" }, + { username: "olivia", password_hash: User.hashPassword("101112"), email: "olivia@gmail.com" }, ]); console.log(`👤 Created ${users.length} users`); diff --git a/database/user.js b/database/user.js index 665ca55..fc63778 100644 --- a/database/user.js +++ b/database/user.js @@ -9,7 +9,7 @@ const User = db.define("user", { primaryKey: true, }, - name: { + username: { type: DataTypes.STRING, allowNull: false, }, From ccbe0fc906d1ece0b72b150d5a5af10acdeef59b Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 31 Jul 2025 15:24:48 -0400 Subject: [PATCH 06/70] get user by id and get all user routes added --- api/users.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/api/users.js b/api/users.js index 9d8192c..abe1d25 100644 --- a/api/users.js +++ b/api/users.js @@ -13,4 +13,16 @@ router.get("/", async (req, res) => { } }); +router.get("/:id", async (req, res) => { + try { + const user = await User.findByPk(req.params.id, { + attributes: { exclude: ["password_hash"] } + }); + + res.json(user); + } catch (err) { + res.status(500).json({ error: "Failed to fetch user" }) + } +}); + module.exports = router; \ No newline at end of file From 32b34e8934763390a235609d49fa19d4e2e02288 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 31 Jul 2025 15:48:22 -0400 Subject: [PATCH 07/70] changing database names --- database/db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/db.js b/database/db.js index b251a9d..3b580f1 100644 --- a/database/db.js +++ b/database/db.js @@ -3,7 +3,7 @@ const { Sequelize } = require("sequelize"); const pg = require("pg"); // Feel free to rename the database to whatever you want! -const dbName = "capstone-1"; +const dbName = "capstone-2"; const db = new Sequelize( process.env.DATABASE_URL || `postgres://localhost:5432/${dbName}`, From 612bd5a0a14ec6c0b78cc697357d44b6828c7786 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Fri, 1 Aug 2025 13:36:22 -0400 Subject: [PATCH 08/70] addded echo_visibility model so users can customize who can see their echoes --- database/echo_visibility.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 database/echo_visibility.js diff --git a/database/echo_visibility.js b/database/echo_visibility.js new file mode 100644 index 0000000..a465273 --- /dev/null +++ b/database/echo_visibility.js @@ -0,0 +1,26 @@ +const { DataTypes} = require("sequelize"); +const db = require("./db"); + +const Echo_visibility = db.define("echo_visibility", { + echo_id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + }, + + scope: { + type: DataTypes.ENUM("self", "friends", "public", "custom"), + allowNull: false, + + }, + + note: { + type: DataTypes.TEXT, + allowNull: true + } +}, { + tableName: 'echo_visibility', + timestamps: false, +}); + +module.exports = Echo_visibility; From 525ded48f293cc50828fc1bd11be1b145b406413 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Fri, 1 Aug 2025 14:37:41 -0400 Subject: [PATCH 09/70] update some models with references and added echo_recipients model --- database/echo_recipients.js | 29 +++++++++++++++++++++++++++++ database/echoes.js | 12 ++++++++---- 2 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 database/echo_recipients.js diff --git a/database/echo_recipients.js b/database/echo_recipients.js new file mode 100644 index 0000000..2e3223e --- /dev/null +++ b/database/echo_recipients.js @@ -0,0 +1,29 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Echo_recipients = db.define("echo_recipients", { + echo_id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + references: { + model: 'echoes', // target table name + key: 'id' + }, + }, + + recipient_id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + references: { + model: 'users', // target table name + key: 'id' + }, + } +}, { + tableName: 'echo_recipients', + timestamps: false +}); + +module.exports = Echo_recipients; diff --git a/database/echoes.js b/database/echoes.js index 1a166ac..bb471e1 100644 --- a/database/echoes.js +++ b/database/echoes.js @@ -1,16 +1,20 @@ const { DataTypes } = require("sequelize"); const db = require("./db"); -const Echoes = db.define("user", { +const Echoes = db.define("echoes", { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, - sender_id: { + sender_id: { type: DataTypes.INTEGER, - allowNull: false + allowNull: false, + references: { + model: 'users', // target table + key: 'id' + } }, type: { @@ -49,7 +53,7 @@ const Echoes = db.define("user", { } }, { - tableName: 'Echoes', + tableName: 'echoes', timestamps: true, createdAt: 'created_at', updatedAt: 'updated_at', From b5e3a2a534f9e97e283e50b142de82d6c09708e8 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Fri, 1 Aug 2025 15:20:23 -0400 Subject: [PATCH 10/70] added tags model --- database/tags.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 database/tags.js diff --git a/database/tags.js b/database/tags.js new file mode 100644 index 0000000..93e9352 --- /dev/null +++ b/database/tags.js @@ -0,0 +1,21 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Tags = db.define("tags", { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + autoIncrement: true, // auto generated tags IDs + }, + + name: { + type: DataTypes.TEXT, + allowNull: false, + } +}, { + tableName: 'tags', + timestamps: true +}); + +module.exports = Tags; From bbf270daec19d6c946d35b4f2a5f12e2b23ea8ad Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Fri, 1 Aug 2025 15:51:31 -0400 Subject: [PATCH 11/70] added echo tags model --- database/echo_tags.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 database/echo_tags.js diff --git a/database/echo_tags.js b/database/echo_tags.js new file mode 100644 index 0000000..8d4fb0e --- /dev/null +++ b/database/echo_tags.js @@ -0,0 +1,29 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Echo_tags = db.define('echo_tags', { + echo_id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + references: { + model: "echoes", // target table + key: "id" + } + }, + + tag_id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + references: { + model: "tags", + key: "id" + } + } +}, { + tableName: 'echo_tas', + timestamps: false +}); + +module.exports = Echo_tags; From 25dc26d30711db2be6849e96dd839f939fd163a6 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Mon, 4 Aug 2025 11:07:36 -0400 Subject: [PATCH 12/70] media model created for image/video/audio uploads --- database/media.js | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 database/media.js diff --git a/database/media.js b/database/media.js new file mode 100644 index 0000000..d3492db --- /dev/null +++ b/database/media.js @@ -0,0 +1,55 @@ +const { DataTypes } = require("sequelize"); +const db = require("/.db"); + +const Media = db.define("media", { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + + echo_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'echoes', + key: 'id' + } + }, + + type: { + type: DataTypes.ENUM('image', 'audio', 'video'), + allowNull: false, + }, + + url: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isUrl: true + } + }, + + file_size: { + type: DataTypes.INTEGER, + allowNull: false + }, + + duration_seconds: { + // only applies to audio or video + type: DataTypes.INTEGER, + allowNull: true + }, + + thumbnail_url: { + // optional thumbnail + type: DataTypes.STRING, + allowNull: true + } +}, { + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at' +}); + +module.exports = Media; From 952c8c753c1d9e1da8937135456dd6ea1a511ed3 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Mon, 4 Aug 2025 11:28:36 -0400 Subject: [PATCH 13/70] added reports model for user reports --- database/reports.js | 56 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 database/reports.js diff --git a/database/reports.js b/database/reports.js new file mode 100644 index 0000000..d83737f --- /dev/null +++ b/database/reports.js @@ -0,0 +1,56 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Reports = db.define("reports", { + id: { + primaryKey: true, + type: DataTypes.INTEGER, + autoIncrement: true + }, + + echo_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'echoes', + key: 'id' + } + }, + + reporter_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + + type: { + type: DataTypes.ENUM('spam', 'harrassment', 'hate_speech', 'explicit', 'other'), + allowNull: false, + }, + + reasons: { + // option message for report + type: DataTypes.TEXT, + allowNull: true + }, + + status: { + type: DataTypes.ENUM('pending', 'reviewed', 'dismissed'), + allowNull: false, + defaultValue: 'pending' + }, + + reviewed_at: { + type: DataTypes.DATE, + allowNull: true + } +}, { + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', +}); + +module.exports = Reports; \ No newline at end of file From 2053cc9a48c6efca35792992e4c3c59a33c1609a Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Mon, 4 Aug 2025 11:53:59 -0400 Subject: [PATCH 14/70] friends model so users can add one another as friends --- database/echo_tags.js | 2 +- database/friends.js | 48 +++++++++++++++++++++++++++++++++++++++++++ database/reactions.js | 0 database/replies.js | 0 database/user.js | 1 + 5 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 database/friends.js create mode 100644 database/reactions.js create mode 100644 database/replies.js diff --git a/database/echo_tags.js b/database/echo_tags.js index 8d4fb0e..f018005 100644 --- a/database/echo_tags.js +++ b/database/echo_tags.js @@ -22,7 +22,7 @@ const Echo_tags = db.define('echo_tags', { } } }, { - tableName: 'echo_tas', + tableName: 'echo_tags', timestamps: false }); diff --git a/database/friends.js b/database/friends.js new file mode 100644 index 0000000..6ce2209 --- /dev/null +++ b/database/friends.js @@ -0,0 +1,48 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Friends = db.define("friends", { + id: { + primaryKey: true, + type: DataTypes.INTEGER, + autoIncrement: true + }, + + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + + friend_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + + status: { + type: DataTypes.ENUM('pending', 'accepted', 'blocked'), + allowNull: false, + defaultValue: 'pending' + } +}, { + tableName: 'friends', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + indexes: [ + { + unique: true, + fields: ['user_id', 'friend_id'], + name: 'unique_user_friend_pair' + } + ] +}); + +module.exports = Friends; \ No newline at end of file diff --git a/database/reactions.js b/database/reactions.js new file mode 100644 index 0000000..e69de29 diff --git a/database/replies.js b/database/replies.js new file mode 100644 index 0000000..e69de29 diff --git a/database/user.js b/database/user.js index fc63778..2bf92ad 100644 --- a/database/user.js +++ b/database/user.js @@ -12,6 +12,7 @@ const User = db.define("user", { username: { type: DataTypes.STRING, allowNull: false, + unique: true }, bio: { From 134e4264fd52718c4a4e42f48caa349c04991fed Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Mon, 4 Aug 2025 13:19:17 -0400 Subject: [PATCH 15/70] replies model for user replies to echos and commments --- database/media.js | 23 +++++++++++++++++++++-- database/replies.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/database/media.js b/database/media.js index d3492db..8d4afef 100644 --- a/database/media.js +++ b/database/media.js @@ -10,13 +10,22 @@ const Media = db.define("media", { echo_id: { type: DataTypes.INTEGER, - allowNull: false, + allowNull: true, references: { model: 'echoes', key: 'id' } }, + reply_id: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: 'replies', + key: 'id' + } + }, + type: { type: DataTypes.ENUM('image', 'audio', 'video'), allowNull: false, @@ -49,7 +58,17 @@ const Media = db.define("media", { }, { timestamps: true, createdAt: 'created_at', - updatedAt: 'updated_at' + updatedAt: 'updated_at', + validate: { + eitherEchoOrReply() { + if (!this.echo_id && !this.reply_id) { + throw new Error('Media must belong to either an echo or reply.'); + } + if (this.echo_id && this.reply_id) { + throw new Error('Media cannot belong to both an echo and a reply.'); + } + } + } }); module.exports = Media; diff --git a/database/replies.js b/database/replies.js index e69de29..9f8f7d2 100644 --- a/database/replies.js +++ b/database/replies.js @@ -0,0 +1,44 @@ +const { DataTypes } = require('sequelize'); +const db = require('./db'); + +const Replies = db.define('replies', { + id: { + primaryKey: true, + autoIncrement: true, + type: DataTypes.INTEGER + }, + + echo_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'echoes', + key: 'id' + } + }, + + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + + parent_reply_id: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: 'replies', + key: 'id' + } + } +}, { + tableName: 'replies', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at' +}); + +module.exports = Replies; \ No newline at end of file From 78f682eb1f65fa067cce3cea7295836f6c87cc39 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Mon, 4 Aug 2025 13:27:19 -0400 Subject: [PATCH 16/70] reaction model allows users to have one reaction to an echo --- database/reactions.js | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/database/reactions.js b/database/reactions.js index e69de29..3d8d87d 100644 --- a/database/reactions.js +++ b/database/reactions.js @@ -0,0 +1,44 @@ +const { DataTypes } = require('sequelize'); +const db = require('./db'); + +const Reactions = db.define('reactions', { + id: { + primaryKey: true, + type: DataTypes.INTEGER, + autoIncrement: true + }, + + echo_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'echoes', + key: 'id' + } + }, + + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + + type: { + type: DataTypes.ENUM('sad', 'funny', 'happy'), + allowNull: false + } +}, { + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + indexes: [{ + unique: true, + fields: ['user_id', 'echo_id'], + name: 'unique_user_echo_reaction_pair' + }] +}); + +module.exports = Reactions; From 3d3ebaed270090dc020597117f715657c8555fc0 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Mon, 4 Aug 2025 13:42:43 -0400 Subject: [PATCH 17/70] done with models, starting associations now --- database/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/database/index.js b/database/index.js index e498df6..6fbf6c4 100644 --- a/database/index.js +++ b/database/index.js @@ -1,5 +1,11 @@ const db = require("./db"); const User = require("./user"); +const Echoes = require('./echoes'); +const Echo_visibility = require('./echo_visibility'); +const Echo_recipients = require('./echo_recipients'); +const Echo_tags = require('./echo_tags'); + + module.exports = { db, From 43566250884b5ddb06cc77efbf2b8d9da902f2ae Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Mon, 4 Aug 2025 15:38:23 -0400 Subject: [PATCH 18/70] all associations between models completed --- database/index.js | 95 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/database/index.js b/database/index.js index 6fbf6c4..8517208 100644 --- a/database/index.js +++ b/database/index.js @@ -4,10 +4,105 @@ const Echoes = require('./echoes'); const Echo_visibility = require('./echo_visibility'); const Echo_recipients = require('./echo_recipients'); const Echo_tags = require('./echo_tags'); +const Friends = require('./friends'); +const Media = require('./media'); +const Reactions = require('./reactions'); +const Replies = require('./replies'); +const Reports = require('./reports'); +const Tags = require('./tags'); +//--------Defining associations------------- +// Echos <> user (sender) +Echoes.belongsTo(User, {foreignKey: 'sender_id', as: 'sender'}); +User.hasMany(Echoes, {foreignKey: 'sender_id', as: 'sent_echoes'}); +// Echoes <> media +Echoes.hasMany(Media, {foreignKey: 'echo_id'}); +Media.belongsTo(Echoes, {foreignKey: 'echo_id'}); + +// Replies <> Echoes +Replies.belongsTo(Echoes, {foreignKey: 'echo_id'}); +Echoes.hasMany(Replies, {foreignKey: 'echo_id'}); + +// Replies <> User +Replies.belongsTo(User, {foreignKey: 'user_id'}); +User.hasMany(Replies, {foreignKey: 'user_id'}); + +// Replies <> Replies (nested replies) +Replies.belongsTo(Replies, {foreignKey: 'parent_reply_id', as: 'parent_reply'}); +Replies.hasMany(Replies, {foreignKey: 'parent_reply_id', as: 'child_replies'}); + +// Media <> Replies +Media.belongsTo(Replies, {foreignKey: 'reply_id'}); +Replies.hasMany(Media, {foreignKey: 'reply_id'}); + +// Reactions <> Echoes +Reactions.belongsTo(Echoes, {foreignKey: 'echo_id'}); +Echoes.hasMany(Reactions, {foreignKey: 'echo_id'}); + +// Reactions <> User +Reactions.belongsTo(User, {foreignKey: 'user_id'}); +User.hasMany(Reactions, {foreignKey: 'user_id'}); + +// Reports <> Echoes +Reports.belongsTo(Echoes, {foreignKey: 'echo_id'}); +Echoes.hasMany(Reports, {foreignKey: 'echo_id'}); + +// Reports <> Users +Reports.belongsTo(User, {foreignKey: 'user_id', as: 'reporter'}); +User.hasMany(Reports, {foreignKey: 'user_id', as: 'submitted_reports'}); + +// Echoes <> Echo_visibility (1:1) +Echoes.hasOne(Echo_visibility, {foreignKey: 'echo_id'}); +Echo_visibility.belongsTo(Echoes, {foreignKey: 'echo_id'}); + +// Echoes <> Tags (many to many through echo_tags) +Echoes.belongsToMany(Tags, { + through: Echo_tags, + foreignKey: 'echo_id', + otherKey: 'tag_id', + as: 'tags' +}); +Tags.belongsToMany(Echoes, { + through: Echo_tags, + foreignKey: 'tag_id', + otherKey: 'echo_id', + as: 'echoes' +}); + +// Echoes <> users (recipients- many to many) +Echoes.belongsToMany(User, { + through: Echo_recipients, + foreignKey: 'echo_id', + otherKey: 'recipient_id', + as: 'recipients' +}); +User.belongsTo(Echoes, { + through: Echo_recipients, + foreignKey: 'recipient_id', + otherKey: 'echo_id', + as: 'received_echoes' +}); + +// Users <> Users (friends) +User.belongsToMany(User, { + through: Friends, + as: 'friends', + foreignKey: 'user_id', + otherKey: 'friend_id' +}) module.exports = { db, User, + Echoes, + Echo_visibility, + Echo_recipients, + Echo_tags, + Friends, + Media, + Reactions, + Replies, + Reports, + Tags }; From 043d8d046c810ae19c1f48142f3a1d08536a7a09 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Mon, 4 Aug 2025 15:44:35 -0400 Subject: [PATCH 19/70] fixed belongsTo to belongsToMany for Echoes <> users associatons --- database/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/database/index.js b/database/index.js index 8517208..a8f3db9 100644 --- a/database/index.js +++ b/database/index.js @@ -78,7 +78,7 @@ Echoes.belongsToMany(User, { otherKey: 'recipient_id', as: 'recipients' }); -User.belongsTo(Echoes, { +User.belongsToMany(Echoes, { through: Echo_recipients, foreignKey: 'recipient_id', otherKey: 'echo_id', @@ -92,6 +92,8 @@ User.belongsToMany(User, { foreignKey: 'user_id', otherKey: 'friend_id' }) + + module.exports = { db, User, From 676401bb98d3cf61e8ef426ee6d5790895b81501 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 6 Aug 2025 11:08:15 -0400 Subject: [PATCH 20/70] added associatons --- database/index.js | 163 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 133 insertions(+), 30 deletions(-) diff --git a/database/index.js b/database/index.js index a8f3db9..f34f19b 100644 --- a/database/index.js +++ b/database/index.js @@ -13,62 +13,160 @@ const Tags = require('./tags'); //--------Defining associations------------- -// Echos <> user (sender) -Echoes.belongsTo(User, {foreignKey: 'sender_id', as: 'sender'}); -User.hasMany(Echoes, {foreignKey: 'sender_id', as: 'sent_echoes'}); +// Echoes <> user (sender) +Echoes.belongsTo(User, { + foreignKey: 'sender_id', + as: 'sender', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +User.hasMany(Echoes, { + foreignKey: 'sender_id', + as: 'sent_echoes', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); // Echoes <> media -Echoes.hasMany(Media, {foreignKey: 'echo_id'}); -Media.belongsTo(Echoes, {foreignKey: 'echo_id'}); +Echoes.hasMany(Media, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Media.belongsTo(Echoes, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); // Replies <> Echoes -Replies.belongsTo(Echoes, {foreignKey: 'echo_id'}); -Echoes.hasMany(Replies, {foreignKey: 'echo_id'}); +Replies.belongsTo(Echoes, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Echoes.hasMany(Replies, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); // Replies <> User -Replies.belongsTo(User, {foreignKey: 'user_id'}); -User.hasMany(Replies, {foreignKey: 'user_id'}); +Replies.belongsTo(User, { + foreignKey: 'user_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +User.hasMany(Replies, { + foreignKey: 'user_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); // Replies <> Replies (nested replies) -Replies.belongsTo(Replies, {foreignKey: 'parent_reply_id', as: 'parent_reply'}); -Replies.hasMany(Replies, {foreignKey: 'parent_reply_id', as: 'child_replies'}); +Replies.belongsTo(Replies, { + foreignKey: 'parent_reply_id', + as: 'parent_reply', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Replies.hasMany(Replies, { + foreignKey: 'parent_reply_id', + as: 'child_replies', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); // Media <> Replies -Media.belongsTo(Replies, {foreignKey: 'reply_id'}); -Replies.hasMany(Media, {foreignKey: 'reply_id'}); +Media.belongsTo(Replies, { + foreignKey: 'reply_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Replies.hasMany(Media, { + foreignKey: 'reply_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); // Reactions <> Echoes -Reactions.belongsTo(Echoes, {foreignKey: 'echo_id'}); -Echoes.hasMany(Reactions, {foreignKey: 'echo_id'}); +Reactions.belongsTo(Echoes, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Echoes.hasMany(Reactions, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); // Reactions <> User -Reactions.belongsTo(User, {foreignKey: 'user_id'}); -User.hasMany(Reactions, {foreignKey: 'user_id'}); +Reactions.belongsTo(User, { + foreignKey: 'user_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +User.hasMany(Reactions, { + foreignKey: 'user_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); // Reports <> Echoes -Reports.belongsTo(Echoes, {foreignKey: 'echo_id'}); -Echoes.hasMany(Reports, {foreignKey: 'echo_id'}); +Reports.belongsTo(Echoes, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Echoes.hasMany(Reports, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); // Reports <> Users -Reports.belongsTo(User, {foreignKey: 'user_id', as: 'reporter'}); -User.hasMany(Reports, {foreignKey: 'user_id', as: 'submitted_reports'}); +Reports.belongsTo(User, { + foreignKey: 'user_id', + as: 'reporter', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +User.hasMany(Reports, { + foreignKey: 'user_id', + as: 'submitted_reports', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); // Echoes <> Echo_visibility (1:1) -Echoes.hasOne(Echo_visibility, {foreignKey: 'echo_id'}); -Echo_visibility.belongsTo(Echoes, {foreignKey: 'echo_id'}); +Echoes.hasOne(Echo_visibility, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Echo_visibility.belongsTo(Echoes, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); // Echoes <> Tags (many to many through echo_tags) Echoes.belongsToMany(Tags, { through: Echo_tags, foreignKey: 'echo_id', otherKey: 'tag_id', - as: 'tags' + as: 'tags', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' }); Tags.belongsToMany(Echoes, { through: Echo_tags, foreignKey: 'tag_id', otherKey: 'echo_id', - as: 'echoes' + as: 'echoes', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' }); // Echoes <> users (recipients- many to many) @@ -76,13 +174,17 @@ Echoes.belongsToMany(User, { through: Echo_recipients, foreignKey: 'echo_id', otherKey: 'recipient_id', - as: 'recipients' + as: 'recipients', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' }); User.belongsToMany(Echoes, { through: Echo_recipients, foreignKey: 'recipient_id', otherKey: 'echo_id', - as: 'received_echoes' + as: 'received_echoes', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' }); // Users <> Users (friends) @@ -90,9 +192,10 @@ User.belongsToMany(User, { through: Friends, as: 'friends', foreignKey: 'user_id', - otherKey: 'friend_id' -}) - + otherKey: 'friend_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); module.exports = { db, From 3518763729865489928d248a5b4352a6b9f1912a Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 6 Aug 2025 11:49:49 -0400 Subject: [PATCH 21/70] added seed data for echoes --- database/seed.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/database/seed.js b/database/seed.js index a61cbcd..b510b0f 100644 --- a/database/seed.js +++ b/database/seed.js @@ -17,7 +17,37 @@ const seed = async () => { // Create more seed data here once you've created your models // Seed files are a great way to test your database schema! - + const echoes = await Echoes.bulkCreate([ + { + sender_id: users[0].id, + type: "self", + text: "This is Jeramy’s private echo", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 60), // unlock in 1 hour + show_sender_name: true + }, + { + sender_id: users[1].id, + type: "friend", + text: "Aiyanna’s echo to friends", + unlock_datetime: new Date(Date.now() - 1000 * 60 * 60), // already unlocked + show_sender_name: false + }, + { + sender_id: users[2].id, + type: "public", + text: "Public echo by Emmanuel", + unlock_datetime: new Date(), + is_archived: true, + show_sender_name: true + }, + { + sender_id: users[3].id, + type: "public", + text: "Olivia’s echo is for everyone!", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 60 * 24), // unlock in 1 day + show_sender_name: true + } + ]); console.log("🌱 Seeded the database"); } catch (error) { console.error("Error seeding database:", error); From a472f6ac3c84122ecd34d2f0d89427fd2a1fc53c Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 6 Aug 2025 11:58:07 -0400 Subject: [PATCH 22/70] added echoes data to seed --- database/seed.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/database/seed.js b/database/seed.js index b510b0f..b7cade1 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,5 +1,6 @@ const db = require("./db"); const { User } = require("./index"); +const { Echoes } = require('./index'); const seed = async () => { try { @@ -48,6 +49,9 @@ const seed = async () => { show_sender_name: true } ]); + + console.log(`Created ${echoes.length} echoes`); + console.log("🌱 Seeded the database"); } catch (error) { console.error("Error seeding database:", error); From a8a023698143f11acbe009aca4557a4491add2c2 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 7 Aug 2025 01:47:17 -0400 Subject: [PATCH 23/70] added route to get all echoes associated with one user id --- api/echoes.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++ api/index.js | 2 ++ api/users.js | 16 +++++++++++-- database/echoes.js | 2 +- database/index.js | 14 ++++++++--- database/media.js | 2 +- database/seed.js | 18 +++++++------- 7 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 api/echoes.js diff --git a/api/echoes.js b/api/echoes.js new file mode 100644 index 0000000..c3d0bac --- /dev/null +++ b/api/echoes.js @@ -0,0 +1,58 @@ +const express = require("express") +const router = express.Router(); +const { Echoes } = require("../database"); +const { authenticateJWT } = require("../auth"); + +router.get("/", async (req, res) => { + try { + const echoes = await Echoes.findAll({}); + res.json(echoes); + } catch (err) { + res.status(500).json({ error: "Failed to fetch all echoes" }); + } +}); + +router.get("/:id", async (req, res) => { + try { + const echo = await Echoes.findByPk(req.params.id, {}); + res.json(echo); + } catch (err) { + res.status(500).json({ error: "Failed to fetch echo" }) + } +}); + +router.post("/", authenticateJWT, async (req, res) => { + try { + const { type, text, unlock_datetime, show_sender_name } = req.body; + const sender_id = req.user.id; + + // checking if any required fields are missing + if (!sender_id || !type || !unlock_datetime) { + return res.status(400).json({error: "Missing required fields"}); + } + + // checking if unlock_datetime is in the future + const unlockTime = new Date(unlock_datetime); + + if (isNaN(unlockTime.getTime())) { + return res.status(400).json({error: "Invalid unlock_datetime format"}); + } + + if (unlockTime < new Date()) { + return res.status(400).json({error:"unlock_datetime must be in the future"}); + } + + // creating new echo + const newEcho = await Echoes.create({ + type, + text, + unlock_datetime, + show_sender_name + }); + + res.status(201).json(newEcho); + } catch (err) { + res.status(500).json({error: "Failed to create echo"}); + } +}); +module.exports = router; \ No newline at end of file diff --git a/api/index.js b/api/index.js index 25fea5f..0c4c8d4 100644 --- a/api/index.js +++ b/api/index.js @@ -2,7 +2,9 @@ const express = require("express"); const router = express.Router(); const testDbRouter = require("./test-db"); const usersRouter = require("./users") +const echoesRouter = require("./echoes"); router.use("/test-db", testDbRouter); router.use("/users", usersRouter); +router.use("/echoes", echoesRouter); module.exports = router; diff --git a/api/users.js b/api/users.js index abe1d25..44425e1 100644 --- a/api/users.js +++ b/api/users.js @@ -1,6 +1,6 @@ const express = require("express") const router = express.Router(); -const { User } = require("../database"); +const { User, Echoes } = require("../database"); router.get("/", async (req, res) => { try { @@ -25,4 +25,16 @@ router.get("/:id", async (req, res) => { } }); -module.exports = router; \ No newline at end of file +router.get("/:id/echoes", async (req, res) => { + try { + const user_echoes = await Echoes.findAll({ + where: { + user_id: req.params.id + } + }); + res.status(200).json(user_echoes); + } catch (err) { + res.status(500).json({error: "Failed to fetch user echoes"}); + } +}); +module.exports = router; diff --git a/database/echoes.js b/database/echoes.js index bb471e1..cb94b29 100644 --- a/database/echoes.js +++ b/database/echoes.js @@ -8,7 +8,7 @@ const Echoes = db.define("echoes", { primaryKey: true, }, - sender_id: { + user_id: { type: DataTypes.INTEGER, allowNull: false, references: { diff --git a/database/index.js b/database/index.js index f34f19b..db30a7d 100644 --- a/database/index.js +++ b/database/index.js @@ -15,13 +15,13 @@ const Tags = require('./tags'); // Echoes <> user (sender) Echoes.belongsTo(User, { - foreignKey: 'sender_id', + foreignKey: 'user_id', as: 'sender', onDelete: 'CASCADE', onUpdate: 'CASCADE' }); User.hasMany(Echoes, { - foreignKey: 'sender_id', + foreignKey: 'user_id', as: 'sent_echoes', onDelete: 'CASCADE', onUpdate: 'CASCADE' @@ -190,12 +190,20 @@ User.belongsToMany(Echoes, { // Users <> Users (friends) User.belongsToMany(User, { through: Friends, - as: 'friends', + as: 'outgoingFriends', // users I added foreignKey: 'user_id', otherKey: 'friend_id', onDelete: 'CASCADE', onUpdate: 'CASCADE' }); +User.belongsToMany(User, { + through: Friends, + as: 'incomingFriends', // user who added me + foreignKey: 'friend_id', + otherKey: 'user_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); module.exports = { db, diff --git a/database/media.js b/database/media.js index 8d4afef..aeb401d 100644 --- a/database/media.js +++ b/database/media.js @@ -1,5 +1,5 @@ const { DataTypes } = require("sequelize"); -const db = require("/.db"); +const db = require("./db"); const Media = db.define("media", { id: { diff --git a/database/seed.js b/database/seed.js index b7cade1..8821dd8 100644 --- a/database/seed.js +++ b/database/seed.js @@ -16,25 +16,23 @@ const seed = async () => { console.log(`👤 Created ${users.length} users`); - // Create more seed data here once you've created your models - // Seed files are a great way to test your database schema! const echoes = await Echoes.bulkCreate([ { - sender_id: users[0].id, + user_id: users[0].id, type: "self", - text: "This is Jeramy’s private echo", + text: "This is Jeramys private echo", unlock_datetime: new Date(Date.now() + 1000 * 60 * 60), // unlock in 1 hour show_sender_name: true }, { - sender_id: users[1].id, + user_id: users[1].id, type: "friend", - text: "Aiyanna’s echo to friends", + text: "Aiyannas echo to friends", unlock_datetime: new Date(Date.now() - 1000 * 60 * 60), // already unlocked show_sender_name: false }, { - sender_id: users[2].id, + user_id: users[2].id, type: "public", text: "Public echo by Emmanuel", unlock_datetime: new Date(), @@ -42,16 +40,16 @@ const seed = async () => { show_sender_name: true }, { - sender_id: users[3].id, + user_id: users[3].id, type: "public", - text: "Olivia’s echo is for everyone!", + text: "Olivias echo is for everyone!", unlock_datetime: new Date(Date.now() + 1000 * 60 * 60 * 24), // unlock in 1 day show_sender_name: true } ]); console.log(`Created ${echoes.length} echoes`); - + console.log("🌱 Seeded the database"); } catch (error) { console.error("Error seeding database:", error); From 0a67a89b79dfe22676d7c3444a733fc18b083144 Mon Sep 17 00:00:00 2001 From: Aiyanna <20997291+aarcher711@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:19:27 -0400 Subject: [PATCH 24/70] just pulled changes from backend main --- auth/routes.js | 112 +++++++++++++++++++++++++++++++++++++++++++++++ database/seed.js | 2 +- 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 auth/routes.js diff --git a/auth/routes.js b/auth/routes.js new file mode 100644 index 0000000..e4095a3 --- /dev/null +++ b/auth/routes.js @@ -0,0 +1,112 @@ +const express = require('express'); +const jwt = require('jsonwebtoken'); +const passport = require('passport'); +const GoogleStrategy = require('passport-google-oauth20').Strategy; +const { User } = require("../database"); + + +const router = express.Router(); + +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + +// Passport serialization +passport.serializeUser((user, done) => { + done(null, user.id); +}); + +passport.deserializeUser(async (id, done) => { + try { + const user = await User.findByPk(id); + done(null, user); + } catch (error) { + done(error, null); + } +}); + +passport.use(new GoogleStrategy({ + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_CALLBACK_URL + }, async (accessToken, refreshToken, profile, done) => { + try { + let user = await User.findOne({where: {googleId: profile.id}}); + + if (!user) { + // Try by email + const email = profile.emails?.[0]?.value; + if (email) { + user = await User.findOne({ where: { email } }); + + if (user) { + user.googleId = profile.id; + await user.save(); + } + } + } + + if (!user) { + // Create new user + const email = profile.emails?.[0]?.value || null; + const username = email ? email.split('@')[0] : `user_${Date.now()}`; + + // Ensure username is unique + let finalUsername = username; + let counter = 1; + while (await User.findOne({ where: { username: finalUsername } })) { + finalUsername = `${username}_${counter}`; + counter++; + } + + user = await User.create({ + googleId: profile.id, + email, + username: finalUsername, + passwordHash: null, + }); + } + + return done(null, user); + } catch (error) { + return done(error, null); + } + })); + + //OAuth flow + router.get("/google", passport.authenticate("google", { + scope: ["profile", "email"] + })); + + //Handle callback + router.get("/google/callback", passport.authenticate("google", {session: false}), + async (req,res) => { + try { + let user = req.user; + + // Generate JWT token + const token = jwt.sign( + { + id: user.id, + username: user.username, + auth0Id: user.auth0Id, + email: user.email, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }); + + res.redirect(`${process.env.FRONTEND_URL}/dashboard`); + } catch (error) { + res.redirect(`${process.env.FRONTEND_URL}/login?error=google&message=Google%20login%20failed`); + + } + + }); + + module.exports = router; \ No newline at end of file diff --git a/database/seed.js b/database/seed.js index a61cbcd..2b0fdbe 100644 --- a/database/seed.js +++ b/database/seed.js @@ -8,7 +8,7 @@ const seed = async () => { const users = await User.bulkCreate([ { username: "jeramy", password_hash: User.hashPassword("123456"), email: "jeramy@gmail.com" }, - { username: "aiyanna", password_hash: User.hashPassword("456"), email: "aiyanna@gmail.com" }, + { username: "aiyanna", password_hash: User.hashPassword("456789"), email: "aiyanna@gmail.com" }, { username: "emmanuel", password_hash: User.hashPassword("789"), email: "emmanuel@gmail.com" }, { username: "olivia", password_hash: User.hashPassword("101112"), email: "olivia@gmail.com" }, ]); From c2c841d805efb1fb2e72b11d5f18db9a89736251 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 7 Aug 2025 11:25:49 -0400 Subject: [PATCH 25/70] changes --- api/echoes.js | 30 ++++++++++++++++++++++++++---- database/echoes.js | 2 +- database/seed.js | 8 ++++---- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/api/echoes.js b/api/echoes.js index c3d0bac..1ca062a 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -1,6 +1,6 @@ const express = require("express") const router = express.Router(); -const { Echoes } = require("../database"); +const { Echoes, Echo_recipients } = require("../database"); const { authenticateJWT } = require("../auth"); router.get("/", async (req, res) => { @@ -12,11 +12,32 @@ router.get("/", async (req, res) => { } }); -router.get("/:id", async (req, res) => { +router.get("/:id", authenticateJWT, async (req, res) => { try { - const echo = await Echoes.findByPk(req.params.id, {}); - res.json(echo); + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id); + const echo_date = new Date(echo.unlock_datetime); + + if (!echo) { + return res.status(404).json({error: "Echo not found"}); + } + + if (echo.recipient_type === "public") { + if (echo.is_unlocked) { + return res.json(echo); + } else { + return res.status(403).json({locked: "Echo is locked"}); + } + } + + if (echo.recipient_type === "self" && echo.user_id === user_id) { + return res.json(echo); + } + + res.status(403).json({no_access: "You cannot access this echo"}); + } catch (err) { + console.log(err); res.status(500).json({ error: "Failed to fetch echo" }) } }); @@ -44,6 +65,7 @@ router.post("/", authenticateJWT, async (req, res) => { // creating new echo const newEcho = await Echoes.create({ + user_id: sender_id, type, text, unlock_datetime, diff --git a/database/echoes.js b/database/echoes.js index cb94b29..b43cb73 100644 --- a/database/echoes.js +++ b/database/echoes.js @@ -17,7 +17,7 @@ const Echoes = db.define("echoes", { } }, - type: { + recipient_type: { type: DataTypes.ENUM("self", "friend", "public"), allowNull: false, }, diff --git a/database/seed.js b/database/seed.js index 8821dd8..04115ae 100644 --- a/database/seed.js +++ b/database/seed.js @@ -19,21 +19,21 @@ const seed = async () => { const echoes = await Echoes.bulkCreate([ { user_id: users[0].id, - type: "self", + recipient_type: "self", text: "This is Jeramys private echo", unlock_datetime: new Date(Date.now() + 1000 * 60 * 60), // unlock in 1 hour show_sender_name: true }, { user_id: users[1].id, - type: "friend", + recipient_type: "friend", text: "Aiyannas echo to friends", unlock_datetime: new Date(Date.now() - 1000 * 60 * 60), // already unlocked show_sender_name: false }, { user_id: users[2].id, - type: "public", + recipient_type: "public", text: "Public echo by Emmanuel", unlock_datetime: new Date(), is_archived: true, @@ -41,7 +41,7 @@ const seed = async () => { }, { user_id: users[3].id, - type: "public", + recipient_type: "public", text: "Olivias echo is for everyone!", unlock_datetime: new Date(Date.now() + 1000 * 60 * 60 * 24), // unlock in 1 day show_sender_name: true From 046a40155dddcb00b1547e7ac8f9d99be57ac1e0 Mon Sep 17 00:00:00 2001 From: Aiyanna <20997291+aarcher711@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:36:09 -0400 Subject: [PATCH 26/70] updated passwords --- database/seed.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/seed.js b/database/seed.js index 04115ae..cb0f756 100644 --- a/database/seed.js +++ b/database/seed.js @@ -9,8 +9,8 @@ const seed = async () => { const users = await User.bulkCreate([ { username: "jeramy", password_hash: User.hashPassword("123456"), email: "jeramy@gmail.com" }, - { username: "aiyanna", password_hash: User.hashPassword("456"), email: "aiyanna@gmail.com" }, - { username: "emmanuel", password_hash: User.hashPassword("789"), email: "emmanuel@gmail.com" }, + { username: "aiyanna", password_hash: User.hashPassword("456789"), email: "aiyanna@gmail.com" }, + { username: "emmanuel", password_hash: User.hashPassword("789101"), email: "emmanuel@gmail.com" }, { username: "olivia", password_hash: User.hashPassword("101112"), email: "olivia@gmail.com" }, ]); From d470a6ff464d910737f74c991fa0d8f77d874b0b Mon Sep 17 00:00:00 2001 From: Aiyanna <20997291+aarcher711@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:37:24 -0400 Subject: [PATCH 27/70] updating passwords on this branch --- database/seed.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/seed.js b/database/seed.js index 2b0fdbe..e982d94 100644 --- a/database/seed.js +++ b/database/seed.js @@ -9,7 +9,7 @@ const seed = async () => { const users = await User.bulkCreate([ { username: "jeramy", password_hash: User.hashPassword("123456"), email: "jeramy@gmail.com" }, { username: "aiyanna", password_hash: User.hashPassword("456789"), email: "aiyanna@gmail.com" }, - { username: "emmanuel", password_hash: User.hashPassword("789"), email: "emmanuel@gmail.com" }, + { username: "emmanuel", password_hash: User.hashPassword("789101"), email: "emmanuel@gmail.com" }, { username: "olivia", password_hash: User.hashPassword("101112"), email: "olivia@gmail.com" }, ]); From c69f01771f1cca8522af72028aba1dcc4ef2ab24 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 7 Aug 2025 11:39:57 -0400 Subject: [PATCH 28/70] removed scope attribute from echo_visibility, already present in echoes model --- api/echoes.js | 4 ++++ database/echo_visibility.js | 10 ++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/api/echoes.js b/api/echoes.js index 1ca062a..9cab8f6 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -34,6 +34,10 @@ router.get("/:id", authenticateJWT, async (req, res) => { return res.json(echo); } + // if (echo.recipient_type === "friend") { + // const echo_visibility + // } + res.status(403).json({no_access: "You cannot access this echo"}); } catch (err) { diff --git a/database/echo_visibility.js b/database/echo_visibility.js index a465273..66b150b 100644 --- a/database/echo_visibility.js +++ b/database/echo_visibility.js @@ -6,12 +6,10 @@ const Echo_visibility = db.define("echo_visibility", { type: DataTypes.INTEGER, primaryKey: true, allowNull: false, - }, - - scope: { - type: DataTypes.ENUM("self", "friends", "public", "custom"), - allowNull: false, - + references: { + model: 'echoes', + key: 'id' + } }, note: { From 0b09d2cef327ca587f6f1b90f731f96e2ddb5330 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Fri, 8 Aug 2025 14:06:19 -0400 Subject: [PATCH 29/70] added echo recipients seed data to test custom echoes --- api/echoes.js | 117 ++++++++++++++++++++++++++++++++---- database/echo_visibility.js | 24 -------- database/echoes.js | 2 +- database/index.js | 15 +---- database/seed.js | 44 +++++++++++--- 5 files changed, 145 insertions(+), 57 deletions(-) delete mode 100644 database/echo_visibility.js diff --git a/api/echoes.js b/api/echoes.js index 9cab8f6..c5a53a3 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -1,27 +1,46 @@ const express = require("express") const router = express.Router(); -const { Echoes, Echo_recipients } = require("../database"); +const { Echoes, Echo_recipients, Friends } = require("../database"); const { authenticateJWT } = require("../auth"); +const { Op } = require("sequelize"); +// route for fetching all echoes router.get("/", async (req, res) => { try { - const echoes = await Echoes.findAll({}); + const echoes = await Echoes.findAll({ + where: { + recipient_type: "public", + } + }); + res.json(echoes); } catch (err) { res.status(500).json({ error: "Failed to fetch all echoes" }); } }); +// fetching echo by id router.get("/:id", authenticateJWT, async (req, res) => { try { const user_id = req.user.id; const echo = await Echoes.findByPk(req.params.id); - const echo_date = new Date(echo.unlock_datetime); if (!echo) { return res.status(404).json({error: "Echo not found"}); } + const isCreator = echo.user_id === user_id; + + // If echo is only visible to self and the user is the creator + if (echo.recipient_type === "self") { + if (isCreator) { + return res.json(echo); + } else { + return res.status(403).json({private: "This echo is private"}); + } + } + + // If echo is public, only show if unlocked if (echo.recipient_type === "public") { if (echo.is_unlocked) { return res.json(echo); @@ -30,14 +49,42 @@ router.get("/:id", authenticateJWT, async (req, res) => { } } - if (echo.recipient_type === "self" && echo.user_id === user_id) { - return res.json(echo); + // if echo is for friends and is unlocked + if (echo.recipient_type === "friend") { + if (isCreator) { + return res.json(echo); // creator can always view + } + + // check if friendship is accepted between echo creator and logged in user + const isFriend = await Friends.findOne({ + where: { + [Op.or]: [ + {user_id: echo.user_id, friend_id: user_id}, + {user_id: user_id, friend_id: echo.user_id} + ], + status: "accepted" + } + }); + + if (isFriend && echo.is_unlocked) { + return res.json(echo); + } } - // if (echo.recipient_type === "friend") { - // const echo_visibility - // } + // if echo is custom and for specific users + if (echo.recipient_type === "custom") { + if (isCreator) { + return res.json(echo); + } + + const isRecipient = await Echo_recipients.findOne({ + where: { + } + }) + } + + // default: no access res.status(403).json({no_access: "You cannot access this echo"}); } catch (err) { @@ -46,13 +93,14 @@ router.get("/:id", authenticateJWT, async (req, res) => { } }); +// creating an echo router.post("/", authenticateJWT, async (req, res) => { try { - const { type, text, unlock_datetime, show_sender_name } = req.body; + const { recipient_type, text, unlock_datetime, show_sender_name, customRecipients } = req.body; const sender_id = req.user.id; // checking if any required fields are missing - if (!sender_id || !type || !unlock_datetime) { + if (!sender_id || !recipient_type || !unlock_datetime) { return res.status(400).json({error: "Missing required fields"}); } @@ -67,18 +115,65 @@ router.post("/", authenticateJWT, async (req, res) => { return res.status(400).json({error:"unlock_datetime must be in the future"}); } + // validating customRecipients array if recipient_type is 'custom' + if (recipient_type === "custom") { + if (!Array.isArray(customRecipients) || customRecipients.length === 0) { + return res.status(400).json({error: "Custom recipient list must be a non-empty array."}); + } + + // prevent sending to self + if (customRecipients.includes(sender_id)) { + return res.status(400).json({error: "Cannot send custom echoes to yourself."}); + } + } + // creating new echo const newEcho = await Echoes.create({ user_id: sender_id, - type, + recipient_type, text, unlock_datetime, show_sender_name }); + // add custom recipients if custom + if (recipient_type === 'custom') { + const echoRecipients = customRecipients.map((recipient_id) => ({ + echo_id: newEcho.id, + recipient_id + })); + + await Echo_recipients.bulkCreate(echoRecipients); + } + res.status(201).json(newEcho); } catch (err) { res.status(500).json({error: "Failed to create echo"}); } }); + +router.patch("/:id/archive", async (req, res) => { + try { + + } catch (err) { + + } +}); + +router.patch("/:id/unlock", async (req, res) => { + try { + + } catch (err) { + + } +}); + +router.delete("/:id", authenticateJWT, async (req, res) => { + try { + + } catch (err) { + + } +}); + module.exports = router; \ No newline at end of file diff --git a/database/echo_visibility.js b/database/echo_visibility.js deleted file mode 100644 index 66b150b..0000000 --- a/database/echo_visibility.js +++ /dev/null @@ -1,24 +0,0 @@ -const { DataTypes} = require("sequelize"); -const db = require("./db"); - -const Echo_visibility = db.define("echo_visibility", { - echo_id: { - type: DataTypes.INTEGER, - primaryKey: true, - allowNull: false, - references: { - model: 'echoes', - key: 'id' - } - }, - - note: { - type: DataTypes.TEXT, - allowNull: true - } -}, { - tableName: 'echo_visibility', - timestamps: false, -}); - -module.exports = Echo_visibility; diff --git a/database/echoes.js b/database/echoes.js index b43cb73..c701a30 100644 --- a/database/echoes.js +++ b/database/echoes.js @@ -18,7 +18,7 @@ const Echoes = db.define("echoes", { }, recipient_type: { - type: DataTypes.ENUM("self", "friend", "public"), + type: DataTypes.ENUM("self", "friend", "public", "custom"), allowNull: false, }, diff --git a/database/index.js b/database/index.js index db30a7d..211158b 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,6 @@ const db = require("./db"); const User = require("./user"); const Echoes = require('./echoes'); -const Echo_visibility = require('./echo_visibility'); const Echo_recipients = require('./echo_recipients'); const Echo_tags = require('./echo_tags'); const Friends = require('./friends'); @@ -139,17 +138,6 @@ User.hasMany(Reports, { onUpdate: 'CASCADE' }); -// Echoes <> Echo_visibility (1:1) -Echoes.hasOne(Echo_visibility, { - foreignKey: 'echo_id', - onDelete: 'CASCADE', - onUpdate: 'CASCADE' -}); -Echo_visibility.belongsTo(Echoes, { - foreignKey: 'echo_id', - onDelete: 'CASCADE', - onUpdate: 'CASCADE' -}); // Echoes <> Tags (many to many through echo_tags) Echoes.belongsToMany(Tags, { @@ -177,7 +165,7 @@ Echoes.belongsToMany(User, { as: 'recipients', onDelete: 'CASCADE', onUpdate: 'CASCADE' -}); +}); User.belongsToMany(Echoes, { through: Echo_recipients, foreignKey: 'recipient_id', @@ -209,7 +197,6 @@ module.exports = { db, User, Echoes, - Echo_visibility, Echo_recipients, Echo_tags, Friends, diff --git a/database/seed.js b/database/seed.js index cb0f756..d3084e3 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,6 +1,6 @@ const db = require("./db"); -const { User } = require("./index"); -const { Echoes } = require('./index'); +const { User, Echoes, Friends, Echo_recipients } = require("./index"); + const seed = async () => { try { @@ -18,21 +18,21 @@ const seed = async () => { const echoes = await Echoes.bulkCreate([ { - user_id: users[0].id, + user_id: users[0].id, // Jeramy recipient_type: "self", text: "This is Jeramys private echo", unlock_datetime: new Date(Date.now() + 1000 * 60 * 60), // unlock in 1 hour show_sender_name: true }, { - user_id: users[1].id, + user_id: users[1].id, // Aiyanna recipient_type: "friend", text: "Aiyannas echo to friends", unlock_datetime: new Date(Date.now() - 1000 * 60 * 60), // already unlocked show_sender_name: false }, { - user_id: users[2].id, + user_id: users[2].id, // Emmanuel recipient_type: "public", text: "Public echo by Emmanuel", unlock_datetime: new Date(), @@ -40,15 +40,45 @@ const seed = async () => { show_sender_name: true }, { - user_id: users[3].id, + user_id: users[3].id, // Olivia recipient_type: "public", text: "Olivias echo is for everyone!", unlock_datetime: new Date(Date.now() + 1000 * 60 * 60 * 24), // unlock in 1 day show_sender_name: true + }, + { + user_id: users[0].id, // Jeramy + recipient_type: "custom", + text: "Jeramy's custom echo", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 45), // unlock in 45 minutes + show_sender_name: true + } + ]); + + console.log(`📣 Created ${echoes.length} echoes`); + + const friends = await Friends.bulkCreate([ + { user_id: users[0].id, friend_id: users[1].id, status: "accepted" }, + { user_id: users[1].id, friend_id: users[0].id, status: "accepted" }, + { user_id: users[0].id, friend_id: users[2].id, status: "pending" }, + { user_id: users[2].id, friend_id: users[3].id, status: "blocked" }, + ]); + + + console.log(`🤝 Created ${friends.length} friendships`) + + const echoRecipients = await Echo_recipients.bulkCreate([ + { + echo_id: echoes[4].id, // Custom echo by Jeramy + recipient_id: users[1].id // Aiyanna + }, + { + echo_id: echoes[4].id, + recipient_id: users[2].id // Emmanuel } ]); - console.log(`Created ${echoes.length} echoes`); + console.log(`📨 Created ${echoRecipients.length} echo_recipients for custom echo`); console.log("🌱 Seeded the database"); } catch (error) { From 02fd16b28deb9bbf9b59cd607bc4749f2174558f Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Fri, 8 Aug 2025 15:08:04 -0400 Subject: [PATCH 30/70] get echoes by id is finished, may be updated later to reflect changes in the echoes model itself --- api/echoes.js | 24 +++++++++++++++++++----- api/users.js | 1 + database/seed.js | 3 ++- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/api/echoes.js b/api/echoes.js index c5a53a3..ef14f3e 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -44,6 +44,8 @@ router.get("/:id", authenticateJWT, async (req, res) => { if (echo.recipient_type === "public") { if (echo.is_unlocked) { return res.json(echo); + } else if (isCreator) { + return res.json(echo); } else { return res.status(403).json({locked: "Echo is locked"}); } @@ -66,9 +68,15 @@ router.get("/:id", authenticateJWT, async (req, res) => { } }); - if (isFriend && echo.is_unlocked) { - return res.json(echo); - } + if (isFriend) { + if (echo.is_unlocked) { + return res.json(echo); + } else { + return res.status(403).json({locked:"Echo is locked"}); + } + } else { + return res.status(403).json({not_friend: "This echo is only accessible to friends of echo creator"}); + } } // if echo is custom and for specific users @@ -76,12 +84,18 @@ router.get("/:id", authenticateJWT, async (req, res) => { if (isCreator) { return res.json(echo); } - const isRecipient = await Echo_recipients.findOne({ where: { - + echo_id: echo.id, + recipient_id: user_id, } }) + + if (isRecipient && echo.is_unlocked) { + return res.json(echo); + } else if (isRecipient && !echo.is_unlocked) { + return res.status(403).json({locked: "Echo is locked"}); + } } // default: no access diff --git a/api/users.js b/api/users.js index 44425e1..cd50d42 100644 --- a/api/users.js +++ b/api/users.js @@ -37,4 +37,5 @@ router.get("/:id/echoes", async (req, res) => { res.status(500).json({error: "Failed to fetch user echoes"}); } }); + module.exports = router; diff --git a/database/seed.js b/database/seed.js index d3084e3..20137b6 100644 --- a/database/seed.js +++ b/database/seed.js @@ -78,9 +78,10 @@ const seed = async () => { } ]); - console.log(`📨 Created ${echoRecipients.length} echo_recipients for custom echo`); + console.log(`📨 Created ${echoRecipients.length} echo_recipients for custom echo`); console.log("🌱 Seeded the database"); + } catch (error) { console.error("Error seeding database:", error); if (error.message.includes("does not exist")) { From c576b4fd091fc6bf6de84a33e3d8e85d41608c7f Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Mon, 11 Aug 2025 11:45:18 -0400 Subject: [PATCH 31/70] changes --- database/echoes.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/database/echoes.js b/database/echoes.js index c701a30..31fe877 100644 --- a/database/echoes.js +++ b/database/echoes.js @@ -8,6 +8,11 @@ const Echoes = db.define("echoes", { primaryKey: true, }, + echo_name: { + type: DataTypes.TEXT, + allowNull: false + }, + user_id: { type: DataTypes.INTEGER, allowNull: false, @@ -23,7 +28,7 @@ const Echoes = db.define("echoes", { }, text: { - type: DataTypes.STRING, + type: DataTypes.TEXT, allowNull: true, }, @@ -46,6 +51,12 @@ const Echoes = db.define("echoes", { allowNull: false, }, + is_saved: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false + }, + show_sender_name: { type: DataTypes.BOOLEAN, defaultValue: false, From 11ac418e3dca7a31a8b9be5acfd0283051d43a97 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Mon, 11 Aug 2025 14:02:59 -0400 Subject: [PATCH 32/70] updated echoes model, changed echoes seed data to reflect thosechanges, added friends route that fetches all users friends that are accepted --- api/friends.js | 54 +++++++++++++++++++++++ api/index.js | 2 + database/echoes.js | 20 ++++++++- database/seed.js | 106 +++++++++++++++++++++++++++++---------------- 4 files changed, 142 insertions(+), 40 deletions(-) create mode 100644 api/friends.js diff --git a/api/friends.js b/api/friends.js new file mode 100644 index 0000000..c9f3fea --- /dev/null +++ b/api/friends.js @@ -0,0 +1,54 @@ +const express = require("express") +const router = express.Router(); +const { User, Friends} = require("../database"); +const { authenticateJWT } = require("../auth"); + +router.get("/", authenticateJWT, async (req, res) => { + try { + const user_id = req.user.id; + const friends = await Friends.findAll({ + where: {user_id: user_id, status: "accepted"}, + }); + res.json(friends); + } catch (err) { + res.status(500).json({ error: "Failed to fetch friends" }); + } +}); + +router.post("/", authenticateJWT, async (req, res) => { + try { + const user_id = req.user.id; + const { friend_id } = req.body; + } catch (err) { + + } +}); + +router.patch("/:id/accept", authenticateJWT, async (req, res) => { + try { + + } catch (err) { + + } +}); + +router.patch("/:id/accept", authenticateJWT, async (req, res) => { + try { + + } catch (err) { + + } +}); + +router.delete("/:id", authenticateJWT, async (req, res) => { + try { + + } catch (err) { + + } +}); + +module.exports = router; + + + diff --git a/api/index.js b/api/index.js index 0c4c8d4..17dd339 100644 --- a/api/index.js +++ b/api/index.js @@ -3,8 +3,10 @@ const router = express.Router(); const testDbRouter = require("./test-db"); const usersRouter = require("./users") const echoesRouter = require("./echoes"); +const friendsRouter = require("./friends"); router.use("/test-db", testDbRouter); router.use("/users", usersRouter); router.use("/echoes", echoesRouter); +router.use("/friends", friendsRouter); module.exports = router; diff --git a/database/echoes.js b/database/echoes.js index 31fe877..155137e 100644 --- a/database/echoes.js +++ b/database/echoes.js @@ -10,7 +10,7 @@ const Echoes = db.define("echoes", { echo_name: { type: DataTypes.TEXT, - allowNull: false + allowNull: true }, user_id: { @@ -59,8 +59,24 @@ const Echoes = db.define("echoes", { show_sender_name: { type: DataTypes.BOOLEAN, - defaultValue: false, + defaultValue: true, allowNull: false, + }, + + location_locked: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + + lat: { + type: DataTypes.DECIMAL(9, 6), + allowNull: true + }, + + lng: { + type: DataTypes.DECIMAL(9, 6), + allowNull: true } }, { diff --git a/database/seed.js b/database/seed.js index 20137b6..ae0b729 100644 --- a/database/seed.js +++ b/database/seed.js @@ -17,43 +17,73 @@ const seed = async () => { console.log(`👤 Created ${users.length} users`); const echoes = await Echoes.bulkCreate([ - { - user_id: users[0].id, // Jeramy - recipient_type: "self", - text: "This is Jeramys private echo", - unlock_datetime: new Date(Date.now() + 1000 * 60 * 60), // unlock in 1 hour - show_sender_name: true - }, - { - user_id: users[1].id, // Aiyanna - recipient_type: "friend", - text: "Aiyannas echo to friends", - unlock_datetime: new Date(Date.now() - 1000 * 60 * 60), // already unlocked - show_sender_name: false - }, - { - user_id: users[2].id, // Emmanuel - recipient_type: "public", - text: "Public echo by Emmanuel", - unlock_datetime: new Date(), - is_archived: true, - show_sender_name: true - }, - { - user_id: users[3].id, // Olivia - recipient_type: "public", - text: "Olivias echo is for everyone!", - unlock_datetime: new Date(Date.now() + 1000 * 60 * 60 * 24), // unlock in 1 day - show_sender_name: true - }, - { - user_id: users[0].id, // Jeramy - recipient_type: "custom", - text: "Jeramy's custom echo", - unlock_datetime: new Date(Date.now() + 1000 * 60 * 45), // unlock in 45 minutes - show_sender_name: true - } - ]); + { + echo_name: "Jeramy's Private Echo", + user_id: users[0].id, // Jeramy + recipient_type: "self", + text: "This is Jeramy's private echo", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 60), // unlock in 1 hour + show_sender_name: true, + is_saved: false, // default + is_archived: false, // default + location_locked: false, // default + lat: null, + lng: null + }, + { + echo_name: "Aiyanna's Friends Echo", + user_id: users[1].id, // Aiyanna + recipient_type: "friend", + text: "Aiyanna's echo to friends", + unlock_datetime: new Date(Date.now() - 1000 * 60 * 60), // already unlocked + show_sender_name: false, + is_saved: false, + is_archived: false, + location_locked: false, + lat: null, + lng: null + }, + { + echo_name: "Public Echo by Emmanuel", + user_id: users[2].id, // Emmanuel + recipient_type: "public", + text: "Public echo by Emmanuel", + unlock_datetime: new Date(), + is_archived: true, + is_saved: false, + show_sender_name: true, + location_locked: false, + lat: null, + lng: null + }, + { + echo_name: "Olivia's Public Echo", + user_id: users[3].id, // Olivia + recipient_type: "public", + text: "Olivia's echo is for everyone!", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 60 * 24), // unlock in 1 day + show_sender_name: true, + is_saved: false, + is_archived: false, + location_locked: false, + lat: null, + lng: null + }, + { + echo_name: "Jeramy's Custom Echo", + user_id: users[0].id, // Jeramy + recipient_type: "custom", + text: "Jeramy's custom echo", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 45), // unlock in 45 minutes + show_sender_name: true, + is_saved: false, + is_archived: false, + location_locked: false, + lat: null, + lng: null + } +]); + console.log(`📣 Created ${echoes.length} echoes`); @@ -81,7 +111,7 @@ const seed = async () => { console.log(`📨 Created ${echoRecipients.length} echo_recipients for custom echo`); console.log("🌱 Seeded the database"); - + } catch (error) { console.error("Error seeding database:", error); if (error.message.includes("does not exist")) { From 8d5a9b9a1153e85d43d6bc7f9d8d039fc9748326 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Tue, 12 Aug 2025 11:26:39 -0400 Subject: [PATCH 33/70] post request for sending friend requests is done --- api/friends.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/api/friends.js b/api/friends.js index c9f3fea..d95d45b 100644 --- a/api/friends.js +++ b/api/friends.js @@ -1,7 +1,8 @@ const express = require("express") const router = express.Router(); -const { User, Friends} = require("../database"); +const { Friends} = require("../database"); const { authenticateJWT } = require("../auth"); +const { Op } = require("sequelize"); router.get("/", authenticateJWT, async (req, res) => { try { @@ -19,8 +20,59 @@ router.post("/", authenticateJWT, async (req, res) => { try { const user_id = req.user.id; const { friend_id } = req.body; - } catch (err) { + // cannot send friend requests to self + if (user_id === friend_id) { + return res.status(400).json({error: "Cannot send a friend request to self"}); + } + + // auto-accept if reverse pending request exists + const reverseRequest = await Friends.findOne({ + where: { user_id: friend_id, friend_id: user_id, status: "pending"} + }); + + if (reverseRequest) { + reverseRequest.status = "accepted"; + await reverseRequest.save(); + return res.status(200).json(reverseRequest); + } + + // checking if already friends + const isFriend = await Friends.findOne({ + where: { + [Op.or]: [ + {user_id, friend_id}, + {user_id: friend_id, friend_id: user_id} + ], + status: "accepted" + } + }); + + if (isFriend) { + return res.status(403).json({friends: "Already friends"}); + } + + // check if there's already an existing request + const requestExists = await Friends.findOne({ + where: { + [Op.or]: [ + {user_id, friend_id}, + {user_id: friend_id, friend_id: user_id} + ], + status: "pending" + } + }); + + if (requestExists) { + return res.status(403).json({request: "Friend request sent already"}); + } + + // create pending request + const newFriendRequest = await Friends.create({ user_id, friend_id}); + res.status(201).json(newFriendRequest); + } catch (err) { + console.log(err); + return res.status(500).json({error: "Failed to send friend request"}); } }); From d3fecc218ba8614b678a60561d27e0d0e6e8b40b Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Tue, 12 Aug 2025 13:38:16 -0400 Subject: [PATCH 34/70] added patch route for echoes to update archive status --- api/echoes.js | 31 +++++++++++++++++++++++++++---- database/echoes.js | 8 +++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/api/echoes.js b/api/echoes.js index ef14f3e..9287cd6 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -110,7 +110,7 @@ router.get("/:id", authenticateJWT, async (req, res) => { // creating an echo router.post("/", authenticateJWT, async (req, res) => { try { - const { recipient_type, text, unlock_datetime, show_sender_name, customRecipients } = req.body; + const { echo_name, recipient_type, text, unlock_datetime, show_sender_name, lat, lng, customRecipients } = req.body; const sender_id = req.user.id; // checking if any required fields are missing @@ -143,11 +143,14 @@ router.post("/", authenticateJWT, async (req, res) => { // creating new echo const newEcho = await Echoes.create({ + echo_name, user_id: sender_id, recipient_type, text, unlock_datetime, - show_sender_name + show_sender_name, + lat, + lng }); // add custom recipients if custom @@ -166,11 +169,31 @@ router.post("/", authenticateJWT, async (req, res) => { } }); -router.patch("/:id/archive", async (req, res) => { +router.patch("/:id/archive", authenticateJWT, async (req, res) => { try { + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id); - } catch (err) { + if (!echo) { + return res.status(404).json({ error: "Echo not found" }); + + } + if (echo.user_id !== user_id) { + return res.status(403).json({ error: "You are not the owner of this echo."}); + } + + // Toggle archived status + echo.is_archived = !echo.is_archived; + await echo.save(); + + return res.status(200).json({ + message: echo.is_archived ? "Echo archived" : "Echo unarchived", + echo + }); + + } catch (err) { + return res.status(500).json({error: "Failed to toggle echo archive status"}); } }); diff --git a/database/echoes.js b/database/echoes.js index 155137e..2b536db 100644 --- a/database/echoes.js +++ b/database/echoes.js @@ -38,11 +38,9 @@ const Echoes = db.define("echoes", { }, is_unlocked: { - type: DataTypes.VIRTUAL, - get() { - const now = new Date(); - return now >= this.unlock_datetime; - } + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false }, is_archived: { From 58483b75375f4a74392be12fb406b5befaeda73c Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Tue, 12 Aug 2025 14:00:53 -0400 Subject: [PATCH 35/70] Implemented echo unlock route with ownership and unlock date validation --- api/echoes.js | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/api/echoes.js b/api/echoes.js index 9287cd6..9abc7c1 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -3,6 +3,7 @@ const router = express.Router(); const { Echoes, Echo_recipients, Friends } = require("../database"); const { authenticateJWT } = require("../auth"); const { Op } = require("sequelize"); +const { response } = require("../app"); // route for fetching all echoes router.get("/", async (req, res) => { @@ -197,11 +198,46 @@ router.patch("/:id/archive", authenticateJWT, async (req, res) => { } }); -router.patch("/:id/unlock", async (req, res) => { +router.patch("/:id/unlock", authenticateJWT, async (req, res) => { try { + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id); - } catch (err) { + // check if echo exists + if (!echo) { + return res.status(404).json({error: "Echo not found."}); + } + + // check if user is owner + if (user_id !== echo.user_id) { + return res.status(403).json({error: "You cannot access this echo."}); + } + + // enforce unlock date + if (new Date() < new Date(echo.unlock_datetime)) { + return res.status(403).json({ error: "This echo is locked until its unlock date."}); + } + // already unlocked + if (echo.is_unlocked) { + return res.status(200).json({ + message: "Echo is already unlocked.", + echo + }) + } + + // Unlock + echo.is_unlocked = true; + await echo.save(); + + return res.status(200).json({ + message: "Echo unlocked", + echo + }); + + } catch (err) { + console.error(err); + res.status(500).json({error: "Error unlocking this echo"}); } }); From b6987574f38cdb74e466b16654a0638722782f68 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Tue, 12 Aug 2025 14:22:41 -0400 Subject: [PATCH 36/70] Implemented echo delete route with existence and ownership validation --- api/echoes.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/api/echoes.js b/api/echoes.js index 9abc7c1..c48f4ce 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -243,9 +243,29 @@ router.patch("/:id/unlock", authenticateJWT, async (req, res) => { router.delete("/:id", authenticateJWT, async (req, res) => { try { + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id); - } catch (err) { + // check if echo exists + if (!echo) { + return res.status(404).json({error: "Echo not found."}); + } + + // check ownership + if (user_id !== echo.user_id) { + return res.status(403).json({ error: "You are not the owner of this echo."}); + } + // delete the echo + await echo.destroy(); + + return res.status(200).json({ + message: "Echo deleted successfully", + id: echo.id + }); + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error deleting this echo"}); } }); From 3463bd91c79a8c039afc2ef703fb695e90d45323 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Tue, 12 Aug 2025 14:31:53 -0400 Subject: [PATCH 37/70] adding comments for requests clarity in echoes --- api/echoes.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/echoes.js b/api/echoes.js index c48f4ce..8f1f83d 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -170,6 +170,7 @@ router.post("/", authenticateJWT, async (req, res) => { } }); +// arching or unarchiving an echo router.patch("/:id/archive", authenticateJWT, async (req, res) => { try { const user_id = req.user.id; @@ -198,6 +199,7 @@ router.patch("/:id/archive", authenticateJWT, async (req, res) => { } }); +// unlocking an echo router.patch("/:id/unlock", authenticateJWT, async (req, res) => { try { const user_id = req.user.id; @@ -241,6 +243,7 @@ router.patch("/:id/unlock", authenticateJWT, async (req, res) => { } }); +// deleting an echo router.delete("/:id", authenticateJWT, async (req, res) => { try { const user_id = req.user.id; From d5a04383e44f596b9e5929c4b8b062458d78a574 Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Tue, 12 Aug 2025 15:26:47 -0400 Subject: [PATCH 38/70] chore: Add S3 integration docs and implementation guide This commit adds documentation for Amazon S3 integration --- api/Amazon_S3_Integration.md | 194 +++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 api/Amazon_S3_Integration.md diff --git a/api/Amazon_S3_Integration.md b/api/Amazon_S3_Integration.md new file mode 100644 index 0000000..d15789b --- /dev/null +++ b/api/Amazon_S3_Integration.md @@ -0,0 +1,194 @@ +# Reason + +I have very little hope I will actually remember the steps I took to complete the Amazon s3 task. + +Below is a step by step breakdown of the process, inspired by [this video](https://www.youtube.com/watch?v=eQAIojcArRY&t=996s) + +## Quick Setup + +If you just want to setup on your local machine S3 access, go to [Bucket Access](#bucket-access) and copy and paste the `.env` variables into your `.env` file. The `ACCESS_KEY` and `SECRET_ACCESS_KEY` and `BUCKET_NAME` are not in this file for _obvious_ reasons(hiding bucket name might just me being paranoid). If the `SECRET_ACCESS_KEY` ends up getting _lost_, then I will have to make a new **IAM** user, so please let me know. + +Next, run npm install if the routes are already setup, and you haven't done this setup before. This way you install the packages in [Express Server File Handling](#express-server-file-handling) and [Amazon S3 SDK](#amazon-s3-sdk). + +```bash +npm install +``` + +If for some reason the relevant packages are not in the `package.json`, then proceed to [Express Server File Handling](#express-server-file-handling) and [Amazon S3 SDK](#amazon-s3-sdk) and follow those install steps. + +Thats it! Assuming the routes have been setup, you can now run requests to the Amazon S3 bucket, and our PostgreSQL DB. If you run into any issues, please reach out to me @EmmanuelR21. + +## Bucket Access + +1. Our bucket is essentially the storage DB that holds the images and video files. The media files are known as **"Objects"** +2. There is an Amazon **IAM** user known as `echo-cache`, which has 3 permissions(given by a custom policy I created), they are: + 1. `PUT` object to DB. (They do not call it POST for some reason) + 2. `GET` object from DB. + 3. `DELETE` object from DB. + +**_IMPORTANT: To be able to use the user within our express server, the following PRIVATE keys must be used in an `.env` file:_** + +```env +BUCKET_NAME='capstone-2-echo-cache' +BUCKET_REGION='us-east-2' +ACCESS_KEY='' +SECRET_ACCESS_KEY='' +``` + +## Express Server File Handling + +Our express server will not understand how to deal with `multipart/form-data`(This is the `Content-Type` of the media we send through the ``) by default, so we will need to use a middleware known as `multer`. We will also need `sharp` which is a package we will use for image/video resizing. `sharp` isn't strictly necessary but it would be nice to have all of our video and image formats in a portrait mode. + +```bash +npm install multer sharp # sharp is not strictly required +``` + +Next put in some of these important variables. **NOTE:** that the `crypto` variable is imported from a built in Javascript library, so there should be no need to install `crypto`. It will be used later for unique ID creation. + +```javascript +const multer = require("multer"); +const crypto = require("crypto"); +const sharp = require("sharp"); // Only needed if we want to create a resized image, for example to put images into portrait mode (not stretching the image, but adding black bars to fill the gaps). + +const storage = multer.memoryStorage(); //This designates the memory to store the media, instead of in disk storage +const upload = multer({ storage: storage }); //Upload function to be used later +``` + +After creating our `upload` variable, we will use it later in our route as a middleware. + +## Amazon S3 SDK + +After having created an access point to the bucket, now what is needed is to install to the express server `@aws-sdk/client-s3` for creating `POST` request to our Bucket, and `@aws-sdk/s3-request-presigner` for `GET` requests to the Bucket: + +```bash +npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner +``` + +After installing, paste these variables into the header of the file, these will all serve a purpose for our `GET`, `POST` and `DELETE`: + +```javascript +const { + S3Client, + PutObjectCommand, + GetObjectCommand, + DeleteObjectsCommand, +} = require("@aws-sdk/client-s3"); +const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); +``` + +Next, we will need to create an `s3Client` using our `.env` variables: + +```javascript +const bucketName = process.env.BUCKET_NAME; +const bucketRegion = process.env.BUCKET_REGION; +const accessKey = process.env.ACCESS_KEY; +const secretAccessKey = process.env.SECRET_ACCESS_KEY; + +const s3 = new S3Client({ + credentials: { + accessKeyId: accessKey, + secretAccessKey: secretAccessKey, + }, + region: bucketRegion, +}); +``` + +### Quick **IMPORTANT** note + +Its important I clarify a few things: + +1. Our Amazon s3 bucket exists on my AWS account, with a free tier for 3 months more or less, or until my credits run out, which I have ample enough for our testing phase. Please see number 3 :) +2. The Sequelize related models functions/method names are all guesswork for now, as I have not fully implemented the activity within our own DB. I will update with proper naming, and behavior when it gets implemented officially. +3. There is **NO** validation being done right now. In theory someone could upload a zetabyte of horse videos and my AWS account bill will go 📈. _Please_ do **NOT** do this and be cautious on the size of content you are uploading. If we reach double digits in GB's, please let me know you are doing this so I can monitor my usage on AWS. Thanks :)! +4. I did not keep security in mind when developing the routes as I was moving quickly to develop the rough idea. I will update this as we continue developing the routes. + +### Posting to our Bucket + +```javascript +/* +- upload.array() will take an array of "files" named "media". The name "media" can be literally anything, just depends on what you name the array of files being sent from the client. +- upload.array() will store the file buffers in req.files when it is done. +*/ +router.post("/", upload.array("media", 10), async (req, res) => { + // We use UUID's to prevent file name collision, which will overwrite one file over the other. We will also use it to retrieve the media later so we store it in our Postgres DB. + const imageIds = []; + // S3 does not allow you to upload several files at once, so we have to loop + for (let i = 0; i < req.files.length; i++) { + const buffer = await sharp(req.files[i].buffer) + .resize({ height: 1920, width: 1080, fit: "cover" }) + .toBuffer(); //This is if you want to resize an image to be in portrait mode. The image wont get affected, but essentially black bars will fill the gaps. + const uniqueImgId = crypto.randomUUID(); + const params = { + Bucket: bucketName, + Key: uniqueImgId, + Body: req.files[i].buffer, + ContentType: req.files[i].mimetype, + }; + + const command = new PutObjectCommand(params); + // The following uses the s3Client variable we made earlier, to store to the AWS DB bucket + await s3.send(command); + // Upon successful completion we also save the UUID in our array + imageIds.push(uniqueImgId); + } + + // After uploading to S3, we now post to our DB, and instead of uploading media, we will upload the array of UUID's. + + /* + Sequelize post to DB code goes here + */ + + res.send(post); +}); +``` + +### Getting from our Bucket + +```javascript +router.get("/:id", async (req, res) => { + // Here sequelize will get our echo by ID: + const echoId = req.params.id; + const echo = await Echo.getByPk(echoId); + // Attaches array of "signed URL's" to the Post data. Not entirely sure yet if this adds to the DB table, need to test. + echo.mediaUrls = []; + // Post.uuids will be the array grabbed by echo id. + for (let i = 0; i < echo.uuids.length; i++) { + const objectParams = { + Bucket: bucketName, + Key: echo.uuids[i], + }; + const command = new GetObjectCommand(objectParams); + const url = await getSignedUrl(s3, command, { expiresIn: 3600 }); + echo.mediaUrls.push(url); + } + // The Post now has the array of signed URLs when sent back to our client. + res.send(echo); +}); +``` + +### Deleting from our Bucket + +```javascript +router.delete("/:id", async (req, res) => { + const echoId = req.params.id; + const echo = await Echo.findByPk(echoId); + const params = { + Bucket: bucketName, + // Takes an array called "Objects" which will have the array of the multiple uuids, to delete all at once. + Delete: { + Objects: echo.uuids.map((uuid) => ({ Key: uuid })), + }, + }; + // Delete multiple objects from the bucket + const command = new DeleteObjectsCommand(params); + await s3.send(command); + + // Sequelize deletes from our db here + await echo.destroy(); + res.send({}); +}); +``` + +## Final Note + +I'm so happy I'm done with this 😭 From 6ddc2192893e7032b4e30c4a41d58d8ccfd45477 Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Tue, 12 Aug 2025 16:02:18 -0400 Subject: [PATCH 39/70] refactor: Prettier format applied --- api/echoes.js | 480 ++++----- package-lock.json | 2405 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 6 +- 3 files changed, 2661 insertions(+), 230 deletions(-) diff --git a/api/echoes.js b/api/echoes.js index 8f1f83d..d013c4a 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -1,275 +1,299 @@ -const express = require("express") +const express = require("express"); const router = express.Router(); -const { Echoes, Echo_recipients, Friends } = require("../database"); -const { authenticateJWT } = require("../auth"); +const { Echoes, Echo_recipients, Friends } = require("../database"); +const { authenticateJWT } = require("../auth"); const { Op } = require("sequelize"); const { response } = require("../app"); -// route for fetching all echoes +// route for fetching all echoes router.get("/", async (req, res) => { - try { - const echoes = await Echoes.findAll({ - where: { - recipient_type: "public", - } - }); - - res.json(echoes); - } catch (err) { - res.status(500).json({ error: "Failed to fetch all echoes" }); - } + try { + const echoes = await Echoes.findAll({ + where: { + recipient_type: "public", + }, + }); + + res.json(echoes); + } catch (err) { + res.status(500).json({ error: "Failed to fetch all echoes" }); + } }); -// fetching echo by id +// fetching echo by id router.get("/:id", authenticateJWT, async (req, res) => { - try { - const user_id = req.user.id; - const echo = await Echoes.findByPk(req.params.id); + try { + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id); - if (!echo) { - return res.status(404).json({error: "Echo not found"}); - } + if (!echo) { + return res.status(404).json({ error: "Echo not found" }); + } - const isCreator = echo.user_id === user_id; + const isCreator = echo.user_id === user_id; - // If echo is only visible to self and the user is the creator - if (echo.recipient_type === "self") { - if (isCreator) { - return res.json(echo); - } else { - return res.status(403).json({private: "This echo is private"}); - } - } - - // If echo is public, only show if unlocked - if (echo.recipient_type === "public") { - if (echo.is_unlocked) { - return res.json(echo); - } else if (isCreator) { - return res.json(echo); - } else { - return res.status(403).json({locked: "Echo is locked"}); - } - } + // If echo is only visible to self and the user is the creator + if (echo.recipient_type === "self") { + if (isCreator) { + return res.json(echo); + } else { + return res.status(403).json({ private: "This echo is private" }); + } + } - // if echo is for friends and is unlocked - if (echo.recipient_type === "friend") { - if (isCreator) { - return res.json(echo); // creator can always view - } - - // check if friendship is accepted between echo creator and logged in user - const isFriend = await Friends.findOne({ - where: { - [Op.or]: [ - {user_id: echo.user_id, friend_id: user_id}, - {user_id: user_id, friend_id: echo.user_id} - ], - status: "accepted" - } - }); - - if (isFriend) { - if (echo.is_unlocked) { - return res.json(echo); - } else { - return res.status(403).json({locked:"Echo is locked"}); - } - } else { - return res.status(403).json({not_friend: "This echo is only accessible to friends of echo creator"}); - } - } + // If echo is public, only show if unlocked + if (echo.recipient_type === "public") { + if (echo.is_unlocked) { + return res.json(echo); + } else if (isCreator) { + return res.json(echo); + } else { + return res.status(403).json({ locked: "Echo is locked" }); + } + } - // if echo is custom and for specific users - if (echo.recipient_type === "custom") { - if (isCreator) { - return res.json(echo); - } - const isRecipient = await Echo_recipients.findOne({ - where: { - echo_id: echo.id, - recipient_id: user_id, - } - }) - - if (isRecipient && echo.is_unlocked) { - return res.json(echo); - } else if (isRecipient && !echo.is_unlocked) { - return res.status(403).json({locked: "Echo is locked"}); - } + // if echo is for friends and is unlocked + if (echo.recipient_type === "friend") { + if (isCreator) { + return res.json(echo); // creator can always view + } + + // check if friendship is accepted between echo creator and logged in user + const isFriend = await Friends.findOne({ + where: { + [Op.or]: [ + { user_id: echo.user_id, friend_id: user_id }, + { user_id: user_id, friend_id: echo.user_id }, + ], + status: "accepted", + }, + }); + + if (isFriend) { + if (echo.is_unlocked) { + return res.json(echo); + } else { + return res.status(403).json({ locked: "Echo is locked" }); } + } else { + return res + .status(403) + .json({ + not_friend: + "This echo is only accessible to friends of echo creator", + }); + } + } - // default: no access - res.status(403).json({no_access: "You cannot access this echo"}); - - } catch (err) { - console.log(err); - res.status(500).json({ error: "Failed to fetch echo" }) + // if echo is custom and for specific users + if (echo.recipient_type === "custom") { + if (isCreator) { + return res.json(echo); + } + const isRecipient = await Echo_recipients.findOne({ + where: { + echo_id: echo.id, + recipient_id: user_id, + }, + }); + + if (isRecipient && echo.is_unlocked) { + return res.json(echo); + } else if (isRecipient && !echo.is_unlocked) { + return res.status(403).json({ locked: "Echo is locked" }); + } } + + // default: no access + res.status(403).json({ no_access: "You cannot access this echo" }); + } catch (err) { + console.log(err); + res.status(500).json({ error: "Failed to fetch echo" }); + } }); -// creating an echo +// creating an echo router.post("/", authenticateJWT, async (req, res) => { - try { - const { echo_name, recipient_type, text, unlock_datetime, show_sender_name, lat, lng, customRecipients } = req.body; - const sender_id = req.user.id; - - // checking if any required fields are missing - if (!sender_id || !recipient_type || !unlock_datetime) { - return res.status(400).json({error: "Missing required fields"}); - } - - // checking if unlock_datetime is in the future - const unlockTime = new Date(unlock_datetime); - - if (isNaN(unlockTime.getTime())) { - return res.status(400).json({error: "Invalid unlock_datetime format"}); - } + try { + const { + echo_name, + recipient_type, + text, + unlock_datetime, + show_sender_name, + lat, + lng, + customRecipients, + } = req.body; + const sender_id = req.user.id; + + // checking if any required fields are missing + if (!sender_id || !recipient_type || !unlock_datetime) { + return res.status(400).json({ error: "Missing required fields" }); + } - if (unlockTime < new Date()) { - return res.status(400).json({error:"unlock_datetime must be in the future"}); - } + // checking if unlock_datetime is in the future + const unlockTime = new Date(unlock_datetime); - // validating customRecipients array if recipient_type is 'custom' - if (recipient_type === "custom") { - if (!Array.isArray(customRecipients) || customRecipients.length === 0) { - return res.status(400).json({error: "Custom recipient list must be a non-empty array."}); - } + if (isNaN(unlockTime.getTime())) { + return res.status(400).json({ error: "Invalid unlock_datetime format" }); + } - // prevent sending to self - if (customRecipients.includes(sender_id)) { - return res.status(400).json({error: "Cannot send custom echoes to yourself."}); - } - } + if (unlockTime < new Date()) { + return res + .status(400) + .json({ error: "unlock_datetime must be in the future" }); + } - // creating new echo - const newEcho = await Echoes.create({ - echo_name, - user_id: sender_id, - recipient_type, - text, - unlock_datetime, - show_sender_name, - lat, - lng - }); - - // add custom recipients if custom - if (recipient_type === 'custom') { - const echoRecipients = customRecipients.map((recipient_id) => ({ - echo_id: newEcho.id, - recipient_id - })); - - await Echo_recipients.bulkCreate(echoRecipients); - } + // validating customRecipients array if recipient_type is 'custom' + if (recipient_type === "custom") { + if (!Array.isArray(customRecipients) || customRecipients.length === 0) { + return res + .status(400) + .json({ error: "Custom recipient list must be a non-empty array." }); + } + + // prevent sending to self + if (customRecipients.includes(sender_id)) { + return res + .status(400) + .json({ error: "Cannot send custom echoes to yourself." }); + } + } - res.status(201).json(newEcho); - } catch (err) { - res.status(500).json({error: "Failed to create echo"}); + // creating new echo + const newEcho = await Echoes.create({ + echo_name, + user_id: sender_id, + recipient_type, + text, + unlock_datetime, + show_sender_name, + lat, + lng, + }); + + // add custom recipients if custom + if (recipient_type === "custom") { + const echoRecipients = customRecipients.map((recipient_id) => ({ + echo_id: newEcho.id, + recipient_id, + })); + + await Echo_recipients.bulkCreate(echoRecipients); } + + res.status(201).json(newEcho); + } catch (err) { + res.status(500).json({ error: "Failed to create echo" }); + } }); -// arching or unarchiving an echo +// arching or unarchiving an echo router.patch("/:id/archive", authenticateJWT, async (req, res) => { - try { - const user_id = req.user.id; - const echo = await Echoes.findByPk(req.params.id); - - if (!echo) { - return res.status(404).json({ error: "Echo not found" }); - - } - - if (echo.user_id !== user_id) { - return res.status(403).json({ error: "You are not the owner of this echo."}); - } + try { + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id); - // Toggle archived status - echo.is_archived = !echo.is_archived; - await echo.save(); - - return res.status(200).json({ - message: echo.is_archived ? "Echo archived" : "Echo unarchived", - echo - }); + if (!echo) { + return res.status(404).json({ error: "Echo not found" }); + } - } catch (err) { - return res.status(500).json({error: "Failed to toggle echo archive status"}); + if (echo.user_id !== user_id) { + return res + .status(403) + .json({ error: "You are not the owner of this echo." }); } + + // Toggle archived status + echo.is_archived = !echo.is_archived; + await echo.save(); + + return res.status(200).json({ + message: echo.is_archived ? "Echo archived" : "Echo unarchived", + echo, + }); + } catch (err) { + return res + .status(500) + .json({ error: "Failed to toggle echo archive status" }); + } }); -// unlocking an echo +// unlocking an echo router.patch("/:id/unlock", authenticateJWT, async (req, res) => { - try { - const user_id = req.user.id; - const echo = await Echoes.findByPk(req.params.id); + try { + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id); - // check if echo exists - if (!echo) { - return res.status(404).json({error: "Echo not found."}); - } + // check if echo exists + if (!echo) { + return res.status(404).json({ error: "Echo not found." }); + } - // check if user is owner - if (user_id !== echo.user_id) { - return res.status(403).json({error: "You cannot access this echo."}); - } + // check if user is owner + if (user_id !== echo.user_id) { + return res.status(403).json({ error: "You cannot access this echo." }); + } - // enforce unlock date - if (new Date() < new Date(echo.unlock_datetime)) { - return res.status(403).json({ error: "This echo is locked until its unlock date."}); - } + // enforce unlock date + if (new Date() < new Date(echo.unlock_datetime)) { + return res + .status(403) + .json({ error: "This echo is locked until its unlock date." }); + } - // already unlocked - if (echo.is_unlocked) { - return res.status(200).json({ - message: "Echo is already unlocked.", - echo - }) - } - - // Unlock - echo.is_unlocked = true; - await echo.save(); - - return res.status(200).json({ - message: "Echo unlocked", - echo - }); - - } catch (err) { - console.error(err); - res.status(500).json({error: "Error unlocking this echo"}); + // already unlocked + if (echo.is_unlocked) { + return res.status(200).json({ + message: "Echo is already unlocked.", + echo, + }); } + + // Unlock + echo.is_unlocked = true; + await echo.save(); + + return res.status(200).json({ + message: "Echo unlocked", + echo, + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Error unlocking this echo" }); + } }); -// deleting an echo +// deleting an echo router.delete("/:id", authenticateJWT, async (req, res) => { - try { - const user_id = req.user.id; - const echo = await Echoes.findByPk(req.params.id); + try { + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id); - // check if echo exists - if (!echo) { - return res.status(404).json({error: "Echo not found."}); - } - - // check ownership - if (user_id !== echo.user_id) { - return res.status(403).json({ error: "You are not the owner of this echo."}); - } - - // delete the echo - await echo.destroy(); + // check if echo exists + if (!echo) { + return res.status(404).json({ error: "Echo not found." }); + } - return res.status(200).json({ - message: "Echo deleted successfully", - id: echo.id - }); - } catch (err) { - console.error(err); - return res.status(500).json({error: "Error deleting this echo"}); + // check ownership + if (user_id !== echo.user_id) { + return res + .status(403) + .json({ error: "You are not the owner of this echo." }); } + + // delete the echo + await echo.destroy(); + + return res.status(200).json({ + message: "Echo deleted successfully", + id: echo.id, + }); + } catch (err) { + console.error(err); + return res.status(500).json({ error: "Error deleting this echo" }); + } }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/package-lock.json b/package-lock.json index 1b30289..73a6108 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@aws-sdk/client-s3": "^3.864.0", + "@aws-sdk/s3-request-presigner": "^3.864.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -16,8 +18,10 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "multer": "^2.0.2", "pg": "^8.16.2", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "sharp": "^0.34.3" }, "devDependencies": { "nodemon": "^3.1.10" @@ -26,6 +30,2087 @@ "win-node-env": "^0.6.1" } }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.864.0.tgz", + "integrity": "sha512-QGYi9bWliewxumsvbJLLyx9WC0a4DP4F+utygBcq0zwPxaM0xDfBspQvP1dsepi7mW5aAjZmJ2+Xb7X0EhzJ/g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.864.0", + "@aws-sdk/credential-provider-node": "3.864.0", + "@aws-sdk/middleware-bucket-endpoint": "3.862.0", + "@aws-sdk/middleware-expect-continue": "3.862.0", + "@aws-sdk/middleware-flexible-checksums": "3.864.0", + "@aws-sdk/middleware-host-header": "3.862.0", + "@aws-sdk/middleware-location-constraint": "3.862.0", + "@aws-sdk/middleware-logger": "3.862.0", + "@aws-sdk/middleware-recursion-detection": "3.862.0", + "@aws-sdk/middleware-sdk-s3": "3.864.0", + "@aws-sdk/middleware-ssec": "3.862.0", + "@aws-sdk/middleware-user-agent": "3.864.0", + "@aws-sdk/region-config-resolver": "3.862.0", + "@aws-sdk/signature-v4-multi-region": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.862.0", + "@aws-sdk/util-user-agent-browser": "3.862.0", + "@aws-sdk/util-user-agent-node": "3.864.0", + "@aws-sdk/xml-builder": "3.862.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/eventstream-serde-browser": "^4.0.5", + "@smithy/eventstream-serde-config-resolver": "^4.1.3", + "@smithy/eventstream-serde-node": "^4.0.5", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-blob-browser": "^4.0.5", + "@smithy/hash-node": "^4.0.5", + "@smithy/hash-stream-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/md5-js": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.864.0.tgz", + "integrity": "sha512-THiOp0OpQROEKZ6IdDCDNNh3qnNn/kFFaTSOiugDpgcE5QdsOxh1/RXq7LmHpTJum3cmnFf8jG59PHcz9Tjnlw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.864.0", + "@aws-sdk/middleware-host-header": "3.862.0", + "@aws-sdk/middleware-logger": "3.862.0", + "@aws-sdk/middleware-recursion-detection": "3.862.0", + "@aws-sdk/middleware-user-agent": "3.864.0", + "@aws-sdk/region-config-resolver": "3.862.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.862.0", + "@aws-sdk/util-user-agent-browser": "3.862.0", + "@aws-sdk/util-user-agent-node": "3.864.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.864.0.tgz", + "integrity": "sha512-LFUREbobleHEln+Zf7IG83lAZwvHZG0stI7UU0CtwyuhQy5Yx0rKksHNOCmlM7MpTEbSCfntEhYi3jUaY5e5lg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@aws-sdk/xml-builder": "3.862.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.864.0.tgz", + "integrity": "sha512-StJPOI2Rt8UE6lYjXUpg6tqSZaM72xg46ljPg8kIevtBAAfdtq9K20qT/kSliWGIBocMFAv0g2mC0hAa+ECyvg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.864.0.tgz", + "integrity": "sha512-E/RFVxGTuGnuD+9pFPH2j4l6HvrXzPhmpL8H8nOoJUosjx7d4v93GJMbbl1v/fkDLqW9qN4Jx2cI6PAjohA6OA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.864.0.tgz", + "integrity": "sha512-PlxrijguR1gxyPd5EYam6OfWLarj2MJGf07DvCx9MAuQkw77HBnsu6+XbV8fQriFuoJVTBLn9ROhMr/ROAYfUg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/credential-provider-env": "3.864.0", + "@aws-sdk/credential-provider-http": "3.864.0", + "@aws-sdk/credential-provider-process": "3.864.0", + "@aws-sdk/credential-provider-sso": "3.864.0", + "@aws-sdk/credential-provider-web-identity": "3.864.0", + "@aws-sdk/nested-clients": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.864.0.tgz", + "integrity": "sha512-2BEymFeXURS+4jE9tP3vahPwbYRl0/1MVaFZcijj6pq+nf5EPGvkFillbdBRdc98ZI2NedZgSKu3gfZXgYdUhQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.864.0", + "@aws-sdk/credential-provider-http": "3.864.0", + "@aws-sdk/credential-provider-ini": "3.864.0", + "@aws-sdk/credential-provider-process": "3.864.0", + "@aws-sdk/credential-provider-sso": "3.864.0", + "@aws-sdk/credential-provider-web-identity": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.864.0.tgz", + "integrity": "sha512-Zxnn1hxhq7EOqXhVYgkF4rI9MnaO3+6bSg/tErnBQ3F8kDpA7CFU24G1YxwaJXp2X4aX3LwthefmSJHwcVP/2g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.864.0.tgz", + "integrity": "sha512-UPyPNQbxDwHVGmgWdGg9/9yvzuedRQVF5jtMkmP565YX9pKZ8wYAcXhcYdNPWFvH0GYdB0crKOmvib+bmCuwkw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.864.0", + "@aws-sdk/core": "3.864.0", + "@aws-sdk/token-providers": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.864.0.tgz", + "integrity": "sha512-nNcjPN4SYg8drLwqK0vgVeSvxeGQiD0FxOaT38mV2H8cu0C5NzpvA+14Xy+W6vT84dxgmJYKk71Cr5QL2Oz+rA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/nested-clients": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.862.0.tgz", + "integrity": "sha512-Wcsc7VPLjImQw+CP1/YkwyofMs9Ab6dVq96iS8p0zv0C6YTaMjvillkau4zFfrrrTshdzFWKptIFhKK8Zsei1g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.862.0.tgz", + "integrity": "sha512-oG3AaVUJ+26p0ESU4INFn6MmqqiBFZGrebST66Or+YBhteed2rbbFl7mCfjtPWUFgquQlvT1UP19P3LjQKeKpw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.864.0.tgz", + "integrity": "sha512-MvakvzPZi9uyP3YADuIqtk/FAcPFkyYFWVVMf5iFs/rCdk0CUzn02Qf4CSuyhbkS6Y0KrAsMgKR4MgklPU79Wg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.862.0.tgz", + "integrity": "sha512-jDje8dCFeFHfuCAxMDXBs8hy8q9NCTlyK4ThyyfAj3U4Pixly2mmzY2u7b7AyGhWsjJNx8uhTjlYq5zkQPQCYw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.862.0.tgz", + "integrity": "sha512-MnwLxCw7Cc9OngEH3SHFhrLlDI9WVxaBkp3oTsdY9JE7v8OE38wQ9vtjaRsynjwu0WRtrctSHbpd7h/QVvtjyA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.862.0.tgz", + "integrity": "sha512-N/bXSJznNBR/i7Ofmf9+gM6dx/SPBK09ZWLKsW5iQjqKxAKn/2DozlnE54uiEs1saHZWoNDRg69Ww4XYYSlG1Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.862.0.tgz", + "integrity": "sha512-KVoo3IOzEkTq97YKM4uxZcYFSNnMkhW/qj22csofLegZi5fk90ztUnnaeKfaEJHfHp/tm1Y3uSoOXH45s++kKQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.864.0.tgz", + "integrity": "sha512-GjYPZ6Xnqo17NnC8NIQyvvdzzO7dm+Ks7gpxD/HsbXPmV2aEfuFveJXneGW9e1BheSKFff6FPDWu8Gaj2Iu1yg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.862.0.tgz", + "integrity": "sha512-72VtP7DZC8lYTE2L3Efx2BrD98oe9WTK8X6hmd3WTLkbIjvgWQWIdjgaFXBs8WevsXkewIctfyA3KEezvL5ggw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.864.0.tgz", + "integrity": "sha512-wrddonw4EyLNSNBrApzEhpSrDwJiNfjxDm5E+bn8n32BbAojXASH8W8jNpxz/jMgNkkJNxCfyqybGKzBX0OhbQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.862.0", + "@smithy/core": "^3.8.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.864.0.tgz", + "integrity": "sha512-H1C+NjSmz2y8Tbgh7Yy89J20yD/hVyk15hNoZDbCYkXg0M358KS7KVIEYs8E2aPOCr1sK3HBE819D/yvdMgokA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.864.0", + "@aws-sdk/middleware-host-header": "3.862.0", + "@aws-sdk/middleware-logger": "3.862.0", + "@aws-sdk/middleware-recursion-detection": "3.862.0", + "@aws-sdk/middleware-user-agent": "3.864.0", + "@aws-sdk/region-config-resolver": "3.862.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.862.0", + "@aws-sdk/util-user-agent-browser": "3.862.0", + "@aws-sdk/util-user-agent-node": "3.864.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.862.0.tgz", + "integrity": "sha512-VisR+/HuVFICrBPY+q9novEiE4b3mvDofWqyvmxHcWM7HumTz9ZQSuEtnlB/92GVM3KDUrR9EmBHNRrfXYZkcQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.864.0.tgz", + "integrity": "sha512-IiVFDxabrqTB1A9qZI6IEa3cOgF2eciUG4UX27HzkMY6UXG0EZhnGkgkgHYMt6j2hGAFOvAh0ogv/XxZLg6Zaw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-format-url": "3.862.0", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.864.0.tgz", + "integrity": "sha512-w2HIn/WIcUyv1bmyCpRUKHXB5KdFGzyxPkp/YK5g+/FuGdnFFYWGfcO8O+How4jwrZTarBYsAHW9ggoKvwr37w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.864.0.tgz", + "integrity": "sha512-gTc2QHOBo05SCwVA65dUtnJC6QERvFaPiuppGDSxoF7O5AQNK0UR/kMSenwLqN8b5E1oLYvQTv3C1idJLRX0cg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/nested-clients": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", + "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", + "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.862.0.tgz", + "integrity": "sha512-eCZuScdE9MWWkHGM2BJxm726MCmWk/dlHjOKvkM0sN1zxBellBMw5JohNss1Z8/TUmnW2gb9XHTOiHuGjOdksA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-endpoints": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.862.0.tgz", + "integrity": "sha512-4kd2PYUMA/fAnIcVVwBIDCa2KCuUPrS3ELgScLjBaESP0NN+K163m40U5RbzNec/elOcJHR8lEThzzSb7vXH6w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", + "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.862.0.tgz", + "integrity": "sha512-BmPTlm0r9/10MMr5ND9E92r8KMZbq5ltYXYpVcUbAsnB1RJ8ASJuRoLne5F7mB3YMx0FJoOTuSq7LdQM3LgW3Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.864.0.tgz", + "integrity": "sha512-d+FjUm2eJEpP+FRpVR3z6KzMdx1qwxEYDz8jzNKwxYLBBquaBaP/wfoMtMQKAcbrR7aT9FZVZF7zDgzNxUvQlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.862.0.tgz", + "integrity": "sha512-6Ed0kmC1NMbuFTEgNmamAUU1h5gShgxL1hBVLbEzUa3trX5aJBz1vU4bXaBTvOYUAnOHtiy1Ml4AMStd6hJnFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", + "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.0.0.tgz", + "integrity": "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", + "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", + "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.8.0.tgz", + "integrity": "sha512-EYqsIYJmkR1VhVE9pccnk353xhs+lB6btdutJEtsp7R055haMJp2yE16eSxw8fv+G0WUY6vqxyYOP8kOqawxYQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.9", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", + "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.5.tgz", + "integrity": "sha512-miEUN+nz2UTNoRYRhRqVTJCx7jMeILdAurStT2XoS+mhokkmz1xAPp95DFW9Gxt4iF2VBqpeF9HbTQ3kY1viOA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.5.tgz", + "integrity": "sha512-LCUQUVTbM6HFKzImYlSB9w4xafZmpdmZsOh9rIl7riPC3osCgGFVP+wwvYVw6pXda9PPT9TcEZxaq3XE81EdJQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.3.tgz", + "integrity": "sha512-yTTzw2jZjn/MbHu1pURbHdpjGbCuMHWncNBpJnQAPxOVnFUAbSIUSwafiphVDjNV93TdBJWmeVAds7yl5QCkcA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.5.tgz", + "integrity": "sha512-lGS10urI4CNzz6YlTe5EYG0YOpsSp3ra8MXyco4aqSkQDuyZPIw2hcaxDU82OUVtK7UY9hrSvgWtpsW5D4rb4g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.5.tgz", + "integrity": "sha512-JFnmu4SU36YYw3DIBVao3FsJh4Uw65vVDIqlWT4LzR6gXA0F3KP0IXFKKJrhaVzCBhAuMsrUUaT5I+/4ZhF7aw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", + "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.5.tgz", + "integrity": "sha512-F7MmCd3FH/Q2edhcKd+qulWkwfChHbc9nhguBlVjSUE6hVHhec3q6uPQ+0u69S6ppvLtR3eStfCuEKMXBXhvvA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.0.0", + "@smithy/chunked-blob-reader-native": "^4.0.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", + "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.5.tgz", + "integrity": "sha512-IJuDS3+VfWB67UC0GU0uYBG/TA30w+PlOaSo0GPm9UHS88A6rCP6uZxNjNYiyRtOcjv7TXn/60cW8ox1yuZsLg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", + "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.5.tgz", + "integrity": "sha512-8n2XCwdUbGr8W/XhMTaxILkVlw2QebkVTn5tm3HOcbPbOpWg89zr6dPXsH8xbeTsbTXlJvlJNTQsKAIoqQGbdA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", + "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.18.tgz", + "integrity": "sha512-ZhvqcVRPZxnZlokcPaTwb+r+h4yOIOCJmx0v2d1bpVlmP465g3qpVSf7wxcq5zZdu4jb0H4yIMxuPwDJSQc3MQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.8.0", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.19", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.19.tgz", + "integrity": "sha512-X58zx/NVECjeuUB6A8HBu4bhx72EoUz+T5jTMIyeNKx2lf+Gs9TmWPNNkH+5QF0COjpInP/xSpJGJ7xEnAklQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/service-error-classification": "^4.0.7", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", + "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", + "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", + "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", + "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", + "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", + "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", + "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", + "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", + "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", + "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", + "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.10.tgz", + "integrity": "sha512-iW6HjXqN0oPtRS0NK/zzZ4zZeGESIFcxj2FkWed3mcK8jdSdHzvnCKXSjvewESKAgGKAbJRA+OsaqKhkdYRbQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.8.0", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", + "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", + "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.26.tgz", + "integrity": "sha512-xgl75aHIS/3rrGp7iTxQAOELYeyiwBu+eEgAk4xfKwJJ0L8VUjhO2shsDpeil54BOFsqmk5xfdesiewbUY5tKQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.26.tgz", + "integrity": "sha512-z81yyIkGiLLYVDetKTUeCZQ8x20EEzvQjrqJtb/mXnevLq2+w3XCEWTJ2pMp401b6BkEkHVfXb/cROBpVauLMQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.5", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", + "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", + "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", + "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.7", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", + "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.7.tgz", + "integrity": "sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -50,6 +2135,12 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.15.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", @@ -83,6 +2174,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -155,6 +2252,12 @@ "node": ">=18" } }, + "node_modules/bowser": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.0.tgz", + "integrity": "sha512-HcOcTudTeEWgbHh0Y1Tyb6fdeR71m4b/QACf0D4KswGTsNeIJQmg38mRENZPAYPZvGFN3fk3604XbQEPdxXdKg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -185,6 +2288,23 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -248,6 +2368,47 @@ "fsevents": "~2.3.2" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -255,6 +2416,21 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -352,6 +2528,15 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", @@ -495,6 +2680,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -722,6 +2925,12 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -929,6 +3138,27 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -999,6 +3229,67 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1336,6 +3627,20 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1523,6 +3828,48 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1595,6 +3942,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1626,6 +3982,35 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1677,6 +4062,12 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1691,6 +4082,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1713,6 +4110,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/package.json b/package.json index 7e0a0af..2004a01 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "license": "ISC", "description": "", "dependencies": { + "@aws-sdk/client-s3": "^3.864.0", + "@aws-sdk/s3-request-presigner": "^3.864.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -20,8 +22,10 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "multer": "^2.0.2", "pg": "^8.16.2", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "sharp": "^0.34.3" }, "devDependencies": { "nodemon": "^3.1.10" From 6cbda53f16affee335983b99c0b2c089183636c2 Mon Sep 17 00:00:00 2001 From: Olivia Wilson-Simmonds Date: Wed, 13 Aug 2025 01:18:56 -0400 Subject: [PATCH 40/70] did some formatting changes and added routes --- api/echoes.js | 177 ++++++++++++++++++++++++++++++++++++++++++++--- database/seed.js | 89 +++++++++++++++++++++++- 2 files changed, 254 insertions(+), 12 deletions(-) diff --git a/api/echoes.js b/api/echoes.js index ef14f3e..4b2287a 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -4,22 +4,119 @@ const { Echoes, Echo_recipients, Friends } = require("../database"); const { authenticateJWT } = require("../auth"); const { Op } = require("sequelize"); -// route for fetching all echoes -router.get("/", async (req, res) => { +/* ---------- helpers ---------- */ +// helper: check if two users are friends (accepted) +async function isFriendWith(db, meId, otherId) { + if (meId === otherId) return true; + const rel = await Friends.findOne({ + where: { + [Op.or]: [ + { user_id: otherId, friend_id: meId }, + { user_id: meId, friend_id: otherId }, + ], + status: "accepted", + }, + }); + return !!rel; +} + +// helper: check if user is a recipient in Echo_recipients +async function isRecipientOf(db, echoId, meId) { + const rec = await Echo_recipients.findOne({ + where: { echo_id: echoId, recipient_id: meId }, + }); + return !!rec; +} + +/* ========================================================= + HOME FEED + (must be BEFORE /:id to avoid treating 'home' as ID) + Visible to signed-in user: + - public + - friend (if accepted friendship OR you’re the author) + - custom (if you’re a recipient OR you’re the author) + - self (only if you’re the author) + ========================================================= */ +router.get("/home", authenticateJWT, async (req, res) => { try { - const echoes = await Echoes.findAll({ - where: { - recipient_type: "public", + const user_id = req.user.id; + + const all = await Echoes.findAll(); + const visible = []; + + for (const e of all) { + const own = e.user_id === user_id; + + if (e.recipient_type === "public") { + visible.push(e); + continue; } - }); + if (e.recipient_type === "self") { + if (own) visible.push(e); + continue; + } + if (e.recipient_type === "friend") { + if (own || (await isFriendWith(null, user_id, e.user_id))) visible.push(e); + continue; + } + if (e.recipient_type === "custom") { + if (own || (await isRecipientOf(null, e.id, user_id))) visible.push(e); + continue; + } + } + + res.json(visible); + } catch (err) { + console.error("GET /api/echoes/home error:", err); + res.status(500).json({ error: "Failed to load homepage echoes" }); + } +}); + +/* ========================================================= + DASHBOARD LISTS (Inbox / Saved) + - Inbox: own OR friends OR custom-where-recipient + - Saved: (stub) return [] until you add a Saved table/flag + ========================================================= */ +router.get("/", authenticateJWT, async (req, res) => { + try { + const user_id = req.user.id; + const tab = (req.query.tab || "Inbox").toLowerCase(); + + if (tab === "saved") { + // TODO: wire to real saved/bookmark table/flag + return res.json([]); + } + + // Inbox + const all = await Echoes.findAll(); + const inbox = []; - res.json(echoes); + for (const e of all) { + const own = e.user_id === user_id; + + if (own) { inbox.push(e); continue; } + + if (e.recipient_type === "friend") { + if (await isFriendWith(null, user_id, e.user_id)) { inbox.push(e); continue; } + } + + if (e.recipient_type === "custom") { + if (await isRecipientOf(null, e.id, user_id)) { inbox.push(e); continue; } + } + + // exclude public/self by others from Inbox per your spec + } + + res.json(inbox); } catch (err) { - res.status(500).json({ error: "Failed to fetch all echoes" }); + console.error("GET /api/echoes error:", err); + res.status(500).json({ error: "Failed to fetch echoes" }); } }); -// fetching echo by id +/* ========================================================= + fetching echo by id + ========================================================= */ router.get("/:id", authenticateJWT, async (req, res) => { try { const user_id = req.user.id; @@ -107,7 +204,67 @@ router.get("/:id", authenticateJWT, async (req, res) => { } }); -// creating an echo +/* ========================================================= + PATCH /api/echoes/:id/unlock + Unlock logic (visibility + time gate) + ========================================================= */ +router.patch("/:id/unlock", authenticateJWT, async (req, res) => { + try { + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id); + if (!echo) return res.status(404).json({ error: "Echo not found" }); + + const isCreator = echo.user_id === user_id; + + // ---- visibility checks (mirror your GET /:id) ---- + if (echo.recipient_type === "self" && !isCreator) { + return res.status(403).json({ private: "This echo is private" }); + } + + if (echo.recipient_type === "friend" && !isCreator) { + const isFriend = await Friends.findOne({ + where: { + [Op.or]: [ + { user_id: echo.user_id, friend_id: user_id }, + { user_id, friend_id: echo.user_id } + ], + status: "accepted", + }, + }); + if (!isFriend) { + return res.status(403).json({ not_friend: "Friends only" }); + } + } + + if (echo.recipient_type === "custom" && !isCreator) { + const isRecipient = await Echo_recipients.findOne({ + where: { echo_id: echo.id, recipient_id: user_id }, + }); + if (!isRecipient) { + return res.status(403).json({ no_access: "Not a recipient" }); + } + } + + // ---- time gate ---- + const now = new Date(); + if (now < new Date(echo.unlock_datetime) && !isCreator) { + return res.status(403).json({ locked: "Unlock time not reached" }); + } + + // At this point, consider the echo "opened" by this user. + // If you later add a table for history/views, insert it here. + + const payload = { ...echo.toJSON(), client_unlocked: true }; + return res.json(payload); + } catch (err) { + console.error("Unlock error:", err); + res.status(500).json({ error: "Failed to unlock echo" }); + } +}); + +/* ========================================================= + creating an echo + ========================================================= */ router.post("/", authenticateJWT, async (req, res) => { try { const { recipient_type, text, unlock_datetime, show_sender_name, customRecipients } = req.body; diff --git a/database/seed.js b/database/seed.js index ae0b729..3a2edc3 100644 --- a/database/seed.js +++ b/database/seed.js @@ -47,7 +47,7 @@ const seed = async () => { echo_name: "Public Echo by Emmanuel", user_id: users[2].id, // Emmanuel recipient_type: "public", - text: "Public echo by Emmanuel", + text: "Public echo by Emmanuel for everyone to see", unlock_datetime: new Date(), is_archived: true, is_saved: false, @@ -73,7 +73,7 @@ const seed = async () => { echo_name: "Jeramy's Custom Echo", user_id: users[0].id, // Jeramy recipient_type: "custom", - text: "Jeramy's custom echo", + text: "Jeramy's custom echo wwere only specific users can see it", unlock_datetime: new Date(Date.now() + 1000 * 60 * 45), // unlock in 45 minutes show_sender_name: true, is_saved: false, @@ -81,6 +81,91 @@ const seed = async () => { location_locked: false, lat: null, lng: null + }, + { + echo_name: "Walk by Saint Lawrence", + user_id: users[0].id, // Jeramy + recipient_type: "friend", + text: "Remember when we first passed Saint Lawrence Triangle?", + unlock_datetime: new Date("2025-09-01T12:00:00Z"), + show_sender_name: true, + location_locked: true, + lat: 40.8365, + lng: -73.8677, + tags: ["memory", "friendship"] + }, + { + echo_name: "Lunch under the tracks", + user_id: users[1].id, // Aiyanna + recipient_type: "public", + text: "Grab a lunch by the 6 train stop.", + unlock_datetime: new Date("2025-09-05T09:00:00Z"), + show_sender_name: false, + location_locked: true, + lat: 40.831589, + lng: -73.866882, + tags: ["food", "bronx"] + }, + { + echo_name: "Morning jog start", + user_id: users[2].id, // Emmanuel + recipient_type: "self", + text: "Started my day running through Saint Lawrence Triangle.", + unlock_datetime: new Date("2025-08-25T06:30:00Z"), + show_sender_name: true, + location_locked: true, + lat: 40.8350, + lng: -73.8680, + is_unlocked: true, + tags: ["fitness", "morning"] + }, + { + echo_name: "BX Park Bench Memory", + user_id: users[3].id, // Olivia + recipient_type: "public", + text: "Meet me by the park bench. You’ll know it when you see it.", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 120), // +2 hours + show_sender_name: true, + location_locked: true, + lat: 40.82965, + lng: -73.86790, + tags: ["bronx", "park", "memory"] + }, + { + echo_name: "Soundview Corner Joke", + user_id: users[3].id, // Olivia + recipient_type: "friend", + text: "This corner still cracks me up 😂", + unlock_datetime: new Date(Date.now() - 1000 * 60 * 60), // already unlocked + show_sender_name: true, + location_locked: true, + lat: 40.82920, + lng: -73.86730, + tags: ["friends", "inside-joke"] + }, + { + echo_name: "Triangle Note for Olivia", + user_id: users[0].id, // Jeramy + recipient_type: "custom", + text: "For Olivia only — look up!", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 30), // +30 min + show_sender_name: true, + location_locked: true, + lat: 40.82990, + lng: -73.86820, + tags: ["custom", "triangle"] + }, + { + echo_name: "Lunch Cart by the School", + user_id: users[1].id, // Aiyanna + recipient_type: "public", + text: "Best empanadas here at noon 🍽️", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 60 * 24), // +1 day + show_sender_name: false, + location_locked: true, + lat: 40.82890, + lng: -73.86690, + tags: ["food", "lunch"] } ]); From 10df9a925599f53cf163c4367871a2209dfaf052 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 13 Aug 2025 11:10:15 -0400 Subject: [PATCH 41/70] GET friends route updated to find bidirectional friendship pairs --- api/friends.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/api/friends.js b/api/friends.js index d95d45b..2cc3f5c 100644 --- a/api/friends.js +++ b/api/friends.js @@ -1,15 +1,22 @@ const express = require("express") const router = express.Router(); -const { Friends} = require("../database"); +const { Friends } = require("../database"); const { authenticateJWT } = require("../auth"); const { Op } = require("sequelize"); router.get("/", authenticateJWT, async (req, res) => { try { - const user_id = req.user.id; + const userId = req.user.id; const friends = await Friends.findAll({ - where: {user_id: user_id, status: "accepted"}, + where: { + [Op.or]: [ + {user_id: userId}, + {friend_id: userId} + ], + status: "accepted" + } }); + res.json(friends); } catch (err) { res.status(500).json({ error: "Failed to fetch friends" }); @@ -49,7 +56,7 @@ router.post("/", authenticateJWT, async (req, res) => { }); if (isFriend) { - return res.status(403).json({friends: "Already friends"}); + return res.status(403).json({ message: "Already friends"}); } // check if there's already an existing request @@ -64,7 +71,7 @@ router.post("/", authenticateJWT, async (req, res) => { }); if (requestExists) { - return res.status(403).json({request: "Friend request sent already"}); + return res.status(403).json({message: "Friend request sent already"}); } // create pending request @@ -84,7 +91,7 @@ router.patch("/:id/accept", authenticateJWT, async (req, res) => { } }); -router.patch("/:id/accept", authenticateJWT, async (req, res) => { +router.patch("/:id/block", authenticateJWT, async (req, res) => { try { } catch (err) { @@ -104,3 +111,5 @@ module.exports = router; + + From 33be068517f16429f8ca08e9fc1fb0f198049faa Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Wed, 13 Aug 2025 12:12:55 -0400 Subject: [PATCH 42/70] refactor: Prettier add trailing commas --- auth/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/auth/index.js b/auth/index.js index 07968c5..42e5369 100644 --- a/auth/index.js +++ b/auth/index.js @@ -76,7 +76,7 @@ router.post("/auth0", async (req, res) => { email: user.email, }, JWT_SECRET, - { expiresIn: "24h" } + { expiresIn: "24h" }, ); res.cookie("token", token, { @@ -137,7 +137,7 @@ router.post("/signup", async (req, res) => { email: user.email, }, JWT_SECRET, - { expiresIn: "24h" } + { expiresIn: "24h" }, ); res.cookie("token", token, { @@ -188,7 +188,7 @@ router.post("/login", async (req, res) => { email: user.email, }, JWT_SECRET, - { expiresIn: "24h" } + { expiresIn: "24h" }, ); res.cookie("token", token, { From 3e39deadf1bf2d092948c8395e331bca6b26d115 Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Wed, 13 Aug 2025 12:13:22 -0400 Subject: [PATCH 43/70] feat: Remove sharp dependency --- package-lock.json | 538 +--------------------------------------------- package.json | 3 +- 2 files changed, 2 insertions(+), 539 deletions(-) diff --git a/package-lock.json b/package-lock.json index 73a6108..9ae1d43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,7 @@ "morgan": "^1.10.0", "multer": "^2.0.2", "pg": "^8.16.2", - "sequelize": "^6.37.7", - "sharp": "^0.34.3" + "sequelize": "^6.37.7" }, "devDependencies": { "nodemon": "^3.1.10" @@ -935,434 +934,6 @@ "node": ">=18.0.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@smithy/abort-controller": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", @@ -2368,47 +1939,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2528,15 +2058,6 @@ "node": ">= 0.8" } }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", @@ -2925,12 +2446,6 @@ "node": ">= 0.10" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3828,48 +3343,6 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, - "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" - } - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -3942,15 +3415,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", diff --git a/package.json b/package.json index 2004a01..7febd29 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,7 @@ "morgan": "^1.10.0", "multer": "^2.0.2", "pg": "^8.16.2", - "sequelize": "^6.37.7", - "sharp": "^0.34.3" + "sequelize": "^6.37.7" }, "devDependencies": { "nodemon": "^3.1.10" From 20eb8e111dbb7f5504faf29b06e2daf2476ab772 Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Wed, 13 Aug 2025 12:15:29 -0400 Subject: [PATCH 44/70] feat: Add image_uuids array field to Echoes model --- database/echoes.js | 147 ++++++++++++++++++++++++--------------------- 1 file changed, 78 insertions(+), 69 deletions(-) diff --git a/database/echoes.js b/database/echoes.js index 2b536db..f2f9be8 100644 --- a/database/echoes.js +++ b/database/echoes.js @@ -1,87 +1,96 @@ const { DataTypes } = require("sequelize"); const db = require("./db"); -const Echoes = db.define("echoes", { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, +const Echoes = db.define( + "echoes", + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, - echo_name: { - type: DataTypes.TEXT, - allowNull: true - }, + echo_name: { + type: DataTypes.TEXT, + allowNull: true, + }, - user_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { - model: 'users', // target table - key: 'id' - } - }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: "users", // target table + key: "id", + }, + }, - recipient_type: { - type: DataTypes.ENUM("self", "friend", "public", "custom"), - allowNull: false, - }, + recipient_type: { + type: DataTypes.ENUM("self", "friend", "public", "custom"), + allowNull: false, + }, - text: { - type: DataTypes.TEXT, - allowNull: true, - }, + image_uuids: { + type: DataTypes.ARRAY(DataTypes.UUID), + allowNull: false, + defaultValue: [], + }, - unlock_datetime: { - type: DataTypes.DATE, - allowNull: false - }, + text: { + type: DataTypes.TEXT, + allowNull: true, + }, - is_unlocked: { - type: DataTypes.BOOLEAN, - defaultValue: false, - allowNull: false - }, + unlock_datetime: { + type: DataTypes.DATE, + allowNull: false, + }, - is_archived: { - type: DataTypes.BOOLEAN, - defaultValue: false, - allowNull: false, - }, + is_unlocked: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false, + }, - is_saved: { - type: DataTypes.BOOLEAN, - defaultValue: false, - allowNull: false - }, + is_archived: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false, + }, - show_sender_name: { - type: DataTypes.BOOLEAN, - defaultValue: true, - allowNull: false, - }, + is_saved: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false, + }, - location_locked: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, + show_sender_name: { + type: DataTypes.BOOLEAN, + defaultValue: true, + allowNull: false, + }, - lat: { - type: DataTypes.DECIMAL(9, 6), - allowNull: true - }, + location_locked: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, - lng: { - type: DataTypes.DECIMAL(9, 6), - allowNull: true - } + lat: { + type: DataTypes.DECIMAL(9, 6), + allowNull: true, + }, -}, { - tableName: 'echoes', - timestamps: true, - createdAt: 'created_at', - updatedAt: 'updated_at', -}); + lng: { + type: DataTypes.DECIMAL(9, 6), + allowNull: true, + }, + }, + { + tableName: "echoes", + timestamps: true, + createdAt: "created_at", + updatedAt: "updated_at", + }, +); module.exports = Echoes; From 91a208d80dc9a3ece9cdedd4af9841bd4383a85d Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 13 Aug 2025 12:16:35 -0400 Subject: [PATCH 45/70] Implemented PATCH route to accept friend requests, restricting action to the request receiver and validating request status --- api/friends.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/api/friends.js b/api/friends.js index 2cc3f5c..df32894 100644 --- a/api/friends.js +++ b/api/friends.js @@ -85,9 +85,40 @@ router.post("/", authenticateJWT, async (req, res) => { router.patch("/:id/accept", authenticateJWT, async (req, res) => { try { + const userId = req.user.id; + const friendRequest = await Friends.findByPk(req.params.id); - } catch (err) { + if (!friendRequest) { + return res.status(404).json({error: "Friend request not found."}); + } + + // Ensure current user is the receiver + if (friendRequest.friend_id !== userId) { + return res.status(403).json({ error: "You are not the recipient of this friend request." }); + } + + // check if already accepted + if (friendRequest.status === "accepted") { + return res.status(400).json({error: "This person is already your friend."}); + } + // must be pending to accept + if (friendRequest.status !== "pending") { + return res.status(400).json({error: "This request is not pending."}); + } + + // Accept friend request + friendRequest.status = "accepted"; + await friendRequest.save(); + + return res.status(200).json({ + message: "Friend request accepted", + buddingFriendship: friendRequest + }); + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error accepting friend request."}); } }); From e66112f0efc53d69efb5fb6fba64b6bc2c0af15e Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 13 Aug 2025 12:19:56 -0400 Subject: [PATCH 46/70] Updated GET route to get user friends and pending friend requests --- api/friends.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/friends.js b/api/friends.js index df32894..3c8a107 100644 --- a/api/friends.js +++ b/api/friends.js @@ -13,7 +13,9 @@ router.get("/", authenticateJWT, async (req, res) => { {user_id: userId}, {friend_id: userId} ], - status: "accepted" + status: { + [Op.or]: ["accepted", "pending"] + } } }); From 82324c98609039d60b63d4bf7f41094b865580de Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Wed, 13 Aug 2025 12:20:56 -0400 Subject: [PATCH 47/70] feat: Add S3 image upload support to echo creation This commit adds AWS S3 integration for handling image uploads when creating new echoes. It configures multer for handling multipart form data and implements the S3 client for posting to the S3 bucket. --- api/echoes.js | 178 +++++++++++++++++++++++++++++++------------------- 1 file changed, 110 insertions(+), 68 deletions(-) diff --git a/api/echoes.js b/api/echoes.js index d013c4a..6d08a6a 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -1,9 +1,33 @@ +require("dotenv").config(); const express = require("express"); const router = express.Router(); const { Echoes, Echo_recipients, Friends } = require("../database"); const { authenticateJWT } = require("../auth"); const { Op } = require("sequelize"); const { response } = require("../app"); +const multer = require("multer"); +const crypto = require("crypto"); +const { + S3Client, + PutObjectCommand, + GetObjectCommand, + DeleteObjectsCommand, +} = require("@aws-sdk/client-s3"); +const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); +const storage = multer.memoryStorage(); +const upload = multer({ storage: storage }); +const bucketName = process.env.BUCKET_NAME; +const bucketRegion = process.env.BUCKET_REGION; +const accessKey = process.env.ACCESS_KEY; +const secretAccessKey = process.env.SECRET_ACCESS_KEY; + +const s3 = new S3Client({ + credentials: { + accessKeyId: accessKey, + secretAccessKey: secretAccessKey, + }, + region: bucketRegion, +}); // route for fetching all echoes router.get("/", async (req, res) => { @@ -76,12 +100,9 @@ router.get("/:id", authenticateJWT, async (req, res) => { return res.status(403).json({ locked: "Echo is locked" }); } } else { - return res - .status(403) - .json({ - not_friend: - "This echo is only accessible to friends of echo creator", - }); + return res.status(403).json({ + not_friend: "This echo is only accessible to friends of echo creator", + }); } } @@ -113,81 +134,102 @@ router.get("/:id", authenticateJWT, async (req, res) => { }); // creating an echo -router.post("/", authenticateJWT, async (req, res) => { - try { - const { - echo_name, - recipient_type, - text, - unlock_datetime, - show_sender_name, - lat, - lng, - customRecipients, - } = req.body; - const sender_id = req.user.id; - - // checking if any required fields are missing - if (!sender_id || !recipient_type || !unlock_datetime) { - return res.status(400).json({ error: "Missing required fields" }); - } - - // checking if unlock_datetime is in the future - const unlockTime = new Date(unlock_datetime); - - if (isNaN(unlockTime.getTime())) { - return res.status(400).json({ error: "Invalid unlock_datetime format" }); - } +router.post( + "/", + [authenticateJWT, upload.array("media", 10)], + async (req, res) => { + try { + const { + echo_name, + recipient_type, + text, + unlock_datetime, + show_sender_name, + lat, + lng, + customRecipients, + } = req.body; + const sender_id = req.user.id; + + // checking if any required fields are missing + if (!sender_id || !recipient_type || !unlock_datetime) { + return res.status(400).json({ error: "Missing required fields" }); + } - if (unlockTime < new Date()) { - return res - .status(400) - .json({ error: "unlock_datetime must be in the future" }); - } + // checking if unlock_datetime is in the future + const unlockTime = new Date(unlock_datetime); - // validating customRecipients array if recipient_type is 'custom' - if (recipient_type === "custom") { - if (!Array.isArray(customRecipients) || customRecipients.length === 0) { + if (isNaN(unlockTime.getTime())) { return res .status(400) - .json({ error: "Custom recipient list must be a non-empty array." }); + .json({ error: "Invalid unlock_datetime format" }); } - // prevent sending to self - if (customRecipients.includes(sender_id)) { + if (unlockTime < new Date()) { return res .status(400) - .json({ error: "Cannot send custom echoes to yourself." }); + .json({ error: "unlock_datetime must be in the future" }); } - } - // creating new echo - const newEcho = await Echoes.create({ - echo_name, - user_id: sender_id, - recipient_type, - text, - unlock_datetime, - show_sender_name, - lat, - lng, - }); + // validating customRecipients array if recipient_type is 'custom' + if (recipient_type === "custom") { + if (!Array.isArray(customRecipients) || customRecipients.length === 0) { + return res.status(400).json({ + error: "Custom recipient list must be a non-empty array.", + }); + } - // add custom recipients if custom - if (recipient_type === "custom") { - const echoRecipients = customRecipients.map((recipient_id) => ({ - echo_id: newEcho.id, - recipient_id, - })); + // prevent sending to self + if (customRecipients.includes(sender_id)) { + return res + .status(400) + .json({ error: "Cannot send custom echoes to yourself." }); + } + } - await Echo_recipients.bulkCreate(echoRecipients); - } + const image_uuids = []; + for (let i = 0; i < req.files.length; i++) { + const uuid = crypto.randomUUID(); + const params = { + Bucket: bucketName, + Key: uuid, + Body: req.files[i].buffer, + ContentType: req.files[i].mimetype, + }; + const command = new PutObjectCommand(params); + await s3.send(command); + image_uuids.push(uuid); + } - res.status(201).json(newEcho); - } catch (err) { - res.status(500).json({ error: "Failed to create echo" }); - } -}); + // creating new echo + const newEcho = await Echoes.create({ + echo_name, + user_id: sender_id, + recipient_type, + text, + unlock_datetime, + show_sender_name, + lat, + lng, + image_uuids, + }); + + // add custom recipients if custom + if (recipient_type === "custom") { + const echoRecipients = customRecipients.map((recipient_id) => ({ + echo_id: newEcho.id, + recipient_id, + })); + + await Echo_recipients.bulkCreate(echoRecipients); + } + + res.status(201).json(newEcho); + } catch (err) { + res.status(500).json({ error: "Failed to create echo" }); + } + }, +); // arching or unarchiving an echo router.patch("/:id/archive", authenticateJWT, async (req, res) => { From c5f7c75bfcf12416267e583b4456b7d804d82d8e Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Wed, 13 Aug 2025 12:43:11 -0400 Subject: [PATCH 48/70] chore: Update S3 documentation with clearer setup instructions, and more accurate POST route --- api/Amazon_S3_Integration.md | 80 ++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/api/Amazon_S3_Integration.md b/api/Amazon_S3_Integration.md index d15789b..151f82c 100644 --- a/api/Amazon_S3_Integration.md +++ b/api/Amazon_S3_Integration.md @@ -8,15 +8,13 @@ Below is a step by step breakdown of the process, inspired by [this video](https If you just want to setup on your local machine S3 access, go to [Bucket Access](#bucket-access) and copy and paste the `.env` variables into your `.env` file. The `ACCESS_KEY` and `SECRET_ACCESS_KEY` and `BUCKET_NAME` are not in this file for _obvious_ reasons(hiding bucket name might just me being paranoid). If the `SECRET_ACCESS_KEY` ends up getting _lost_, then I will have to make a new **IAM** user, so please let me know. -Next, run npm install if the routes are already setup, and you haven't done this setup before. This way you install the packages in [Express Server File Handling](#express-server-file-handling) and [Amazon S3 SDK](#amazon-s3-sdk). +Next install the npm packages if you haven't done this setup before. This way you install the packages in [Express Server File Handling](#express-server-file-handling) and [Amazon S3 SDK](#amazon-s3-sdk). ```bash npm install ``` -If for some reason the relevant packages are not in the `package.json`, then proceed to [Express Server File Handling](#express-server-file-handling) and [Amazon S3 SDK](#amazon-s3-sdk) and follow those install steps. - -Thats it! Assuming the routes have been setup, you can now run requests to the Amazon S3 bucket, and our PostgreSQL DB. If you run into any issues, please reach out to me @EmmanuelR21. +Thats it! You can now run requests to the Amazon S3 bucket, and our PostgreSQL DB. If you run into any issues, please reach out to me @EmmanuelR21. ## Bucket Access @@ -48,13 +46,13 @@ Next put in some of these important variables. **NOTE:** that the `crypto` varia ```javascript const multer = require("multer"); const crypto = require("crypto"); -const sharp = require("sharp"); // Only needed if we want to create a resized image, for example to put images into portrait mode (not stretching the image, but adding black bars to fill the gaps). +const sharp = require("sharp"); // To create a resized image, for example to put images into portrait mode. -const storage = multer.memoryStorage(); //This designates the memory to store the media, instead of in disk storage -const upload = multer({ storage: storage }); //Upload function to be used later +const storage = multer.memoryStorage(); //This designates the server to store the media in memory, instead of on disk. +const upload = multer({ storage: storage }); ``` -After creating our `upload` variable, we will use it later in our route as a middleware. +Our `upload` variable will be used later in our route as a middleware. ## Amazon S3 SDK @@ -109,37 +107,41 @@ Its important I clarify a few things: - upload.array() will take an array of "files" named "media". The name "media" can be literally anything, just depends on what you name the array of files being sent from the client. - upload.array() will store the file buffers in req.files when it is done. */ -router.post("/", upload.array("media", 10), async (req, res) => { - // We use UUID's to prevent file name collision, which will overwrite one file over the other. We will also use it to retrieve the media later so we store it in our Postgres DB. - const imageIds = []; - // S3 does not allow you to upload several files at once, so we have to loop - for (let i = 0; i < req.files.length; i++) { - const buffer = await sharp(req.files[i].buffer) - .resize({ height: 1920, width: 1080, fit: "cover" }) - .toBuffer(); //This is if you want to resize an image to be in portrait mode. The image wont get affected, but essentially black bars will fill the gaps. - const uniqueImgId = crypto.randomUUID(); - const params = { - Bucket: bucketName, - Key: uniqueImgId, - Body: req.files[i].buffer, - ContentType: req.files[i].mimetype, - }; - - const command = new PutObjectCommand(params); - // The following uses the s3Client variable we made earlier, to store to the AWS DB bucket - await s3.send(command); - // Upon successful completion we also save the UUID in our array - imageIds.push(uniqueImgId); - } - - // After uploading to S3, we now post to our DB, and instead of uploading media, we will upload the array of UUID's. - - /* - Sequelize post to DB code goes here - */ - - res.send(post); -}); +router.post( + "/", + [authenticateJWT, upload.array("media", 10)], + async (req, res) => { + // We use UUID's to prevent file name collision, which will overwrite one file over the other. We will also use it to retrieve the media later so we store it in our Postgres DB. + const image_uuids = []; + // S3 does not allow you to upload several files at once, so we have to loop + for (let i = 0; i < req.files.length; i++) { + const buffer = await sharp(req.files[i].buffer) + .resize({ height: 1920, width: 1080, fit: "contain" }) + .toBuffer(); //This is for resizing an image to portrait mode. The image wont get affected, but black bars will fill the gaps. + const uniqueImgId = crypto.randomUUID(); + const params = { + Bucket: bucketName, + Key: uniqueImgId, + Body: buffer, //This is strictly for image resizing, if uploading videos you would have to use req.files[i].buffer and or use another package to resize the video. + ContentType: req.files[i].mimetype, + }; + + const command = new PutObjectCommand(params); + // The following uses the s3Client variable we made earlier, to store to the AWS DB bucket + await s3.send(command); + // Upon successful completion we also save the UUID in our array + image_uuids.push(uniqueImgId); + } + await Echoes.create({ + /* + Include here all relevant info for creating the echo + */ + image_uuids, + }); + + res.send(post); + }, +); ``` ### Getting from our Bucket From 00d1824dea533c80acff3f1f847ec3b9746f9c1b Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 13 Aug 2025 13:31:06 -0400 Subject: [PATCH 49/70] Implemented PATCH route for receivers to block senders on pending friend requests, preventing further requests --- api/friends.js | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/api/friends.js b/api/friends.js index 3c8a107..e8bff54 100644 --- a/api/friends.js +++ b/api/friends.js @@ -115,7 +115,7 @@ router.patch("/:id/accept", authenticateJWT, async (req, res) => { return res.status(200).json({ message: "Friend request accepted", - buddingFriendship: friendRequest + friendRequest }); } catch (err) { @@ -126,9 +126,35 @@ router.patch("/:id/accept", authenticateJWT, async (req, res) => { router.patch("/:id/block", authenticateJWT, async (req, res) => { try { - + const userId = req.user.id; + const friendship = await Friends.findByPk(req.params.id); + + if (!friendship) { + return res.status(404).json({error: "Friendship not found."}); + } + + // Ensure current user is the receiver + if (friendship.friend_id !== userId) { + return res.status(403).json({ error: "You are not the recipient of this friend request." }); + } + + // Checking if user is blocked already + if (friendship.status === "blocked") { + return res.status(400).json({ error: "User already blocked." }) + } + + // Block user from sending friend requests + friendship.status = "blocked"; + await friendship.save(); + + return res.status(200).json({ + message: "User successfully blocked", + friendStatus: friendship + }); + } catch (err) { - + console.error(err); + return res.status(500).json({error: "Error blocking user."}); } }); From 46c40fb78cd0e39f702da00ed72a387febd59676 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 13 Aug 2025 13:48:08 -0400 Subject: [PATCH 50/70] Implemented DELETE route so users can removed current friends or any pending friend requests --- api/friends.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/api/friends.js b/api/friends.js index e8bff54..6cc82b7 100644 --- a/api/friends.js +++ b/api/friends.js @@ -160,9 +160,29 @@ router.patch("/:id/block", authenticateJWT, async (req, res) => { router.delete("/:id", authenticateJWT, async (req, res) => { try { - + const userId = req.user.id; + const friendship = await Friends.findByPk(req.params.id); + + // friendship not found + if (!friendship) { + return res.status(404).json({error: "Friendship not found."}); + } + + // Ensure current user is in the friendship pair + if (friendship.friend_id !== userId && friendship.user_id !== userId) { + return res.status(403).json({ error: "You are not a part of this friendship." }); + } + + await friendship.destroy(); + + return res.status(200).json({ + message: "Friend or friend request removed successfully.", + friendship + }); + } catch (err) { - + console.error(err); + return res.status(500).json({error: "Error removing friend or friend request"}); } }); From a524f281f7a1bab629951e802b292dc702575d10 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 13 Aug 2025 14:06:35 -0400 Subject: [PATCH 51/70] changes --- api/friends.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/friends.js b/api/friends.js index 6cc82b7..d92b379 100644 --- a/api/friends.js +++ b/api/friends.js @@ -14,7 +14,7 @@ router.get("/", authenticateJWT, async (req, res) => { {friend_id: userId} ], status: { - [Op.or]: ["accepted", "pending"] + [Op.or]: ["accepted", "pending", "blocked"] } } }); From 93add04a2bdba5e17b58363bcb39a006774090cd Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Wed, 13 Aug 2025 14:34:37 -0400 Subject: [PATCH 52/70] feat: Add signed URLs for echo image model. Succesful GET from S3. --- api/echoes.js | 12 ++++++++++++ database/echoes.js | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/api/echoes.js b/api/echoes.js index 6d08a6a..b48f159 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -56,6 +56,18 @@ router.get("/:id", authenticateJWT, async (req, res) => { const isCreator = echo.user_id === user_id; + const signed_urls = []; + for (let i = 0; i < echo.image_uuids.length; i++) { + const objectParams = { + Bucket: bucketName, + Key: echo.image_uuids[i], + }; + const command = new GetObjectCommand(objectParams); + const url = await getSignedUrl(s3, command, { expiresIn: 3600 }); + signed_urls.push(url); + } + echo.signed_urls = signed_urls; + // If echo is only visible to self and the user is the creator if (echo.recipient_type === "self") { if (isCreator) { diff --git a/database/echoes.js b/database/echoes.js index f2f9be8..eb974a3 100644 --- a/database/echoes.js +++ b/database/echoes.js @@ -35,6 +35,12 @@ const Echoes = db.define( defaultValue: [], }, + signed_urls: { + type: DataTypes.ARRAY(DataTypes.TEXT("long")), + allowNull: false, + defaultValue: [], + }, + text: { type: DataTypes.TEXT, allowNull: true, From 1f07e409236b23d86a5bd3bfa4f8583674593846 Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Wed, 13 Aug 2025 14:40:42 -0400 Subject: [PATCH 53/70] update GET route --- api/Amazon_S3_Integration.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/api/Amazon_S3_Integration.md b/api/Amazon_S3_Integration.md index 151f82c..c89c37e 100644 --- a/api/Amazon_S3_Integration.md +++ b/api/Amazon_S3_Integration.md @@ -147,23 +147,20 @@ router.post( ### Getting from our Bucket ```javascript -router.get("/:id", async (req, res) => { - // Here sequelize will get our echo by ID: - const echoId = req.params.id; +router.get("/:id", authenticateJWT, async (req, res) => { const echo = await Echo.getByPk(echoId); - // Attaches array of "signed URL's" to the Post data. Not entirely sure yet if this adds to the DB table, need to test. - echo.mediaUrls = []; - // Post.uuids will be the array grabbed by echo id. - for (let i = 0; i < echo.uuids.length; i++) { + const signed_urls = []; + for (let i = 0; i < echo.image_uuids.length; i++) { const objectParams = { Bucket: bucketName, - Key: echo.uuids[i], + Key: echo.image_uuids[i], }; const command = new GetObjectCommand(objectParams); const url = await getSignedUrl(s3, command, { expiresIn: 3600 }); - echo.mediaUrls.push(url); + signed_urls.push(url); } - // The Post now has the array of signed URLs when sent back to our client. + // I prefer this method as opposed to using Sequelize's "update" method, as the AWS signed url is rather long, and might throw an error to the server (although it will still attach itself properly) + echo.signed_urls = signed_urls; res.send(echo); }); ``` From 514800ba08f3dba22e6b02a0b701b77f910c4ec3 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 13 Aug 2025 15:42:37 -0400 Subject: [PATCH 54/70] Implemented POST route to post replies, only allowed by authenticated users --- api/echoes.js | 3 +-- api/replies.js | 57 +++++++++++++++++++++++++++++++++++++++++++++ database/replies.js | 5 ++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 api/replies.js diff --git a/api/echoes.js b/api/echoes.js index 8f1f83d..a0935a2 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -3,7 +3,6 @@ const router = express.Router(); const { Echoes, Echo_recipients, Friends } = require("../database"); const { authenticateJWT } = require("../auth"); const { Op } = require("sequelize"); -const { response } = require("../app"); // route for fetching all echoes router.get("/", async (req, res) => { @@ -109,7 +108,7 @@ router.get("/:id", authenticateJWT, async (req, res) => { }); // creating an echo -router.post("/", authenticateJWT, async (req, res) => { +router.post("/", authenticateJWT, async (req, res) => { try { const { echo_name, recipient_type, text, unlock_datetime, show_sender_name, lat, lng, customRecipients } = req.body; const sender_id = req.user.id; diff --git a/api/replies.js b/api/replies.js new file mode 100644 index 0000000..1c5d78d --- /dev/null +++ b/api/replies.js @@ -0,0 +1,57 @@ +const express = require("express"); +const router = express.Router(); +const { Replies, Echoes } = require("../database"); +const { authenticateJWT } = require("../auth"); +const { Op } = require("sequelize"); +const { rawListeners } = require("../app"); + +router.post("/echoes/:id/reply", authenticateJWT, async(req, res) => { + try { + const userId = req.user.id; + const echoId = parseInt(req.params.id, 10); + const { parent_reply_id, message } = req.body; + + // check that echo exists + const echo = await Echoes.findByPk(echoId); + if (!echo) { + return res.status(404).json({error: "Echo not found."}); + } + + // message field required + if (!message) { + return res.status(400).json({error: "Message is required."}); + } + + // if parent reply is provided, ensure it belongs to the same echo + if (parent_reply_id) { + const parentReply = await Replies.findByPk(parent_reply_id); + if (!parentReply || parentReply.echo_id !== echoId) { + return res.status(400).json({error: "Invalid parent reply for this echo."}); + } + } + + const newReply = await Replies.create({ + echo_id: echoId, + user_id: userId, + parent_reply_id, + message + }); + + res.status(201).json({ + message: "Reply added successfully.", + newReply + }); + } catch (err) { + console.error(err); + return res.status(500).json({ error: "Error posting reply."}); + } +}); + +router.get("echoes/:id/replies", async (req, res) => { + +}); + +router.delete("/echoes/:echo_id/replies/:reply_id", authenticateJWT, async (req, res) => { + +}); + diff --git a/database/replies.js b/database/replies.js index 9f8f7d2..e19885c 100644 --- a/database/replies.js +++ b/database/replies.js @@ -33,6 +33,11 @@ const Replies = db.define('replies', { model: 'replies', key: 'id' } + }, + + message: { + type: DataTypes.TEXT, + allowNull: false, } }, { tableName: 'replies', From b62aea7bda209f8ff339a8a243242e6cc3c9cb20 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 13 Aug 2025 16:43:57 -0400 Subject: [PATCH 55/70] Implemented GEt route to find all replies associated with a specific echo --- api/replies.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/api/replies.js b/api/replies.js index 1c5d78d..85bfcee 100644 --- a/api/replies.js +++ b/api/replies.js @@ -47,8 +47,22 @@ router.post("/echoes/:id/reply", authenticateJWT, async(req, res) => { } }); -router.get("echoes/:id/replies", async (req, res) => { +router.get("/echoes/:id/replies", async (req, res) => { + try { + const echoId = parseInt(req.params.id, 10); + + const echoReplies = await Replies.findAll({ + where: { + echo_id: echoId + } + }); + + return res.json(echoReplies); + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error fetching echo replies."}); + } }); router.delete("/echoes/:echo_id/replies/:reply_id", authenticateJWT, async (req, res) => { From f5185565c44bcdcd067ee1da151385ebc15816c0 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 13 Aug 2025 17:51:02 -0400 Subject: [PATCH 56/70] Implemented DELETE route for users to remove replies on echoes with uses, echo and reply validations --- api/replies.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/api/replies.js b/api/replies.js index 85bfcee..cd4bd66 100644 --- a/api/replies.js +++ b/api/replies.js @@ -66,6 +66,42 @@ router.get("/echoes/:id/replies", async (req, res) => { }); router.delete("/echoes/:echo_id/replies/:reply_id", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + const echoId = parseInt(req.params.echo_id, 10); + const replyId = parseInt(req.params.reply_id, 10); + + // check if echo exists + const echo = await Echoes.findByPk(echoId); + if (!echo) { + return res.status(404).json({error: "Echo not found."}); + } + // Find reply and ensure it belongs to the echo and user + const reply = await Replies.findOne({ + where: { + id: replyId, + echo_id: echoId, + user_id: userId + } + }) + + // check if reply exists + if (!reply) { + return res.status(404).json({error: "Reply not found or you are not authorized to delete it."}); + } + + // delete reply + await reply.destroy(); + + return res.status(200).json({ + message: "Reply deleted successfully.", + reply + }); + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error deleting reply."}); + } }); From 7b00126b29cbc95717a17b3e7a649cb4e95d45f9 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 13 Aug 2025 17:55:44 -0400 Subject: [PATCH 57/70] seed: populate sample replies, including nested replies, for testing echo reply features --- api/replies.js | 1 - database/seed.js | 13 ++++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/api/replies.js b/api/replies.js index cd4bd66..7b85e46 100644 --- a/api/replies.js +++ b/api/replies.js @@ -3,7 +3,6 @@ const router = express.Router(); const { Replies, Echoes } = require("../database"); const { authenticateJWT } = require("../auth"); const { Op } = require("sequelize"); -const { rawListeners } = require("../app"); router.post("/echoes/:id/reply", authenticateJWT, async(req, res) => { try { diff --git a/database/seed.js b/database/seed.js index ae0b729..dbdb255 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,5 +1,5 @@ const db = require("./db"); -const { User, Echoes, Friends, Echo_recipients } = require("./index"); +const { User, Echoes, Friends, Echo_recipients, Replies } = require("./index"); const seed = async () => { @@ -110,6 +110,17 @@ const seed = async () => { console.log(`📨 Created ${echoRecipients.length} echo_recipients for custom echo`); + // replies + const replies = await Replies.bulkCreate([ + { echo_id: echoes[0].id, user_id: users[1].id, message: "Hey Jeramy, nice private echo!" }, + { echo_id: echoes[0].id, user_id: users[2].id, message: "Agreed, this is cool." }, + { echo_id: echoes[1].id, user_id: users[0].id, message: "Glad to be your friend, Aiyanna!" }, + { echo_id: echoes[2].id, user_id: users[3].id, message: "Public echoes are great!" }, + // Nested reply (replying to the first reply above) + { echo_id: echoes[0].id, user_id: users[0].id, parent_reply_id: 1, message: "Thanks, Aiyanna!" } + ]); + console.log(`💬 Created ${replies.length} replies`); + console.log("🌱 Seeded the database"); } catch (error) { From e61d7eb51d5e16e7cf3251050cd57c0e7bfdb8d8 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 13 Aug 2025 18:05:43 -0400 Subject: [PATCH 58/70] Fixed bugs with replies routes --- api/index.js | 2 ++ api/replies.js | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api/index.js b/api/index.js index 17dd339..44bec49 100644 --- a/api/index.js +++ b/api/index.js @@ -4,9 +4,11 @@ const testDbRouter = require("./test-db"); const usersRouter = require("./users") const echoesRouter = require("./echoes"); const friendsRouter = require("./friends"); +const repliesRouter = require("./replies"); router.use("/test-db", testDbRouter); router.use("/users", usersRouter); router.use("/echoes", echoesRouter); router.use("/friends", friendsRouter); +router.use("/replies", repliesRouter); module.exports = router; diff --git a/api/replies.js b/api/replies.js index 7b85e46..664523a 100644 --- a/api/replies.js +++ b/api/replies.js @@ -4,7 +4,7 @@ const { Replies, Echoes } = require("../database"); const { authenticateJWT } = require("../auth"); const { Op } = require("sequelize"); -router.post("/echoes/:id/reply", authenticateJWT, async(req, res) => { +router.post("/echoes/:id", authenticateJWT, async(req, res) => { try { const userId = req.user.id; const echoId = parseInt(req.params.id, 10); @@ -46,7 +46,7 @@ router.post("/echoes/:id/reply", authenticateJWT, async(req, res) => { } }); -router.get("/echoes/:id/replies", async (req, res) => { +router.get("/echoes/:id", async (req, res) => { try { const echoId = parseInt(req.params.id, 10); @@ -64,7 +64,7 @@ router.get("/echoes/:id/replies", async (req, res) => { } }); -router.delete("/echoes/:echo_id/replies/:reply_id", authenticateJWT, async (req, res) => { +router.delete("/:reply_id/echoes/:echo_id", authenticateJWT, async (req, res) => { try { const userId = req.user.id; const echoId = parseInt(req.params.echo_id, 10); @@ -104,3 +104,4 @@ router.delete("/echoes/:echo_id/replies/:reply_id", authenticateJWT, async (req, } }); +module.exports = router; \ No newline at end of file From 38d5fd02e2dd02f5a0067f00d33965f85080df3e Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Wed, 13 Aug 2025 21:49:00 -0400 Subject: [PATCH 59/70] Implemented POST route so users can react to echos using user and reaction type validation --- api/reactions.js | 86 +++++++++++++++++++++++++++++++++++++++++++ database/reactions.js | 2 +- 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 api/reactions.js diff --git a/api/reactions.js b/api/reactions.js new file mode 100644 index 0000000..17bf9e8 --- /dev/null +++ b/api/reactions.js @@ -0,0 +1,86 @@ +const express = require("express"); +const router = express.Router(); +const { Reactions, Echoes } = require("../database"); +const { authenticateJWT } = require("../auth"); +const { Op } = require("sequelize"); + +router.post("/echoes/:id", authenticateJWT, async(req, res) => { + try { + let { type } = req.body; + const userId = req.user.id; + const echoId = parseInt(req.params.id, 10); + + // validate echoId + if (isNaN(echoId)) { + return res.status(400).json({error: "Invalid echo Id."}); + } + + // validate reaction type + type = type?.trim(); + const allowedTypes = Reactions.getAttributes().type.values; + if (!type || !allowedTypes.includes(type)) { + return res.status(400).json({error: `Invalid reaction type. Allowed values: ${allowedTypes.join(', ')}`}); + } + + // check if echo exists + const echo = await Echoes.findByPk(echoId); + if (!echo) { + return res.status(404).json({error: "Echo not found."}); + } + + // check for existing reaction + const existingReaction = await Reactions.findOne({ + where: { echo_id: echoId, user_id: userId } + }); + if (existingReaction) { + if (existingReaction.type === type) { + await existingReaction.destroy(); + return res.status(200).json({ + message: "Reaction removed" + }); + } + existingReaction.type = type; + await existingReaction.save(); + return res.status(200).json({ + message: "Reaction updated", + reaction: existingReaction + }); + } + + // creating new reaction + const reaction = await Reactions.create({ + echo_id: echoId, + user_id: userId, + type + }); + + return res.status(201).json({ + message: "Reacted to echo.", + reaction + }); + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error adding reaction to echo."}); + } +}); + +router.get("/echoes/:id", async (req, res) => { + try { + + + } catch (err) { + + } +}); + +router.delete("/echoes/:id", authenticateJWT, async (req, res) => { + try { + + + } catch (err) { + + } +}); + +module.exports = router; \ No newline at end of file diff --git a/database/reactions.js b/database/reactions.js index 3d8d87d..3c2e62a 100644 --- a/database/reactions.js +++ b/database/reactions.js @@ -27,7 +27,7 @@ const Reactions = db.define('reactions', { }, type: { - type: DataTypes.ENUM('sad', 'funny', 'happy'), + type: DataTypes.ENUM('sad', 'funny', 'happy', 'like', 'love', 'angry', 'wow'), allowNull: false } }, { From 073a7f9f59ea3fa4c1f2a58bba3b42e31c12e9ce Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 14 Aug 2025 12:21:09 -0400 Subject: [PATCH 60/70] Implemented GET route to fetch all reactions for an echo --- api/reactions.js | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/api/reactions.js b/api/reactions.js index 17bf9e8..871e7dd 100644 --- a/api/reactions.js +++ b/api/reactions.js @@ -67,20 +67,29 @@ router.post("/echoes/:id", authenticateJWT, async(req, res) => { router.get("/echoes/:id", async (req, res) => { try { - + const echoId = parseInt(req.params.id, 10); - } catch (err) { - - } -}); + // validate echoId + if (isNaN(echoId)) { + return res.status(400).json({error: "Invalid echo ID."}); + } -router.delete("/echoes/:id", authenticateJWT, async (req, res) => { - try { - + // check if echo exists + const echo = await Echoes.findByPk(echoId); + if (!echo) { + return res.status(404).json({error: "Echo not found."}); + } - } catch (err) { + // find all echo reactions + const echoReactions = await Reactions.findAll({ + where: { echo_id: echoId } + }); - } -}); + return res.json(echoReactions); + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error finding reactions for echo."}); + } +}); module.exports = router; \ No newline at end of file From ae5f0520c5faa977763345e6e98969638bde6e55 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 14 Aug 2025 12:31:36 -0400 Subject: [PATCH 61/70] Mounted reactions router at /reactions and added seed data for reactions --- api/index.js | 2 ++ database/seed.js | 30 ++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/api/index.js b/api/index.js index 44bec49..a123720 100644 --- a/api/index.js +++ b/api/index.js @@ -5,10 +5,12 @@ const usersRouter = require("./users") const echoesRouter = require("./echoes"); const friendsRouter = require("./friends"); const repliesRouter = require("./replies"); +const reactionsRouter = require("./reactions"); router.use("/test-db", testDbRouter); router.use("/users", usersRouter); router.use("/echoes", echoesRouter); router.use("/friends", friendsRouter); router.use("/replies", repliesRouter); +router.use("/reactions", reactionsRouter); module.exports = router; diff --git a/database/seed.js b/database/seed.js index dbdb255..03bd48a 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,5 +1,5 @@ const db = require("./db"); -const { User, Echoes, Friends, Echo_recipients, Replies } = require("./index"); +const { User, Echoes, Friends, Echo_recipients, Replies, Reactions } = require("./index"); const seed = async () => { @@ -121,7 +121,33 @@ const seed = async () => { ]); console.log(`💬 Created ${replies.length} replies`); - console.log("🌱 Seeded the database"); + // reactions + const reactions = await Reactions.bulkCreate([ + // Echo 0 (self) — only Jeramy + { echo_id: echoes[0].id, user_id: users[0].id, type: 'love' }, + + // Echo 1 (friend, unlocked) — creator (Aiyanna) + Jeramy (friend accepted) + { echo_id: echoes[1].id, user_id: users[1].id, type: 'happy' }, + { echo_id: echoes[1].id, user_id: users[0].id, type: 'like' }, + + // Echo 2 (public, unlocked) — everyone can react + { echo_id: echoes[2].id, user_id: users[2].id, type: 'like' }, // creator + { echo_id: echoes[2].id, user_id: users[0].id, type: 'wow' }, + { echo_id: echoes[2].id, user_id: users[1].id, type: 'love' }, + { echo_id: echoes[2].id, user_id: users[3].id, type: 'funny' }, + + // Echo 3 (public, locked) — only Olivia + { echo_id: echoes[3].id, user_id: users[3].id, type: 'angry' }, + + // Echo 4 (custom, locked) — only Jeramy (creator) and recipients (u1, u2) + { echo_id: echoes[4].id, user_id: users[0].id, type: 'happy' }, // creator + { echo_id: echoes[4].id, user_id: users[1].id, type: 'wow' }, // recipient + { echo_id: echoes[4].id, user_id: users[2].id, type: 'like' } // recipient + ]); + + console.log(`😎 Created ${reactions.length} reactions`); + + console.log("🌱 Seeded the database"); } catch (error) { console.error("Error seeding database:", error); From fd022a1597cbd021c3f29f4bd6fd4614d6f3c831 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 14 Aug 2025 13:10:04 -0400 Subject: [PATCH 62/70] Implemented GET route to get all tags --- api/index.js | 3 +++ api/tags.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 api/tags.js diff --git a/api/index.js b/api/index.js index a123720..cd2e4f7 100644 --- a/api/index.js +++ b/api/index.js @@ -6,6 +6,7 @@ const echoesRouter = require("./echoes"); const friendsRouter = require("./friends"); const repliesRouter = require("./replies"); const reactionsRouter = require("./reactions"); +const tagsRouter = require("./tags"); router.use("/test-db", testDbRouter); router.use("/users", usersRouter); @@ -13,4 +14,6 @@ router.use("/echoes", echoesRouter); router.use("/friends", friendsRouter); router.use("/replies", repliesRouter); router.use("/reactions", reactionsRouter); +router.use("/tags", tagsRouter); module.exports = router; + diff --git a/api/tags.js b/api/tags.js new file mode 100644 index 0000000..c8045d7 --- /dev/null +++ b/api/tags.js @@ -0,0 +1,46 @@ +const express = require("express"); +const router = express.Router(); +const { Tags } = require("../database"); +const { authenticateJWT } = require("../auth"); +const { Op } = require("sequelize"); + +router.get("/", async(req, res) => { + try { + const tags = await Tags.findAll(); + + // check if tags exist + if (!tags) { + return res.status(404).json({error: "No tags found."}); + } + + // if tags found but none created + if (tags.length === 0) { + return res.status(400).json({error: "No tags have been created."}); + } + + res.json(tags); + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error finding all tags."}); + } +}); + +router.post("/", authenticateJWT, async(req, res) => { + try { + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error adding tag to echo."}); + } +}); + +router.get("/:id/echoes", async (req, res) => { + try { + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error finding echos with this tag."}); + } +}); + +module.exports = router; \ No newline at end of file From 2a4e3dbe48d47d64ed6749bed91c011df49fddb5 Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Thu, 14 Aug 2025 13:19:28 -0400 Subject: [PATCH 63/70] Seeded database with tags --- database/seed.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/database/seed.js b/database/seed.js index 03bd48a..98bd14e 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,5 +1,5 @@ const db = require("./db"); -const { User, Echoes, Friends, Echo_recipients, Replies, Reactions } = require("./index"); +const { User, Echoes, Friends, Echo_recipients, Replies, Reactions, Tags } = require("./index"); const seed = async () => { @@ -147,6 +147,19 @@ const seed = async () => { console.log(`😎 Created ${reactions.length} reactions`); + const tags = await Tags.bulkCreate([ + { name: 'funny' }, + { name: 'inspirational' }, + { name: 'personal' }, + { name: 'friends' }, + { name: 'public' }, + { name: 'locked' }, + { name: 'announcement' }, + { name: 'random' } + ]); + + console.log(`🏷️ Created ${tags.length} tags`); + console.log("🌱 Seeded the database"); } catch (error) { From 60f9d9d6eda2214a74c9f187a41210ff03575dfe Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Fri, 15 Aug 2025 11:53:31 -0400 Subject: [PATCH 64/70] feat: Implement S3 parallel upload and add file size validation, and S3 deletion. --- api/echoes.js | 39 +++++++++++++++----- package-lock.json | 91 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 122 insertions(+), 9 deletions(-) diff --git a/api/echoes.js b/api/echoes.js index b48f159..88efa61 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -9,11 +9,11 @@ const multer = require("multer"); const crypto = require("crypto"); const { S3Client, - PutObjectCommand, GetObjectCommand, DeleteObjectsCommand, } = require("@aws-sdk/client-s3"); const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); +const { Upload } = require("@aws-sdk/lib-storage"); const storage = multer.memoryStorage(); const upload = multer({ storage: storage }); const bucketName = process.env.BUCKET_NAME; @@ -200,16 +200,29 @@ router.post( } const image_uuids = []; + req.files.forEach((file) => { + if (file.size > Math.pow(10, 9)) { + res + .status(400) + .json({ error: "File size exceeds limit, no file uploaded." }); + return; + } + }); for (let i = 0; i < req.files.length; i++) { const uuid = crypto.randomUUID(); - const params = { - Bucket: bucketName, - Key: uuid, - Body: req.files[i].buffer, - ContentType: req.files[i].mimetype, - }; - const command = new PutObjectCommand(params); - await s3.send(command); + const parallelUpload = new Upload({ + client: s3, + params: { + Bucket: bucketName, + Key: uuid, + Body: req.files[i].buffer, + ContentType: req.files[i].mimetype, + }, + }); + parallelUpload.on("httpUploadProgress", (progress) => { + console.log(progress); + }); + await parallelUpload.done(); image_uuids.push(uuid); } @@ -336,6 +349,14 @@ router.delete("/:id", authenticateJWT, async (req, res) => { .status(403) .json({ error: "You are not the owner of this echo." }); } + const params = { + Bucket: bucketName, + Delete: { + Objects: echo.image_uuids.map((uuid) => ({ Key: uuid })), + }, + }; + const command = new DeleteObjectsCommand(params); + await s3.send(command); // delete the echo await echo.destroy(); diff --git a/package-lock.json b/package-lock.json index 9ae1d43..0f74a0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-s3": "^3.864.0", + "@aws-sdk/lib-storage": "^3.864.0", "@aws-sdk/s3-request-presigner": "^3.864.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", @@ -525,6 +526,27 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/lib-storage": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.864.0.tgz", + "integrity": "sha512-Me/HlMXXPv3tStPQufdwnYGholY14JmmzCdOjhnG7gnaClBEnroZKcHuQhrgMm+KyfbzCQ2+9YHsULOfFrg7Mw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/smithy-client": "^4.4.10", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.864.0" + } + }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { "version": "3.862.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.862.0.tgz", @@ -1758,6 +1780,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -1853,6 +1895,16 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2159,6 +2211,15 @@ "node": ">= 0.6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -2415,6 +2476,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -3446,6 +3527,16 @@ "node": ">= 0.8" } }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", diff --git a/package.json b/package.json index 7febd29..ed8225a 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "description": "", "dependencies": { "@aws-sdk/client-s3": "^3.864.0", + "@aws-sdk/lib-storage": "^3.864.0", "@aws-sdk/s3-request-presigner": "^3.864.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", From d70b33f94708053e22546ff86a6dc43014054a88 Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Fri, 15 Aug 2025 16:31:54 -0400 Subject: [PATCH 65/70] chore: Add media table for handling S3 data --- api/echoes.js | 77 ++++++++++++++++++++++++--------- database/echoes.js | 12 ------ database/media.js | 103 +++++++++++++++++++++++---------------------- 3 files changed, 110 insertions(+), 82 deletions(-) diff --git a/api/echoes.js b/api/echoes.js index 88efa61..fe1742c 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -1,7 +1,7 @@ require("dotenv").config(); const express = require("express"); const router = express.Router(); -const { Echoes, Echo_recipients, Friends } = require("../database"); +const { Echoes, Echo_recipients, Friends, Media } = require("../database"); const { authenticateJWT } = require("../auth"); const { Op } = require("sequelize"); const { response } = require("../app"); @@ -48,7 +48,9 @@ router.get("/", async (req, res) => { router.get("/:id", authenticateJWT, async (req, res) => { try { const user_id = req.user.id; - const echo = await Echoes.findByPk(req.params.id); + const echo = await Echoes.findByPk(req.params.id, { + include: { model: Media }, + }); if (!echo) { return res.status(404).json({ error: "Echo not found" }); @@ -56,17 +58,15 @@ router.get("/:id", authenticateJWT, async (req, res) => { const isCreator = echo.user_id === user_id; - const signed_urls = []; - for (let i = 0; i < echo.image_uuids.length; i++) { + for (let i = 0; i < echo.media.length; i++) { const objectParams = { Bucket: bucketName, - Key: echo.image_uuids[i], + Key: echo.media[i].uuid, }; const command = new GetObjectCommand(objectParams); const url = await getSignedUrl(s3, command, { expiresIn: 3600 }); - signed_urls.push(url); + await echo.media[i].update({ signed_url: url }); } - echo.signed_urls = signed_urls; // If echo is only visible to self and the user is the creator if (echo.recipient_type === "self") { @@ -198,16 +198,42 @@ router.post( .json({ error: "Cannot send custom echoes to yourself." }); } } + function getFileType(file) { + const name = file.originalname.split(".")[1].toLowerCase(); + if ( + name === "jpg" || + name === "jpeg" || + name === "png" || + name === "heic" || + name === "heif" || + name === "gif" || + name === "webp" + ) { + return "image"; + } else if (name === "mp4" || name === "mov" || name === "webm") { + return "video"; + } else { + return "other"; + } + } - const image_uuids = []; - req.files.forEach((file) => { + for (let i = 0; i < req.files.length; i++) { + const file = req.files[i]; if (file.size > Math.pow(10, 9)) { - res - .status(400) - .json({ error: "File size exceeds limit, no file uploaded." }); - return; + return res.status(400).json({ + error: "File size exceeds limit of 1 GB, no file uploaded.", + }); } - }); + } + + // Validate file types + for (let i = 0; i < req.files.length; i++) { + const file = req.files[i]; + if (getFileType(file) === "other") { + return res.status(400).json({ error: "File type not supported!" }); + } + } + const image_data_list = []; for (let i = 0; i < req.files.length; i++) { const uuid = crypto.randomUUID(); const parallelUpload = new Upload({ @@ -223,7 +249,11 @@ router.post( console.log(progress); }); await parallelUpload.done(); - image_uuids.push(uuid); + image_data_list.push({ + uuid, + fileType: getFileType(req.files[i]), + size: req.files[i].size, + }); } // creating new echo @@ -236,9 +266,16 @@ router.post( show_sender_name, lat, lng, - image_uuids, }); - + for (let i = 0; i < image_data_list.length; i++) { + const image_data = image_data_list[i]; + await Media.create({ + uuid: image_data.uuid, + echo_id: newEcho.id, + type: image_data.fileType, + file_size: image_data.size, + }); + } // add custom recipients if custom if (recipient_type === "custom") { const echoRecipients = customRecipients.map((recipient_id) => ({ @@ -251,6 +288,7 @@ router.post( res.status(201).json(newEcho); } catch (err) { + console.log(err); res.status(500).json({ error: "Failed to create echo" }); } }, @@ -336,7 +374,7 @@ router.patch("/:id/unlock", authenticateJWT, async (req, res) => { router.delete("/:id", authenticateJWT, async (req, res) => { try { const user_id = req.user.id; - const echo = await Echoes.findByPk(req.params.id); + const echo = await Echoes.findByPk(req.params.id, { include: Media }); // check if echo exists if (!echo) { @@ -349,10 +387,11 @@ router.delete("/:id", authenticateJWT, async (req, res) => { .status(403) .json({ error: "You are not the owner of this echo." }); } + const params = { Bucket: bucketName, Delete: { - Objects: echo.image_uuids.map((uuid) => ({ Key: uuid })), + Objects: echo.media.map((media) => ({ Key: media.uuid })), }, }; const command = new DeleteObjectsCommand(params); diff --git a/database/echoes.js b/database/echoes.js index eb974a3..9a3650c 100644 --- a/database/echoes.js +++ b/database/echoes.js @@ -29,18 +29,6 @@ const Echoes = db.define( allowNull: false, }, - image_uuids: { - type: DataTypes.ARRAY(DataTypes.UUID), - allowNull: false, - defaultValue: [], - }, - - signed_urls: { - type: DataTypes.ARRAY(DataTypes.TEXT("long")), - allowNull: false, - defaultValue: [], - }, - text: { type: DataTypes.TEXT, allowNull: true, diff --git a/database/media.js b/database/media.js index aeb401d..f3b2734 100644 --- a/database/media.js +++ b/database/media.js @@ -1,74 +1,75 @@ const { DataTypes } = require("sequelize"); const db = require("./db"); -const Media = db.define("media", { +const Media = db.define( + "media", + { id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, }, echo_id: { - type: DataTypes.INTEGER, - allowNull: true, - references: { - model: 'echoes', - key: 'id' - } - }, + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: "echoes", + key: "id", + }, + }, reply_id: { - type: DataTypes.INTEGER, - allowNull: true, - references: { - model: 'replies', - key: 'id' - } + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: "replies", + key: "id", + }, }, type: { - type: DataTypes.ENUM('image', 'audio', 'video'), - allowNull: false, - }, + type: DataTypes.ENUM("image", "audio", "video"), + allowNull: false, + }, - url: { - type: DataTypes.STRING, - allowNull: false, - validate: { - isUrl: true - } + uuid: { + type: DataTypes.UUID, + allowNull: false, }, file_size: { - type: DataTypes.INTEGER, - allowNull: false - }, + type: DataTypes.BIGINT, + allowNull: false, + }, duration_seconds: { - // only applies to audio or video - type: DataTypes.INTEGER, - allowNull: true - }, + // only applies to audio or video + type: DataTypes.INTEGER, + allowNull: true, + }, - thumbnail_url: { - // optional thumbnail - type: DataTypes.STRING, - allowNull: true - } -}, { + signed_url: { + type: DataTypes.TEXT("long"), + allowNull: true, + defaultValue: "", + }, + }, + { timestamps: true, - createdAt: 'created_at', - updatedAt: 'updated_at', + createdAt: "created_at", + updatedAt: "updated_at", validate: { - eitherEchoOrReply() { - if (!this.echo_id && !this.reply_id) { - throw new Error('Media must belong to either an echo or reply.'); - } - if (this.echo_id && this.reply_id) { - throw new Error('Media cannot belong to both an echo and a reply.'); - } + eitherEchoOrReply() { + if (!this.echo_id && !this.reply_id) { + throw new Error("Media must belong to either an echo or reply."); } - } -}); + if (this.echo_id && this.reply_id) { + throw new Error("Media cannot belong to both an echo and a reply."); + } + }, + }, + }, +); -module.exports = Media; +module.exports = Media; From 87fcc81c34df7d548321e8fd2c957d5f99ec11fe Mon Sep 17 00:00:00 2001 From: jeramyleon Date: Mon, 18 Aug 2025 12:59:41 -0400 Subject: [PATCH 66/70] deleted the wrong router when merging, is fixed now --- api/echoes.js | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/api/echoes.js b/api/echoes.js index cbae005..8cf640c 100644 --- a/api/echoes.js +++ b/api/echoes.js @@ -328,46 +328,31 @@ router.post("/", authenticateJWT, async (req, res) => { }); // unlocking an echo -router.patch("/:id/unlock", authenticateJWT, async (req, res) => { +router.patch("/:id/archive", authenticateJWT, async (req, res) => { try { - const user_id = req.user.id; + const user_id = req.user.id; const echo = await Echoes.findByPk(req.params.id); - // check if echo exists if (!echo) { - return res.status(404).json({error: "Echo not found."}); - } + return res.status(404).json({ error: "Echo not found" }); - // check if user is owner - if (user_id !== echo.user_id) { - return res.status(403).json({error: "You cannot access this echo."}); } - // enforce unlock date - if (new Date() < new Date(echo.unlock_datetime)) { - return res.status(403).json({ error: "This echo is locked until its unlock date."}); + if (echo.user_id !== user_id) { + return res.status(403).json({ error: "You are not the owner of this echo."}); } - // already unlocked - if (echo.is_unlocked) { - return res.status(200).json({ - message: "Echo is already unlocked.", - echo - }) - } - - // Unlock - echo.is_unlocked = true; - await echo.save(); + // Toggle archived status + echo.is_archived = !echo.is_archived; + await echo.save(); return res.status(200).json({ - message: "Echo unlocked", + message: echo.is_archived ? "Echo archived" : "Echo unarchived", echo - }); + }); } catch (err) { - console.error(err); - res.status(500).json({error: "Error unlocking this echo"}); + return res.status(500).json({error: "Failed to toggle echo archive status"}); } }); From aaa89ec4c58faf3d7f748da5a3343842ab9d8b75 Mon Sep 17 00:00:00 2001 From: Olivia Wilson-Simmonds Date: Mon, 18 Aug 2025 19:04:56 -0400 Subject: [PATCH 67/70] added the neccessary routes needed in order to make the component in frontend work properly --- api/users.js | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/api/users.js b/api/users.js index cd50d42..4bcf06f 100644 --- a/api/users.js +++ b/api/users.js @@ -1,6 +1,8 @@ const express = require("express") const router = express.Router(); -const { User, Echoes } = require("../database"); +const { User, Echoes, Friends } = require("../database"); +const { Op } = require("sequelize"); +const { authenticateJWT } = require("../auth"); router.get("/", async (req, res) => { try { @@ -12,6 +14,45 @@ router.get("/", async (req, res) => { res.status(500).json({ error: "Failed to fetch users" }); } }); +// GET /api/users/me – return the logged-in user +router.get("/me", authenticateJWT, async (req, res) => { + try { + const me = await User.findByPk(req.user.id, { + attributes: { exclude: ["password_hash"] }, + }); + if (!me) return res.status(404).json({ error: "User not found." }); + res.json(me); + } catch (e) { + console.error(e); + res.status(500).json({ error: "Failed to load current user." }); + } +}); + + +// PATCH /api/users/:id – update own profile +router.patch("/:id", authenticateJWT, async (req, res) => { + const id = parseInt(req.params.id, 10); + if (req.user.id !== id) { + return res.status(403).json({ error: "You can only edit your own profile." }); + } + + // Update only fields that exist in your model + // (Your User model has username, bio, avatar_url, email, etc. — not firstName/lastName/img) + const allowed = ["username", "bio", "avatar_url", "email"]; + const patch = {}; + for (const k of allowed) if (req.body[k] !== undefined) patch[k] = req.body[k]; + + try { + const user = await User.findByPk(id); + if (!user) return res.status(404).json({ error: "User not found." }); + await user.update(patch); + + res.json({ user }); + } catch (e) { + console.error(e); + res.status(500).json({ error: "Failed to update profile." }); + } +}); router.get("/:id", async (req, res) => { try { @@ -24,6 +65,29 @@ router.get("/:id", async (req, res) => { res.status(500).json({ error: "Failed to fetch user" }) } }); +// GET /api/users/:id/friends – accepted friends of a user +router.get("/:id/friends", async (req, res) => { + const id = parseInt(req.params.id, 10); + try { + const rows = await Friends.findAll({ + where: { + [Op.or]: [{ user_id: id }, { friend_id: id }], + status: "accepted", + }, + }); + const otherIds = rows.map(r => (r.user_id === id ? r.friend_id : r.user_id)); + const list = otherIds.length + ? await User.findAll({ + where: { id: otherIds }, + attributes: { exclude: ["password_hash"] }, + }) + : []; + res.json(list); + } catch (e) { + console.error(e); + res.status(500).json({ error: "Failed to fetch user's friends." }); + } +}); router.get("/:id/echoes", async (req, res) => { try { From c997ba02c275dc01c32595183134e9dbccfcc353 Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Tue, 19 Aug 2025 12:18:51 -0400 Subject: [PATCH 68/70] added email --- auth/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/auth/index.js b/auth/index.js index 07968c5..1413d36 100644 --- a/auth/index.js +++ b/auth/index.js @@ -76,7 +76,7 @@ router.post("/auth0", async (req, res) => { email: user.email, }, JWT_SECRET, - { expiresIn: "24h" } + { expiresIn: "24h" }, ); res.cookie("token", token, { @@ -104,7 +104,7 @@ router.post("/auth0", async (req, res) => { // Signup route router.post("/signup", async (req, res) => { try { - const { username, password } = req.body; + const { username, password, email } = req.body; if (!username || !password) { return res @@ -137,7 +137,7 @@ router.post("/signup", async (req, res) => { email: user.email, }, JWT_SECRET, - { expiresIn: "24h" } + { expiresIn: "24h" }, ); res.cookie("token", token, { @@ -188,7 +188,7 @@ router.post("/login", async (req, res) => { email: user.email, }, JWT_SECRET, - { expiresIn: "24h" } + { expiresIn: "24h" }, ); res.cookie("token", token, { From cdc38203b5461c3db213bf750652540dfab5e220 Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Tue, 19 Aug 2025 12:25:23 -0400 Subject: [PATCH 69/70] implemeneted email into JWT --- auth/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/auth/index.js b/auth/index.js index 1413d36..9ebd6b2 100644 --- a/auth/index.js +++ b/auth/index.js @@ -106,10 +106,10 @@ router.post("/signup", async (req, res) => { try { const { username, password, email } = req.body; - if (!username || !password) { + if (!username || !password || !email) { return res .status(400) - .send({ error: "Username and password are required" }); + .send({ error: "Username, password and email are required" }); } if (password.length < 6) { @@ -126,7 +126,7 @@ router.post("/signup", async (req, res) => { // Create new user const passwordHash = User.hashPassword(password); - const user = await User.create({ username, passwordHash }); + const user = await User.create({ username, passwordHash, email }); // Generate JWT token const token = jwt.sign( @@ -149,7 +149,7 @@ router.post("/signup", async (req, res) => { res.send({ message: "User created successfully", - user: { id: user.id, username: user.username }, + user: { id: user.id, username: user.username, email: username.email }, }); } catch (error) { console.error("Signup error:", error); From 7b7c638668b8d0c30b4ca3a0b2c1e430da605170 Mon Sep 17 00:00:00 2001 From: EmmanuelR21 Date: Tue, 19 Aug 2025 14:01:20 -0400 Subject: [PATCH 70/70] fixed passwordhash not saving correctly --- auth/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/auth/index.js b/auth/index.js index 9ebd6b2..5cf1524 100644 --- a/auth/index.js +++ b/auth/index.js @@ -126,7 +126,11 @@ router.post("/signup", async (req, res) => { // Create new user const passwordHash = User.hashPassword(password); - const user = await User.create({ username, passwordHash, email }); + const user = await User.create({ + username, + password_hash: passwordHash, + email, + }); // Generate JWT token const token = jwt.sign(