From c31be3f879728857b9b2d1f3f272d84f4a30d397 Mon Sep 17 00:00:00 2001 From: Torsten Schulz Date: Fri, 27 Sep 2024 07:40:06 +0200 Subject: [PATCH] Finished guestbook and gallery. started diary --- .../controllers/socialnetworkController.js | 149 +++- backend/models/associations.js | 22 + backend/models/community/diary.js | 31 + backend/models/community/diary_history.js | 35 + backend/models/community/guestbook.js | 47 + backend/models/index.js | 6 + backend/models/trigger.js | 24 +- backend/package-lock.json | 826 +++++++++++++++++- backend/package.json | 3 + backend/routers/socialnetworkRouter.js | 14 +- backend/services/BaseService.js | 12 + backend/services/socialnetworkService.js | 411 ++++++++- backend/utils/sequelize.js | 2 +- backend/utils/syncDatabase.js | 6 +- frontend/src/App.vue | 36 +- frontend/src/components/AppFooter.vue | 15 +- frontend/src/components/DialogWidget.vue | 14 +- frontend/src/components/FolderItem.vue | 46 +- .../src/dialogues/auth/RegisterDialog.vue | 11 +- .../socialnetwork/CreateFolderDialog.vue | 95 +- .../{ImageDialog.vue => EditImageDialog.vue} | 10 +- .../socialnetwork/ShowImageDialog.vue | 65 ++ .../socialnetwork/UserProfileDialog.vue | 304 ++++++- .../src/dialogues/standard/ChooseDialog.vue | 62 ++ frontend/src/i18n/locales/de/general.json | 4 +- .../src/i18n/locales/de/socialnetwork.json | 21 + frontend/src/router/index.js | 7 + frontend/src/store/modules/dialogs.js | 14 +- frontend/src/views/admin/ContactsView.vue | 13 +- frontend/src/views/auth/ActivateView.vue | 7 +- frontend/src/views/home/NoLoginView.vue | 5 +- frontend/src/views/social/GalleryView.vue | 66 +- frontend/src/views/social/GuestbookView.vue | 93 ++ frontend/src/views/social/SearchView.vue | 7 +- 34 files changed, 2298 insertions(+), 185 deletions(-) create mode 100644 backend/models/community/diary.js create mode 100644 backend/models/community/diary_history.js create mode 100644 backend/models/community/guestbook.js rename frontend/src/dialogues/socialnetwork/{ImageDialog.vue => EditImageDialog.vue} (96%) create mode 100644 frontend/src/dialogues/socialnetwork/ShowImageDialog.vue create mode 100644 frontend/src/dialogues/standard/ChooseDialog.vue create mode 100644 frontend/src/views/social/GuestbookView.vue diff --git a/backend/controllers/socialnetworkController.js b/backend/controllers/socialnetworkController.js index afc5291..c5bdb3c 100644 --- a/backend/controllers/socialnetworkController.js +++ b/backend/controllers/socialnetworkController.js @@ -13,6 +13,16 @@ class SocialNetworkController { this.getFolderImageList = this.getFolderImageList.bind(this); this.getImageByHash = this.getImageByHash.bind(this); this.changeImage = this.changeImage.bind(this); + this.getFoldersByUsername = this.getFoldersByUsername.bind(this); + this.deleteFolder = this.deleteFolder.bind(this); + this.createGuestbookEntry = this.createGuestbookEntry.bind(this); + this.getGuestbookEntries = this.getGuestbookEntries.bind(this); + this.deleteGuestbookEntry = this.deleteGuestbookEntry.bind(this); + this.getGuestbookImage = this.getGuestbookImage.bind(this); + this.createDiaryEntry = this.createDiaryEntry.bind(this); + this.updateDiaryEntry = this.updateDiaryEntry.bind(this); + this.deleteDiaryEntry = this.deleteDiaryEntry.bind(this); + this.getDiaryEntries = this.getDiaryEntries.bind(this); } async userSearch(req, res) { @@ -45,7 +55,8 @@ class SocialNetworkController { try { const userId = req.headers.userid; const folderData = req.body; - const folder = await this.socialNetworkService.createFolder(userId, folderData); + const { folderId } = req.params; + const folder = await this.socialNetworkService.createFolder(userId, folderData, folderId); res.status(201).json(folder); } catch (error) { console.error('Error in createFolder:', error); @@ -115,7 +126,6 @@ class SocialNetworkController { const userId = req.headers.userid; const { hash } = req.params; const filePath = await this.socialNetworkService.getImageFilePath(userId, hash); - console.log(filePath); res.sendFile(filePath, err => { if (err) { console.error('Error sending file:', err); @@ -141,6 +151,141 @@ class SocialNetworkController { res.status(403).json({ error: error.message || 'Access denied or image not found' }); } } + + async getFoldersByUsername(req, res) { + try { + const { username } = req.params; + const requestingUserId = req.headers.userid; + if (!username || !requestingUserId) { + return res.status(400).json({ error: 'Invalid username or requesting user ID.' }); + } + const folders = await this.socialNetworkService.getFoldersByUsername(username, requestingUserId); + if (!folders) { + return res.status(404).json({ error: 'No folders found or access denied.' }); + } + res.status(200).json(folders); + } catch (error) { + console.error('Error in getFoldersByUsername:', error); + res.status(500).json({ error: error.message }); + } + } + + async deleteFolder(req, res) { + try { + const userId = req.headers.userid; + const { folderId } = req.params; + await this.socialNetworkService.deleteFolder(userId, folderId); + res.status(204).send(); + } catch (error) { + console.error('Error in deleteFolder:', error); + res.status(500).json({ error: error.message }); + } + } + + async createGuestbookEntry(req, res) { + try { + const { htmlContent, recipientName } = req.body; + const hashedSenderId = req.headers.userid; + const image = req.file ? req.file : null; + const entry = await this.socialNetworkService.createGuestbookEntry( + hashedSenderId, + recipientName, + htmlContent, + image + ); + res.status(201).json(entry); + } catch (error) { + console.error('Error in createGuestbookEntry:', error); + res.status(500).json({ error: error.message }); + } + } + + async getGuestbookEntries(req, res) { + try { + const hashedUserId = req.headers.userid; + const { username, page = 1 } = req.params; + const entries = await this.socialNetworkService.getGuestbookEntries(hashedUserId, username, page); + res.status(200).json(entries); + } catch (error) { + console.error('Error in getGuestbookEntries:', error); + res.status(500).json({ error: error.message }); + } + } + + async deleteGuestbookEntry(req, res) { + try { + const hashedUserId = req.headers.userid; + const { entryId } = req.params; + await this.socialNetworkService.deleteGuestbookEntry(hashedUserId, entryId); + res.status(200).json({ message: 'Entry deleted successfully' }); + } catch (error) { + console.error('Error in deleteGuestbookEntry:', error); + res.status(500).json({ error: error.message }); + } + } + + async getGuestbookImage(req, res) { + try { + const userId = req.headers.userid; + const { guestbookUserName, entryId } = req.params; + const filePath = await this.socialNetworkService.getGuestbookImageFilePath(userId, guestbookUserName, entryId); + res.sendFile(filePath, err => { + if (err) { + console.error('Error sending file:', err); + res.status(500).json({ error: 'Error sending file' }); + } + }); + } catch (error) { + console.error('Error in getImageByHash:', error); + res.status(403).json({ error: error.message || 'Access denied or image not found' }); + } + } + + async createDiaryEntry(req, res) { + try { + const { userId, text } = req.body; + const entry = await this.socialNetworkService.createDiaryEntry(userId, text); + res.status(201).json(entry); + } catch (error) { + console.error('Error in createDiaryEntry:', error); + res.status(500).json({ error: error.message }); + } + } + + async updateDiaryEntry(req, res) { + try { + const { diaryId } = req.params; + const { userId, text } = req.body; + const updatedEntry = await this.socialNetworkService.updateDiaryEntry(diaryId, userId, text); + res.status(200).json(updatedEntry); + } catch (error) { + console.error('Error in updateDiaryEntry:', error); + res.status(500).json({ error: error.message }); + } + } + + async deleteDiaryEntry(req, res) { + try { + const { diaryId } = req.params; + const { userId } = req.body; + const result = await this.socialNetworkService.deleteDiaryEntry(diaryId, userId); + res.status(200).json({ message: 'Entry deleted successfully', result }); + } catch (error) { + console.error('Error in deleteDiaryEntry:', error); + res.status(500).json({ error: error.message }); + } + } + + async getDiaryEntries(req, res) { + try { + const { userId } = req.params; + const entries = await this.socialNetworkService.getDiaryEntries(userId); + res.status(200).json(entries); + } catch (error) { + console.error('Error in getDiaryEntries:', error); + res.status(500).json({ error: error.message }); + } + } } export default SocialNetworkController; diff --git a/backend/models/associations.js b/backend/models/associations.js index 5a94d26..0e23b68 100644 --- a/backend/models/associations.js +++ b/backend/models/associations.js @@ -17,6 +17,7 @@ import ImageVisibilityUser from './community/image_visibility_user.js'; import FolderImageVisibility from './community/folder_image_visibility.js'; import ImageImageVisibility from './community/image_image_visibility.js'; import FolderVisibilityUser from './community/folder_visibility_user.js'; +import GuestbookEntry from './community/guestbook.js'; export default function setupAssociations() { SettingsType.hasMany(UserParamType, { foreignKey: 'settingsId', as: 'user_param_types' }); @@ -95,4 +96,25 @@ export default function setupAssociations() { foreignKey: 'visibilityUserId', otherKey: 'folderId' }); + + User.hasMany(GuestbookEntry, { + foreignKey: 'recipientId', + as: 'receivedEntries' + }); + + User.hasMany(GuestbookEntry, { + foreignKey: 'senderId', + as: 'sentEntries' + }); + + GuestbookEntry.belongsTo(User, { + foreignKey: 'recipientId', + as: 'recipient' + }); + + GuestbookEntry.belongsTo(User, { + foreignKey: 'senderId', + as: 'sender' + }); + } diff --git a/backend/models/community/diary.js b/backend/models/community/diary.js new file mode 100644 index 0000000..b4cb534 --- /dev/null +++ b/backend/models/community/diary.js @@ -0,0 +1,31 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../../utils/sequelize.js'; + +class Diary extends Model { } + +Diary.init({ + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + text: { + type: DataTypes.TEXT, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + } +}, { + sequelize, + modelName: 'Diary', + tableName: 'diary', + schema: 'community', + timestamps: true, +}); + +export default Diary; diff --git a/backend/models/community/diary_history.js b/backend/models/community/diary_history.js new file mode 100644 index 0000000..00e9d9d --- /dev/null +++ b/backend/models/community/diary_history.js @@ -0,0 +1,35 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../../utils/sequelize.js'; + +class DiaryHistory extends Model { } + +DiaryHistory.init({ + diaryId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + oldText: { + type: DataTypes.TEXT, + allowNull: false, + }, + oldCreatedAt: { + type: DataTypes.DATE, + allowNull: false, + }, + oldUpdatedAt: { + type: DataTypes.DATE, + allowNull: false, + }, +}, { + sequelize, + modelName: 'DiaryHistory', + tableName: 'diary_history', + schema: 'community', + timestamps: false, +}); + +export default DiaryHistory; diff --git a/backend/models/community/guestbook.js b/backend/models/community/guestbook.js new file mode 100644 index 0000000..250fc42 --- /dev/null +++ b/backend/models/community/guestbook.js @@ -0,0 +1,47 @@ +import { sequelize } from '../../utils/sequelize.js'; +import { DataTypes } from 'sequelize'; +import User from './user.js'; + +const GuestbookEntry = sequelize.define('guestbook_entry', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false, + }, + recipientId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: User, + key: 'id' + } + }, + senderId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: User, + key: 'id' + } + }, + senderUsername: { + type: DataTypes.STRING, + allowNull: true, + }, + contentHtml: { + type: DataTypes.TEXT, + allowNull: false, + }, + imageUrl: { + type: DataTypes.STRING, + allowNull: true, + }, +}, { + tableName: 'guestbook_entry', + schema: 'community', + timestamps: true, + underscored: true, +}); + +export default GuestbookEntry; diff --git a/backend/models/index.js b/backend/models/index.js index 0e2d576..7bd594b 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -19,6 +19,9 @@ import ImageVisibilityUser from './community/image_visibility_user.js'; import FolderImageVisibility from './community/folder_image_visibility.js'; import ImageImageVisibility from './community/image_image_visibility.js'; import FolderVisibilityUser from './community/folder_visibility_user.js'; +import GuestbookEntry from './community/guestbook.js'; +import DiaryHistory from './community/diary_history.js'; +import Diary from './community/diary.js'; const models = { SettingsType, @@ -42,6 +45,9 @@ const models = { FolderImageVisibility, ImageImageVisibility, FolderVisibilityUser, + GuestbookEntry, + DiaryHistory, + Diary, }; export default models; diff --git a/backend/models/trigger.js b/backend/models/trigger.js index ba97988..52855c1 100644 --- a/backend/models/trigger.js +++ b/backend/models/trigger.js @@ -22,23 +22,43 @@ export async function createTriggers() { `; const createInsertTrigger = ` - CREATE TRIGGER trigger_create_user_param_visibility + CREATE OR REPLACE TRIGGER trigger_create_user_param_visibility AFTER INSERT ON community.user_param FOR EACH ROW EXECUTE FUNCTION create_user_param_visibility_trigger(); `; const createUpdateTrigger = ` - CREATE TRIGGER trigger_update_user_param_visibility + CREATE OR REPLACE TRIGGER trigger_update_user_param_visibility AFTER UPDATE ON community.user_param FOR EACH ROW EXECUTE FUNCTION create_user_param_visibility_trigger(); `; + const createDiaryHistoryTriggerFunction = ` + CREATE OR REPLACE FUNCTION insert_diary_history() + RETURNS TRIGGER AS $$ + BEGIN + INSERT INTO community.diary_history (diaryId, userId, oldText, oldCreatedAt, oldUpdatedAt) + VALUES (OLD.id, OLD.userId, OLD.text, OLD.createdAt, OLD.updatedAt); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + `; + + const createDiaryHistoryTrigger = ` + CREATE OR REPLACE TRIGGER diary_update_trigger + BEFORE UPDATE ON community.diary + FOR EACH ROW + EXECUTE FUNCTION insert_diary_history(); + `; + try { await sequelize.query(createTriggerFunction); await sequelize.query(createInsertTrigger); await sequelize.query(createUpdateTrigger); + await sequelize.query(createDiaryHistoryTriggerFunction); + await sequelize.query(createDiaryHistoryTrigger); console.log('Triggers created successfully'); } catch (error) { diff --git a/backend/package-lock.json b/backend/package-lock.json index 99b13fd..b3d13c7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,16 +12,19 @@ "amqplib": "^0.10.4", "bcrypt": "^5.1.1", "cors": "^2.8.5", + "dompurify": "^3.1.7", "dotenv": "^16.4.5", "express": "^4.19.2", "i18n": "^0.15.1", "joi": "^17.13.3", + "jsdom": "^25.0.1", "multer": "^1.4.5-lts.1", "mysql2": "^3.10.3", "nodemailer": "^6.9.14", "pg": "^8.12.0", "pg-hstore": "^2.3.4", "sequelize": "^6.37.3", + "sharp": "^0.33.5", "socket.io": "^4.7.5", "uuid": "^10.0.0" }, @@ -42,6 +45,15 @@ "node": ">=0.8" } }, + "node_modules/@emnapi/runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -55,6 +67,348 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "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.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "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.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "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.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "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.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "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.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "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.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "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.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "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.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "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.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "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.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -420,6 +774,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -601,11 +960,22 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "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==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -616,8 +986,16 @@ "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==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "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==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "node_modules/color-support": { "version": "1.1.3", @@ -627,6 +1005,17 @@ "color-support": "bin.js" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -779,6 +1168,17 @@ "node": ">= 8" } }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/d": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", @@ -792,6 +1192,49 @@ "node": ">=0.12" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", @@ -808,6 +1251,11 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -824,6 +1272,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -862,6 +1318,11 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", + "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==" + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -980,6 +1441,17 @@ "node": ">= 0.6" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -1249,6 +1721,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1456,6 +1941,17 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -1471,6 +1967,29 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -1550,6 +2069,11 @@ "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==" + }, "node_modules/is-core-module": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", @@ -1573,6 +2097,11 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -1729,6 +2258,119 @@ "node": ">=14" } }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -2129,6 +2771,11 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nwsapi": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", + "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2173,6 +2820,17 @@ "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", "dev": true }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2375,6 +3033,14 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -2478,6 +3144,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==" + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -2493,6 +3164,17 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -2697,6 +3379,44 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2740,6 +3460,14 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "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==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/socket.io": { "version": "4.7.5", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", @@ -2879,6 +3607,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -2908,6 +3641,22 @@ "node": ">=0.12" } }, + "node_modules/tldts": { + "version": "6.1.47", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.47.tgz", + "integrity": "sha512-R/K2tZ5MiY+mVrnSkNJkwqYT2vUv1lcT6wJvd2emGaMJ7PHUGRY4e3tUsdFCXgqxi2QgbHjL3yJgXCo40v9Hxw==", + "dependencies": { + "tldts-core": "^6.1.47" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.47", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.47.tgz", + "integrity": "sha512-6SWyFMnlst1fEt7GQVAAu16EGgFK0cLouH/2Mk6Ftlwhv3Ol40L0dlpGMcnnNiiOMyD2EV/aF3S+U2nKvvLvrA==" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2921,11 +3670,28 @@ "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==" }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "optional": true + }, "node_modules/type": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", @@ -3038,11 +3804,52 @@ "node": ">= 0.8" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -3143,6 +3950,19 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index 210ebfb..5189a09 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,16 +14,19 @@ "amqplib": "^0.10.4", "bcrypt": "^5.1.1", "cors": "^2.8.5", + "dompurify": "^3.1.7", "dotenv": "^16.4.5", "express": "^4.19.2", "i18n": "^0.15.1", "joi": "^17.13.3", + "jsdom": "^25.0.1", "multer": "^1.4.5-lts.1", "mysql2": "^3.10.3", "nodemailer": "^6.9.14", "pg": "^8.12.0", "pg-hstore": "^2.3.4", "sequelize": "^6.37.3", + "sharp": "^0.33.5", "socket.io": "^4.7.5", "uuid": "^10.0.0" }, diff --git a/backend/routers/socialnetworkRouter.js b/backend/routers/socialnetworkRouter.js index 192070b..8e5de4e 100644 --- a/backend/routers/socialnetworkRouter.js +++ b/backend/routers/socialnetworkRouter.js @@ -8,8 +8,8 @@ const router = express.Router(); const socialNetworkController = new SocialNetworkController(); router.post('/usersearch', authenticate, socialNetworkController.userSearch); -router.get('/profile/:userId', authenticate, socialNetworkController.profile); -router.post('/folders', authenticate, socialNetworkController.createFolder); +router.get('/profile/main/:userId', authenticate, socialNetworkController.profile); +router.post('/folders/:folderId', authenticate, socialNetworkController.createFolder); router.get('/folders', authenticate, socialNetworkController.getFolders); router.get('/folder/:folderId', authenticate, socialNetworkController.getFolderImageList); router.post('/images', authenticate, upload.single('image'), socialNetworkController.uploadImage); @@ -17,5 +17,15 @@ router.get('/images/:imageId', authenticate, socialNetworkController.getImage); router.put('/images/:imageId', authenticate, socialNetworkController.changeImage); router.get('/imagevisibilities', authenticate, socialNetworkController.getImageVisibilityTypes); router.get('/image/:hash', authenticate, socialNetworkController.getImageByHash); +router.get('/profile/images/folders/:username', authenticate, socialNetworkController.getFoldersByUsername); +router.delete('/folders/:folderId', authenticate, socialNetworkController.deleteFolder); +router.post('/guestbook/entries', authenticate, upload.single('image'), socialNetworkController.createGuestbookEntry); +router.get('/guestbook/entries/:username/:page', authenticate, socialNetworkController.getGuestbookEntries); +router.delete('/guestbook/entries/:entryId', authenticate, socialNetworkController.deleteGuestbookEntry); +router.get('/guestbook/image/:guestbookUserName/:entryId', authenticate, socialNetworkController.getGuestbookImage); +router.post('/diary', authenticate, socialNetworkController.createDiaryEntry); +router.put('/diary/:diaryId', authenticate, socialNetworkController.updateDiaryEntry); +router.delete('/diary/:diaryId', authenticate, socialNetworkController.deleteDiaryEntry); +router.get('/diary/:userId', authenticate, socialNetworkController.getDiaryEntries); export default router; diff --git a/backend/services/BaseService.js b/backend/services/BaseService.js index b8fbc95..4498f62 100644 --- a/backend/services/BaseService.js +++ b/backend/services/BaseService.js @@ -51,6 +51,18 @@ class BaseService { const ageDate = new Date(ageDifMs); return Math.abs(ageDate.getUTCFullYear() - 1970); } + + + async isUserAdult(userId) { + const birthdateParam = await this.getUserParams(userId, ['birthdate']); + if (!birthdateParam || birthdateParam.length === 0) { + return false; + } + const birthdate = birthdateParam[0].value; + const age = this.calculateAge(birthdate); + return age >= 18; + } + } export default BaseService; diff --git a/backend/services/socialnetworkService.js b/backend/services/socialnetworkService.js index 1f84428..1f8e933 100644 --- a/backend/services/socialnetworkService.js +++ b/backend/services/socialnetworkService.js @@ -11,10 +11,17 @@ import Image from '../models/community/image.js'; import ImageVisibilityType from '../models/type/image_visibility.js'; import FolderImageVisibility from '../models/community/folder_image_visibility.js'; import ImageImageVisibility from '../models/community/image_image_visibility.js'; -import { v4 as uuidv4 } from 'uuid'; -import fs from 'fs'; +import { v4 as uuidv4 } from 'uuid'; +import fs from 'fs'; +import fsPromises from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; +import FolderVisibilityUser from '../models/community/folder_visibility_user.js'; +import ImageVisibilityUser from '../models/community/image_visibility_user.js'; +import GuestbookEntry from '../models/community/guestbook.js'; +import { JSDOM } from 'jsdom'; +import DOMPurify from 'dompurify'; +import sharp from 'sharp'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -33,22 +40,39 @@ class SocialNetworkService extends BaseService { return this.constructUserProfile(user, requestingUserId); } - async createFolder(hashedUserId, data) { + async createFolder(hashedUserId, data, folderId) { await this.checkUserAccess(hashedUserId); const user = await User.findOne({ - hashedId: hashedUserId + where: { hashedId: hashedUserId } }); - const parentFolder = Folder.findOne({ - id: data.parentId, - userId: user.id - }); - if (!parentFolder) { - throw new Error('foldernotfound'); + if (!user) { + throw new Error('User not found'); } - const newFolder = await Folder.create({ - parentId: data.parentId, - userId: user.id, - name: data.name + const parentFolder = data.parentId ? await Folder.findOne({ + where: { id: data.parentId, userId: user.id } + }) : null; + if (data.parentId && !parentFolder) { + throw new Error('Parent folder not found'); + } + let newFolder; + if (folderId === 0) { + newFolder = await Folder.create({ + parentId: data.parentId || null, + userId: user.id, + name: data.name + }); + } else { + newFolder = await Folder.findOne({ + where: { id: folderId, userId: user.id } + }); + if (!newFolder) { + throw new Error('Folder not found or user does not own the folder'); + } + newFolder.name = data.name; + await newFolder.save(); + } + await FolderImageVisibility.destroy({ + where: { folderId: newFolder.id } }); for (const visibilityId of data.visibilities) { await FolderImageVisibility.create({ @@ -61,32 +85,63 @@ class SocialNetworkService extends BaseService { async getFolders(hashedId) { const userId = await this.checkUserAccess(hashedId); - let rootFolder = await Folder.findOne({ where: { parentId: null, userId } }); + let rootFolder = await Folder.findOne({ + where: { parentId: null, userId }, + include: [{ + model: ImageVisibilityType, + through: { model: FolderImageVisibility }, + attributes: ['id'], + }] + }); if (!rootFolder) { const user = await User.findOne({ where: { id: userId } }); const visibility = await ImageVisibilityType.findOne({ - where: { - description: 'everyone' - } + where: { description: 'everyone' } }); rootFolder = await Folder.create({ name: user.username, parentId: null, - userId, + userId + }); + await FolderImageVisibility.create({ + folderId: rootFolder.id, visibilityTypeId: visibility.id }); + rootFolder = await Folder.findOne({ + where: { id: rootFolder.id }, + include: [{ + model: ImageVisibilityType, + through: { model: FolderImageVisibility }, + attributes: ['id'], + }] + }); } const children = await this.getSubFolders(rootFolder.id, userId); - rootFolder = rootFolder.get(); + rootFolder = rootFolder.get(); + rootFolder.visibilityTypeIds = rootFolder.image_visibility_types.map(v => v.id); + delete rootFolder.image_visibility_types; rootFolder.children = children; return rootFolder; } async getSubFolders(parentId, userId) { - const folders = await Folder.findAll({ where: { parentId, userId } }); + const folders = await Folder.findAll({ + where: { parentId, userId }, + include: [{ + model: ImageVisibilityType, + through: { model: FolderImageVisibility }, + attributes: ['id'], + }], + order: [ + ['name', 'asc'] + ] + }); for (const folder of folders) { const children = await this.getSubFolders(folder.id, userId); + const visibilityTypeIds = folder.image_visibility_types.map(v => v.id); + folder.setDataValue('visibilityTypeIds', visibilityTypeIds); folder.setDataValue('children', children); + folder.setDataValue('image_visibility_types', undefined); } return folders.map(folder => folder.get()); } @@ -112,32 +167,56 @@ class SocialNetworkService extends BaseService { async uploadImage(hashedId, file, formData) { const userId = await this.getUserId(hashedId); - const newFileName = this.generateUniqueFileName(file.originalname); - const filePath = this.buildFilePath(newFileName); - await this.saveFile(file.buffer, filePath); - const newImage = await this.createImageRecord(formData, userId, file, newFileName); + const processedImageName = await this.processAndUploadUserImage(file); + const newImage = await this.createImageRecord(formData, userId, file, processedImageName); await this.saveImageVisibilities(newImage.id, formData.visibility); return newImage; } + async processAndUploadUserImage(file) { + try { + const img = sharp(file.buffer); + const metadata = await img.metadata(); + if (!metadata || !['jpeg', 'png', 'webp', 'gif'].includes(metadata.format)) { + throw new Error('File is not a valid image'); + } + if (metadata.width < 75 || metadata.height < 75) { + throw new Error('Image dimensions are too small. Minimum size is 75x75 pixels.'); + } + const resizedImg = img.resize({ + width: Math.min(metadata.width, 500), + height: Math.min(metadata.height, 500), + fit: sharp.fit.inside, + withoutEnlargement: true + }); + const newFileName = this.generateUniqueFileName(file.originalname); + const filePath = this.buildFilePath(newFileName, 'user'); + await resizedImg.toFile(filePath); + return newFileName; + } catch (error) { + throw new Error(`Failed to process image: ${error.message}`); + } + } + + async getUserId(hashedId) { return await this.checkUserAccess(hashedId); } generateUniqueFileName(originalFileName) { const uniqueHash = uuidv4(); - return `${uniqueHash}`; + return uniqueHash; } - buildFilePath(fileName) { - const userImagesPath = path.join(__dirname, '../images/user'); - return path.join(userImagesPath, fileName); + buildFilePath(fileName, type) { + const basePath = path.join(__dirname, '..', 'images', type); + return path.join(basePath, fileName); } async saveFile(buffer, filePath) { try { - await fs.mkdir(path.dirname(filePath), { recursive: true }); - await fs.writeFile(filePath, buffer); + await fsPromises.mkdir(path.dirname(filePath), { recursive: true }); + await fsPromises.writeFile(filePath, buffer); } catch (error) { throw new Error(`Failed to save file: ${error.message}`); } @@ -273,9 +352,15 @@ class SocialNetworkService extends BaseService { }); } - async constructUserProfile(user, requestingUserId) { + async constructUserProfile(user, hashedUserId) { const userParams = {}; - const requestingUserAge = await this.getUserAge(requestingUserId); + const requestingUser = await User.findOne({ + where: { hashedId: hashedUserId }, + }); + if (!requestingUser) { + throw new Error('User not found'); + } + const requestingUserAge = await this.getUserAge(requestingUser.id); for (const param of user.user_params) { const visibility = param.param_visibilities?.[0]?.visibility_type?.description || 'Invisible'; if (visibility === 'Invisible') continue; @@ -340,9 +425,9 @@ class SocialNetworkService extends BaseService { if (!hasAccess) { throw new Error('Access denied'); } - const imagePath = this.buildFilePath(image.hash); + const imagePath = this.buildFilePath(image.hash, 'user'); if (!fs.existsSync(imagePath)) { - throw new Error('File not found'); + throw new Error(`File "${imagePath}" not found`); } return imagePath; } @@ -350,7 +435,7 @@ class SocialNetworkService extends BaseService { async checkUserImageAccess(userId, imageId) { const image = await Image.findByPk(imageId); if (image.userId === userId) { - return true; + return true; } const accessRules = await ImageImageVisibility.findAll({ where: { imageId } @@ -374,6 +459,258 @@ class SocialNetworkService extends BaseService { } return image.folderId; } + + async getFoldersByUsername(username, hashedUserId) { + const user = await User.findOne({ where: { username } }); + if (!user) { + throw new Error('User not found'); + } + const requestingUserId = await this.checkUserAccess(hashedUserId); + let rootFolder = await Folder.findOne({ where: { parentId: null, userId: user.id } }); + if (!rootFolder) { + return null; + } + const accessibleFolders = await this.getAccessibleFolders(rootFolder.id, requestingUserId); + rootFolder = rootFolder.get(); + rootFolder.children = accessibleFolders; + return rootFolder; + } + + async getAccessibleFolders(folderId, requestingUserId) { + const folderIdString = String(folderId); + const requestingUserIdString = String(requestingUserId); + const requestingUser = await User.findOne({ where: { id: requestingUserIdString } }); + const isAdult = this.isUserAdult(requestingUser.id); + const accessibleFolders = await Folder.findAll({ + where: { parentId: folderIdString }, + include: [ + { + model: ImageVisibilityType, + through: { model: FolderImageVisibility }, + where: { + [Op.or]: [ + { description: 'everyone' }, + { description: 'adults', ...(isAdult ? {} : { [Op.not]: null }) }, + { description: 'friends-and-adults', ...(isAdult ? {} : { [Op.not]: null }) } + ] + }, + required: false + }, + { + model: ImageVisibilityUser, + through: { model: FolderVisibilityUser }, + where: { user_id: requestingUserIdString }, + required: false + } + ] + }); + const folderList = []; + for (let folder of accessibleFolders) { + const children = await this.getAccessibleFolders(folder.id, requestingUserIdString); + folder = folder.get(); + folder.children = children; + folderList.push(folder); + } + return folderList; + } + + async deleteFolder(hashedUserId, folderId) { + const userId = await this.checkUserAccess(hashedUserId); + const folder = await Folder.findOne({ + where: { id: folderId, userId } + }); + if (!folder) { + throw new Error('Folder not found or access denied'); + } + await FolderImageVisibility.destroy({ where: { folderId: folder.id } }); + await folder.destroy(); + return true; + } + + async createGuestbookEntry(hashedSenderId, recipientName, htmlContent, image) { + const sender = await User.findOne({ where: { hashedId: hashedSenderId } }); + if (!sender) { + throw new Error('Sender not found'); + } + const recipient = await User.findOne({ where: { username: recipientName } }); + if (!recipient) { + throw new Error('Recipient not found'); + } + const sanitizedContent = this.sanitizeHtml(htmlContent); + let uploadedImage = null; + if (image) { + uploadedImage = await this.processAndUploadGuestbookImage(image); + } + const entry = await GuestbookEntry.create({ + senderId: sender.id, + recipientId: recipient.id, + senderUsername: sender.username, + contentHtml: sanitizedContent, + imageUrl: uploadedImage + }); + + return entry; + } + + sanitizeHtml(htmlContent) { + const window = new JSDOM('').window; + const purify = DOMPurify(window); + const cleanHtml = purify.sanitize(htmlContent, { + ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'ol', 'li', 'br'], + ALLOWED_ATTR: ['href', 'title'], + FORBID_TAGS: ['script', 'style', 'iframe', 'img'], + ALLOWED_URI_REGEXP: /^https?:\/\//, + }); + + return cleanHtml; + } + + async processAndUploadGuestbookImage(image) { + try { + const img = sharp(image.buffer); + const metadata = await img.metadata(); + if (!metadata || !['jpeg', 'png', 'webp', 'gif'].includes(metadata.format)) { + throw new Error('File is not a valid image'); + } + if (metadata.width < 20 || metadata.height < 20) { + throw new Error('Image dimensions are too small. Minimum size is 20x20 pixels.'); + } + const resizedImg = img.resize({ + width: Math.min(metadata.width, 500), + height: Math.min(metadata.height, 500), + fit: sharp.fit.inside, + withoutEnlargement: true + }); + const newFileName = this.generateUniqueFileName(image.originalname); + const filePath = this.buildFilePath(newFileName, 'guestbook'); + await resizedImg.toFile(filePath); + return newFileName; + } catch (error) { + throw new Error(`Failed to process image: ${error.message}`); + } + } + + async getGuestbookEntries(hashedUserId, username, page = 1) { + const pageSize = 20; + const offset = (page - 1) * pageSize; + this.checkUserAccess(hashedUserId); + const user = await User.findOne({ where: { username: username } }); + if (!user) { + throw new Error('User not found'); + } + const entries = await GuestbookEntry.findAndCountAll({ + where: { recipientId: user.id }, + include: [ + { model: User, as: 'sender', attributes: ['username'] }, + ], + limit: pageSize, + offset: offset, + order: [['createdAt', 'DESC']] + }); + const resultList = entries.rows.map(entry => ({ + id: entry.id, + sender: entry.sender ? entry.sender.username : entry.senderUsername, + contentHtml: entry.contentHtml, + withImage: entry.imageUrl !== null, + createdAt: entry.createdAt + })); + return { + entries: resultList, + currentPage: page, + totalPages: Math.ceil(entries.count / pageSize), + }; + } + + async getGuestbookImageFilePath(hashedUserId, guestbookOwnerName, entryId) { + await this.checkUserAccess(hashedUserId); + + const guestbookOwner = await User.findOne({ + where: { username: guestbookOwnerName }, + }); + if (!guestbookOwner) { + throw new Error('usernotfound'); + } + const entry = await GuestbookEntry.findOne({ + where: { id: entryId, recipientId: guestbookOwner.id }, + }); + if (!entry) { + throw new Error('entrynotfound'); + } + if (!entry.imageUrl) { + console.log(entry); + throw new Error('entryhasnoimage'); + } + console.log(`Image URL: ${entry.imageUrl}`); + const imagePath = this.buildFilePath(entry.imageUrl, 'guestbook'); + if (!fs.existsSync(imagePath)) { + throw new Error(imagenotfound); + } + return imagePath; + } + + async deleteGuestbookEntry(hashedUserId, entryId) { + const user = await User.findOne({ where: { hashedId: hashedUserId } }); + if (!user) { + throw new Error('User not found'); + } + const entry = await GuestbookEntry.findOne({ where: { id: entryId } }); + if (!entry) { + throw new Error('Entry not found'); + } + if (entry.senderId !== user.id && entry.recipientId !== user.id) { + throw new Error('Not authorized to delete this entry'); + } + await entry.destroy(); + } + + async createDiaryEntry(userId, text) { + const newEntry = await Diary.create({ + userId: userId, + text: text, + createdAt: new Date(), + updatedAt: new Date(), + }); + return newEntry; + } + + async updateDiaryEntry(diaryId, userId, newText) { + const existingEntry = await Diary.findOne({ + where: { id: diaryId, userId: userId } + }); + if (!existingEntry) { + throw new Error('Diary entry not found or unauthorized access'); + } + await DiaryHistory.create({ + diaryId: existingEntry.id, + userId: existingEntry.userId, + oldText: existingEntry.text, + oldCreatedAt: existingEntry.createdAt, + oldUpdatedAt: existingEntry.updatedAt, + }); + existingEntry.text = newText; + existingEntry.updatedAt = new Date(); + await existingEntry.save(); + return existingEntry; + } + + async deleteDiaryEntry(diaryId, userId) { + const entryToDelete = await Diary.findOne({ + where: { id: diaryId, userId: userId } + }); + if (!entryToDelete) { + throw new Error('Diary entry not found or unauthorized access'); + } + await entryToDelete.destroy(); + return true; + } + + async getDiaryEntries(userId) { + const entries = await Diary.findAll({ + where: { userId: userId }, + order: [['createdAt', 'DESC']] + }); + return entries; + } } -export default SocialNetworkService; +export default SocialNetworkService; \ No newline at end of file diff --git a/backend/utils/sequelize.js b/backend/utils/sequelize.js index 4df21eb..ca0676a 100644 --- a/backend/utils/sequelize.js +++ b/backend/utils/sequelize.js @@ -30,4 +30,4 @@ const syncModels = async (models) => { } }; -export { sequelize, initializeDatabase }; +export { sequelize, initializeDatabase, syncModels }; diff --git a/backend/utils/syncDatabase.js b/backend/utils/syncDatabase.js index e03b681..d1d09e8 100644 --- a/backend/utils/syncDatabase.js +++ b/backend/utils/syncDatabase.js @@ -1,4 +1,4 @@ -import { initializeDatabase } from './sequelize.js'; +import { initializeDatabase, syncModels } from './sequelize.js'; import initializeTypes from './initializeTypes.js'; import initializeSettings from './initializeSettings.js'; import initializeUserRights from './initializeUserRights.js'; @@ -10,10 +10,8 @@ import { createTriggers } from '../models/trigger.js'; const syncDatabase = async () => { try { await initializeDatabase(); + await syncModels(models); setupAssociations(); - for (const model of Object.values(models)) { - await model.sync(); - } createTriggers(); await initializeSettings(); diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b76aabd..2c62767 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -4,7 +4,17 @@ - + + + + + + + + + + + @@ -254,4 +261,7 @@ export default { color: #7E471B; border: 1px solid #7E471B; } +.is-active { + z-index: 990; +} diff --git a/frontend/src/components/FolderItem.vue b/frontend/src/components/FolderItem.vue index 2027e17..bbc11d6 100644 --- a/frontend/src/components/FolderItem.vue +++ b/frontend/src/components/FolderItem.vue @@ -1,20 +1,25 @@ @@ -26,14 +25,12 @@ import { mapActions } from 'vuex'; import apiClient from '@/utils/axios.js'; import DialogWidget from '@/components/DialogWidget.vue'; -import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue'; import SelectDropdownWidget from '@/components/form/SelectDropdownWidget.vue'; export default { name: 'RegisterDialog', components: { DialogWidget, - ErrorDialog, SelectDropdownWidget, }, data() { @@ -85,7 +82,7 @@ export default { }, async register() { if (!this.canRegister) { - this.$refs.errorDialog.open('tr:register.passwordMismatch'); + this.$root.$refs.errrorDialog.open('tr:register.passwordMismatch'); return; } @@ -102,14 +99,14 @@ export default { this.$refs.dialog.close(); this.$router.push('/activate'); } else { - this.$refs.errorDialog.open("tr:register.failure"); + this.$root.$refs.errrorDialog.open("tr:register.failure"); } } catch (error) { if (error.response && error.response.status === 409) { - this.$refs.errorDialog.open('tr:register.' + error.response.data.error); + this.$root.$refs.errrorDialog.open('tr:register.' + error.response.data.error); } else { console.error('Error registering user:', error); - this.$refs.errorDialog.open('tr:register.' + error.response.data.error); + this.$root.$refs.errrorDialog.open('tr:register.' + error.response.data.error); } } }, diff --git a/frontend/src/dialogues/socialnetwork/CreateFolderDialog.vue b/frontend/src/dialogues/socialnetwork/CreateFolderDialog.vue index ef01bf5..7f0adbe 100644 --- a/frontend/src/dialogues/socialnetwork/CreateFolderDialog.vue +++ b/frontend/src/dialogues/socialnetwork/CreateFolderDialog.vue @@ -5,36 +5,38 @@
+
+
- + + + + +
+ + + diff --git a/frontend/src/dialogues/socialnetwork/UserProfileDialog.vue b/frontend/src/dialogues/socialnetwork/UserProfileDialog.vue index 583cfa1..53d3e60 100644 --- a/frontend/src/dialogues/socialnetwork/UserProfileDialog.vue +++ b/frontend/src/dialogues/socialnetwork/UserProfileDialog.vue @@ -1,7 +1,8 @@ @@ -300,4 +327,21 @@ export default { object-fit: contain; cursor: pointer; } + +.icon { + cursor: pointer; + margin-left: 10px; +} + +.edit-icon { + color: green; +} + +.delete-icon { + color: red; +} + +.tree { + padding: 0; +} diff --git a/frontend/src/views/social/GuestbookView.vue b/frontend/src/views/social/GuestbookView.vue new file mode 100644 index 0000000..2761698 --- /dev/null +++ b/frontend/src/views/social/GuestbookView.vue @@ -0,0 +1,93 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/social/SearchView.vue b/frontend/src/views/social/SearchView.vue index 3a595c3..80d9c36 100644 --- a/frontend/src/views/social/SearchView.vue +++ b/frontend/src/views/social/SearchView.vue @@ -52,19 +52,16 @@ {{ $t('socialnetwork.usersearch.no_results') }} -