Implement permission management and enhance user interface for permissions in the application

Add new permission routes and integrate permission checks across various existing routes to ensure proper access control. Update the UserClub model to include role and permissions fields, allowing for more granular user access management. Enhance the frontend by introducing a user dropdown menu for managing permissions and displaying relevant options based on user roles. Improve the overall user experience by implementing permission-based visibility for navigation links and actions throughout the application.
This commit is contained in:
Torsten Schulz (local)
2025-10-17 09:44:10 +02:00
parent 2dd5e28cbc
commit 56f0ce2f27
31 changed files with 2854 additions and 92 deletions

View File

@@ -25,7 +25,7 @@ export const addClub = async (req, res) => {
}
const newClub = await ClubService.createClub(clubName);
await ClubService.addUserToClub(user.id, newClub.id);
await ClubService.addUserToClub(user.id, newClub.id, true); // true = isOwner
res.status(200).json(newClub);
} catch (error) {
console.error('[addClub] - error:', error);

View File

@@ -0,0 +1,152 @@
import permissionService from '../services/permissionService.js';
/**
* Get user's permissions for a club
*/
export const getUserPermissions = async (req, res) => {
try {
const { clubId } = req.params;
const userId = req.user.id;
const permissions = await permissionService.getUserClubPermissions(userId, parseInt(clubId));
if (!permissions) {
return res.status(404).json({ error: 'Keine Berechtigungen gefunden' });
}
res.json(permissions);
} catch (error) {
console.error('Error getting user permissions:', error);
res.status(500).json({ error: 'Fehler beim Abrufen der Berechtigungen' });
}
};
/**
* Get all club members with their permissions
*/
export const getClubMembersWithPermissions = async (req, res) => {
try {
const { clubId } = req.params;
const userId = req.user.id;
const members = await permissionService.getClubMembersWithPermissions(
parseInt(clubId),
userId
);
res.json(members);
} catch (error) {
console.error('Error getting club members with permissions:', error);
if (error.message === 'Keine Berechtigung zum Anzeigen von Berechtigungen') {
return res.status(403).json({ error: error.message });
}
res.status(500).json({ error: 'Fehler beim Abrufen der Mitglieder' });
}
};
/**
* Update user role
*/
export const updateUserRole = async (req, res) => {
try {
const { clubId, userId: targetUserId } = req.params;
const { role } = req.body;
const updatingUserId = req.user.id;
const result = await permissionService.setUserRole(
parseInt(targetUserId),
parseInt(clubId),
role,
updatingUserId
);
res.json(result);
} catch (error) {
console.error('Error updating user role:', error);
res.status(400).json({ error: error.message });
}
};
/**
* Update user custom permissions
*/
export const updateUserPermissions = async (req, res) => {
try {
const { clubId, userId: targetUserId } = req.params;
const { permissions } = req.body;
const updatingUserId = req.user.id;
const result = await permissionService.setCustomPermissions(
parseInt(targetUserId),
parseInt(clubId),
permissions,
updatingUserId
);
res.json(result);
} catch (error) {
console.error('Error updating user permissions:', error);
res.status(400).json({ error: error.message });
}
};
/**
* Get available roles
*/
export const getAvailableRoles = async (req, res) => {
try {
const roles = permissionService.getAvailableRoles();
res.json(roles);
} catch (error) {
console.error('Error getting available roles:', error);
res.status(500).json({ error: 'Fehler beim Abrufen der Rollen' });
}
};
/**
* Get permission structure
*/
export const getPermissionStructure = async (req, res) => {
try {
const structure = permissionService.getPermissionStructure();
res.json(structure);
} catch (error) {
console.error('Error getting permission structure:', error);
res.status(500).json({ error: 'Fehler beim Abrufen der Berechtigungsstruktur' });
}
};
/**
* Update user status (activate/deactivate)
*/
export const updateUserStatus = async (req, res) => {
try {
const { clubId, userId: targetUserId } = req.params;
const { approved } = req.body;
const updatingUserId = req.user.id;
const result = await permissionService.setUserStatus(
parseInt(targetUserId),
parseInt(clubId),
approved,
updatingUserId
);
res.json(result);
} catch (error) {
console.error('Error updating user status:', error);
res.status(400).json({ error: error.message });
}
};
export default {
getUserPermissions,
getClubMembersWithPermissions,
updateUserRole,
updateUserPermissions,
updateUserStatus,
getAvailableRoles,
getPermissionStructure
};

View File

@@ -0,0 +1,187 @@
import permissionService from '../services/permissionService.js';
/**
* Authorization Middleware
* Checks if user has permission to access a resource
*/
/**
* Check if user has permission for a specific resource and action
* @param {string} resource - Resource name (diary, members, teams, etc.)
* @param {string} action - Action type (read, write, delete)
* @returns {Function} Express middleware function
*/
export const authorize = (resource, action = 'read') => {
return async (req, res, next) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
// Get clubId from various possible sources
const clubId = req.params.clubId || req.params.id || req.body.clubId || req.query.clubId;
if (!clubId) {
return res.status(400).json({ error: 'Club-ID fehlt' });
}
// Check permission
const hasPermission = await permissionService.hasPermission(
userId,
parseInt(clubId),
resource,
action
);
if (!hasPermission) {
return res.status(403).json({
error: 'Keine Berechtigung',
details: `Fehlende Berechtigung: ${resource}.${action}`
});
}
// Store permissions in request for later use
const userPermissions = await permissionService.getUserClubPermissions(
userId,
parseInt(clubId)
);
req.userPermissions = userPermissions;
next();
} catch (error) {
console.error('Authorization error:', error);
res.status(500).json({ error: 'Fehler bei der Berechtigungsprüfung' });
}
};
};
/**
* Check if user is club owner
* @returns {Function} Express middleware function
*/
export const requireOwner = () => {
return async (req, res, next) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
const clubId = req.params.clubId || req.params.id || req.body.clubId || req.query.clubId;
if (!clubId) {
return res.status(400).json({ error: 'Club-ID fehlt' });
}
const userPermissions = await permissionService.getUserClubPermissions(
userId,
parseInt(clubId)
);
if (!userPermissions || !userPermissions.isOwner) {
return res.status(403).json({
error: 'Keine Berechtigung',
details: 'Nur der Club-Ersteller hat Zugriff'
});
}
req.userPermissions = userPermissions;
next();
} catch (error) {
console.error('Owner check error:', error);
res.status(500).json({ error: 'Fehler bei der Berechtigungsprüfung' });
}
};
};
/**
* Check if user is admin (owner or admin role)
* @returns {Function} Express middleware function
*/
export const requireAdmin = () => {
return async (req, res, next) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
const clubId = req.params.clubId || req.params.id || req.body.clubId || req.query.clubId;
if (!clubId) {
return res.status(400).json({ error: 'Club-ID fehlt' });
}
const userPermissions = await permissionService.getUserClubPermissions(
userId,
parseInt(clubId)
);
if (!userPermissions || (userPermissions.role !== 'admin' && !userPermissions.isOwner)) {
return res.status(403).json({
error: 'Keine Berechtigung',
details: 'Administrator-Rechte erforderlich'
});
}
req.userPermissions = userPermissions;
next();
} catch (error) {
console.error('Admin check error:', error);
res.status(500).json({ error: 'Fehler bei der Berechtigungsprüfung' });
}
};
};
/**
* Check if user has any of the specified roles
* @param {string[]} roles - Array of allowed roles
* @returns {Function} Express middleware function
*/
export const requireRole = (roles) => {
return async (req, res, next) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
const clubId = req.params.clubId || req.params.id || req.body.clubId || req.query.clubId;
if (!clubId) {
return res.status(400).json({ error: 'Club-ID fehlt' });
}
const userPermissions = await permissionService.getUserClubPermissions(
userId,
parseInt(clubId)
);
if (!userPermissions || !roles.includes(userPermissions.role)) {
return res.status(403).json({
error: 'Keine Berechtigung',
details: `Erforderliche Rolle: ${roles.join(', ')}`
});
}
req.userPermissions = userPermissions;
next();
} catch (error) {
console.error('Role check error:', error);
res.status(500).json({ error: 'Fehler bei der Berechtigungsprüfung' });
}
};
};
export default {
authorize,
requireOwner,
requireAdmin,
requireRole
};

