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 @@
{{ image.title }}
+{{ message }}
+