From 069c97fa90cdbd54b512f0f29d867b54631efe67 Mon Sep 17 00:00:00 2001 From: Torsten Schulz Date: Wed, 4 Dec 2024 19:08:26 +0100 Subject: [PATCH] websockets implemented --- backend/app.js | 6 +- backend/controllers/authController.js | 2 + backend/controllers/chatController.js | 55 +- backend/controllers/falukantController.js | 18 + backend/controllers/friendshipController.js | 69 ++ backend/controllers/navigationController.js | 2 +- .../controllers/socialnetworkController.js | 19 +- backend/middleware/authMiddleware.js | 5 +- backend/models/associations.js | 23 +- backend/models/community/user.js | 22 +- backend/models/community/user_param.js | 38 +- backend/models/falukant/data/region.js | 39 + backend/models/falukant/data/user.js | 57 ++ backend/models/falukant/type/region.js | 29 + backend/models/index.js | 8 +- backend/models/type/user_param_value.js | 5 + backend/package-lock.json | 105 ++- backend/package.json | 3 +- backend/routers/chatRouter.js | 4 + backend/routers/falukantRouter.js | 9 + backend/routers/friendshipRouter.js | 15 + backend/routers/socialnetworkRouter.js | 1 + backend/server.js | 45 +- backend/services/BaseService.js | 6 +- backend/services/authService.js | 81 +- backend/services/chatService.js | 181 +++-- backend/services/falukantService.js | 7 + backend/services/forumService.js | 7 +- backend/services/friendshipService.js | 180 +++++ backend/services/settingsService.js | 3 +- backend/services/socialnetworkService.js | 69 +- backend/services/webSocketService.js | 39 + backend/utils/encryption.js | 22 +- .../utils/falukant/initializeFalukantTypes.js | 81 ++ backend/utils/initializeFalukant.js | 8 + backend/utils/initializeTypes.js | 2 +- backend/utils/redis.js | 62 +- backend/utils/sequelize.js | 2 + backend/utils/socket.js | 73 ++ backend/utils/syncDatabase.js | 22 +- frontend/package-lock.json | 364 +++++++-- frontend/package.json | 3 + frontend/src/api/friendshipApi.js | 22 + frontend/src/assets/styles.scss | 20 + frontend/src/components/AppNavigation.vue | 68 +- frontend/src/components/FolderItem.vue | 8 +- .../socialnetwork/CreateFolderDialog.vue | 8 +- .../socialnetwork/UserProfileDialog.vue | 7 +- frontend/src/i18n/index.js | 4 + frontend/src/i18n/locales/de/friends.json | 23 + frontend/src/i18n/locales/de/general.json | 12 +- frontend/src/i18n/locales/de/navigation.json | 4 +- frontend/src/i18n/locales/en/friends.json | 3 + frontend/src/main.js | 9 + frontend/src/router/index.js | 7 + frontend/src/store/index.js | 74 +- frontend/src/utils/axios.js | 11 +- frontend/src/utils/eventBus.js | 2 + frontend/src/utils/menuLoader.js | 1 - frontend/src/views/social/FriendsView.vue | 208 +++++ frontend/src/views/social/GalleryView.vue | 5 + frontend/vite.config.js | 2 +- package-lock.json | 759 ++++++++++++++---- package.json | 2 +- 64 files changed, 2488 insertions(+), 562 deletions(-) create mode 100644 backend/controllers/falukantController.js create mode 100644 backend/controllers/friendshipController.js create mode 100644 backend/models/falukant/data/region.js create mode 100644 backend/models/falukant/data/user.js create mode 100644 backend/models/falukant/type/region.js create mode 100644 backend/routers/falukantRouter.js create mode 100644 backend/routers/friendshipRouter.js create mode 100644 backend/services/falukantService.js create mode 100644 backend/services/friendshipService.js create mode 100644 backend/services/webSocketService.js create mode 100644 backend/utils/falukant/initializeFalukantTypes.js create mode 100644 backend/utils/initializeFalukant.js create mode 100644 backend/utils/socket.js create mode 100644 frontend/src/api/friendshipApi.js create mode 100644 frontend/src/i18n/locales/de/friends.json create mode 100644 frontend/src/i18n/locales/en/friends.json create mode 100644 frontend/src/utils/eventBus.js create mode 100644 frontend/src/views/social/FriendsView.vue diff --git a/backend/app.js b/backend/app.js index 57b44eb..3e90b0e 100644 --- a/backend/app.js +++ b/backend/app.js @@ -7,9 +7,11 @@ import navigationRouter from './routers/navigationRouter.js'; import settingsRouter from './routers/settingsRouter.js'; import adminRouter from './routers/adminRouter.js'; import contactRouter from './routers/contactRouter.js'; -import cors from 'cors'; import socialnetworkRouter from './routers/socialnetworkRouter.js'; import forumRouter from './routers/forumRouter.js'; +import falukantRouter from './routers/falukantRouter.js'; +import friendshipRouter from './routers/friendshipRouter.js'; +import cors from 'cors'; import './jobs/sessionCleanup.js'; const __filename = fileURLToPath(import.meta.url); @@ -36,6 +38,8 @@ app.use('/images', express.static(path.join(__dirname, '../frontend/public/image app.use('/api/contact', contactRouter); app.use('/api/socialnetwork', socialnetworkRouter); app.use('/api/forum', forumRouter); +app.use('/api/falukant', falukantRouter); +app.use('/api/friendships', friendshipRouter); app.use((req, res) => { res.status(404).send('404 Not Found'); diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 56e25d5..63fa36a 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -24,6 +24,7 @@ class AuthController { const { username, password } = req.body; try { const result = await userService.loginUser({ username, password }); + console.log('User logged in successfully', result); res.status(200).json(result); } catch (error) { if (error.message === 'credentialsinvalid') { @@ -37,6 +38,7 @@ class AuthController { async logout(req, res) { const { userid: hashedUserId } = req.headers; await userService.logoutUser(hashedUserId); + res.status(200).json({ result: 'loggedout' }); } async forgotPassword(req, res) { diff --git a/backend/controllers/chatController.js b/backend/controllers/chatController.js index 01084ef..9afc85a 100644 --- a/backend/controllers/chatController.js +++ b/backend/controllers/chatController.js @@ -1,11 +1,4 @@ -import { - getMessages as getMessagesService, - findMatch, - registerUser as registerUserService, - addMessage, - endChat, - removeUser as removeUserService -} from '../services/chatService.js'; +import chatService from '../services/chatService.js'; class ChatController { constructor() { @@ -15,12 +8,15 @@ class ChatController { this.sendMessage = this.sendMessage.bind(this); this.stopChat = this.stopChat.bind(this); this.removeUser = this.removeUser.bind(this); + this.initOneToOne = this.initOneToOne.bind(this); + this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this); + this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this); } async getMessages(req, res) { const { to, from } = req.body; try { - const messages = await getMessagesService(to, from); + const messages = await chatService.getMessages(to, from); res.status(200).json(messages); } catch (error) { res.status(500).json({ error: error.message }); @@ -30,7 +26,7 @@ class ChatController { async findRandomChatMatch(req, res) { const { genders, age, id } = req.body; try { - const match = await findMatch(genders, age, id); + const match = await chatService.findMatch(genders, age, id); if (match) { res.status(200).json({ status: 'matched', user: match }); } else { @@ -44,7 +40,7 @@ class ChatController { async registerUser(req, res) { const { gender, age } = req.body; try { - const userId = await registerUserService(gender, age); + const userId = await chatService.registerUser(gender, age); res.status(200).json({ id: userId }); } catch (error) { res.status(500).json({ error: error.message }); @@ -54,7 +50,7 @@ class ChatController { async sendMessage(req, res) { const { from, to, text } = req.body; try { - const message = await addMessage(from, to, text); + const message = await chatService.addMessage(from, to, text); res.status(200).json(message); } catch (error) { res.status(500).json({ error: error.message }); @@ -64,7 +60,7 @@ class ChatController { async removeUser(req, res) { const { id } = req.body; try { - await removeUserService(id); + await chatService.removeUser(id); res.sendStatus(200); } catch (error) { res.status(500).json({ error: error.message }); @@ -74,12 +70,43 @@ class ChatController { async stopChat(req, res) { const { id } = req.body; try { - await endChat(id); + await chatService.endChat(id); res.sendStatus(200); } catch (error) { res.status(500).json({ error: error.message }); } } + + async initOneToOne(req, res) { + const { userid: hashedUserId } = req.headers; + const { partnerHashId } = req.body; + try { + await chatService.initOneToOne(hashedUserId, partnerHashId); + res.status(200).json({ message: 'One-to-one chat initialization is pending implementation.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + } + + async sendOneToOneMessage(req, res) { + const { user1HashId, user2HashId, message } = req.body; + try { + await chatService.sendOneToOneMessage(user1HashId, user2HashId, message); + res.status(200).json({ status: 'message sent' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + } + + async getOneToOneMessageHistory(req, res) { + const { user1HashId, user2HashId } = req.query; + try { + const history = await chatService.getOneToOneMessageHistory(user1HashId, user2HashId); + res.status(200).json({ history }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + } } export default ChatController; diff --git a/backend/controllers/falukantController.js b/backend/controllers/falukantController.js new file mode 100644 index 0000000..90ca564 --- /dev/null +++ b/backend/controllers/falukantController.js @@ -0,0 +1,18 @@ +import * as falukantService from '../services/falukantService.js'; + +class FalukantController { + constructor() { + this.exampleMethod = this.exampleMethod.bind(this); + } + + async exampleMethod(req, res) { + try { + const result = await falukantService.exampleMethod(); + res.status(200).json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } + } +} + +export default FalukantController; diff --git a/backend/controllers/friendshipController.js b/backend/controllers/friendshipController.js new file mode 100644 index 0000000..04b4b99 --- /dev/null +++ b/backend/controllers/friendshipController.js @@ -0,0 +1,69 @@ +import friendshipService from '../services/friendshipService.js'; + +const friendshipController = { + async endFriendship(req, res) { + try { + const { userid: hashedUserId } = req.headers; + const { friendUserId } = req.body; + + await friendshipService.endFriendship(hashedUserId, friendUserId); + res.status(200).json({ message: 'Friendship ended successfully' }); + } catch (error) { + console.error('Error in endFriendship:', error); + res.status(400).json({ error: error.message }); + } + }, + + async acceptFriendship(req, res) { + try { + const { userid: hashedUserId } = req.headers; + const { friendUserId } = req.body; + + await friendshipService.acceptFriendship(hashedUserId, friendUserId); + res.status(200).json({ message: 'Friendship accepted successfully' }); + } catch (error) { + console.error('Error in acceptFriendship:', error); + res.status(400).json({ error: error.message }); + } + }, + + async rejectFriendship(req, res) { + try { + const { userid: hashedUserId } = req.headers; + const { friendUserId } = req.body; + + await friendshipService.rejectFriendship(hashedUserId, friendUserId); + res.status(200).json({ message: 'Friendship rejected successfully' }); + } catch (error) { + console.error('Error in rejectFriendship:', error); + res.status(400).json({ error: error.message }); + } + }, + + async withdrawRequest(req, res) { + try { + const { userid: hashedUserId } = req.headers; + const { friendUserId } = req.body; + await friendshipService.withdrawRequest(hashedUserId, friendUserId); + res.status(200).json({ message: 'Friendship request withdrawn successfully' }); + } catch (error) { + console.error('Error in withdrawRequest:', error); + res.status(400).json({ error: error.message }); + } + }, + + async getFriendships(req, res) { + try { + const { userid: hashedUserId } = req.headers; + const { acceptedOnly } = req.query; + console.log('Friendships:', acceptedOnly); + const friendships = await friendshipService.getFriendships(hashedUserId, acceptedOnly === 'true'); + res.status(200).json(friendships); + } catch (error) { + console.error('Error in getFriendships:', error); + res.status(400).json({ error: error.message }); + } + }, +}; + +export default friendshipController; diff --git a/backend/controllers/navigationController.js b/backend/controllers/navigationController.js index 08e1461..fde4886 100644 --- a/backend/controllers/navigationController.js +++ b/backend/controllers/navigationController.js @@ -16,7 +16,7 @@ const menuStructure = { children: { manageFriends: { visible: ["all"], - path: "/socialnetwork/friends", + path: "/friends", icon: "friends24.png" } }, diff --git a/backend/controllers/socialnetworkController.js b/backend/controllers/socialnetworkController.js index 29ca110..dd5de06 100644 --- a/backend/controllers/socialnetworkController.js +++ b/backend/controllers/socialnetworkController.js @@ -26,12 +26,14 @@ class SocialNetworkController { this.addFriend = this.addFriend.bind(this); this.removeFriend = this.removeFriend.bind(this); this.acceptFriendship = this.acceptFriendship.bind(this); + this.getLoggedInFriends = this.getLoggedInFriends.bind(this); } async userSearch(req, res) { try { + const { userid: hashedUserId } = req.headers; const { username, ageFrom, ageTo, genders } = req.body; - const users = await this.socialNetworkService.searchUsers({ username, ageFrom, ageTo, genders }); + const users = await this.socialNetworkService.searchUsers({ hashedUserId, username, ageFrom, ageTo, genders }); res.status(200).json(users); } catch (error) { console.error('Error in userSearch:', error); @@ -295,6 +297,7 @@ class SocialNetworkController { try { const { userid: hashedUserid } = req.headers; const { friendUserid } = req.body; + console.log('--------', friendUserid, hashedUserid); await this.socialNetworkService.addFriend(hashedUserid, friendUserid); res.status(201).json({ message: 'added' }); } catch (error) { @@ -326,6 +329,20 @@ class SocialNetworkController { res.status(500).json({ error: error.message }); } } + + async getLoggedInFriends(req, res) { + try { + const { userid: userId } = req.headers; + if (!userId) { + return res.status(400).json({ error: 'Missing user ID' }); + } + const loggedInFriends = await this.socialNetworkService.getLoggedInFriends(userId); + res.status(200).json(loggedInFriends); + } catch (error) { + console.error('Error in getLoggedInFriends:', error); + res.status(500).json({ error: error.message }); + } + } } export default SocialNetworkController; diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js index 6054345..0031658 100644 --- a/backend/middleware/authMiddleware.js +++ b/backend/middleware/authMiddleware.js @@ -2,8 +2,7 @@ import User from '../models/community/user.js'; import { updateUserTimestamp } from '../utils/redis.js'; export const authenticate = async (req, res, next) => { - const userId = req.headers.userid; - const authCode = req.headers.authcode; + const { userid: userId, authcode: authCode } = req.headers; if (!userId || !authCode) { return res.status(401).json({ error: 'Unauthorized: Missing credentials' }); } @@ -12,7 +11,7 @@ export const authenticate = async (req, res, next) => { return res.status(401).json({ error: 'Unauthorized: Invalid credentials' }); } try { - await updateUserTimestamp(userId); + await updateUserTimestamp(user.id); } catch (error) { console.error('Fehler beim Aktualisieren des Zeitstempels:', error); } diff --git a/backend/models/associations.js b/backend/models/associations.js index edfc167..273c26d 100644 --- a/backend/models/associations.js +++ b/backend/models/associations.js @@ -28,6 +28,9 @@ 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'; +import FalukantUser from './falukant/data/user.js'; +import RegionType from './falukant/type/region.js'; +import RegionData from './falukant/data/region.js'; export default function setupAssociations() { // UserParam related associations @@ -40,12 +43,13 @@ export default function setupAssociations() { User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' }); UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' }); - // UserRight related associations + UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_value_type' }); + UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_type_value' }); + UserRight.belongsTo(User, { foreignKey: 'userId', as: 'user_with_rights' }); UserRight.belongsTo(UserRightType, { foreignKey: 'rightTypeId', as: 'rightType' }); UserRightType.hasMany(UserRight, { foreignKey: 'rightTypeId', as: 'user_rights' }); - // UserParamVisibility related associations UserParam.hasMany(UserParamVisibility, { foreignKey: 'param_id', as: 'param_visibilities' }); UserParamVisibility.belongsTo(UserParam, { foreignKey: 'param_id', as: 'param' }); @@ -162,4 +166,19 @@ export default function setupAssociations() { Friendship.belongsTo(User, { foreignKey: 'user2Id', as: 'friendReceiver' }); User.hasMany(Friendship, { foreignKey: 'user1Id', as: 'friendSender' }); User.hasMany(Friendship, { foreignKey: 'user2Id', as: 'friendReceiver' }); + + User.hasMany(FalukantUser, { foreignKey: 'userId', as: 'falukantData' }); + FalukantUser.belongsTo(User, { foreignKey: 'userId', as: 'user' }); + + RegionType.hasMany(RegionType, { foreignKey: 'parentId', as: 'children' }); + RegionType.belongsTo(RegionType, { foreignKey: 'parentId', as: 'parent' }); + + RegionData.hasMany(RegionData, { foreignKey: 'parentId', as: 'children' }); + RegionData.belongsTo(RegionData, { foreignKey: 'parentId', as: 'parent' }); + + RegionData.belongsTo(RegionType, { foreignKey: 'regionTypeId', as: 'regionType' }); + RegionType.hasMany(RegionData, { foreignKey: 'regionTypeId', as: 'regions' }); + + FalukantUser.belongsTo(RegionData, { foreignKey: 'mainBranchRegionId', as: 'mainBranchRegion' }); + RegionData.hasMany(FalukantUser, { foreignKey: 'mainBranchRegionId', as: 'users' }); } diff --git a/backend/models/community/user.js b/backend/models/community/user.js index cc75eb7..eb48b9d 100644 --- a/backend/models/community/user.js +++ b/backend/models/community/user.js @@ -5,14 +5,27 @@ import crypto from 'crypto'; const User = sequelize.define('user', { email: { - type: DataTypes.BLOB, + type: DataTypes.BLOB, allowNull: false, unique: true, set(value) { if (value) { - this.setDataValue('email', Buffer.from(encrypt(value), 'hex')); + const encrypted = encrypt(value); + this.setDataValue('email', encrypted); } }, + get() { + const encrypted = this.getDataValue('email'); + if (encrypted) { + return decrypt(encrypted); + } + return null; + } + }, + salt: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: () => crypto.randomBytes(16).toString('hex') }, username: { type: DataTypes.STRING, @@ -58,11 +71,6 @@ const User = sequelize.define('user', { user.hashedId = hashedId; await user.save(); } - }, - getterMethods: { - email() { - return decrypt(this.getDataValue('email').toString('hex')); - } } }); diff --git a/backend/models/community/user_param.js b/backend/models/community/user_param.js index 4ff18df..72e0542 100644 --- a/backend/models/community/user_param.js +++ b/backend/models/community/user_param.js @@ -2,7 +2,7 @@ import { sequelize } from '../../utils/sequelize.js'; import { DataTypes } from 'sequelize'; import User from './user.js'; import UserParamType from '../type/user_param.js'; -import { encrypt, decrypt, generateIv } from '../../utils/encryption.js'; +import { encrypt, decrypt } from '../../utils/encryption.js'; const UserParam = sequelize.define('user_param', { userId: { @@ -10,45 +10,43 @@ const UserParam = sequelize.define('user_param', { allowNull: false, references: { model: User, - key: 'id' - } + key: 'id', + }, }, paramTypeId: { type: DataTypes.INTEGER, allowNull: false, references: { model: UserParamType, - key: 'id' - } + key: 'id', + }, }, value: { type: DataTypes.STRING, allowNull: false, set(value) { + console.log('.... [set param value]', value); if (value) { try { - const iv = generateIv(); - this.setDataValue('iv', iv.toString('hex')); - this.setDataValue('value', encrypt(value.toString(), iv)); + const encrypted = encrypt(value.toString()); + console.log('.... [encrypted param value]', encrypted); + this.setDataValue('value', encrypted); } catch (error) { - this.setDataValue('value', ''); + console.error('.... Error encrypting param value:', error); + this.setDataValue('value', ''); } } }, get() { try { - const value = this.getDataValue('value'); - const iv = Buffer.from(this.getDataValue('iv'), 'hex'); - return decrypt(value, iv); + const value = this.getDataValue('value'); + return decrypt(value); } catch (error) { + console.error('.... Error decrypting param value:', error); return ''; } - } + }, }, - iv: { - type: DataTypes.STRING, - allowNull: false - } }, { tableName: 'user_param', schema: 'community', @@ -56,9 +54,9 @@ const UserParam = sequelize.define('user_param', { indexes: [ { unique: true, - fields: ['user_id', 'param_type_id'] - } - ] + fields: ['user_id', 'param_type_id'], + }, + ], }); export default UserParam; diff --git a/backend/models/falukant/data/region.js b/backend/models/falukant/data/region.js new file mode 100644 index 0000000..c321558 --- /dev/null +++ b/backend/models/falukant/data/region.js @@ -0,0 +1,39 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../../../utils/sequelize.js'; +import RegionType from '../type/region.js'; + +class RegionData extends Model { } + +RegionData.init({ + name: { + type: DataTypes.STRING, + allowNull: false, + }, + regionTypeId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: RegionType, + key: 'id', + schema: 'falukant_type' + } + }, + parentId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: 'region', + key: 'id', + schema: 'falukant_data', + } + } +}, { + sequelize, + modelName: 'RegionData', + tableName: 'region', + schema: 'falukant_data', + timestamps: false, + underscored: true, +}); + +export default RegionData; diff --git a/backend/models/falukant/data/user.js b/backend/models/falukant/data/user.js new file mode 100644 index 0000000..4ed95d4 --- /dev/null +++ b/backend/models/falukant/data/user.js @@ -0,0 +1,57 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../../../utils/sequelize.js'; +import RegionData from './region.js'; + +class FalukantUser extends Model { } + +FalukantUser.init({ + userId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: { + tableName: 'user', + schema: 'community' + }, + key: 'id' + } + }, + money: { + type: DataTypes.DECIMAL(10, 2), + allowNull: false, + defaultValue: 0.00, + }, + creditAmount: { + type: DataTypes.DECIMAL(10, 2), + allowNull: false, + defaultValue: 0.00, + }, + todayCreditTaken: { + type: DataTypes.DECIMAL(10, 2), + allowNull: false, + defaultValue: 0.00, + }, + creditInterestRate: { + type: DataTypes.DECIMAL(5, 2), + allowNull: false, + defaultValue: 0.00, + }, + mainBranchRegionId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: RegionData, + key: 'id', + schema: 'falukant_data' + } + } +}, { + sequelize, + modelName: 'FalukantUser', + tableName: 'falukant_user', + schema: 'falukant_data', + timestamps: true, + underscored: true, +}); + +export default FalukantUser; diff --git a/backend/models/falukant/type/region.js b/backend/models/falukant/type/region.js new file mode 100644 index 0000000..8148202 --- /dev/null +++ b/backend/models/falukant/type/region.js @@ -0,0 +1,29 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../../../utils/sequelize.js'; + +class RegionType extends Model { } + +RegionType.init({ + labelTr: { + type: DataTypes.STRING, + allowNull: false, + }, + parentId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: 'region', + key: 'id', + schema: 'falukant_type', + } + } +}, { + sequelize, + modelName: 'RegionType', + tableName: 'region', + schema: 'falukant_type', + timestamps: false, + underscored: true, +}); + +export default RegionType; diff --git a/backend/models/index.js b/backend/models/index.js index ee27131..ada7b08 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -32,6 +32,9 @@ 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'; +import FalukantUser from './falukant/data/user.js'; +import RegionType from './falukant/type/region.js'; +import RegionData from './falukant/data/region.js'; const models = { SettingsType, @@ -40,7 +43,7 @@ const models = { UserRightType, User, UserParam, - Login, + Login, UserRight, InterestType, InterestTranslationType, @@ -68,6 +71,9 @@ const models = { MessageHistory, MessageImage, Friendship, + RegionType, + RegionData, + FalukantUser, }; export default models; diff --git a/backend/models/type/user_param_value.js b/backend/models/type/user_param_value.js index f5bc8d3..462b28a 100644 --- a/backend/models/type/user_param_value.js +++ b/backend/models/type/user_param_value.js @@ -3,6 +3,11 @@ import { DataTypes } from 'sequelize'; import UserParam from './user_param.js'; const UserParamValue = sequelize.define('user_param_value', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, userParamTypeId: { type: DataTypes.INTEGER, allowNull: false diff --git a/backend/package-lock.json b/backend/package-lock.json index 073e8c2..1aa9e91 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -29,7 +29,8 @@ "sequelize": "^6.37.3", "sharp": "^0.33.5", "socket.io": "^4.7.5", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "ws": "^8.18.0" }, "devDependencies": { "sequelize-cli": "^6.6.2" @@ -1200,9 +1201,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -1481,16 +1482,16 @@ } }, "node_modules/engine.io": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", - "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", @@ -1509,13 +1510,33 @@ } }, "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "engines": { "node": ">= 0.6" } }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1646,16 +1667,16 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2497,26 +2518,6 @@ "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", @@ -3647,15 +3648,15 @@ } }, "node_modules/socket.io": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", - "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.2", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, @@ -3672,6 +3673,26 @@ "ws": "~8.17.1" } }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -4120,9 +4141,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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" }, diff --git a/backend/package.json b/backend/package.json index a3311a4..fec01b8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -31,7 +31,8 @@ "sequelize": "^6.37.3", "sharp": "^0.33.5", "socket.io": "^4.7.5", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "ws": "^8.18.0" }, "devDependencies": { "sequelize-cli": "^6.6.2" diff --git a/backend/routers/chatRouter.js b/backend/routers/chatRouter.js index 5cee572..12513d8 100644 --- a/backend/routers/chatRouter.js +++ b/backend/routers/chatRouter.js @@ -1,5 +1,6 @@ import { Router } from 'express'; import ChatController from '../controllers/chatController.js'; +import { authenticate } from '../middleware/authMiddleware.js'; const router = Router(); const chatController = new ChatController(); @@ -10,5 +11,8 @@ router.post('/register', chatController.registerUser); router.post('/sendMessage', chatController.sendMessage); router.post('/leave', chatController.stopChat); router.post('/exit', chatController.removeUser); +router.post('/initOneToOne', authenticate, chatController.initOneToOne); +router.post('/oneToOne/sendMessage', authenticate, chatController.sendOneToOneMessage); // Neue Route zum Senden einer Nachricht +router.get('/oneToOne/messageHistory', authenticate, chatController.getOneToOneMessageHistory); // Neue Route zum Abrufen der Nachrichtengeschichte export default router; diff --git a/backend/routers/falukantRouter.js b/backend/routers/falukantRouter.js new file mode 100644 index 0000000..3c09543 --- /dev/null +++ b/backend/routers/falukantRouter.js @@ -0,0 +1,9 @@ +import express from 'express'; +import FalukantController from '../controllers/falukantController.js'; + +const router = express.Router(); +const falukantController = new FalukantController(); + +router.get('/example', falukantController.exampleMethod); + +export default router; diff --git a/backend/routers/friendshipRouter.js b/backend/routers/friendshipRouter.js new file mode 100644 index 0000000..0c04f98 --- /dev/null +++ b/backend/routers/friendshipRouter.js @@ -0,0 +1,15 @@ +import { Router } from 'express'; +import friendshipController from '../controllers/friendshipController.js'; +import { authenticate } from '../middleware/authMiddleware.js'; + +const friendshipRouter = Router(); + +friendshipRouter.use(authenticate); + +friendshipRouter.post('/end', friendshipController.endFriendship); +friendshipRouter.post('/accept', friendshipController.acceptFriendship); +friendshipRouter.post('/reject', friendshipController.rejectFriendship); +friendshipRouter.post('/withdraw', friendshipController.withdrawRequest); +friendshipRouter.get('/', friendshipController.getFriendships); + +export default friendshipRouter; diff --git a/backend/routers/socialnetworkRouter.js b/backend/routers/socialnetworkRouter.js index ba8d3de..a0e6322 100644 --- a/backend/routers/socialnetworkRouter.js +++ b/backend/routers/socialnetworkRouter.js @@ -32,5 +32,6 @@ router.get('/diary/:page', socialNetworkController.getDiaryEntries); router.post('/friend', socialNetworkController.addFriend); router.delete('/friend/:friendUserId', socialNetworkController.removeFriend); router.put('/friend/:friendUserId', socialNetworkController.acceptFriendship); +router.get('/friends/loggedin', socialNetworkController.getLoggedInFriends); export default router; diff --git a/backend/server.js b/backend/server.js index 6fc23ed..79ab3a2 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,44 +1,17 @@ import http from 'http'; -import { Server } from 'socket.io'; -import amqp from 'amqplib/callback_api.js'; import app from './app.js'; +import { setupWebSocket } from './utils/socket.js'; import { syncDatabase } from './utils/syncDatabase.js'; const server = http.createServer(app); -const io = new Server(server); -const RABBITMQ_URL = 'amqp://localhost'; -const QUEUE = 'chat_messages'; - -amqp.connect(RABBITMQ_URL, (err, connection) => { - if (err) { - throw err; - } - - connection.createChannel((err, channel) => { - if (err) { - throw err; - } - channel.assertQueue(QUEUE, { durable: false }); - io.on('connection', (socket) => { - channel.consume(QUEUE, (msg) => { - const message = JSON.parse(msg.content.toString()); - io.emit('newMessage', message); - }, { noAck: true }); - socket.on('newMessage', (message) => { - channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(message))); - }); - socket.on('disconnect', () => { - }); - }); - - syncDatabase().then(() => { - server.listen(3001, () => { - console.log('Server is running on port 3001'); - }); - }).catch(err => { - console.error('Failed to sync database:', err); - process.exit(1); - }); +setupWebSocket(server); + +syncDatabase().then(() => { + server.listen(3001, () => { + console.log('Server is running on port 3001'); }); +}).catch(err => { + console.error('Failed to sync database:', err); + process.exit(1); }); diff --git a/backend/services/BaseService.js b/backend/services/BaseService.js index 790fd87..0cba963 100644 --- a/backend/services/BaseService.js +++ b/backend/services/BaseService.js @@ -9,9 +9,11 @@ import UserRightType from '../models/type/user_right.js'; class BaseService { async getUserByHashedId(hashedId) { - const user = await User.findOne({ where: { hashedId } }); + console.log('async getUserByHashedId: ', hashedId); + const user = await User.findOne({ where: { hashedId: hashedId } }); if (!user) { - throw new Error('User not found'); + console.log('User not found: ', hashedId); + throw new Error('User not found: ', hashedId); } return user; } diff --git a/backend/services/authService.js b/backend/services/authService.js index 80e5417..99ef82f 100644 --- a/backend/services/authService.js +++ b/backend/services/authService.js @@ -6,20 +6,64 @@ import UserParam from '../models/community/user_param.js'; import UserParamType from '../models/type/user_param.js'; import { sendAccountActivationEmail, sendPasswordResetEmail } from './emailService.js'; import { sequelize } from '../utils/sequelize.js'; +import { Op } from 'sequelize'; import { setUserSession, deleteUserSession } from '../utils/redis.js'; +import { encrypt } from '../utils/encryption.js'; +import { notifyUser } from '../utils/socket.js'; +import Friendship from '../models/community/friendship.js'; const saltRounds = 10; +const getFriends = async (userId) => { + console.log('getFriends', userId); + try { + const friendships = await Friendship.findAll({ + where: { + [Op.or]: [ + { user1Id: userId }, + { user2Id: userId }, + ], + accepted: true, + }, + include: [ + { + model: User, + as: 'friendSender', + attributes: ['hashedId', 'username'], + }, + { + model: User, + as: 'friendReceiver', + attributes: ['hashedId', 'username'], + }, + ], + }); + + console.log('did read out friends'); + return friendships.map((friendship) => { + if (friendship.user1Id === userId) { + return friendship.friendReceiver; + } else { + return friendship.friendSender; + } + }); + } catch (error) { + console.error('Error fetching friends:', error); + throw error; + } +}; + export const registerUser = async ({ email, username, password, language }) => { - const encryptionKey = process.env.SECRET_KEY; - const results = await sequelize.query( - `SELECT * FROM community.user WHERE pgp_sym_decrypt(email::bytea, :key) = :email`, - { - replacements: { key: encryptionKey, email }, - type: sequelize.QueryTypes.SELECT - } - ); - if (results.length > 0) { + const encryptedEmail = encrypt(email); + const query = ` + SELECT id FROM community.user + WHERE email = :encryptedEmail + `; + const existingUser = await sequelize.query(query, { + replacements: { encryptedEmail }, + type: sequelize.QueryTypes.SELECT, + }); + if (existingUser.length > 0) { throw new Error('emailinuse'); } const hashedPassword = await bcrypt.hash(password, saltRounds); @@ -59,6 +103,13 @@ export const loginUser = async ({ username, password }) => { const authCode = crypto.randomBytes(20).toString('hex'); user.authCode = authCode; await user.save(); + const friends = await getFriends(user.id); + for (const friend of friends) { + await notifyUser(friend.hashedId, 'friendloginchanged', { + userId: user.hashedId, + status: 'online', + }); + } const sessionData = { id: user.hashedId, username: user.username, @@ -86,14 +137,14 @@ export const loginUser = async ({ username, password }) => { id: user.hashedId, username: user.username, active: user.active, - param: mappedParams, + param: mappedParams, authCode }; }; export const logoutUser = async (hashedUserId) => { try { - const user = User.findOne({ + const user = await User.findOne({ where: { hashedId: hashedUserId } @@ -101,8 +152,14 @@ export const logoutUser = async (hashedUserId) => { if (!user) { return; } + const friends = await getFriends(user.id); + for (const friend of friends) { + await notifyUser(friend.hashedId, 'friendloginchanged', { + userId: user.hashedId, + status: 'online', + }); + } await deleteUserSession(user.id); - console.log('Benutzer erfolgreich aus Redis entfernt:', userId); } catch (error) { console.error('Fehler beim Logout:', error); throw new Error('logoutfailed'); diff --git a/backend/services/chatService.js b/backend/services/chatService.js index e3c3ded..b1516ef 100644 --- a/backend/services/chatService.js +++ b/backend/services/chatService.js @@ -1,76 +1,137 @@ import { v4 as uuidv4 } from 'uuid'; +import amqp from 'amqplib/callback_api.js'; -let messages = []; -let searchQueue = []; -let users = []; -let currentChats = []; +const RABBITMQ_URL = 'amqp://localhost'; +const QUEUE = 'oneToOne_messages'; -export const getMessages = (toId, fromId) => { - const userChats = currentChats.filter(chat => chat.includes(toId) && chat.includes(fromId)); - if (userChats.length === 0) { - fromId = ''; +class ChatService { + constructor() { + this.messages = []; + this.searchQueue = []; + this.users = []; + this.randomChats = []; + this.oneToOneChats = []; + amqp.connect(RABBITMQ_URL, (err, connection) => { + if (err) throw err; + connection.createChannel((err, channel) => { + if (err) throw err; + this.channel = channel; + channel.assertQueue(QUEUE, { durable: false }); + }); + }); } - const userMessages = messages.filter(message => message.to = toId && ["system", fromId].includes(message.from)); - messages = messages.filter(message => message.to === toId && ["system", fromId].includes(message.from)); - return userMessages; -}; -export const addMessage = (from, to, text) => { - const userChats = currentChats.filter(chat => chat.includes(from) && chat.includes(to)); - if (userChats.length === 0) { - return; + getMessages(toId, fromId) { + const userChats = this.randomChats.filter(chat => chat.includes(toId) && chat.includes(fromId)); + if (userChats.length === 0) { + fromId = ''; + } + const userMessages = this.messages.filter(message => message.to === toId && ["system", fromId].includes(message.from)); + this.messages = this.messages.filter(message => message.to === toId && ["system", fromId].includes(message.from)); + return userMessages; } - messages.push({ from: from, to: to, text: text }); - return { text: text }; -}; -export const findMatch = (genders, age, id) => { - const currentUsersChat = currentChats.filter(chat => chat.includes(id)); - if (currentUsersChat.length > 0) { - return findUser(currentUsersChat[0][0] === id ? currentUsersChat[0][1] : currentUsersChat[0][0]); + async addMessage(from, to, text) { + const userChats = this.randomChats.filter(chat => chat.includes(from) && chat.includes(to)); + if (userChats.length === 0) { + return; + } + this.messages.push({ from: from, to: to, text: text }); + return { text: text }; } - let filteredSearchQueue = users.filter(user => - searchQueue.some(sq => sq.id === user.id) && user.id !== id - && currentChats.filter(chat => chat.includes(user.id)).length === 0 - ).sort(() => Math.random() - 0.5); - for (let i = 0; i < filteredSearchQueue.length; i++) { - const user = filteredSearchQueue[i]; - const ageMatch = user.age >= age.min && user.age <= age.max; - const genderMatch = genders.includes(user.gender); - if (ageMatch && genderMatch) { - for (let j = searchQueue.length - 1; j >= 0; j--) { - if ([id, user.id].includes(searchQueue[j].id)) { - searchQueue.splice(j, 1); + + findMatch(genders, age, id) { + const currentUsersChat = this.randomChats.filter(chat => chat.includes(id)); + if (currentUsersChat.length > 0) { + return this.findUser(currentUsersChat[0][0] === id ? currentUsersChat[0][1] : currentUsersChat[0][0]); + } + let filteredSearchQueue = this.users.filter(user => + this.searchQueue.some(sq => sq.id === user.id) && user.id !== id + && this.randomChats.filter(chat => chat.includes(user.id)).length === 0 + ).sort(() => Math.random() - 0.5); + + for (let i = 0; i < filteredSearchQueue.length; i++) { + const user = filteredSearchQueue[i]; + const ageMatch = user.age >= age.min && user.age <= age.max; + const genderMatch = genders.includes(user.gender); + if (ageMatch && genderMatch) { + for (let j = this.searchQueue.length - 1; j >= 0; j--) { + if ([id, user.id].includes(this.searchQueue[j].id)) { + this.searchQueue.splice(j, 1); + } } + this.randomChats.push([user.id, id]); + return user; } - currentChats.push([user.id, id]); - return user; + } + if (!this.searchQueue.find(user => user.id === id)) { + this.searchQueue.push({ id, genders, age }); + } + return null; + } + + findUser(id) { + return this.users.find(user => user.id === id); + } + + async registerUser(gender, age) { + const id = uuidv4(); + this.users.push({ gender, age, id }); + return id; + } + + async removeUser(id) { + this.searchQueue = this.searchQueue.filter(user => user.id !== id); + this.users = this.users.filter(user => user.id !== id); + this.randomChats = this.randomChats.filter(pair => pair[0] === id || pair[1] === id); + this.messages = this.messages.filter(message => message.from === id || message.to === id); + } + + async endChat(userId) { + this.randomChats = this.randomChats.filter(chat => !chat.includes(userId)); + this.messages.push({ to: userId, from: 'system', activity: 'otheruserleft' }); + } + + async initOneToOne(user1HashId, user2HashId) { + const chat = this.searchOneToOneChat(user1HashId, user2HashId); + if (!chat) { + this.oneToOneChats.push({ user1Id: user1HashId, user2Id: user2HashId, history: [] }); } } - if (!searchQueue.find(user => user.id === id)) { - searchQueue.push({ id, genders, age }); + + async sendOneToOneMessage(user1HashId, user2HashId, message) { + const messageBundle = { + timestamp: Date.now(), + sender: user1HashId, + recipient: user2HashId, + message: message, + }; + const chat = this.searchOneToOneChat(user1HashId, user2HashId); + if (chat) { + chat.history.push(messageBundle); + } else { + this.oneToOneChats.push({ + user1Id: user1HashId, + user2Id: user2HashId, + history: [messageBundle], + }); + } + if (this.channel) { + this.channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(messageBundle))); + } } - return null; -}; -const findUser = (id) => { - return users.find(user => user.id === id); -}; + async getOneToOneMessageHistory(user1HashId, user2HashId) { + const chat = this.searchOneToOneChat(user1HashId, user2HashId); + return chat ? chat.history : []; + } -export const registerUser = (gender, age) => { - const id = uuidv4(); - users.push({ gender, age, id }); - return id; -}; + searchOneToOneChat(user1HashId, user2HashId) { + return this.oneToOneChats.find(chat => + (chat.user1Id === user1HashId && chat.user2Id === user2HashId) || + (chat.user1Id === user2HashId && chat.user2Id === user1HashId) + ); + } +} -export const removeUser = (id) => { - searchQueue = searchQueue.filter(user => user.id !== id); - users = users.filter(user => user.id !== id); - currentChats = currentChats.filter(pair => pair[0] === id || pair[1] === id); - messages = messages.filter(message => message.from === id || message.to === id); -}; - -export const endChat = (userId) => { - currentChats = currentChats.filter(chat => !chat.includes(userId)); - messages.push({ to: userId, from: 'system', activity: 'otheruserleft'}) -} \ No newline at end of file +export default new ChatService(); diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js new file mode 100644 index 0000000..c84c698 --- /dev/null +++ b/backend/services/falukantService.js @@ -0,0 +1,7 @@ +class FalukantService { + async exampleMethod() { + // Logik für die Methode + } +} + +export default new FalukantService(); diff --git a/backend/services/forumService.js b/backend/services/forumService.js index c221954..1b0b28a 100644 --- a/backend/services/forumService.js +++ b/backend/services/forumService.js @@ -10,7 +10,8 @@ import User from '../models/community/user.js'; import ForumForumPermission from '../models/forum/forum_forum_permission.js'; import Title from '../models/forum/title.js'; import Message from '../models/forum/message.js'; - +import { notifyAllUsers } from '../utils/socket.js'; + class ForumService extends BaseService { async createForum(hashedUserId, name, permissions) { @@ -30,6 +31,7 @@ class ForumService extends BaseService { } } } + await notifyAllUsers('forumschanged', {}); return newForum; } @@ -60,6 +62,7 @@ class ForumService extends BaseService { await forum.destroy({ transaction }); await transaction.commit(); + await notifyAllUsers('forumschanged', {}); return forum; } catch (error) { await transaction.rollback(); @@ -193,6 +196,7 @@ class ForumService extends BaseService { } const newTopic = await Title.create({ title, forumId, createdBy: user.id }); await Message.create({ titleId: newTopic.id, text: content, createdBy: user.id}) + await notifyAllUsers('topicschanged', { forumId, topic: newTopic }); return this.getForum(hashedUserId, forumId, 1); } @@ -289,6 +293,7 @@ class ForumService extends BaseService { } console.log('[ForumService.addMessage] - create new message'); await Message.create({ titleId: topicId, text: content, createdBy: user.id }); + await notifyAllUsers('messageschanged', { topicId, message }); console.log('[ForumService.addMessage] - return topic'); return this.getTopic(hashedUserId, topicId); } diff --git a/backend/services/friendshipService.js b/backend/services/friendshipService.js new file mode 100644 index 0000000..42b1d56 --- /dev/null +++ b/backend/services/friendshipService.js @@ -0,0 +1,180 @@ +import BaseService from './BaseService.js'; +import Friendship from '../models/community/friendship.js'; +import User from '../models/community/user.js'; +import { Op } from 'sequelize'; +import UserParam from '../models/community/user_param.js'; +import UserParamType from '../models/type/user_param.js'; +import UserParamValue from '../models/type/user_param_value.js'; +import { notifyUser } from '../utils/socket.js'; + +class FriendshipService extends BaseService { + genders = {}; + + async endFriendship(hashedUserId, friendUserId) { + const user = await this.getUserByHashedId(hashedUserId); + const friend = await this.getUserByHashedId(friendUserId); + if (!user) throw new Error('User not found.'); + + const friendship = await Friendship.findOne({ + where: { + [Op.or]: [ + { user1Id: user.id, user2Id: friend.id }, + { user1Id: friend.id, user2Id: user.id }, + ], + }, + }); + + if (!friendship) throw new Error('Friendship not found.'); + await friendship.destroy(); + notifyUser(friend.hashedId, 'friendshipChanged', { userId: user.hashedId }); + } + + async acceptFriendship(hashedUserId, friendUserId) { + const user = await this.getUserByHashedId(hashedUserId); + const friend = await this.getUserByHashedId(friendUserId); + if (!user) throw new Error('User not found.'); + const friendship = await Friendship.findOne({ + where: { user1Id: friend.id, user2Id: user.id, accepted: false }, + }); + + if (!friendship) throw new Error('Cannot accept this friendship.'); + friendship.accepted = true; + await friendship.save(); + notifyUser(friend.hashedId, 'friendshipChanged', { userId: user.hashedId }); + } + + async rejectFriendship(hashedUserId, friendUserId) { + const user = await this.getUserByHashedId(hashedUserId); + const friend = await this.getUserByHashedId(friendUserId); + if (!user) throw new Error('User not found.'); + const friendship = await Friendship.findOne({ + where: { user1Id: friend.id, user2Id: user.id, accepted: false }, + }); + + if (!friendship) throw new Error('Cannot reject this friendship.'); + friendship.denied = true; + await friendship.save(); + notifyUser(friend.hashedId, 'friendshipChanged', { userId: user.hashedId }); + } + + async withdrawRequest(hashedUserId, friendUserId) { + const user = await this.getUserByHashedId(hashedUserId); + const friend = await this.getUserByHashedId(friendUserId); + if (!user) throw new Error('User not found.'); + + const friendship = await Friendship.findOne({ + where: { user1Id: user.id, user2Id: friend.id, accepted: false }, + }); + + if (!friendship) throw new Error('Cannot withdraw this request.'); + await friendship.destroy(); + notifyUser(friend.hashedId, 'friendshipChanged', { userId: user.hashedId }); + } + + + async getFriendships(hashedUserId, acceptedOnly) { + const user = await this.getUserByHashedId(hashedUserId); + if (!user) throw new Error('User not found.'); + + const whereCondition = acceptedOnly + ? { accepted: true, withdrawn: false, denied: false } + : {}; + + const friendships = await Friendship.findAll({ + where: { + ...whereCondition, + [Op.or]: [ + { user1Id: user.id }, + { user2Id: user.id }, + ], + }, + include: [ + { + model: User, + as: 'friendSender', + attributes: ['username', 'hashedId'], + include: [ + { + model: UserParam, + as: 'user_params', + required: false, + include: [ + { + model: UserParamType, + as: 'paramType', + required: true, + where: { description: 'gender' }, + attributes: ['description'], + }, + ], + attributes: ['value'], + }, + ], + }, + { + model: User, + as: 'friendReceiver', + attributes: ['username', 'hashedId'], + include: [ + { + model: UserParam, + as: 'user_params', + required: false, + include: [ + { + model: UserParamType, + as: 'paramType', + required: true, + where: { description: 'gender' }, + attributes: ['description'], + }, + ], + attributes: ['value'], + }, + ], + }, + ], + }); + + const processedFriendships = await Promise.all( + friendships.map(async (friendship) => { + const isInitiator = friendship.user1Id === user.id; + const otherUser = isInitiator ? friendship.friendReceiver : friendship.friendSender; + const genderParam = otherUser.user_params?.find(param => param.paramType.description === 'gender'); + const gender = genderParam ? await this.getGender(genderParam) : null; + + return { + id: friendship.id, + user: { + username: otherUser.username, + hashedId: otherUser.hashedId, + gender, + }, + accepted: friendship.accepted, + denied: friendship.denied, + withdrawn: friendship.withdrawn, + isInitiator, + }; + }) + ); + + return processedFriendships; + } + + async getGender(genderParam) { + if (!this.genders) { + this.genders = {}; + } + if (this.genders[genderParam.value]) return this.genders[genderParam.value]; + const genderObject = await UserParamValue.findOne({ + where: { id: genderParam.value }, + }); + if (genderObject) { + this.genders[genderParam.value] = genderObject.value; + return genderObject.value; + } + return ''; + } +} + +export default new FriendshipService(); diff --git a/backend/services/settingsService.js b/backend/services/settingsService.js index 3ff3b63..edc36a8 100644 --- a/backend/services/settingsService.js +++ b/backend/services/settingsService.js @@ -156,7 +156,7 @@ class SettingsService extends BaseService{ include: [ { model: UserParamType, - as: 'user_param_type', + as: 'user_param_value_type', where: { description: type } } ] @@ -264,7 +264,6 @@ class SettingsService extends BaseService{ async updateVisibility(hashedUserId, userParamTypeId, visibilityId) { try { const user = await this.getUserByHashedId(hashedUserId); - console.log(JSON.stringify(user)); if (!user) { throw new Error('User not found'); } diff --git a/backend/services/socialnetworkService.js b/backend/services/socialnetworkService.js index d6d8827..5210c30 100644 --- a/backend/services/socialnetworkService.js +++ b/backend/services/socialnetworkService.js @@ -24,15 +24,22 @@ import DOMPurify from 'dompurify'; import sharp from 'sharp'; import Diary from '../models/community/diary.js'; import Friendship from '../models/community/friendship.js'; +import { getUserSession } from '../utils/redis.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); class SocialNetworkService extends BaseService { - async searchUsers({ username, ageFrom, ageTo, genders }) { + async searchUsers({ hashedUserId, username, ageFrom, ageTo, genders }) { const whereClause = this.buildSearchWhereClause(username); + const user = await this.loadUserByHash(hashedUserId); + console.log(hashedUserId, user); + if (!user) { + throw new Error('User not found'); + } + whereClause.id = { [Op.ne]: user.id }; const users = await User.findAll({ where: whereClause, include: this.getUserParamsInclude() }); - return this.filterUsersByCriteria(users, ageFrom, ageTo, genders); + return await this.filterUsersByCriteria(users, ageFrom, ageTo, genders); } async getProfile(hashedUserId, requestingUserId) { @@ -48,16 +55,18 @@ class SocialNetworkService extends BaseService { if (!user) { throw new Error('User not found'); } + console.log('given data', data, folderId); 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'); } + console.log('parentFolder', parentFolder); let newFolder; - if (folderId === 0) { + if (parentFolder) { newFolder = await Folder.create({ - parentId: data.parentId || null, + parentId: parentFolder.id || null, userId: user.id, name: data.name }); @@ -267,7 +276,8 @@ class SocialNetworkService extends BaseService { } async loadUserByHash(hashedId) { - return await User.findOne({ hashedId }); + console.log('Loading user by hashedId:', hashedId); + return await User.findOne({ where: { hashedId: hashedId } }); } async loadUserByName(userName) { @@ -300,10 +310,10 @@ class SocialNetworkService extends BaseService { ]; } - filterUsersByCriteria(users, ageFrom, ageTo, genders) { + async filterUsersByCriteria(users, ageFrom, ageTo, genders) { const results = []; for (const user of users) { - const userDetails = this.extractUserDetails(user); + const userDetails = await this.extractUserDetails(user); if (this.isUserValid(userDetails, ageFrom, ageTo, genders)) { results.push(userDetails); } @@ -311,11 +321,11 @@ class SocialNetworkService extends BaseService { return results; } - extractUserDetails(user) { + async extractUserDetails(user) { const birthdateParam = user.user_params.find(param => param.paramType.description === 'birthdate'); const genderParam = user.user_params.find(param => param.paramType.description === 'gender'); const age = birthdateParam ? this.calculateAge(birthdateParam.value) : null; - const gender = genderParam ? this.getGenderValue(genderParam.value) : null; + const gender = genderParam ? await this.getGenderValue(genderParam.value) : null; return { id: user.hashedId, username: user.username, @@ -735,8 +745,9 @@ class SocialNetworkService extends BaseService { } async addFriend(hashedUserid, friendUserid) { + console.log('--------', friendUserid, hashedUserid); const requestingUserId = await this.checkUserAccess(hashedUserid); - const friend = await this.loadUserByHash(friendUserid); + const friend = await User.findOne({ where: { hashedId: friendUserid } }); if (!friend) { throw new Error('notfound'); } @@ -748,10 +759,10 @@ class SocialNetworkService extends BaseService { ] } }); + console.log('friendship', friend, requestingUserId); if (friendship) { - if (friendship.withdrawn) { - friendship.withdrawn = false; - + if (friendship.withdrawn && friendship.user1Id === requestingUserId) { + friendship.update({ withdrawn: false }); } else { throw new Error('alreadyexists'); } @@ -811,5 +822,37 @@ class SocialNetworkService extends BaseService { throw new Error('notfound'); } } + + async getLoggedInFriends(hashedUserId) { + const userId = await this.checkUserAccess(hashedUserId); + const activeFriendships = await Friendship.findAll({ + where: { + accepted: true, + denied: false, + withdrawn: false, + [Op.or]: [ + { user1Id: userId }, + { user2Id: userId } + ] + } + }); + const friendIds = activeFriendships.map(friendship => + friendship.user1Id === userId ? friendship.user2Id : friendship.user1Id + ); + const loggedInFriends = []; + for (const friendId of friendIds) { + const session = await getUserSession(friendId); + if (session && session.id) { + const friend = await User.findOne({ where: { hashedId: session.id } }); + if (friend) { + loggedInFriends.push({ + id: friend.hashedId, + username: friend.username, + }); + } + } + } + return loggedInFriends; + } } export default SocialNetworkService; diff --git a/backend/services/webSocketService.js b/backend/services/webSocketService.js new file mode 100644 index 0000000..38bd600 --- /dev/null +++ b/backend/services/webSocketService.js @@ -0,0 +1,39 @@ +// services/webSocketService.js +import { Server } from 'socket.io'; +import amqp from 'amqplib/callback_api.js'; + +const RABBITMQ_URL = 'amqp://localhost'; +const QUEUE = 'chat_messages'; + +export function setupWebSocket(server) { + const io = new Server(server); + + amqp.connect(RABBITMQ_URL, (err, connection) => { + if (err) throw err; + + connection.createChannel((err, channel) => { + if (err) throw err; + + channel.assertQueue(QUEUE, { durable: false }); + + io.on('connection', (socket) => { + console.log('Client connected via WebSocket'); + + // Konsumiert Nachrichten aus RabbitMQ und sendet sie an den WebSocket-Client + channel.consume(QUEUE, (msg) => { + const message = JSON.parse(msg.content.toString()); + io.emit('newMessage', message); // Broadcast an alle Clients + }, { noAck: true }); + + // Empfangt eine Nachricht vom WebSocket-Client und sendet sie an die RabbitMQ-Warteschlange + socket.on('newMessage', (message) => { + channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(message))); + }); + + socket.on('disconnect', () => { + console.log('Client disconnected'); + }); + }); + }); + }); +} diff --git a/backend/utils/encryption.js b/backend/utils/encryption.js index d79da0e..8947ba3 100644 --- a/backend/utils/encryption.js +++ b/backend/utils/encryption.js @@ -1,22 +1,28 @@ import crypto from 'crypto'; -const algorithm = 'aes-256-ecb'; +const algorithm = 'aes-256-ecb'; + const key = crypto.scryptSync(process.env.SECRET_KEY, 'salt', 32); export const generateIv = () => { - return crypto.randomBytes(16); + return crypto.randomBytes(16).toString('base64'); }; export const encrypt = (text) => { - const cipher = crypto.createCipheriv(algorithm, key, null); - let encrypted = cipher.update(text, 'utf8', 'hex'); + const cipher = crypto.createCipheriv(algorithm, key, null); + let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); return encrypted; }; export const decrypt = (text) => { - const decipher = crypto.createDecipheriv(algorithm, key, null); - let decrypted = decipher.update(text, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); - return decrypted; + try { + const decipher = crypto.createDecipheriv(algorithm, key, null); + let decrypted = decipher.update(text, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + } catch (error) { + console.log(error); + return null; + } }; diff --git a/backend/utils/falukant/initializeFalukantTypes.js b/backend/utils/falukant/initializeFalukantTypes.js new file mode 100644 index 0000000..fc2b284 --- /dev/null +++ b/backend/utils/falukant/initializeFalukantTypes.js @@ -0,0 +1,81 @@ +import RegionData from "../../models/falukant/data/region.js"; +import RegionType from "../../models/falukant/type/region.js"; + +const regionTypes = []; +const regionTypeTrs = [ + "country", + "duchy", + "markgravate", + "shire", + "county", + "city" +]; +const regions = [ + { labelTr: "falukant", regionType: "country", parentTr: null }, + { labelTr: "duchy1", regionType: "duchy", parentTr: "falukant" }, + { labelTr: "markgravate", regionType: "markgravate", parentTr: "duchy1" }, + { labelTr: "shire1", regionType: "shire", parentTr: "markgravate" }, + { labelTr: "county1", regionType: "county", parentTr: "shire1" }, + { labelTr: "town1", regionType: "city", parentTr: "county1" }, + { labelTr: "town2", regionType: "city", parentTr: "county1" }, + { labelTr: "town3", regionType: "city", parentTr: "county1" }, + { labelTr: "town4", regionType: "city", parentTr: "county1" }, +]; + +// Step 1: Initialize region types +export const initializeFalukantTypes = async () => { + await initializeFalukantTypeRegions(); +} + +const initializeFalukantTypeRegions = async () => { + for (const regionType of regionTypeTrs) { + const [regionTypeRecord] = await RegionType.findOrCreate({ + where: { labelTr: regionType }, + defaults: { + parentId: regionTypes[regionTypes.length - 1]?.id + } + }); + + regionTypes.push(regionTypeRecord); + } +}; + +// Utility: Find region type object by region type +const findRegionTypeObjectByRegionType = async (regionType) => { + return regionTypes.find(region => region.labelTr === regionType) || null; +}; + +// Utility: Find region object by label +const findRegionObjectByLabelTr = async (labelTr) => { + return await RegionData.findOne({ where: { name: labelTr } }); +}; + +// Step 2: Initialize regions +export const initializeFalukantRegions = async () => { + for (const region of regions) { + const regionType = await findRegionTypeObjectByRegionType(region.regionType); + if (!regionType) { + console.error(`Region type not found for: ${region.regionType}`); + continue; + } + + const parentRegion = region.parentTr + ? await findRegionObjectByLabelTr(region.parentTr) + : null; + + if (region.parentTr && !parentRegion) { + console.error(`Parent region not found for: ${region.parentTr}`); + continue; + } + + console.log('Creating/Fetching Region:', region.labelTr, 'Type:', regionType.labelTr, 'Parent:', parentRegion?.name); + + await RegionData.findOrCreate({ + where: { name: region.labelTr }, + defaults: { + regionTypeId: regionType.id, + parentId: parentRegion?.id || null + } + }); + } +}; diff --git a/backend/utils/initializeFalukant.js b/backend/utils/initializeFalukant.js new file mode 100644 index 0000000..c984cb3 --- /dev/null +++ b/backend/utils/initializeFalukant.js @@ -0,0 +1,8 @@ +import { initializeFalukantTypes, initializeFalukantRegions } from './falukant/initializeFalukantTypes.js'; + +const initializeFalukant = async () => { + await initializeFalukantTypes(); + await initializeFalukantRegions(); +} + +export default initializeFalukant; \ No newline at end of file diff --git a/backend/utils/initializeTypes.js b/backend/utils/initializeTypes.js index 01471e6..80cbfa8 100644 --- a/backend/utils/initializeTypes.js +++ b/backend/utils/initializeTypes.js @@ -153,7 +153,7 @@ const initializeTypes = async () => { }; for (const key of Object.keys(interestsList)) { - try { + try { const value = interestsList[key]; const [item, created] = await Interest.findOrCreate({ where: { name: key }, diff --git a/backend/utils/redis.js b/backend/utils/redis.js index 5604c50..c46f862 100644 --- a/backend/utils/redis.js +++ b/backend/utils/redis.js @@ -16,38 +16,54 @@ redisClient.connect().catch(console.error); const setUserSession = async (userId, sessionData) => { try { - await redisClient.hSet(`user:${userId}`, sessionData); + await redisClient.sendCommand(['JSON.SET', `user:${userId}`, '.', JSON.stringify(sessionData)]); + console.log(userId, sessionData); + const sessionDataStr = await redisClient.sendCommand(['JSON.GET', `user:${userId}`]); + console.log(sessionDataStr); } catch (error) { console.error('Fehler beim Setzen der Benutzersitzung:', error); } -}; +}; const deleteUserSession = async (userId) => { try { - await redisClient.del(`user:${userId}`); + const result = await redisClient.del(`user:${userId}`); + if (result === 1) { + console.log(`Benutzersitzung für Benutzer ${userId} erfolgreich gelöscht.`); + } else { + console.warn(`Benutzersitzung für Benutzer ${userId} war nicht vorhanden.`); + } } catch (error) { console.error('Fehler beim Löschen der Benutzersitzung:', error); } }; +const convertToOriginalType = (value) => { + if (value === 'true') return true; + if (value === 'false') return false; + if (!isNaN(value) && value.trim() !== '') return Number(value); + return value; +}; + const getUserSession = async (userId) => { try { - return await redisClient.hGetAll(`user:${userId}`); + const sessionData = await redisClient.sendCommand(['JSON.GET', `user:${userId}`]); + return JSON.parse(sessionData); } catch (error) { console.error('Fehler beim Abrufen der Benutzersitzung:', error); return null; } }; -const updateUserTimestamp = async (hashedId) => { +const updateUserTimestamp = async (userId) => { try { - const userKey = `user:${hashedId}`; + const userKey = `user:${userId}`; const userExists = await redisClient.exists(userKey); if (userExists) { - await redisClient.hSet(userKey, 'timestamp', Date.now()); - console.log(`Zeitstempel für Benutzer ${hashedId} aktualisiert.`); - } else { - console.warn(`Benutzer mit der hashedId ${hashedId} wurde nicht gefunden.`); + const sessionDataString = await redisClient.sendCommand(['JSON.GET', `user:${userId}`]); + const sessionData = JSON.parse(sessionDataString); + sessionData.timestamp = Date.now(); + await redisClient.sendCommand(['JSON.SET', `user:${userId}`, '.', JSON.stringify(sessionData)]); } } catch (error) { console.error('Fehler beim Aktualisieren des Zeitstempels:', error); @@ -58,17 +74,29 @@ const cleanupExpiredSessions = async () => { try { const keys = await redisClient.keys('user:*'); const now = Date.now(); + for (const key of keys) { - const session = await redisClient.hGetAll(key); - if (session.timestamp && now - parseInt(session.timestamp) > EXPIRATION_TIME) { - const userId = key.split(':')[1]; - await redisClient.del(key); - await User.update({ authCode: '' }, { where: { hashedId: userId } }); - console.log(`Abgelaufene Sitzung für Benutzer ${userId} gelöscht.`); + try { + const sessionStr = await redisClient.sendCommand(['JSON.GET', key]); + if (sessionStr) { + const session = JSON.parse(sessionStr); + if (session.timestamp && now - parseInt(session.timestamp) > EXPIRATION_TIME) { + const userId = key.split(':')[1]; + await redisClient.del(key); + await User.update({ authCode: '' }, { where: { hashedId: userId } }); + console.log(`Abgelaufene Sitzung für Benutzer ${userId} mit RedisJSON gelöscht.`); + } + } + } catch (error) { + if (error.message.includes('WRONGTYPE')) { + console.warn(`Schlüssel ${key} ist kein JSON-Objekt, wird übersprungen.`); + } else { + console.error(`Fehler beim Bereinigen für Schlüssel ${key}:`, error); + } } } } catch (error) { - console.error('Fehler beim Bereinigen abgelaufener Sitzungen:', error); + console.error('Fehler beim Bereinigen abgelaufener Sitzungen mit RedisJSON:', error); } }; diff --git a/backend/utils/sequelize.js b/backend/utils/sequelize.js index 0739a8a..919ad91 100644 --- a/backend/utils/sequelize.js +++ b/backend/utils/sequelize.js @@ -17,6 +17,8 @@ const createSchemas = async () => { await sequelize.query('CREATE SCHEMA IF NOT EXISTS type'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS service'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS forum'); + await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_data'); + await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_type'); }; const initializeDatabase = async () => { diff --git a/backend/utils/socket.js b/backend/utils/socket.js new file mode 100644 index 0000000..996a85b --- /dev/null +++ b/backend/utils/socket.js @@ -0,0 +1,73 @@ +import { Server } from 'socket.io'; +import BaseService from '../services/BaseService.js'; + +const baseService = new BaseService(); + +let io; +const userSockets = {}; + +export function setupWebSocket(server) { + io = new Server(server, { + cors: { + origin: '*', + }, + }); + + io.on('connection', (socket) => { + socket.on('setUserId', (userId) => { + if (userId) { + socket.userId = userId; + userSockets[userId] = socket.id; + } + }); + socket.on('disconnect', () => { + if (socket.userId) { + delete userSockets[socket.userId]; + } + }); + }); +} + +export function getIo() { + if (!io) { + throw new Error('Socket.io ist nicht initialisiert!'); + } + return io; +} + +export function getUserSockets() { + return userSockets; +} + +export async function notifyUser(recipientHashedUserId, event, data) { + const io = getIo(); + const userSockets = getUserSockets(); + + try { + const recipientUser = await baseService.getUserByHashedId(recipientHashedUserId); + if (recipientUser) { + const socketId = userSockets[recipientUser.hashedId]; + if (socketId) { + io.to(socketId).emit(event, data); + } + } else { + console.log(`Benutzer mit gehashter ID ${recipientHashedUserId} nicht gefunden.`); + } + } catch (err) { + console.error('Fehler beim Senden der Benachrichtigung:', err); + } +} + +export async function notifyAllUsers(event, data) { + const io = getIo(); + const userSockets = getUserSockets(); + + try { + for (const [userId, socketId] of Object.entries(userSockets)) { + io.to(socketId).emit(event, data); + console.log(`Benachrichtigung an Benutzer mit ID ${userId} gesendet.`); + } + } catch (err) { + console.error('Fehler beim Senden der Benachrichtigung an alle Benutzer:', err); + } +} \ No newline at end of file diff --git a/backend/utils/syncDatabase.js b/backend/utils/syncDatabase.js index f4adfb5..f441b06 100644 --- a/backend/utils/syncDatabase.js +++ b/backend/utils/syncDatabase.js @@ -5,6 +5,7 @@ import initializeTypes from './initializeTypes.js'; import initializeSettings from './initializeSettings.js'; import initializeUserRights from './initializeUserRights.js'; import initializeImageTypes from './initializeImageTypes.js'; +import initializeFalukant from './initializeFalukant.js'; import setupAssociations from '../models/associations.js'; import models from '../models/index.js'; import { createTriggers } from '../models/trigger.js'; @@ -13,32 +14,35 @@ import initializeForum from './initializeForum.js'; const syncDatabase = async () => { try { console.log("Initializing database schemas..."); - await initializeDatabase(); // Stellt sicher, dass alle Schemas erstellt sind + await initializeDatabase(); console.log("Synchronizing models..."); - await syncModels(models); // Modelle synchronisieren + await syncModels(models); console.log("Setting up associations..."); - setupAssociations(); // Assoziationen definieren + setupAssociations(); console.log("Creating triggers..."); - await createTriggers(); // Trigger erstellen + await createTriggers(); console.log("Initializing settings..."); - await initializeSettings(); // Einstellungsdaten initialisieren + await initializeSettings(); console.log("Initializing types..."); - await initializeTypes(); // Typen initialisieren + await initializeTypes(); console.log("Initializing user rights..."); - await initializeUserRights(); // Benutzerrechte initialisieren + await initializeUserRights(); console.log("Initializing image types..."); - await initializeImageTypes(); // Bildtypen initialisieren + await initializeImageTypes(); console.log("Initializing forums..."); - await initializeForum(); // Foren initialisieren + await initializeForum(); + console.log("Initializing Falukant..."); + await initializeFalukant(); + console.log('Database synchronization complete.'); } catch (error) { console.error('Unable to synchronize the database:', error); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b3cc9d8..4441bac 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,11 +12,14 @@ "axios": "^1.7.2", "date-fns": "^3.6.0", "dotenv": "^16.4.5", + "mitt": "^3.0.1", + "socket.io-client": "^4.8.1", "tinymce": "^7.3.0", "vue": "~3.4.31", "vue-i18n": "^10.0.0-beta.2", "vue-multiselect": "^3.1.0", "vue-router": "^4.0.13", + "vuetify": "^3.7.4", "vuex": "^4.1.0" }, "devDependencies": { @@ -464,12 +467,12 @@ } }, "node_modules/@intlify/core-base": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.1.tgz", - "integrity": "sha512-6kpRGjhos95ph7QmEtP4tnWFTW102s71CLQAQwfsIGqOAcoJhzcYFpzIQ0gKXzqAIXsMD/hwM5qJ4ewqMHw3gg==", + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.5.tgz", + "integrity": "sha512-F3snDTQs0MdvnnyzTDTVkOYVAZOE/MHwRvF7mn7Jw1yuih4NrFYLNYIymGlLmq4HU2iIdzYsZ7f47bOcwY73XQ==", "dependencies": { - "@intlify/message-compiler": "10.0.1", - "@intlify/shared": "10.0.1" + "@intlify/message-compiler": "10.0.5", + "@intlify/shared": "10.0.5" }, "engines": { "node": ">= 16" @@ -479,11 +482,11 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.1.tgz", - "integrity": "sha512-fPeykrcgVT5eOIlshTHiPCN8FV3AZyBOdMS3XaXzfQ6eL5wqfc29I/EdIv5YXVW5X8e/BgYeWjBC0Cuznsl/2g==", + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.5.tgz", + "integrity": "sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==", "dependencies": { - "@intlify/shared": "10.0.1", + "@intlify/shared": "10.0.5", "source-map-js": "^1.0.2" }, "engines": { @@ -494,9 +497,9 @@ } }, "node_modules/@intlify/shared": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.1.tgz", - "integrity": "sha512-b4h7IWdZl710DnAhET8lgfgZ4Y9A2IZx/gbli3Ec/zHtYCoPqLHmiM7kUNBrSZj7d/SSjcMMZHuz5I09x3PYZw==", + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.5.tgz", + "integrity": "sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==", "engines": { "node": ">= 16" }, @@ -510,213 +513,262 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz", - "integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.26.0.tgz", + "integrity": "sha512-gJNwtPDGEaOEgejbaseY6xMFu+CPltsc8/T+diUTTbOQLqD+bnrJq9ulH6WD69TqwqWmrfRAtUv30cCFZlbGTQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz", - "integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.26.0.tgz", + "integrity": "sha512-YJa5Gy8mEZgz5JquFruhJODMq3lTHWLm1fOy+HIANquLzfIOzE9RA5ie3JjCdVb9r46qfAQY/l947V0zfGJ0OQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz", - "integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.26.0.tgz", + "integrity": "sha512-ErTASs8YKbqTBoPLp/kA1B1Um5YSom8QAc4rKhg7b9tyyVqDBlQxy7Bf2wW7yIlPGPg2UODDQcbkTlruPzDosw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz", - "integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.26.0.tgz", + "integrity": "sha512-wbgkYDHcdWW+NqP2mnf2NOuEbOLzDblalrOWcPyY6+BRbVhliavon15UploG7PpBRQ2bZJnbmh8o3yLoBvDIHA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.26.0.tgz", + "integrity": "sha512-Y9vpjfp9CDkAG4q/uwuhZk96LP11fBz/bYdyg9oaHYhtGZp7NrbkQrj/66DYMMP2Yo/QPAsVHkV891KyO52fhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.26.0.tgz", + "integrity": "sha512-A/jvfCZ55EYPsqeaAt/yDAG4q5tt1ZboWMHEvKAH9Zl92DWvMIbnZe/f/eOXze65aJaaKbL+YeM0Hz4kLQvdwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz", - "integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.26.0.tgz", + "integrity": "sha512-paHF1bMXKDuizaMODm2bBTjRiHxESWiIyIdMugKeLnjuS1TCS54MF5+Y5Dx8Ui/1RBPVRE09i5OUlaLnv8OGnA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz", - "integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.26.0.tgz", + "integrity": "sha512-cwxiHZU1GAs+TMxvgPfUDtVZjdBdTsQwVnNlzRXC5QzIJ6nhfB4I1ahKoe9yPmoaA/Vhf7m9dB1chGPpDRdGXg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz", - "integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.26.0.tgz", + "integrity": "sha512-4daeEUQutGRCW/9zEo8JtdAgtJ1q2g5oHaoQaZbMSKaIWKDQwQ3Yx0/3jJNmpzrsScIPtx/V+1AfibLisb3AMQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz", - "integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.26.0.tgz", + "integrity": "sha512-eGkX7zzkNxvvS05ROzJ/cO/AKqNvR/7t1jA3VZDi2vRniLKwAWxUr85fH3NsvtxU5vnUUKFHKh8flIBdlo2b3Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz", - "integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.26.0.tgz", + "integrity": "sha512-Odp/lgHbW/mAqw/pU21goo5ruWsytP7/HCC/liOt0zcGG0llYWKrd10k9Fj0pdj3prQ63N5yQLCLiE7HTX+MYw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz", - "integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.26.0.tgz", + "integrity": "sha512-MBR2ZhCTzUgVD0OJdTzNeF4+zsVogIR1U/FsyuFerwcqjZGvg2nYe24SAHp8O5sN8ZkRVbHwlYeHqcSQ8tcYew==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz", - "integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.26.0.tgz", + "integrity": "sha512-YYcg8MkbN17fMbRMZuxwmxWqsmQufh3ZJFxFGoHjrE7bv0X+T6l3glcdzd7IKLiwhT+PZOJCblpnNlz1/C3kGQ==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz", - "integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.26.0.tgz", + "integrity": "sha512-ZuwpfjCwjPkAOxpjAEjabg6LRSfL7cAJb6gSQGZYjGhadlzKKywDkCUnJ+KEfrNY1jH5EEoSIKLCb572jSiglA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz", - "integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.26.0.tgz", + "integrity": "sha512-+HJD2lFS86qkeF8kNu0kALtifMpPCZU80HvwztIKnYwym3KnA1os6nsX4BGSTLtS2QVAGG1P3guRgsYyMA0Yhg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz", - "integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.26.0.tgz", + "integrity": "sha512-WUQzVFWPSw2uJzX4j6YEbMAiLbs0BUysgysh8s817doAYhR5ybqTI1wtKARQKo6cGop3pHnrUJPFCsXdoFaimQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz", - "integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.26.0.tgz", + "integrity": "sha512-D4CxkazFKBfN1akAIY6ieyOqzoOoBV1OICxgUblWxff/pSjCA2khXlASUx7mK6W1oP4McqhgcCsu6QaLj3WMWg==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz", - "integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.26.0.tgz", + "integrity": "sha512-2x8MO1rm4PGEP0xWbubJW5RtbNLk3puzAMaLQd3B3JHVw4KcHlmXcO+Wewx9zCoo7EUFiMlu/aZbCJ7VjMzAag==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@tinymce/tinymce-vue": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@tinymce/tinymce-vue/-/tinymce-vue-6.0.1.tgz", @@ -729,10 +781,11 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" }, "node_modules/@vitejs/plugin-vue": { "version": "5.1.3", @@ -991,6 +1044,22 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1044,6 +1113,26 @@ "url": "https://dotenvx.com" } }, + "node_modules/engine.io-client": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz", + "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1468,6 +1557,17 @@ "node": ">= 0.6" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -1622,12 +1722,13 @@ } }, "node_modules/rollup": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz", - "integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.26.0.tgz", + "integrity": "sha512-ilcl12hnWonG8f+NxU6BlgysVA0gvY2l8N0R84S1HcINbW20bvwuCngJkkInV6LXhwRpucsW5k1ovDwEdBVrNg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -1637,22 +1738,24 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.3", - "@rollup/rollup-android-arm64": "4.21.3", - "@rollup/rollup-darwin-arm64": "4.21.3", - "@rollup/rollup-darwin-x64": "4.21.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.3", - "@rollup/rollup-linux-arm-musleabihf": "4.21.3", - "@rollup/rollup-linux-arm64-gnu": "4.21.3", - "@rollup/rollup-linux-arm64-musl": "4.21.3", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.3", - "@rollup/rollup-linux-riscv64-gnu": "4.21.3", - "@rollup/rollup-linux-s390x-gnu": "4.21.3", - "@rollup/rollup-linux-x64-gnu": "4.21.3", - "@rollup/rollup-linux-x64-musl": "4.21.3", - "@rollup/rollup-win32-arm64-msvc": "4.21.3", - "@rollup/rollup-win32-ia32-msvc": "4.21.3", - "@rollup/rollup-win32-x64-msvc": "4.21.3", + "@rollup/rollup-android-arm-eabi": "4.26.0", + "@rollup/rollup-android-arm64": "4.26.0", + "@rollup/rollup-darwin-arm64": "4.26.0", + "@rollup/rollup-darwin-x64": "4.26.0", + "@rollup/rollup-freebsd-arm64": "4.26.0", + "@rollup/rollup-freebsd-x64": "4.26.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.26.0", + "@rollup/rollup-linux-arm-musleabihf": "4.26.0", + "@rollup/rollup-linux-arm64-gnu": "4.26.0", + "@rollup/rollup-linux-arm64-musl": "4.26.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.26.0", + "@rollup/rollup-linux-riscv64-gnu": "4.26.0", + "@rollup/rollup-linux-s390x-gnu": "4.26.0", + "@rollup/rollup-linux-x64-gnu": "4.26.0", + "@rollup/rollup-linux-x64-musl": "4.26.0", + "@rollup/rollup-win32-arm64-msvc": "4.26.0", + "@rollup/rollup-win32-ia32-msvc": "4.26.0", + "@rollup/rollup-win32-x64-msvc": "4.26.0", "fsevents": "~2.3.2" } }, @@ -1761,6 +1864,32 @@ "node": ">= 0.4" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1919,12 +2048,12 @@ } }, "node_modules/vue-i18n": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.1.tgz", - "integrity": "sha512-SQVlSm/1S6AaG1wexvwq3ebXUrrkx75ZHD78UAs4/rYD/X3tsQxfm6ElpT4ZPegJQEgRtOJjGripqSrfqAENtg==", + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.5.tgz", + "integrity": "sha512-9/gmDlCblz3i8ypu/afiIc/SUIfTTE1mr0mZhb9pk70xo2csHAM9mp2gdQ3KD2O0AM3Hz/5ypb+FycTj/lHlPQ==", "dependencies": { - "@intlify/core-base": "10.0.1", - "@intlify/shared": "10.0.1", + "@intlify/core-base": "10.0.5", + "@intlify/shared": "10.0.5", "@vue/devtools-api": "^6.5.0" }, "engines": { @@ -1960,6 +2089,35 @@ "vue": "^3.2.0" } }, + "node_modules/vuetify": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.4.tgz", + "integrity": "sha512-Y8UU5wUDQXC3oz2uumPb8IOdvB4XMCxtxnmqdOc+LihNuPlkSgxIwf92ndRzbOtJFKHsggFUxpyLqpQp+A+5kg==", + "engines": { + "node": "^12.20 || >=14.13" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/johnleider" + }, + "peerDependencies": { + "typescript": ">=4.7", + "vite-plugin-vuetify": ">=1.0.0", + "vue": "^3.3.0", + "webpack-plugin-vuetify": ">=2.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vite-plugin-vuetify": { + "optional": true + }, + "webpack-plugin-vuetify": { + "optional": true + } + } + }, "node_modules/vuex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", @@ -1989,6 +2147,34 @@ "funding": { "url": "https://github.com/sponsors/ljharb" } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 97e1bb6..40b75f0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,11 +12,14 @@ "axios": "^1.7.2", "date-fns": "^3.6.0", "dotenv": "^16.4.5", + "mitt": "^3.0.1", + "socket.io-client": "^4.8.1", "tinymce": "^7.3.0", "vue": "~3.4.31", "vue-i18n": "^10.0.0-beta.2", "vue-multiselect": "^3.1.0", "vue-router": "^4.0.13", + "vuetify": "^3.7.4", "vuex": "^4.1.0" }, "devDependencies": { diff --git a/frontend/src/api/friendshipApi.js b/frontend/src/api/friendshipApi.js new file mode 100644 index 0000000..eaa9777 --- /dev/null +++ b/frontend/src/api/friendshipApi.js @@ -0,0 +1,22 @@ +import apiClient from "@/utils/axios.js"; + +export const getFriendships = async (acceptedOnly) => { + const response = await apiClient.get(`/api/friendships?acceptedOnly=${acceptedOnly}`); + return response.data; +}; + +export const endFriendship = async (friendUserId) => { + await apiClient.post("/api/friendships/end", { friendUserId }); +}; + +export const acceptFriendship = async (friendUserId) => { + await apiClient.post("/api/friendships/accept", { friendUserId }); +}; + +export const rejectFriendship = async (friendUserId) => { + await apiClient.post("/api/friendships/reject", { friendUserId }); +}; + +export const withdrawRequest = async (friendUserId) => { + await apiClient.post("/api/friendships/withdraw", { friendUserId }); +}; diff --git a/frontend/src/assets/styles.scss b/frontend/src/assets/styles.scss index ad3ddd3..56a7d4b 100644 --- a/frontend/src/assets/styles.scss +++ b/frontend/src/assets/styles.scss @@ -92,4 +92,24 @@ span.button:hover { background: #fdf1db; color: #7E471B; border: 1px solid #7E471B; +} + +.font-color-gender-male { + color: #1E90FF; +} + +.font-color-gender-female { + color: #FF69B4; +} + +.font-color-gender-transmale { + color: #00CED1; +} + +.font-color-gender-transfemale { + color: #FFB6C1; +} + +.font-color-gender-nonbinary { + color: #DAA520; } \ No newline at end of file diff --git a/frontend/src/components/AppNavigation.vue b/frontend/src/components/AppNavigation.vue index 1d4855d..455fd1c 100644 --- a/frontend/src/components/AppNavigation.vue +++ b/frontend/src/components/AppNavigation.vue @@ -1,7 +1,8 @@