View File

@@ -0,0 +1,17 @@
-- Add role and permissions columns to user_club table
ALTER TABLE `user_club`
ADD COLUMN `role` VARCHAR(50) DEFAULT 'member' COMMENT 'User role: admin, trainer, team_manager, member' AFTER `approved`,
ADD COLUMN `permissions` JSON NULL COMMENT 'Specific permissions: {diary: {read: true, write: true}, members: {...}, ...}' AFTER `role`,
ADD COLUMN `is_owner` BOOLEAN DEFAULT FALSE COMMENT 'True if user created the club' AFTER `permissions`;
-- Create index for faster role lookups
CREATE INDEX `idx_user_club_role` ON `user_club` (`role`);
CREATE INDEX `idx_user_club_owner` ON `user_club` (`is_owner`);
-- Set existing approved users as members
UPDATE `user_club` SET `role` = 'member' WHERE `approved` = 1 AND `role` IS NULL;
-- If there's a user who created the club (we need to identify them somehow)
-- For now, we'll need to manually set the owner after migration

View File

@@ -0,0 +1,38 @@
-- Update existing user_club entries with default permissions
-- This migration sets default values for role and is_owner for existing club memberships
-- Set default role to 'member' for all approved users who don't have a role yet
UPDATE `user_club`
SET `role` = 'member'
WHERE `approved` = 1
AND (`role` IS NULL OR `role` = '');
-- Optionally: Set the first approved user of each club as owner
-- This finds the user with the lowest user_id per club (oldest member) and marks them as owner
UPDATE `user_club` AS uc1
INNER JOIN (
SELECT `club_id`, MIN(`user_id`) as `first_user_id`
FROM `user_club`
WHERE `approved` = 1
GROUP BY `club_id`
) AS uc2 ON uc1.`club_id` = uc2.`club_id` AND uc1.`user_id` = uc2.`first_user_id`
SET
uc1.`is_owner` = 1,
uc1.`role` = 'admin';
-- Verify the changes
SELECT
uc.`club_id`,
c.`name` as club_name,
uc.`user_id`,
u.`email` as user_email,
uc.`role`,
uc.`is_owner`,
uc.`approved`
FROM `user_club` uc
LEFT JOIN `club` c ON c.`id` = uc.`club_id`
LEFT JOIN `user` u ON u.`id` = uc.`user_id`
WHERE uc.`approved` = 1
ORDER BY uc.`club_id`, uc.`is_owner` DESC, uc.`user_id`;

View File

