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 @@
+
+
+
+
{{ translatedMessage }}
+
+
+
+
+
+
+
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