From 7ab6939863f09555fbdced387f4ddc26f6df0e8a Mon Sep 17 00:00:00 2001 From: Torsten Schulz Date: Sun, 22 Sep 2024 01:26:59 +0200 Subject: [PATCH] first initialization gallery --- .../controllers/socialnetworkController.js | 30 ++- backend/models/associations.js | 38 +++ backend/models/community/folder.js | 8 - .../community/folder_image_visibility.js | 37 +++ .../community/folder_visibility_user.js | 34 +++ backend/models/community/image.js | 8 - .../community/image_image_visibility.js | 37 +++ .../models/community/image_visibility_user.js | 26 ++ backend/models/index.js | 12 +- backend/models/type/image_visibility.js | 22 ++ backend/routers/socialnetworkRouter.js | 4 +- backend/services/socialnetworkService.js | 93 ++++++- backend/utils/initializeImageTypes.js | 19 ++ backend/utils/syncDatabase.js | 2 + frontend/public/images/icons/folder.png | Bin 2448 -> 8613 bytes frontend/public/images/icons/folder16.png | Bin 0 -> 4981 bytes frontend/public/images/icons/folder24.png | Bin 0 -> 5828 bytes frontend/src/components/FolderItem.vue | 52 ++++ .../src/components/form/MultiselectWidget.vue | 4 +- .../socialnetwork/CreateFolderDialog.vue | 137 +++++++++++ .../src/i18n/locales/de/socialnetwork.json | 29 +++ frontend/src/router/index.js | 7 + frontend/src/views/social/GalleryView.vue | 227 ++++++++++++++++++ 23 files changed, 792 insertions(+), 34 deletions(-) create mode 100644 backend/models/community/folder_image_visibility.js create mode 100644 backend/models/community/folder_visibility_user.js create mode 100644 backend/models/community/image_image_visibility.js create mode 100644 backend/models/community/image_visibility_user.js create mode 100644 backend/models/type/image_visibility.js create mode 100644 backend/utils/initializeImageTypes.js create mode 100644 frontend/public/images/icons/folder16.png create mode 100644 frontend/public/images/icons/folder24.png create mode 100644 frontend/src/components/FolderItem.vue create mode 100644 frontend/src/dialogues/socialnetwork/CreateFolderDialog.vue create mode 100644 frontend/src/views/social/GalleryView.vue diff --git a/backend/controllers/socialnetworkController.js b/backend/controllers/socialnetworkController.js index 223ba72..4bc1d8e 100644 --- a/backend/controllers/socialnetworkController.js +++ b/backend/controllers/socialnetworkController.js @@ -9,6 +9,8 @@ class SocialNetworkController { this.getFolders = this.getFolders.bind(this); this.uploadImage = this.uploadImage.bind(this); this.getImage = this.getImage.bind(this); + this.getImageVisibilityTypes = this.getImageVisibilityTypes.bind(this); + this.getFolderImageList = this.getFolderImageList.bind(this); } async userSearch(req, res) { @@ -39,8 +41,9 @@ class SocialNetworkController { async createFolder(req, res) { try { + const userId = req.headers.userid; const folderData = req.body; - const folder = await this.socialNetworkService.createFolder(folderData); + const folder = await this.socialNetworkService.createFolder(userId, folderData); res.status(201).json(folder); } catch (error) { console.error('Error in createFolder:', error); @@ -59,10 +62,23 @@ class SocialNetworkController { } } + async getFolderImageList(req, res) { + try { + const userId = req.headers.userid; + const { folderId } = req.params; + const images = await this.socialNetworkService.getFolderImageList(userId, folderId); + res.status(200).json(images); + } catch (error) { + console.error('Error in getFolderImageList:', error); + res.status(500).json({ error: error.message }); + } + } + async uploadImage(req, res) { try { + const userId = req.headers.userid; const imageData = req.body; - const image = await this.socialNetworkService.uploadImage(imageData); + const image = await this.socialNetworkService.uploadImage(userId, imageData); res.status(201).json(image); } catch (error) { console.error('Error in uploadImage:', error); @@ -80,6 +96,16 @@ class SocialNetworkController { res.status(500).json({ error: error.message }); } } + + async getImageVisibilityTypes(req, res) { + try { + const types = await this.socialNetworkService.getPossibleImageVisibilities(); + res.status(200).json(types); + } catch (error) { + console.log(error); + res.status(500).json({ error: error.message }); + } + } } export default SocialNetworkController; diff --git a/backend/models/associations.js b/backend/models/associations.js index e0b9cbe..5a94d26 100644 --- a/backend/models/associations.js +++ b/backend/models/associations.js @@ -12,6 +12,11 @@ import UserParamVisibilityType from './type/user_param_visibility.js'; import UserParamVisibility from './community/user_param_visibility.js'; import Folder from './community/folder.js'; import Image from './community/image.js'; +import ImageVisibilityType from './type/image_visibility.js'; +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'; export default function setupAssociations() { SettingsType.hasMany(UserParamType, { foreignKey: 'settingsId', as: 'user_param_types' }); @@ -57,4 +62,37 @@ export default function setupAssociations() { Image.belongsTo(User, { foreignKey: 'userId' }); User.hasMany(Image, { foreignKey: 'userId' }); + + Folder.belongsToMany(ImageVisibilityType, { + through: FolderImageVisibility, + foreignKey: 'folderId', + otherKey: 'visibilityTypeId' + }); + ImageVisibilityType.belongsToMany(Folder, { + through: FolderImageVisibility, + foreignKey: 'visibilityTypeId', + otherKey: 'folderId' + }); + + Image.belongsToMany(ImageVisibilityType, { + through: ImageImageVisibility, + foreignKey: 'imageId', + otherKey: 'visibilityTypeId' + }); + ImageVisibilityType.belongsToMany(Image, { + through: ImageImageVisibility, + foreignKey: 'visibilityTypeId', + otherKey: 'imageId' + }); + + Folder.belongsToMany(ImageVisibilityUser, { + through: FolderVisibilityUser, + foreignKey: 'folderId', + otherKey: 'visibilityUserId' + }); + ImageVisibilityUser.belongsToMany(Folder, { + through: FolderVisibilityUser, + foreignKey: 'visibilityUserId', + otherKey: 'folderId' + }); } diff --git a/backend/models/community/folder.js b/backend/models/community/folder.js index 798ab5b..202de60 100644 --- a/backend/models/community/folder.js +++ b/backend/models/community/folder.js @@ -23,14 +23,6 @@ const Folder = sequelize.define('folder', { key: 'id', }, }, - visibilityType: { - type: DataTypes.INTEGER, - allowNull: false, - references: { - model: UserParamVisibilityType, - key: 'id', - }, - }, }, { tableName: 'folder', schema: 'community', diff --git a/backend/models/community/folder_image_visibility.js b/backend/models/community/folder_image_visibility.js new file mode 100644 index 0000000..0ade275 --- /dev/null +++ b/backend/models/community/folder_image_visibility.js @@ -0,0 +1,37 @@ +import { sequelize } from '../../utils/sequelize.js'; +import { DataTypes } from 'sequelize'; + +const FolderImageVisibility = sequelize.define('folder_image_visibility', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false + }, + folderId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'folder', + key: 'id' + } + }, + visibilityTypeId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: { + schema: 'type', + tableName: 'image_visibility_type' + }, + key: 'id' + } + } +}, { + tableName: 'folder_image_visibility', + schema: 'community', + timestamps: false, + underscored: true, +}); + +export default FolderImageVisibility; diff --git a/backend/models/community/folder_visibility_user.js b/backend/models/community/folder_visibility_user.js new file mode 100644 index 0000000..82d0561 --- /dev/null +++ b/backend/models/community/folder_visibility_user.js @@ -0,0 +1,34 @@ +import { sequelize } from '../../utils/sequelize.js'; +import { DataTypes } from 'sequelize'; + +const FolderVisibilityUser = sequelize.define('folder_visibility_user', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false + }, + folderId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'folder', + key: 'id' + } + }, + visibilityUserId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'image_visibility_user', + key: 'id' + } + } +}, { + tableName: 'folder_visibility_user', + schema: 'community', + timestamps: false, + underscored: true, +}); + +export default FolderVisibilityUser; diff --git a/backend/models/community/image.js b/backend/models/community/image.js index 879486a..2a768bf 100644 --- a/backend/models/community/image.js +++ b/backend/models/community/image.js @@ -36,14 +36,6 @@ const Image = sequelize.define('image', { key: 'id', }, }, - visibilityType: { - type: DataTypes.INTEGER, - allowNull: false, - references: { - model: UserParamVisibilityType, - key: 'id', - }, - }, }, { tableName: 'image', schema: 'community', diff --git a/backend/models/community/image_image_visibility.js b/backend/models/community/image_image_visibility.js new file mode 100644 index 0000000..ac22561 --- /dev/null +++ b/backend/models/community/image_image_visibility.js @@ -0,0 +1,37 @@ +import { sequelize } from '../../utils/sequelize.js'; +import { DataTypes } from 'sequelize'; + +const ImageImageVisibility = sequelize.define('image_image_visibility', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false + }, + imageId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'image', + key: 'id' + } + }, + visibilityTypeId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: { + schema: 'type', + tableName: 'image_visibility_type' + }, + key: 'id' + } + } +}, { + tableName: 'image_image_visibility', + schema: 'community', + timestamps: false, + underscored: true, +}); + +export default ImageImageVisibility; diff --git a/backend/models/community/image_visibility_user.js b/backend/models/community/image_visibility_user.js new file mode 100644 index 0000000..58a2564 --- /dev/null +++ b/backend/models/community/image_visibility_user.js @@ -0,0 +1,26 @@ +import { sequelize } from '../../utils/sequelize.js'; +import { DataTypes } from 'sequelize'; + +const ImageVisibilityUser = sequelize.define('image_visibility_user', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false + }, + image_id: { + type: DataTypes.INTEGER, + allowNull: false + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false + } +}, { + tableName: 'image_visibility_user', + timestamps: false, + underscored: true, + schema: 'community' +}); + +export default ImageVisibilityUser; diff --git a/backend/models/index.js b/backend/models/index.js index fc270b3..0e2d576 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -14,6 +14,11 @@ import UserParamVisibilityType from './type/user_param_visibility.js'; import UserParamVisibility from './community/user_param_visibility.js'; import Folder from './community/folder.js'; import Image from './community/image.js'; +import ImageVisibilityType from './type/image_visibility.js'; +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'; const models = { SettingsType, @@ -25,13 +30,18 @@ const models = { Login, UserRight, InterestType, - InterestTranslationType, + InterestTranslationType, Interest, ContactMessage, UserParamVisibilityType, UserParamVisibility, Folder, Image, + ImageVisibilityType, + ImageVisibilityUser, + FolderImageVisibility, + ImageImageVisibility, + FolderVisibilityUser, }; export default models; diff --git a/backend/models/type/image_visibility.js b/backend/models/type/image_visibility.js new file mode 100644 index 0000000..c34d526 --- /dev/null +++ b/backend/models/type/image_visibility.js @@ -0,0 +1,22 @@ +import { sequelize } from '../../utils/sequelize.js'; +import { DataTypes } from 'sequelize'; + +const ImageVisibilityType = sequelize.define('image_visibility_type', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false + }, + description: { + type: DataTypes.STRING, + allowNull: false + } +}, { + tableName: 'image_visibility_type', + schema: 'type', + timestamps: false, + underscored: true, +}); + +export default ImageVisibilityType; diff --git a/backend/routers/socialnetworkRouter.js b/backend/routers/socialnetworkRouter.js index f7fe174..6bb5e71 100644 --- a/backend/routers/socialnetworkRouter.js +++ b/backend/routers/socialnetworkRouter.js @@ -5,13 +5,13 @@ import SocialNetworkController from '../controllers/socialnetworkController.js'; const router = express.Router(); const socialNetworkController = new SocialNetworkController(); -router.post('/usersearch', authenticate, socialNetworkController.userSearch); -router.get('/profile/:userId', authenticate, socialNetworkController.profile); router.post('/usersearch', authenticate, socialNetworkController.userSearch); router.get('/profile/:userId', authenticate, socialNetworkController.profile); router.post('/folders', authenticate, socialNetworkController.createFolder); router.get('/folders', authenticate, socialNetworkController.getFolders); +router.get('/folder/:folderId', authenticate, socialNetworkController.getFolderImageList); router.post('/images', authenticate, socialNetworkController.uploadImage); router.get('/images/:imageId', authenticate, socialNetworkController.getImage); +router.get('/imagevisibilities', authenticate, socialNetworkController.getImageVisibilityTypes); export default router; diff --git a/backend/services/socialnetworkService.js b/backend/services/socialnetworkService.js index 73b52c7..52b2a49 100644 --- a/backend/services/socialnetworkService.js +++ b/backend/services/socialnetworkService.js @@ -8,6 +8,8 @@ import UserParamVisibility from '../models/community/user_param_visibility.js'; import UserParamVisibilityType from '../models/type/user_param_visibility.js'; import Folder from '../models/community/folder.js'; import Image from '../models/community/image.js'; +import ImageVisibilityType from '../models/type/image_visibility.js'; +import FolderImageVisibility from '../models/community/folder_image_visibility.js'; class SocialNetworkService extends BaseService { async searchUsers({ username, ageFrom, ageTo, genders }) { @@ -23,20 +25,84 @@ class SocialNetworkService extends BaseService { return this.constructUserProfile(user, requestingUserId); } - async createFolder(data) { - this.validateFolderData(data); - await this.checkUserAccess(data.userId); - return await Folder.create(data); + async createFolder(hashedUserId, data) { + await this.checkUserAccess(hashedUserId); + const user = await User.findOne({ + hashedId: hashedUserId + }); + const parentFolder = Folder.findOne({ + id: data.parentId, + userId: user.id + }); + if (!parentFolder) { + throw new Error('foldernotfound'); + } + const newFolder = await Folder.create({ + parentId: data.parentId, + userId: user.id, + name: data.name + }); + for (const visibilityId of data.visibilities) { + await FolderImageVisibility.create({ + folderId: newFolder.id, + visibilityTypeId: visibilityId + }); + } + return newFolder; } - async getFolders(userId) { - await this.checkUserAccess(userId); - return await Folder.findAll({ where: { userId } }); + async getFolders(hashedId) { + const userId = await this.checkUserAccess(hashedId); + let rootFolder = await Folder.findOne({ where: { parentId: null, userId } }); + if (!rootFolder) { + const user = await User.findOne({ where: { id: userId } }); + const visibility = await ImageVisibilityType.findOne({ + where: { + description: 'everyone' + } + }); + rootFolder = await Folder.create({ + name: user.username, + parentId: null, + userId, + visibilityTypeId: visibility.id + }); + } + const children = await this.getSubFolders(rootFolder.id, userId); + rootFolder = rootFolder.get(); + rootFolder.children = children; + return rootFolder; } - async uploadImage(imageData) { + async getSubFolders(parentId, userId) { + const folders = await Folder.findAll({ where: { parentId, userId } }); + for (const folder of folders) { + const children = await this.getSubFolders(folder.id, userId); + folder.setDataValue('children', children); + } + return folders.map(folder => folder.get()); + } + + async getFolderImageList(hashedId, folderId) { + const userId = await this.checkUserAccess(hashedId); + const folder = await Folder.findOne({ + where: { + id: folderId, + userId + } + }); + if (!folder) throw new Error('Folder not found'); + return await Image.findAll({ + where: { + folderId: folder.id + } + }); + } + + async uploadImage(hashedId, imageData) { this.validateImageData(imageData); - await this.checkUserAccess(imageData.userId); + const userId = await this.checkUserAccess(hashedId); + imageData.id = userId; return await Image.create(imageData); } @@ -47,9 +113,10 @@ class SocialNetworkService extends BaseService { return image; } - async checkUserAccess(userId) { - const user = await User.findByPk(userId); + async checkUserAccess(hashedId) { + const user = await User.findOne({ hashedId: hashedId }); if (!user || !user.active) throw new Error('Access denied: User not found or inactive'); + return user.id; } validateFolderData(data) { @@ -184,6 +251,10 @@ class SocialNetworkService extends BaseService { }); return userParamValue ? userParamValue.value : value; } + + async getPossibleImageVisibilities() { + return await ImageVisibilityType.findAll(); + } } export default SocialNetworkService; diff --git a/backend/utils/initializeImageTypes.js b/backend/utils/initializeImageTypes.js new file mode 100644 index 0000000..dc85ac6 --- /dev/null +++ b/backend/utils/initializeImageTypes.js @@ -0,0 +1,19 @@ +import ImageVisibilityType from '../models/type/image_visibility.js'; + +const initializeImageTypes = async () => { + const visibilities = [ + 'everyone', + 'friends', + 'adults', + 'friends-and-adults', + 'selected-users', + ]; + for (const visibility of visibilities) { + await ImageVisibilityType.findOrCreate({ + where: { description: visibility }, + defaults: { description: visibility } + }); + } +} + +export default initializeImageTypes; diff --git a/backend/utils/syncDatabase.js b/backend/utils/syncDatabase.js index 0f14f1d..e03b681 100644 --- a/backend/utils/syncDatabase.js +++ b/backend/utils/syncDatabase.js @@ -2,6 +2,7 @@ import { initializeDatabase } from './sequelize.js'; import initializeTypes from './initializeTypes.js'; import initializeSettings from './initializeSettings.js'; import initializeUserRights from './initializeUserRights.js'; +import initializeImageTypes from './initializeImageTypes.js'; import setupAssociations from '../models/associations.js'; import models from '../models/index.js'; import { createTriggers } from '../models/trigger.js'; @@ -18,6 +19,7 @@ const syncDatabase = async () => { await initializeSettings(); await initializeTypes(); await initializeUserRights(); + await initializeImageTypes(); } catch (error) { console.error('Unable to synchronize the database:', error); } diff --git a/frontend/public/images/icons/folder.png b/frontend/public/images/icons/folder.png index 9f8c935144ffe572ebe995b2e0f7a0c26da97035..ed0114a6f1dcf8b30741d9146131c77ac4d4a4fb 100644 GIT binary patch literal 8613 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 literal 0 HcmV?d00001 diff --git a/frontend/src/components/FolderItem.vue b/frontend/src/components/FolderItem.vue new file mode 100644 index 0000000..b1d54e1 --- /dev/null +++ b/frontend/src/components/FolderItem.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/frontend/src/components/form/MultiselectWidget.vue b/frontend/src/components/form/MultiselectWidget.vue index f8f5bdf..13f0d29 100644 --- a/frontend/src/components/form/MultiselectWidget.vue +++ b/frontend/src/components/form/MultiselectWidget.vue @@ -12,10 +12,10 @@ :track-by="'value'" >