@@ -6,6 +6,7 @@ import Club from './Club.js';
const UserClub = sequelize.define('UserClub', {
userId: {
type: DataTypes.INTEGER,
primaryKey: true,
references: {
model: User,
key: 'id',
@@ -13,6 +14,7 @@ const UserClub = sequelize.define('UserClub', {
},
clubId: {
type: DataTypes.INTEGER,
primaryKey: true,
references: {
model: Club,
key: 'id',
@@ -22,6 +24,23 @@ const UserClub = sequelize.define('UserClub', {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
role: {
type: DataTypes.STRING(50),
defaultValue: 'member',
allowNull: false,
comment: 'User role: admin, trainer, team_manager, member'
},
permissions: {
type: DataTypes.JSON,
allowNull: true,
comment: 'Specific permissions: {diary: {read: true, write: true}, members: {...}, ...}'
},
isOwner: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
comment: 'True if user created the club'
}
}, {
underscored: true,
tableName: 'user_club',

View File

@@ -1,5 +1,6 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import { authorize } from '../middleware/authorizationMiddleware.js';
import { getClubs, addClub, getClub, requestClubAccess, getPendingApprovals, approveClubAccess, rejectClubAccess } from '../controllers/clubsController.js';
const router = express.Router();
@@ -8,8 +9,8 @@ router.get('/', authenticate, getClubs);
router.post('/', authenticate, addClub);
router.get('/:clubid', authenticate, getClub);
router.get('/request/:clubid', authenticate, requestClubAccess);
router.get('/pending/:clubid', authenticate, getPendingApprovals);
router.post('/approve', authenticate, approveClubAccess);
router.post('/reject', authenticate, rejectClubAccess);
router.get('/pending/:clubid', authenticate, authorize('approvals', 'read'), getPendingApprovals);
router.post('/approve', authenticate, authorize('approvals', 'write'), approveClubAccess);
router.post('/reject', authenticate, authorize('approvals', 'write'), rejectClubAccess);
export default router;

View File

@@ -1,5 +1,6 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import { authorize } from '../middleware/authorizationMiddleware.js';
import {
getDatesForClub,
createDateForClub,
@@ -14,14 +15,14 @@ import {
const router = express.Router();
router.post('/note', authenticate, addDiaryNote);
router.delete('/note/:noteId', authenticate, deleteDiaryNote);
router.post('/tag', authenticate, addDiaryTag);
router.post('/tag/:clubId/add-tag', authenticate, addTagToDiaryDate);
router.delete('/:clubId/tag', authenticate, deleteTagFromDiaryDate);
router.get('/:clubId', authenticate, getDatesForClub);
router.post('/:clubId', authenticate, createDateForClub);
router.put('/:clubId', authenticate, updateTrainingTimes);
router.delete('/:clubId/:dateId', authenticate, deleteDateForClub);
router.post('/note', authenticate, authorize('diary', 'write'), addDiaryNote);
router.delete('/note/:noteId', authenticate, authorize('diary', 'delete'), deleteDiaryNote);
router.post('/tag', authenticate, authorize('diary', 'write'), addDiaryTag);
router.post('/tag/:clubId/add-tag', authenticate, authorize('diary', 'write'), addTagToDiaryDate);
router.delete('/:clubId/tag', authenticate, authorize('diary', 'delete'), deleteTagFromDiaryDate);
router.get('/:clubId', authenticate, authorize('diary', 'read'), getDatesForClub);
router.post('/:clubId', authenticate, authorize('diary', 'write'), createDateForClub);
router.put('/:clubId', authenticate, authorize('diary', 'write'), updateTrainingTimes);
router.delete('/:clubId/:dateId', authenticate, authorize('diary', 'delete'), deleteDateForClub);
export default router;

View File

@@ -1,20 +1,21 @@
import express from 'express';
import { uploadCSV, getLeaguesForCurrentSeason, getMatchesForLeagues, getMatchesForLeague, getLeagueTable, fetchLeagueTableFromMyTischtennis, updateMatchPlayers, getPlayerMatchStats } from '../controllers/matchController.js';
import { authenticate } from '../middleware/authMiddleware.js';
import { authorize } from '../middleware/authorizationMiddleware.js';
import multer from 'multer';
const router = express.Router();
const upload = multer({ dest: 'uploads/' });
router.post('/import', authenticate, upload.single('file'), uploadCSV);
router.get('/leagues/current/:clubId', authenticate, getLeaguesForCurrentSeason);
router.get('/leagues/:clubId/matches/:leagueId', authenticate, getMatchesForLeague);
router.get('/leagues/:clubId/matches', authenticate, getMatchesForLeagues);
router.get('/leagues/:clubId/table/:leagueId', authenticate, getLeagueTable);
router.post('/leagues/:clubId/table/:leagueId/fetch', authenticate, fetchLeagueTableFromMyTischtennis);
router.patch('/:matchId/players', authenticate, updateMatchPlayers);
router.get('/leagues/:clubId/stats/:leagueId', authenticate, getPlayerMatchStats);
router.post('/import', authenticate, authorize('schedule', 'write'), upload.single('file'), uploadCSV);
router.get('/leagues/current/:clubId', authenticate, authorize('schedule', 'read'), getLeaguesForCurrentSeason);
router.get('/leagues/:clubId/matches/:leagueId', authenticate, authorize('schedule', 'read'), getMatchesForLeague);
router.get('/leagues/:clubId/matches', authenticate, authorize('schedule', 'read'), getMatchesForLeagues);
router.get('/leagues/:clubId/table/:leagueId', authenticate, authorize('schedule', 'read'), getLeagueTable);
router.post('/leagues/:clubId/table/:leagueId/fetch', authenticate, authorize('mytischtennis', 'write'), fetchLeagueTableFromMyTischtennis);
router.patch('/:matchId/players', authenticate, authorize('schedule', 'write'), updateMatchPlayers);
router.get('/leagues/:clubId/stats/:leagueId', authenticate, authorize('schedule', 'read'), getPlayerMatchStats);
export default router;

View File

@@ -1,6 +1,7 @@
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis, rotateMemberImage } from '../controllers/memberController.js';
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import { authorize } from '../middleware/authorizationMiddleware.js';
import multer from 'multer';
const router = express.Router();
@@ -8,12 +9,12 @@ const router = express.Router();
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
router.post('/image/:clubId/:memberId', authenticate, upload.single('image'), uploadMemberImage);
router.get('/image/:clubId/:memberId', authenticate, getMemberImage);
router.get('/get/:id/:showAll', authenticate, getClubMembers);
router.post('/set/:id', authenticate, setClubMembers);
router.get('/notapproved/:id', authenticate, getWaitingApprovals);
router.post('/update-ratings/:id', authenticate, updateRatingsFromMyTischtennis);
router.post('/rotate-image/:clubId/:memberId', authenticate, rotateMemberImage);
router.post('/image/:clubId/:memberId', authenticate, authorize('members', 'write'), upload.single('image'), uploadMemberImage);
router.get('/image/:clubId/:memberId', authenticate, authorize('members', 'read'), getMemberImage);
router.get('/get/:id/:showAll', authenticate, authorize('members', 'read'), getClubMembers);
router.post('/set/:id', authenticate, authorize('members', 'write'), setClubMembers);
router.get('/notapproved/:id', authenticate, authorize('members', 'read'), getWaitingApprovals);
router.post('/update-ratings/:id', authenticate, authorize('mytischtennis', 'write'), updateRatingsFromMyTischtennis);
router.post('/rotate-image/:clubId/:memberId', authenticate, authorize('members', 'write'), rotateMemberImage);
export default router;

View File

@@ -2,23 +2,24 @@ import express from 'express';
import myTischtennisController from '../controllers/myTischtennisController.js';
import myTischtennisUrlController from '../controllers/myTischtennisUrlController.js';
import { authenticate } from '../middleware/authMiddleware.js';
import { authorize } from '../middleware/authorizationMiddleware.js';
const router = express.Router();
// All routes require authentication
router.use(authenticate);
// GET /api/mytischtennis/account - Get account
// GET /api/mytischtennis/account - Get account (alle dürfen lesen)
router.get('/account', myTischtennisController.getAccount);
// GET /api/mytischtennis/status - Check status
// GET /api/mytischtennis/status - Check status (alle dürfen lesen)
router.get('/status', myTischtennisController.getStatus);
// POST /api/mytischtennis/account - Create or update account
// POST /api/mytischtennis/account - Create or update account (alle dürfen bearbeiten)
router.post('/account', myTischtennisController.upsertAccount);
// DELETE /api/mytischtennis/account - Delete account
router.delete('/account', myTischtennisController.deleteAccount);
// DELETE /api/mytischtennis/account - Delete account (nur Admin)
router.delete('/account', authorize('mytischtennis_admin', 'write'), myTischtennisController.deleteAccount);
// POST /api/mytischtennis/verify - Verify login
router.post('/verify', myTischtennisController.verifyLogin);

View File

@@ -0,0 +1,30 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import { authorize, requireAdmin } from '../middleware/authorizationMiddleware.js';
import permissionController from '../controllers/permissionController.js';
const router = express.Router();
// Get available roles (no club context needed)
router.get('/roles/available', authenticate, permissionController.getAvailableRoles);
// Get permission structure (no club context needed)
router.get('/structure/all', authenticate, permissionController.getPermissionStructure);
// Get current user's permissions for a club (no authorization check - needed to load permissions)
router.get('/:clubId', authenticate, permissionController.getUserPermissions);
// Get all club members with their permissions (admin only)
router.get('/:clubId/members', authenticate, authorize('permissions', 'read'), permissionController.getClubMembersWithPermissions);
// Update user role (admin only)
router.put('/:clubId/user/:userId/role', authenticate, authorize('permissions', 'write'), permissionController.updateUserRole);
// Update user permissions (admin only)
router.put('/:clubId/user/:userId/permissions', authenticate, authorize('permissions', 'write'), permissionController.updateUserPermissions);
// Update user status (admin only)
router.put('/:clubId/user/:userId/status', authenticate, authorize('permissions', 'write'), permissionController.updateUserStatus);
export default router;

View File

@@ -10,6 +10,7 @@ import {
} from '../controllers/predefinedActivityController.js';
import multer from 'multer';
import { authenticate } from '../middleware/authMiddleware.js';
import { authorize } from '../middleware/authorizationMiddleware.js';
import { uploadPredefinedActivityImage, deletePredefinedActivityImage } from '../controllers/predefinedActivityImageController.js';
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
import path from 'path';
@@ -18,16 +19,16 @@ import fs from 'fs';
const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() });
router.post('/', authenticate, createPredefinedActivity);
router.get('/', authenticate, getAllPredefinedActivities);
router.get('/:id', authenticate, getPredefinedActivityById);
router.put('/:id', authenticate, updatePredefinedActivity);
router.post('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage);
router.put('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage);
router.delete('/:id/image/:imageId', authenticate, deletePredefinedActivityImage);
router.get('/search/query', authenticate, searchPredefinedActivities);
router.post('/merge', authenticate, mergePredefinedActivities);
router.post('/deduplicate', authenticate, deduplicatePredefinedActivities);
router.post('/', authenticate, authorize('predefined_activities', 'write'), createPredefinedActivity);
router.get('/', authenticate, authorize('predefined_activities', 'read'), getAllPredefinedActivities);
router.get('/:id', authenticate, authorize('predefined_activities', 'read'), getPredefinedActivityById);
router.put('/:id', authenticate, authorize('predefined_activities', 'write'), updatePredefinedActivity);
router.post('/:id/image', authenticate, authorize('predefined_activities', 'write'), upload.single('image'), uploadPredefinedActivityImage);
router.put('/:id/image', authenticate, authorize('predefined_activities', 'write'), upload.single('image'), uploadPredefinedActivityImage);
router.delete('/:id/image/:imageId', authenticate, authorize('predefined_activities', 'delete'), deletePredefinedActivityImage);
router.get('/search/query', authenticate, authorize('predefined_activities', 'read'), searchPredefinedActivities);
router.post('/merge', authenticate, authorize('predefined_activities', 'write'), mergePredefinedActivities);
router.post('/deduplicate', authenticate, authorize('predefined_activities', 'write'), deduplicatePredefinedActivities);
router.get('/:id/image/:imageId', async (req, res) => {
try {
const { id, imageId } = req.params;

View File

@@ -1,5 +1,6 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import { authorize } from '../middleware/authorizationMiddleware.js';
import {
getTeams,
getTeam,
@@ -12,21 +13,21 @@ import {
const router = express.Router();
// Get all teams for a club
router.get('/club/:clubid', authenticate, getTeams);
router.get('/club/:clubid', authenticate, authorize('teams', 'read'), getTeams);
// Get leagues for a club
router.get('/leagues/:clubid', authenticate, getLeagues);
router.get('/leagues/:clubid', authenticate, authorize('teams', 'read'), getLeagues);
// Get a specific team
router.get('/:teamid', authenticate, getTeam);
router.get('/:teamid', authenticate, authorize('teams', 'read'), getTeam);
// Create a new team
router.post('/club/:clubid', authenticate, createTeam);
router.post('/club/:clubid', authenticate, authorize('teams', 'write'), createTeam);
// Update a team
router.put('/:teamid', authenticate, updateTeam);
router.put('/:teamid', authenticate, authorize('teams', 'write'), updateTeam);
// Delete a team
router.delete('/:teamid', authenticate, deleteTeam);
router.delete('/:teamid', authenticate, authorize('teams', 'delete'), deleteTeam);
export default router;

View File

@@ -0,0 +1,141 @@
import User from '../models/User.js';
import Club from '../models/Club.js';
import UserClub from '../models/UserClub.js';
import sequelize from '../database.js';
/**
* Create test users with different roles
*/
const TEST_USERS = [
{
email: 'admin@test.de',
password: 'test123',
role: 'admin',
isOwner: false
},
{
email: 'trainer@test.de',
password: 'test123',
role: 'trainer',
isOwner: false
},
{
email: 'teammanager@test.de',
password: 'test123',
role: 'team_manager',
isOwner: false
},
{
email: 'tournamentmanager@test.de',
password: 'test123',
role: 'tournament_manager',
isOwner: false
},
{
email: 'member1@test.de',
password: 'test123',
role: 'member',
isOwner: false
},
{
email: 'member2@test.de',
password: 'test123',
role: 'member',
isOwner: false
}
];
async function createTestUsers() {
console.log('Creating test users...\n');
try {
// Get first club (or specify club ID)
const clubs = await Club.findAll({ limit: 1 });
if (clubs.length === 0) {
console.error('❌ No clubs found! Please create a club first.');
process.exit(1);
}
const club = clubs[0];
console.log(`Using club: ${club.name} (ID: ${club.id})\n`);
for (const userData of TEST_USERS) {
console.log(`Creating user: ${userData.email} (${userData.role})...`);
// Check if user already exists
let user = await User.findOne({ where: { email: userData.email } });
if (user) {
console.log(` ⚠️ User already exists, using existing user`);
} else {
// Create user
user = await User.create({
email: userData.email,
password: userData.password,
isActive: true
});
console.log(` ✓ User created`);
}
// Check if user is already in club
let userClub = await UserClub.findOne({
where: {
userId: user.id,
clubId: club.id
}
});
if (userClub) {
console.log(` ⚠️ User already in club, updating role...`);
await userClub.update({
role: userData.role,
isOwner: userData.isOwner,
approved: true
});
console.log(` ✓ Updated to role: ${userData.role}`);
} else {
// Add user to club
userClub = await UserClub.create({
userId: user.id,
clubId: club.id,
role: userData.role,
isOwner: userData.isOwner,
approved: true
});
console.log(` ✓ Added to club with role: ${userData.role}`);
}
}
console.log('\n✅ Test users created successfully!\n');
// Show summary
console.log('Summary:');
console.log('========================================');
console.log(`Club: ${club.name}`);
console.log('\nTest Users:');
for (const userData of TEST_USERS) {
console.log(` ${userData.email.padEnd(25)} | ${userData.role.padEnd(15)} | Password: test123`);
}
console.log('\n========================================');
console.log('You can now login with any of these users!');
console.log('All passwords are: test123');
} catch (error) {
console.error('❌ Error creating test users:', error);
throw error;
} finally {
await sequelize.close();
}
}
// Run
createTestUsers().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});

View File

@@ -0,0 +1,128 @@
import UserClub from '../models/UserClub.js';
import Club from '../models/Club.js';
import User from '../models/User.js';
import sequelize from '../database.js';
/**
* Migration script to set up permissions for existing clubs
* This script:
* 1. Sets default role='member' for all approved users without a role
* 2. Identifies and marks the first user (by creation date) of each club as owner
*/
async function migratePermissions() {
console.log('Starting permissions migration...\n');
try {
// Get all clubs
const clubs = await Club.findAll({
include: [{
model: UserClub,
include: [{
model: User,
as: 'user'
}],
where: {
approved: true
},
order: [['createdAt', 'ASC']]
}]
});
console.log(`Found ${clubs.length} club(s)\n`);
for (const club of clubs) {
console.log(`\n--- Club: ${club.name} (ID: ${club.id}) ---`);
const userClubs = await UserClub.findAll({
where: {
clubId: club.id,
approved: true
},
include: [{
model: User,
as: 'user'
}],
order: [['createdAt', 'ASC']]
});
if (userClubs.length === 0) {
console.log(' No approved members found.');
continue;
}
// First user becomes owner
const firstUser = userClubs[0];
console.log(` Members found: ${userClubs.length}`);
console.log(` First member (will be owner): ${firstUser.user.email}`);
for (let i = 0; i < userClubs.length; i++) {
const userClub = userClubs[i];
const isFirstUser = i === 0;
// Set role if not set
if (!userClub.role) {
userClub.role = isFirstUser ? 'admin' : 'member';
}
// Set owner flag
userClub.isOwner = isFirstUser;
await userClub.save();
console.log(` ✓ Updated ${userClub.user.email}: role=${userClub.role}, isOwner=${userClub.isOwner}`);
}
}
console.log('\n✅ Migration completed successfully!');
console.log('\nSummary:');
// Show summary
const owners = await UserClub.findAll({
where: {
isOwner: true
},
include: [
{
model: User,
as: 'user'
},
{
model: Club,
as: 'club'
}
]
});
console.log(`\nClub Owners (${owners.length}):`);
for (const owner of owners) {
console.log(` - ${owner.club.name}: ${owner.user.email}`);
}
const admins = await UserClub.count({
where: { role: 'admin' }
});
const members = await UserClub.count({
where: { role: 'member' }
});
console.log(`\nRole Distribution:`);
console.log(` - Admins: ${admins}`);
console.log(` - Members: ${members}`);
} catch (error) {
console.error('❌ Migration failed:', error);
throw error;
} finally {
await sequelize.close();
}
}
// Run migration
migratePermissions().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});

View File

@@ -0,0 +1,103 @@
import UserClub from '../models/UserClub.js';
import Club from '../models/Club.js';
import User from '../models/User.js';
import sequelize from '../database.js';
/**
* Quick fix: Set first user of each club as owner/admin
* This is a simplified version for immediate use
*/
async function quickFixOwners() {
console.log('Quick Fix: Setting club owners...\n');
try {
const clubs = await Club.findAll();
console.log(`Found ${clubs.length} club(s)\n`);
for (const club of clubs) {
console.log(`Club: ${club.name} (ID: ${club.id})`);
// Find all approved members, ordered by creation date
const userClubs = await UserClub.findAll({
where: {
clubId: club.id,
approved: true
},
include: [{
model: User,
as: 'user',
attributes: ['id', 'email']
}],
order: [['createdAt', 'ASC']]
});
if (userClubs.length === 0) {
console.log(' ⚠️ No approved members\n');
continue;
}
// First user becomes owner
const firstUserClub = userClubs[0];
// Reset all users first (remove owner flag)
await UserClub.update(
{ isOwner: false },
{
where: {
clubId: club.id,
approved: true
}
}
);
// Set first user as owner and admin
await firstUserClub.update({
isOwner: true,
role: 'admin'
});
console.log(` ✅ Owner: ${firstUserClub.user.email}`);
// Set role for other members if not set
for (let i = 1; i < userClubs.length; i++) {
const uc = userClubs[i];
if (!uc.role) {
await uc.update({ role: 'member' });
console.log(` 👤 Member: ${uc.user.email}`);
}
}
console.log('');
}
console.log('✅ Quick fix completed!\n');
// Show all owners
const owners = await UserClub.findAll({
where: { isOwner: true },
include: [
{ model: User, as: 'user', attributes: ['email'] },
{ model: Club, as: 'club', attributes: ['name'] }
]
});
console.log('Current Club Owners:');
for (const owner of owners) {
console.log(` 📍 ${owner.club.name}: ${owner.user.email} (role: ${owner.role})`);
}
} catch (error) {
console.error('❌ Error:', error);
throw error;
} finally {
await sequelize.close();
}
}
quickFixOwners().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});

View File

@@ -39,6 +39,7 @@ import clubTeamRoutes from './routes/clubTeamRoutes.js';
import teamDocumentRoutes from './routes/teamDocumentRoutes.js';
import seasonRoutes from './routes/seasonRoutes.js';
import memberActivityRoutes from './routes/memberActivityRoutes.js';
import permissionRoutes from './routes/permissionRoutes.js';
import schedulerService from './services/schedulerService.js';
const app = express();
@@ -90,6 +91,7 @@ app.use('/api/club-teams', clubTeamRoutes);
app.use('/api/team-documents', teamDocumentRoutes);
app.use('/api/seasons', seasonRoutes);
app.use('/api/member-activities', memberActivityRoutes);
app.use('/api/permissions', permissionRoutes);
app.use(express.static(path.join(__dirname, '../frontend/dist')));

View File

@@ -4,6 +4,7 @@ import User from '../models/User.js';
import Member from '../models/Member.js';
import { Op, fn, where, col } from 'sequelize';
import { checkAccess } from '../utils/userUtils.js';
import permissionService from './permissionService.js';
class ClubService {
async getAllClubs() {
@@ -20,8 +21,15 @@ class ClubService {
return await Club.create({ name: clubName });
}
async addUserToClub(userId, clubId) {
return await UserClub.create({ userId: userId, clubId: clubId, approved: true });
async addUserToClub(userId, clubId, isOwner = false) {
const userClub = await UserClub.create({
userId: userId,
clubId: clubId,
approved: true,
isOwner: isOwner,
role: isOwner ? 'admin' : 'member'
});
return userClub;
}
async getUserClubAccess(userId, clubId) {

View File

@@ -0,0 +1,366 @@
import UserClub from '../models/UserClub.js';
import Club from '../models/Club.js';
import User from '../models/User.js';
/**
* Permission Service
* Handles all permission-related logic
*/
// Default permissions for each role
const ROLE_PERMISSIONS = {
admin: {
diary: { read: true, write: true, delete: true },
members: { read: true, write: true, delete: true },
teams: { read: true, write: true, delete: true },
schedule: { read: true, write: true, delete: true },
tournaments: { read: true, write: true, delete: true },
statistics: { read: true, write: true },
settings: { read: true, write: true },
permissions: { read: true, write: true }, // Can manage other users' permissions
approvals: { read: true, write: true },
mytischtennis_admin: { read: true, write: true },
predefined_activities: { read: true, write: true, delete: true }
},
trainer: {
diary: { read: true, write: true, delete: true },
members: { read: true, write: true, delete: false },
teams: { read: true, write: true, delete: false },
schedule: { read: true, write: true, delete: false },
tournaments: { read: true, write: true, delete: false },
statistics: { read: true, write: false },
settings: { read: false, write: false },
permissions: { read: false, write: false },
approvals: { read: false, write: false },
mytischtennis_admin: { read: false, write: false },
predefined_activities: { read: true, write: true, delete: true }
},
team_manager: {
diary: { read: false, write: false, delete: false },
members: { read: true, write: false, delete: false },
teams: { read: true, write: true, delete: false },
schedule: { read: true, write: true, delete: false },
tournaments: { read: true, write: false, delete: false },
statistics: { read: true, write: false },
settings: { read: false, write: false },
permissions: { read: false, write: false },
approvals: { read: false, write: false },
mytischtennis_admin: { read: false, write: false },
predefined_activities: { read: false, write: false, delete: false }
},
tournament_manager: {
diary: { read: false, write: false, delete: false },
members: { read: true, write: false, delete: false },
teams: { read: false, write: false, delete: false },
schedule: { read: false, write: false, delete: false },
tournaments: { read: true, write: true, delete: false },
statistics: { read: true, write: false },
settings: { read: false, write: false },
permissions: { read: false, write: false },
approvals: { read: false, write: false },
mytischtennis_admin: { read: false, write: false },
predefined_activities: { read: false, write: false, delete: false }
},
member: {
diary: { read: false, write: false, delete: false },
members: { read: false, write: false, delete: false },
teams: { read: false, write: false, delete: false },
schedule: { read: false, write: false, delete: false },
tournaments: { read: false, write: false, delete: false },
statistics: { read: true, write: false },
settings: { read: false, write: false },
permissions: { read: false, write: false },
approvals: { read: false, write: false },
mytischtennis_admin: { read: false, write: false },
predefined_activities: { read: false, write: false, delete: false }
}
};
class PermissionService {
/**
* Get user's permissions for a specific club
*/
async getUserClubPermissions(userId, clubId) {
const userClub = await UserClub.findOne({
where: {
userId,
clubId,
approved: true
}
});
if (!userClub) {
return null;
}
// If user is owner, they have full admin rights
if (userClub.isOwner) {
return {
role: 'admin',
isOwner: true,
permissions: ROLE_PERMISSIONS.admin
};
}
// Get role from database, fallback to 'member' if null/undefined
const role = userClub.role || 'member';
// Get role-based permissions
const rolePermissions = ROLE_PERMISSIONS[role] || ROLE_PERMISSIONS.member;
// Merge with custom permissions if any
const customPermissions = userClub.permissions || {};
const mergedPermissions = this.mergePermissions(rolePermissions, customPermissions);
return {
role: role,
isOwner: false,
permissions: mergedPermissions
};
}
/**
* Check if user has specific permission
*/
async hasPermission(userId, clubId, resource, action) {
const userPermissions = await this.getUserClubPermissions(userId, clubId);
if (!userPermissions) {
return false;
}
// Owner always has permission
if (userPermissions.isOwner) {
return true;
}
// MyTischtennis settings are accessible to all approved members
if (resource === 'mytischtennis') {
return true;
}
const resourcePermissions = userPermissions.permissions[resource];
if (!resourcePermissions) {
return false;
}
return resourcePermissions[action] === true;
}
/**
* Set user role in club
*/
async setUserRole(userId, clubId, role, updatedByUserId) {
// Check if updater has permission
const canManagePermissions = await this.hasPermission(updatedByUserId, clubId, 'permissions', 'write');
if (!canManagePermissions) {
throw new Error('Keine Berechtigung zum Ändern von Rollen');
}
// Check if target user is owner
const targetUserClub = await UserClub.findOne({
where: { userId, clubId }
});
if (!targetUserClub) {
throw new Error('Benutzer ist kein Mitglied dieses Clubs');
}
if (targetUserClub.isOwner) {
throw new Error('Die Rolle des Club-Erstellers kann nicht geändert werden');
}
// Validate role
if (!ROLE_PERMISSIONS[role]) {
throw new Error('Ungültige Rolle');
}
await targetUserClub.update({ role });
return {
success: true,
message: 'Rolle erfolgreich aktualisiert'
};
}
/**
* Set custom permissions for user
*/
async setCustomPermissions(userId, clubId, customPermissions, updatedByUserId) {
// Check if updater has permission
const canManagePermissions = await this.hasPermission(updatedByUserId, clubId, 'permissions', 'write');
if (!canManagePermissions) {
throw new Error('Keine Berechtigung zum Ändern von Berechtigungen');
}
// Check if target user is owner
const targetUserClub = await UserClub.findOne({
where: { userId, clubId }
});
if (!targetUserClub) {
throw new Error('Benutzer ist kein Mitglied dieses Clubs');
}
if (targetUserClub.isOwner) {
throw new Error('Die Berechtigungen des Club-Erstellers können nicht geändert werden');
}
await targetUserClub.update({ permissions: customPermissions });
return {
success: true,
message: 'Berechtigungen erfolgreich aktualisiert'
};
}
/**
* Set user status (activate/deactivate)
*/
async setUserStatus(userId, clubId, approved, updatedByUserId) {
// Check if updater has permission
const canManagePermissions = await this.hasPermission(updatedByUserId, clubId, 'permissions', 'write');
if (!canManagePermissions) {
throw new Error('Keine Berechtigung zum Ändern des Status');
}
// Check if target user is owner
const targetUserClub = await UserClub.findOne({
where: { userId, clubId }
});
if (!targetUserClub) {
throw new Error('Benutzer ist kein Mitglied dieses Clubs');
}
if (targetUserClub.isOwner) {
throw new Error('Der Status des Club-Erstellers kann nicht geändert werden');
}
await targetUserClub.update({ approved });
return {
success: true,
message: approved ? 'Benutzer erfolgreich aktiviert' : 'Benutzer erfolgreich deaktiviert'
};
}
/**
* Get all club members with their permissions
*/
async getClubMembersWithPermissions(clubId, requestingUserId) {
// Check if requester has permission to read permissions
const canReadPermissions = await this.hasPermission(requestingUserId, clubId, 'permissions', 'read');
if (!canReadPermissions) {
throw new Error('Keine Berechtigung zum Anzeigen von Berechtigungen');
}
const userClubs = await UserClub.findAll({
where: {
clubId
},
include: [{
model: User,
as: 'user',
attributes: ['id', 'email']
}]
});
return userClubs.map(uc => ({
userId: uc.userId,
user: uc.user,
role: uc.role,
isOwner: uc.isOwner,
approved: uc.approved,
permissions: uc.permissions,
effectivePermissions: this.getEffectivePermissions(uc)
}));
}
/**
* Get effective permissions (role + custom)
*/
getEffectivePermissions(userClub) {
if (userClub.isOwner) {
return ROLE_PERMISSIONS.admin;
}
const rolePermissions = ROLE_PERMISSIONS[userClub.role] || ROLE_PERMISSIONS.member;
const customPermissions = userClub.permissions || {};
return this.mergePermissions(rolePermissions, customPermissions);
}
/**
* Merge role permissions with custom permissions
*/
mergePermissions(rolePermissions, customPermissions) {
const merged = { ...rolePermissions };
for (const resource in customPermissions) {
if (!merged[resource]) {
merged[resource] = {};
}
merged[resource] = {
...merged[resource],
...customPermissions[resource]
};
}
return merged;
}
/**
* Mark user as club owner (used when creating a club)
*/
async setClubOwner(userId, clubId) {
const userClub = await UserClub.findOne({
where: { userId, clubId }
});
if (!userClub) {
throw new Error('UserClub relationship not found');
}
await userClub.update({
isOwner: true,
role: 'admin',
approved: true
});
}
/**
* Get all available roles
*/
getAvailableRoles() {
return [
{ value: 'admin', label: 'Administrator', description: 'Vollzugriff auf alle Funktionen' },
{ value: 'trainer', label: 'Trainer', description: 'Kann Trainingseinheiten, Mitglieder und Teams verwalten' },
{ value: 'team_manager', label: 'Mannschaftsführer', description: 'Kann Teams und Spielpläne verwalten' },
{ value: 'tournament_manager', label: 'Turnierleiter', description: 'Kann Turniere verwalten' },
{ value: 'member', label: 'Mitglied', description: 'Kann nur Trainings-Statistiken ansehen' }
];
}
/**
* Get permission structure for frontend
*/
getPermissionStructure() {
return {
diary: { label: 'Trainingstagebuch', actions: ['read', 'write', 'delete'] },
members: { label: 'Mitglieder', actions: ['read', 'write', 'delete'] },
teams: { label: 'Teams', actions: ['read', 'write', 'delete'] },
schedule: { label: 'Spielpläne', actions: ['read', 'write', 'delete'] },
tournaments: { label: 'Turniere', actions: ['read', 'write', 'delete'] },
statistics: { label: 'Statistiken', actions: ['read', 'write'] },
settings: { label: 'Einstellungen', actions: ['read', 'write'] },
permissions: { label: 'Berechtigungsverwaltung', actions: ['read', 'write'] },
approvals: { label: 'Freigaben (Mitgliedsanträge)', actions: ['read', 'write'] },
mytischtennis_admin: { label: 'MyTischtennis Admin', actions: ['read', 'write'] },
predefined_activities: { label: 'Vordefinierte Aktivitäten', actions: ['read', 'write', 'delete'] }
};
}
}
export default new PermissionService();

View File

@@ -11,7 +11,7 @@ class SchedulerService {
/**
* Start the scheduler
*/
*/
start() {
if (this.isRunning) {
devLog('Scheduler is already running');