diff --git a/backend/controllers/socialnetworkController.js b/backend/controllers/socialnetworkController.js index 0f79d4c..29ca110 100644 --- a/backend/controllers/socialnetworkController.js +++ b/backend/controllers/socialnetworkController.js @@ -23,6 +23,9 @@ class SocialNetworkController { this.updateDiaryEntry = this.updateDiaryEntry.bind(this); this.deleteDiaryEntry = this.deleteDiaryEntry.bind(this); this.getDiaryEntries = this.getDiaryEntries.bind(this); + this.addFriend = this.addFriend.bind(this); + this.removeFriend = this.removeFriend.bind(this); + this.acceptFriendship = this.acceptFriendship.bind(this); } async userSearch(req, res) { @@ -278,7 +281,7 @@ class SocialNetworkController { async getDiaryEntries(req, res) { try { - const { userid: userId} = req.headers; + const { userid: userId } = req.headers; const { page } = req.params; const entries = await this.socialNetworkService.getDiaryEntries(userId, page); res.status(200).json(entries); @@ -287,6 +290,42 @@ class SocialNetworkController { res.status(500).json({ error: error.message }); } } + + async addFriend(req, res) { + try { + const { userid: hashedUserid } = req.headers; + const { friendUserid } = req.body; + await this.socialNetworkService.addFriend(hashedUserid, friendUserid); + res.status(201).json({ message: 'added' }); + } catch (error) { + console.error('Error in addFriend:', error); + res.status(500).json({ error: error.message }); + } + } + + async removeFriend(req, res) { + try { + const { userid: hashedUserid } = req.headers; + const { friendUserid } = req.params; + await this.socialNetworkService.removeFriend(hashedUserid, friendUserid); + res.status(200).json({ message: 'removed' }); + } catch (error) { + console.error('Error in removeFriend:', error); + res.status(500).json({ error: error.message }); + } + } + + async acceptFriendship(req, res) { + try { + const { userid: hashedUserid } = req.headers; + const { friendUserid } = req.params; + await this.socialNetworkService.acceptFriendship(hashedUserid, friendUserid); + res.status(200).json({ message: 'accepted' }); + } catch (error) { + console.error('Error in acceptFriendship:', error); + res.status(500).json({ error: error.message }); + } + } } export default SocialNetworkController; diff --git a/backend/models/associations.js b/backend/models/associations.js index 5508905..edfc167 100644 --- a/backend/models/associations.js +++ b/backend/models/associations.js @@ -27,6 +27,7 @@ import TitleHistory from './forum/title_history.js'; import ForumPermission from './forum/forum_permission.js'; import ForumUserPermission from './forum/forum_user_permission.js'; import ForumForumPermission from './forum/forum_forum_permission.js'; +import Friendship from './community/friendship.js'; export default function setupAssociations() { // UserParam related associations @@ -156,4 +157,9 @@ export default function setupAssociations() { ForumPermission.hasMany(ForumUserPermission, { foreignKey: 'permissionId' }); ForumUserPermission.belongsTo(ForumPermission, { foreignKey: 'permissionId' }); + + Friendship.belongsTo(User, { foreignKey: 'user1Id', as: 'friendSender' }); + Friendship.belongsTo(User, { foreignKey: 'user2Id', as: 'friendReceiver' }); + User.hasMany(Friendship, { foreignKey: 'user1Id', as: 'friendSender' }); + User.hasMany(Friendship, { foreignKey: 'user2Id', as: 'friendReceiver' }); } diff --git a/backend/models/community/friendship.js b/backend/models/community/friendship.js new file mode 100644 index 0000000..50e50fa --- /dev/null +++ b/backend/models/community/friendship.js @@ -0,0 +1,37 @@ +import { sequelize } from '../../utils/sequelize.js'; +import { DataTypes } from 'sequelize'; + +const Friendship = sequelize.define('friendship', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + user1Id: { + type: DataTypes.INTEGER, + allowNull: false + }, + user2Id: { + type: DataTypes.INTEGER, + allowNull: false + }, + accepted: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + denied: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + withdrawn: { + type: DataTypes.BOOLEAN, + defaultValue: false + } +}, { + tableName: 'friendship', + schema: 'community', + underscored: true, + timestamps: true, +}); + +export default Friendship; diff --git a/backend/models/index.js b/backend/models/index.js index 68a4df4..ee27131 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -31,6 +31,7 @@ import Message from './forum/message.js'; import MessageHistory from './forum/message_history.js'; import MessageImage from './forum/message_image.js'; import ForumForumPermission from './forum/forum_forum_permission.js'; +import Friendship from './community/friendship.js'; const models = { SettingsType, @@ -66,6 +67,7 @@ const models = { Message, MessageHistory, MessageImage, + Friendship, }; export default models; diff --git a/backend/routers/socialnetworkRouter.js b/backend/routers/socialnetworkRouter.js index 017b35d..ba8d3de 100644 --- a/backend/routers/socialnetworkRouter.js +++ b/backend/routers/socialnetworkRouter.js @@ -7,25 +7,30 @@ const upload = multer(); const router = express.Router(); const socialNetworkController = new SocialNetworkController(); -router.post('/usersearch', authenticate, socialNetworkController.userSearch); -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); -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/:diaryEntryId', authenticate, socialNetworkController.updateDiaryEntry); -router.delete('/diary/:entryId', authenticate, socialNetworkController.deleteDiaryEntry); -router.get('/diary/:page', authenticate, socialNetworkController.getDiaryEntries); +router.use(authenticate); + +router.post('/usersearch', socialNetworkController.userSearch); +router.get('/profile/main/:userId', socialNetworkController.profile); +router.post('/folders/:folderId', socialNetworkController.createFolder); +router.get('/folders', socialNetworkController.getFolders); +router.get('/folder/:folderId', socialNetworkController.getFolderImageList); +router.post('/images', upload.single('image'), socialNetworkController.uploadImage); +router.get('/images/:imageId', socialNetworkController.getImage); +router.put('/images/:imageId', socialNetworkController.changeImage); +router.get('/imagevisibilities', socialNetworkController.getImageVisibilityTypes); +router.get('/image/:hash', socialNetworkController.getImageByHash); +router.get('/profile/images/folders/:username', socialNetworkController.getFoldersByUsername); +router.delete('/folders/:folderId', socialNetworkController.deleteFolder); +router.post('/guestbook/entries', upload.single('image'), socialNetworkController.createGuestbookEntry); +router.get('/guestbook/entries/:username/:page', socialNetworkController.getGuestbookEntries); +router.delete('/guestbook/entries/:entryId', socialNetworkController.deleteGuestbookEntry); +router.get('/guestbook/image/:guestbookUserName/:entryId', socialNetworkController.getGuestbookImage); +router.post('/diary', socialNetworkController.createDiaryEntry); +router.put('/diary/:diaryEntryId', socialNetworkController.updateDiaryEntry); +router.delete('/diary/:entryId', socialNetworkController.deleteDiaryEntry); +router.get('/diary/:page', socialNetworkController.getDiaryEntries); +router.post('/friend', socialNetworkController.addFriend); +router.delete('/friend/:friendUserId', socialNetworkController.removeFriend); +router.put('/friend/:friendUserId', socialNetworkController.acceptFriendship); export default router; diff --git a/backend/services/socialnetworkService.js b/backend/services/socialnetworkService.js index c795f75..d6d8827 100644 --- a/backend/services/socialnetworkService.js +++ b/backend/services/socialnetworkService.js @@ -23,6 +23,7 @@ import { JSDOM } from 'jsdom'; import DOMPurify from 'dompurify'; import sharp from 'sharp'; import Diary from '../models/community/diary.js'; +import Friendship from '../models/community/friendship.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -347,7 +348,15 @@ class SocialNetworkService extends BaseService { { model: UserParamType, as: 'paramType' }, { model: UserParamVisibility, as: 'param_visibilities', include: [{ model: UserParamVisibilityType, as: 'visibility_type' }] } ], - order: [['order_id', 'asc']] + order: [['order_id', 'asc']], + }, + { + model: Friendship, + as: 'friendSender', + }, + { + model: Friendship, + as: 'friendReceiver', } ] }); @@ -370,9 +379,26 @@ class SocialNetworkService extends BaseService { }; } } + let friendship = null; + if (user.friendSender && user.friendSender.length > 0) { + friendship = { + isSender: true, + accepted: user.friendSender[0].dataValues.accepted, + denied: user.friendSender[0].dataValues.denied, + withdrawn: user.friendSender[0].dataValues.withdrawn, + } + } else if (user.friendReceiver && user.friendReceiver.length > 0) { + friendship = { + isSender: false, + accepted: user.friendReceiver[0].dataValues.accepted, + denied: user.friendReceiver[0].dataValues.denied, + withdrawn: user.friendReceiver[0].dataValues.withdrawn, + } + } return { username: user.username, registrationDate: user.registrationDate, + friendship: friendship, params: userParams }; } @@ -707,5 +733,83 @@ class SocialNetworkService extends BaseService { }); return { entries: entries.rows, totalPages: Math.ceil(entries.count / 20) }; } + + async addFriend(hashedUserid, friendUserid) { + const requestingUserId = await this.checkUserAccess(hashedUserid); + const friend = await this.loadUserByHash(friendUserid); + if (!friend) { + throw new Error('notfound'); + } + const friendship = await Friendship.findOne({ + where: { + [Op.or]: [ + { user1Id: requestingUserId, user2Id: friend.id }, + { user1Id: friend.id, user2Id: requestingUserId } + ] + } + }); + if (friendship) { + if (friendship.withdrawn) { + friendship.withdrawn = false; + + } else { + throw new Error('alreadyexists'); + } + } else { + await Friendship.create({ user1Id: requestingUserId, user2Id: friend.id }); + } + return { accepted: false, withdrawn: false, denied: false }; + } + + async removeFriend(hashedUserid, friendUserid) { + const requestingUserId = await this.checkUserAccess(hashedUserid); + const friend = await this.loadUserByHash(friendUserid); + if (!friend) { + throw new Error('notfound'); + } + const friendship = await Friendship.findOne({ + where: { + [Op.or]: [ + { user1Id: requestingUserId, user2Id: friend.id }, + { user1Id: friend.id, user2Id: requestingUserId } + ] + } + }); + if (!friendship) { + throw new Error('notfound'); + } + if (friendship.user1Id === requestingUserId) { + friendship.update({ withdrawn: true }) + } else { + friendship.update({ denied: true }); + } + return true; + } + + async acceptFriendship(hashedUserid, friendUserid) { + const requestingUserId = await this.checkUserAccess(hashedUserid); + const friend = await this.loadUserByHash(friendUserid); + if (!friend) { + throw new Error('notfound'); + } + const friendship = await Friendship.findOne({ + where: { + [Op.or]: [ + { user1Id: requestingUserId, user2Id: friend.id }, + { user1Id: friend.id, user2Id: requestingUserId } + ] + } + }); + if (!friendship) { + throw new Error('notfound'); + } + if (friendship.user1Id === requestingUserId && friendship.withdrawn) { + friendship.update({ withdrawn: false }); + } else if (friendship.user2Id === requestingUserId && friendship.denied) { + friendship.update({ denied: false, accepted: true }); + } else { + throw new Error('notfound'); + } + } } export default SocialNetworkService; diff --git a/frontend/public/images/icons/cancel-friendship.png b/frontend/public/images/icons/cancel-friendship.png new file mode 100644 index 0000000..e10a268 Binary files /dev/null and b/frontend/public/images/icons/cancel-friendship.png differ diff --git a/frontend/public/images/icons/request-friendship.png b/frontend/public/images/icons/request-friendship.png new file mode 100644 index 0000000..a737557 Binary files /dev/null and b/frontend/public/images/icons/request-friendship.png differ diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 2a106ad..fab6ec0 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -8,13 +8,15 @@ - + - + + + - diff --git a/frontend/src/dialogues/standard/MessageDialog.vue b/frontend/src/dialogues/standard/MessageDialog.vue new file mode 100644 index 0000000..ff9457c --- /dev/null +++ b/frontend/src/dialogues/standard/MessageDialog.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/frontend/src/i18n/locales/de/socialnetwork.json b/frontend/src/i18n/locales/de/socialnetwork.json index b32677e..3edea90 100644 --- a/frontend/src/i18n/locales/de/socialnetwork.json +++ b/frontend/src/i18n/locales/de/socialnetwork.json @@ -231,6 +231,23 @@ "page": "Seite <> von <>" }, "createNewMesssage": "Antwort senden" + }, + "friendship": { + "error": { + "alreadyexists": "Die Freundschaftsanfrage existiert bereits" + }, + "state": { + "none": "Nicht befreundet", + "waiting": "Freundschaftsanfrage gesendet, aber nicht beantwortet", + "open": "Freundschaft wurde angefragt", + "denied": "Freundschaftsanfrage abgelehnt", + "withdrawn": "Freundschaftsanfrage zurückgezogen", + "accepted": "Befreundet" + }, + "added": "Du hast eine Freundschaftsanfrage gestellt.", + "withdrawn": "Du hast Deine Freundschaftsanfrage zurückgezogen.", + "denied": "Du hast die Freundschaftsanfrage abgelehnt.", + "accepted": "Die Freundschaft wurde geschlossen." } } } \ No newline at end of file