diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js index cc3d99a..27af914 100644 --- a/backend/controllers/adminController.js +++ b/backend/controllers/adminController.js @@ -23,6 +23,17 @@ class AdminController { this.getRooms = this.getRooms.bind(this); this.createRoom = this.createRoom.bind(this); this.deleteRoom = this.deleteRoom.bind(this); + + // User administration + this.searchUsers = this.searchUsers.bind(this); + this.getUser = this.getUser.bind(this); + this.updateUser = this.updateUser.bind(this); + + // Rights + this.listRightTypes = this.listRightTypes.bind(this); + this.listUserRights = this.listUserRights.bind(this); + this.addUserRight = this.addUserRight.bind(this); + this.removeUserRight = this.removeUserRight.bind(this); } async getOpenInterests(req, res) { @@ -35,6 +46,93 @@ class AdminController { } } + // --- User Administration --- + async searchUsers(req, res) { + try { + const { userid: requester } = req.headers; + const { q } = req.query; + const result = await AdminService.searchUsers(requester, q || ''); + res.status(200).json(result); + } catch (error) { + const status = error.message === 'noaccess' ? 403 : 500; + res.status(status).json({ error: error.message }); + } + } + + async getUser(req, res) { + try { + const { userid: requester } = req.headers; + const { id } = req.params; + const result = await AdminService.getUserByHashedId(requester, id); + res.status(200).json(result); + } catch (error) { + const status = error.message === 'noaccess' ? 403 : (error.message === 'notfound' ? 404 : 500); + res.status(status).json({ error: error.message }); + } + } + + async updateUser(req, res) { + try { + const { userid: requester } = req.headers; + const { id } = req.params; + const result = await AdminService.updateUser(requester, id, req.body || {}); + res.status(200).json(result); + } catch (error) { + const status = error.message === 'noaccess' ? 403 : (error.message === 'notfound' ? 404 : 500); + res.status(status).json({ error: error.message }); + } + } + + // --- Rights --- + async listRightTypes(req, res) { + try { + const { userid: requester } = req.headers; + const types = await AdminService.listUserRightTypes(requester); + res.status(200).json(types); + } catch (error) { + const status = error.message === 'noaccess' ? 403 : 500; + res.status(status).json({ error: error.message }); + } + } + + async listUserRights(req, res) { + try { + const { userid: requester } = req.headers; + const { id } = req.params; + const rights = await AdminService.listUserRightsForUser(requester, id); + res.status(200).json(rights); + } catch (error) { + const status = error.message === 'noaccess' ? 403 : (error.message === 'notfound' ? 404 : 500); + res.status(status).json({ error: error.message }); + } + } + + async addUserRight(req, res) { + try { + const { userid: requester } = req.headers; + const { id } = req.params; + const { rightTypeId } = req.body || {}; + const result = await AdminService.addUserRight(requester, id, rightTypeId); + res.status(201).json({ status: 'ok' }); + } catch (error) { + const status = error.message === 'noaccess' ? 403 : (error.message === 'notfound' || error.message === 'wrongtype' ? 404 : 500); + res.status(status).json({ error: error.message }); + } + } + + async removeUserRight(req, res) { + try { + const { userid: requester } = req.headers; + const { id } = req.params; + const { rightTypeId } = req.body || {}; + await AdminService.removeUserRight(requester, id, rightTypeId); + res.status(204).send(); + } catch (error) { + const status = error.message === 'noaccess' ? 403 : (error.message === 'notfound' ? 404 : 500); + res.status(status).json({ error: error.message }); + } + } + async changeInterest(req, res) { try { const { userid: userId } = req.headers; diff --git a/backend/routers/adminRouter.js b/backend/routers/adminRouter.js index 0dd8707..deea3e6 100644 --- a/backend/routers/adminRouter.js +++ b/backend/routers/adminRouter.js @@ -15,6 +15,17 @@ router.post('/chat/rooms', authenticate, adminController.createRoom); router.put('/chat/rooms/:id', authenticate, adminController.updateRoom); router.delete('/chat/rooms/:id', authenticate, adminController.deleteRoom); +// --- Users Admin --- +router.get('/users/search', authenticate, adminController.searchUsers); +router.get('/users/:id', authenticate, adminController.getUser); +router.put('/users/:id', authenticate, adminController.updateUser); + +// --- Rights Admin --- +router.get('/rights/types', authenticate, adminController.listRightTypes); +router.get('/rights/:id', authenticate, adminController.listUserRights); +router.post('/rights/:id', authenticate, adminController.addUserRight); +router.delete('/rights/:id', authenticate, adminController.removeUserRight); + router.get('/interests/open', authenticate, adminController.getOpenInterests); router.post('/interest', authenticate, adminController.changeInterest); router.post('/interest/translation', authenticate, adminController.changeTranslation); diff --git a/backend/services/adminService.js b/backend/services/adminService.js index 15830f4..0025037 100644 --- a/backend/services/adminService.js +++ b/backend/services/adminService.js @@ -409,6 +409,103 @@ class AdminService { await character.save(); } + // --- User Administration --- + async searchUsers(requestingHashedUserId, query) { + if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) { + throw new Error('noaccess'); + } + if (!query || query.trim().length === 0) return []; + + const users = await User.findAll({ + where: { + [Op.or]: [ + { username: { [Op.iLike]: `%${query}%` } }, + // email is encrypted, can't search directly reliably; skip email search + ] + }, + attributes: ['id', 'hashedId', 'username', 'active', 'registrationDate'] + }); + return users.map(u => ({ id: u.hashedId, username: u.username, active: u.active, registrationDate: u.registrationDate })); + } + + async getUserByHashedId(requestingHashedUserId, targetHashedId) { + if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) { + throw new Error('noaccess'); + } + const user = await User.findOne({ + where: { hashedId: targetHashedId }, + attributes: ['id', 'hashedId', 'username', 'active', 'registrationDate'] + }); + if (!user) throw new Error('notfound'); + return { id: user.hashedId, username: user.username, active: user.active, registrationDate: user.registrationDate }; + } + + async updateUser(requestingHashedUserId, targetHashedId, data) { + if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) { + throw new Error('noaccess'); + } + const user = await User.findOne({ where: { hashedId: targetHashedId } }); + if (!user) throw new Error('notfound'); + + const updates = {}; + if (typeof data.username === 'string' && data.username.trim().length > 0) { + updates.username = data.username.trim(); + } + if (typeof data.active === 'boolean') { + updates.active = data.active; + } + if (Object.keys(updates).length === 0) return { id: user.hashedId, username: user.username, active: user.active }; + await user.update(updates); + return { id: user.hashedId, username: user.username, active: user.active }; + } + + // --- User Rights Administration --- + async listUserRightTypes(requestingHashedUserId) { + if (!(await this.hasUserAccess(requestingHashedUserId, 'rights'))) { + throw new Error('noaccess'); + } + const types = await UserRightType.findAll({ attributes: ['id', 'title'] }); + // map to tr keys if needed; keep title as key used elsewhere + return types.map(t => ({ id: t.id, title: t.title })); + } + + async listUserRightsForUser(requestingHashedUserId, targetHashedId) { + if (!(await this.hasUserAccess(requestingHashedUserId, 'rights'))) { + throw new Error('noaccess'); + } + const user = await User.findOne({ where: { hashedId: targetHashedId }, attributes: ['id', 'hashedId', 'username'] }); + if (!user) throw new Error('notfound'); + const rights = await UserRight.findAll({ + where: { userId: user.id }, + include: [{ model: UserRightType, as: 'rightType' }] + }); + return rights.map(r => ({ rightTypeId: r.rightTypeId, title: r.rightType?.title })); + } + + async addUserRight(requestingHashedUserId, targetHashedId, rightTypeId) { + if (!(await this.hasUserAccess(requestingHashedUserId, 'rights'))) { + throw new Error('noaccess'); + } + const user = await User.findOne({ where: { hashedId: targetHashedId } }); + if (!user) throw new Error('notfound'); + const type = await UserRightType.findByPk(rightTypeId); + if (!type) throw new Error('wrongtype'); + const existing = await UserRight.findOne({ where: { userId: user.id, rightTypeId } }); + if (existing) return existing; // idempotent + const created = await UserRight.create({ userId: user.id, rightTypeId }); + return created; + } + + async removeUserRight(requestingHashedUserId, targetHashedId, rightTypeId) { + if (!(await this.hasUserAccess(requestingHashedUserId, 'rights'))) { + throw new Error('noaccess'); + } + const user = await User.findOne({ where: { hashedId: targetHashedId } }); + if (!user) throw new Error('notfound'); + await UserRight.destroy({ where: { userId: user.id, rightTypeId } }); + return true; + } + // --- Chat Room Admin --- async getRoomTypes(userId) { if (!(await this.hasUserAccess(userId, 'chatrooms'))) { diff --git a/frontend/src/components/admin/AdminUserSearch.vue b/frontend/src/components/admin/AdminUserSearch.vue new file mode 100644 index 0000000..2c6e39b --- /dev/null +++ b/frontend/src/components/admin/AdminUserSearch.vue @@ -0,0 +1,41 @@ + + + + + + + diff --git a/frontend/src/i18n/locales/de/admin.json b/frontend/src/i18n/locales/de/admin.json index 6aa84b5..44f5160 100644 --- a/frontend/src/i18n/locales/de/admin.json +++ b/frontend/src/i18n/locales/de/admin.json @@ -23,6 +23,18 @@ "editcontactrequest": { "title": "[Admin] - Kontaktanfrage bearbeiten" }, + "user": { + "name": "Benutzername", + "active": "Aktiv", + "blocked": "Gesperrt", + "actions": "Aktionen", + "search": "Suchen" + }, + "rights": { + "add": "Recht hinzufügen", + "select": "Bitte wählen", + "current": "Aktuelle Rechte" + }, "forum": { "title": "[Admin] - Forum", "currentForums": "Existierende Foren", diff --git a/frontend/src/i18n/locales/en/admin.json b/frontend/src/i18n/locales/en/admin.json index dcaf06d..fa3060f 100644 --- a/frontend/src/i18n/locales/en/admin.json +++ b/frontend/src/i18n/locales/en/admin.json @@ -1,5 +1,17 @@ { "admin": { + "user": { + "name": "Username", + "active": "Active", + "blocked": "Blocked", + "actions": "Actions", + "search": "Search" + }, + "rights": { + "add": "Add right", + "select": "Please select", + "current": "Current rights" + }, "match3": { "title": "Manage Match3 Levels", "newLevel": "Create New Level", diff --git a/frontend/src/router/adminRoutes.js b/frontend/src/router/adminRoutes.js index 99c3482..a5c89e9 100644 --- a/frontend/src/router/adminRoutes.js +++ b/frontend/src/router/adminRoutes.js @@ -1,9 +1,11 @@ import AdminInterestsView from '../views/admin/InterestsView.vue'; import AdminContactsView from '../views/admin/ContactsView.vue'; import RoomsView from '../views/admin/RoomsView.vue'; +import UserRightsView from '../views/admin/UserRightsView.vue'; import ForumAdminView from '../dialogues/admin/ForumAdminView.vue'; import AdminFalukantEditUserView from '../views/admin/falukant/EditUserView.vue'; import AdminMinigamesView from '../views/admin/MinigamesView.vue'; +import AdminUsersView from '../views/admin/UsersView.vue'; const adminRoutes = [ { @@ -12,12 +14,24 @@ const adminRoutes = [ component: AdminInterestsView, meta: { requiresAuth: true } }, + { + path: '/admin/users', + name: 'AdminUsers', + component: AdminUsersView, + meta: { requiresAuth: true } + }, { path: '/admin/contacts', name: 'AdminContacts', component: AdminContactsView, meta: { requiresAuth: true } }, + { + path: '/admin/rights', + name: 'AdminUserRights', + component: UserRightsView, + meta: { requiresAuth: true } + }, { path: '/admin/forum', name: 'AdminForums', diff --git a/frontend/src/views/admin/UserRightsView.vue b/frontend/src/views/admin/UserRightsView.vue new file mode 100644 index 0000000..fe376cb --- /dev/null +++ b/frontend/src/views/admin/UserRightsView.vue @@ -0,0 +1,87 @@ + + + + + + + diff --git a/frontend/src/views/admin/UsersView.vue b/frontend/src/views/admin/UsersView.vue new file mode 100644 index 0000000..b056fa1 --- /dev/null +++ b/frontend/src/views/admin/UsersView.vue @@ -0,0 +1,68 @@ + + + + + + +