diff --git a/PERMISSIONS_GUIDE.md b/PERMISSIONS_GUIDE.md
new file mode 100644
index 0000000..e65be02
--- /dev/null
+++ b/PERMISSIONS_GUIDE.md
@@ -0,0 +1,210 @@
+# Berechtigungssystem - Dokumentation
+
+## Übersicht
+
+Das Trainingstagebuch verfügt nun über ein vollständiges rollenbasiertes Berechtigungssystem (RBAC - Role-Based Access Control). Der Club-Ersteller hat automatisch Admin-Rechte und kann anderen Mitgliedern Rollen und spezifische Berechtigungen zuweisen.
+
+## Rollen
+
+### 1. Administrator (admin)
+- **Vollzugriff** auf alle Funktionen
+- Kann Berechtigungen anderer Benutzer verwalten
+- Der Club-Ersteller ist automatisch Administrator und kann nicht degradiert werden
+
+### 2. Trainer (trainer)
+- Kann Trainingseinheiten planen und verwalten
+- Kann Mitglieder anlegen und bearbeiten
+- Kann Spielpläne einsehen und bearbeiten
+- Kann Turniere organisieren
+- **Kann nicht**: Einstellungen ändern, Berechtigungen verwalten
+
+### 3. Mannschaftsführer (team_manager)
+- Kann Teams und Spielpläne verwalten
+- Kann Spieler für Matches einteilen
+- Kann Spielergebnisse eintragen
+- **Kann nicht**: Trainingseinheiten planen, Mitglieder verwalten
+
+### 4. Mitglied (member)
+- Nur Lesezugriff auf alle Bereiche
+- Kann eigene Daten einsehen
+- **Kann nicht**: Daten ändern oder löschen
+
+## Berechtigungsbereiche
+
+- **diary**: Trainingstagebuch
+- **members**: Mitgliederverwaltung
+- **teams**: Team-Management
+- **schedule**: Spielpläne
+- **tournaments**: Turniere
+- **statistics**: Statistiken
+- **settings**: Einstellungen
+- **permissions**: Berechtigungsverwaltung
+- **mytischtennis**: MyTischtennis-Integration (für alle zugänglich)
+
+## Backend-Integration
+
+### Migration ausführen
+
+```sql
+mysql -u username -p database_name < backend/migrations/add_permissions_to_user_club.sql
+```
+
+### Authorization Middleware verwenden
+
+```javascript
+import { authorize, requireAdmin, requireOwner } from '../middleware/authorizationMiddleware.js';
+
+// Beispiel: Nur Lesezugriff erforderlich
+router.get('/diary/:clubId', authenticate, authorize('diary', 'read'), getDiary);
+
+// Beispiel: Schreibzugriff erforderlich
+router.post('/diary/:clubId', authenticate, authorize('diary', 'write'), createDiary);
+
+// Beispiel: Admin-Rechte erforderlich
+router.put('/settings/:clubId', authenticate, requireAdmin(), updateSettings);
+
+// Beispiel: Nur Owner
+router.delete('/club/:clubId', authenticate, requireOwner(), deleteClub);
+```
+
+### Permission Service verwenden
+
+```javascript
+import permissionService from '../services/permissionService.js';
+
+// Berechtigungen prüfen
+const hasPermission = await permissionService.hasPermission(userId, clubId, 'diary', 'write');
+
+// Rolle setzen
+await permissionService.setUserRole(userId, clubId, 'trainer', adminUserId);
+
+// Custom Permissions setzen
+await permissionService.setCustomPermissions(
+ userId,
+ clubId,
+ { diary: { write: false }, members: { write: true } },
+ adminUserId
+);
+```
+
+## Frontend-Integration
+
+### Composable verwenden
+
+```vue
+
+```
+
+### Direktiven verwenden
+
+```vue
+
+
+
+
+
+
+
+
+ Inhalt nur für Berechtigte
+
+
+ Admin-Bereich
+
+
+ Owner-Bereich
+
+```
+
+### Store verwenden
+
+```javascript
+import { useStore } from 'vuex';
+
+const store = useStore();
+
+// Berechtigungen abrufen
+const permissions = store.getters.currentPermissions;
+const hasPermission = store.getters.hasPermission('diary', 'write');
+const isOwner = store.getters.isClubOwner;
+const userRole = store.getters.userRole;
+
+// Berechtigungen laden (wird automatisch beim Club-Wechsel gemacht)
+await store.dispatch('loadPermissions', clubId);
+```
+
+## Admin-UI
+
+Die Berechtigungsverwaltung ist unter `/permissions` verfügbar und nur für Administratoren sichtbar.
+
+**Funktionen:**
+- Übersicht aller Clubmitglieder mit ihren Rollen
+- Rollen zuweisen/ändern
+- Custom Permissions für einzelne Benutzer definieren
+- Erklärung der verfügbaren Rollen
+
+## MyTischtennis-Integration
+
+Die MyTischtennis-Einstellungen und -Funktionen sind für **alle Club-Mitglieder** zugänglich, unabhängig von ihrer Rolle. Dies ermöglicht es jedem, die Anbindung einzurichten und Daten abzurufen.
+
+## Sicherheitshinweise
+
+1. **Der Club-Ersteller** (Owner) kann nicht degradiert oder gelöscht werden
+2. **Owner-Rechte** können nicht übertragen werden
+3. **Backend-Validierung** wird immer durchgeführt, auch wenn das Frontend Elemente ausblendet
+4. **Alle API-Routen** sind durch Middleware geschützt
+5. **Permissions werden gecacht** im localStorage für bessere Performance
+
+## Beispiel-Szenarien
+
+### Szenario 1: Trainer hinzufügen
+1. Admin öffnet `/permissions`
+2. Wählt Benutzer aus
+3. Ändert Rolle zu "Trainer"
+4. Benutzer kann jetzt Trainingseinheiten planen
+
+### Szenario 2: Custom Permissions
+1. Admin öffnet `/permissions`
+2. Wählt Benutzer aus
+3. Klickt auf "Anpassen"
+4. Setzt individuelle Berechtigungen (z.B. nur Diary-Schreibrecht)
+5. Speichert
+
+### Szenario 3: Neues Mitglied
+1. Mitglied registriert sich und fordert Zugang an
+2. Admin genehmigt Anfrage (Standardrolle: "member")
+3. Mitglied hat Lesezugriff
+4. Bei Bedarf kann Admin die Rolle später ändern
+
+## Troubleshooting
+
+**Problem**: Berechtigungen werden nicht aktualisiert
+- **Lösung**: Seite neu laden oder Club neu auswählen
+
+**Problem**: "Keine Berechtigung" trotz korrekter Rolle
+- **Lösung**: Prüfen, ob Custom Permissions die Rolle überschreiben
+
+**Problem**: Owner kann keine Änderungen vornehmen
+- **Lösung**: Owner sollte automatisch alle Rechte haben. Prüfen Sie die `isOwner`-Flag in der Datenbank
+
+## API-Endpunkte
+
+```
+GET /api/permissions/:clubId - Eigene Berechtigungen abrufen
+GET /api/permissions/:clubId/members - Alle Mitglieder mit Berechtigungen (Admin)
+PUT /api/permissions/:clubId/user/:userId/role - Rolle ändern (Admin)
+PUT /api/permissions/:clubId/user/:userId/permissions - Custom Permissions setzen (Admin)
+GET /api/permissions/roles/available - Verfügbare Rollen abrufen
+GET /api/permissions/structure/all - Berechtigungsstruktur abrufen
+```
+
+
diff --git a/PERMISSIONS_MIGRATION.md b/PERMISSIONS_MIGRATION.md
new file mode 100644
index 0000000..08336ce
--- /dev/null
+++ b/PERMISSIONS_MIGRATION.md
@@ -0,0 +1,235 @@
+# Berechtigungssystem - Migrations-Anleitung
+
+## Übersicht
+
+Diese Anleitung hilft Ihnen, das neue Berechtigungssystem für bestehende Clubs einzurichten.
+
+## Schritt 1: Datenbank-Schema erweitern
+
+Führen Sie zuerst die SQL-Migration aus, um die neuen Spalten hinzuzufügen:
+
+```bash
+mysql -u username -p database_name < backend/migrations/add_permissions_to_user_club.sql
+```
+
+Dies fügt folgende Spalten zur `user_club` Tabelle hinzu:
+- `role` (VARCHAR) - Benutzerrolle (admin, trainer, team_manager, member)
+- `permissions` (JSON) - Custom Permissions
+- `is_owner` (BOOLEAN) - Markiert den Club-Ersteller
+
+## Schritt 2: Bestehende Daten migrieren
+
+Sie haben zwei Optionen:
+
+### Option A: Node.js Script (Empfohlen)
+
+Das Script identifiziert automatisch den ersten Benutzer jedes Clubs (nach `createdAt`) und setzt ihn als Owner.
+
+```bash
+cd /home/torsten/Programs/trainingstagebuch/backend
+node scripts/migratePermissions.js
+```
+
+**Ausgabe:**
+```
+Starting permissions migration...
+
+Found 3 club(s)
+
+--- Club: TTC Beispiel (ID: 1) ---
+ Members found: 5
+ First member (will be owner): admin@example.com
+ ✓ Updated admin@example.com: role=admin, isOwner=true
+ ✓ Updated user1@example.com: role=member, isOwner=false
+ ✓ Updated user2@example.com: role=member, isOwner=false
+ ...
+
+✅ Migration completed successfully!
+
+Summary:
+Club Owners (3):
+ - TTC Beispiel: admin@example.com
+ - SV Teststadt: owner@test.de
+ - TSC Demo: demo@example.com
+
+Role Distribution:
+ - Admins: 3
+ - Members: 12
+```
+
+### Option B: SQL Script
+
+Wenn Sie lieber SQL verwenden möchten:
+
+```bash
+mysql -u username -p database_name < backend/migrations/update_existing_user_club_permissions.sql
+```
+
+Dieses Script:
+1. Setzt `role = 'member'` für alle genehmigten Benutzer ohne Rolle
+2. Markiert den Benutzer mit der niedrigsten `user_id` pro Club als Owner
+
+## Schritt 3: Manuelle Anpassungen (Optional)
+
+### Falscher Owner?
+
+Falls das Script den falschen Benutzer als Owner markiert hat, können Sie dies manuell korrigieren:
+
+```sql
+-- Alten Owner zurücksetzen
+UPDATE user_club
+SET is_owner = 0, role = 'member'
+WHERE club_id = 1 AND user_id = 123;
+
+-- Neuen Owner setzen
+UPDATE user_club
+SET is_owner = 1, role = 'admin'
+WHERE club_id = 1 AND user_id = 456;
+```
+
+### Weitere Admins ernennen
+
+```sql
+UPDATE user_club
+SET role = 'admin'
+WHERE club_id = 1 AND user_id = 789;
+```
+
+### Trainer ernennen
+
+```sql
+UPDATE user_club
+SET role = 'trainer'
+WHERE club_id = 1 AND user_id = 101;
+```
+
+## Schritt 4: Verifizierung
+
+### Backend neu starten
+
+```bash
+# Server neu starten (wenn er läuft)
+sudo systemctl restart tt-tagebuch
+```
+
+### Im Browser testen
+
+1. Loggen Sie sich ein
+2. Wählen Sie einen Club aus
+3. Navigieren Sie zu "Berechtigungen" (nur für Admins sichtbar)
+4. Überprüfen Sie, dass alle Mitglieder korrekt angezeigt werden
+
+### SQL Verifizierung
+
+```sql
+-- Alle Club-Mitglieder mit ihren Berechtigungen anzeigen
+SELECT
+ c.name as club_name,
+ u.email as user_email,
+ uc.role,
+ uc.is_owner,
+ uc.approved
+FROM user_club uc
+JOIN club c ON c.id = uc.club_id
+JOIN user u ON u.id = uc.user_id
+WHERE uc.approved = 1
+ORDER BY c.name, uc.is_owner DESC, uc.role, u.email;
+```
+
+## Troubleshooting
+
+### Problem: "Keine Berechtigung" trotz Owner-Status
+
+**Lösung:** Überprüfen Sie in der Datenbank:
+
+```sql
+SELECT role, is_owner, approved
+FROM user_club
+WHERE user_id = YOUR_USER_ID AND club_id = YOUR_CLUB_ID;
+```
+
+Sollte sein: `role='admin'`, `is_owner=1`, `approved=1`
+
+### Problem: Owner kann nicht geändert werden
+
+Das ist korrekt! Der Owner (Club-Ersteller) kann seine eigenen Rechte nicht verlieren. Dies ist eine Sicherheitsmaßnahme.
+
+### Problem: Berechtigungen werden nicht geladen
+
+**Lösung:**
+1. Browser-Cache leeren
+2. LocalStorage leeren: `localStorage.clear()` in der Browser-Console
+3. Neu einloggen
+
+### Problem: "Lade Mitglieder..." bleibt hängen
+
+**Mögliche Ursachen:**
+1. Migration noch nicht ausgeführt
+2. Backend nicht neu gestartet
+3. Frontend nicht neu gebaut
+
+**Lösung:**
+```bash
+# Backend
+cd /home/torsten/Programs/trainingstagebuch/backend
+node scripts/migratePermissions.js
+
+# Frontend
+cd /home/torsten/Programs/trainingstagebuch/frontend
+npm run build
+
+# Server neu starten
+sudo systemctl restart tt-tagebuch
+```
+
+## Nach der Migration
+
+### Neue Clubs
+
+Bei neuen Clubs wird der Ersteller automatisch als Owner mit Admin-Rechten eingerichtet. Keine manuelle Aktion erforderlich.
+
+### Neue Mitglieder
+
+Neue Mitglieder erhalten automatisch die Rolle "member" (Lesezugriff). Admins können die Rolle später ändern.
+
+### Berechtigungen verwalten
+
+Admins können über die Web-UI unter `/permissions` Berechtigungen verwalten:
+1. Rollen zuweisen (Admin, Trainer, Mannschaftsführer, Mitglied)
+2. Custom Permissions definieren (für spezielle Anwendungsfälle)
+
+## Wichtige Hinweise
+
+⚠️ **Sicherung erstellen:**
+```bash
+mysqldump -u username -p database_name > backup_before_permissions_$(date +%Y%m%d).sql
+```
+
+⚠️ **Owner-Rechte:**
+- Der Owner (is_owner=1) kann nicht degradiert oder gelöscht werden
+- Jeder Club hat genau einen Owner
+- Owner-Rechte können nicht übertragen werden (nur durch direkte DB-Änderung)
+
+⚠️ **MyTischtennis:**
+- MyTischtennis-Funktionen sind für ALLE Mitglieder zugänglich
+- Keine Berechtigungsprüfung für MyTischtennis-Endpunkte
+
+## Rollback (falls nötig)
+
+Falls Sie das Berechtigungssystem zurücknehmen müssen:
+
+```sql
+-- Spalten entfernen (Achtung: Datenverlust!)
+ALTER TABLE user_club
+DROP COLUMN role,
+DROP COLUMN permissions,
+DROP COLUMN is_owner;
+
+-- Indizes entfernen
+DROP INDEX idx_user_club_role ON user_club;
+DROP INDEX idx_user_club_owner ON user_club;
+```
+
+Dann Backend-Code auf vorherige Version zurücksetzen.
+
+
diff --git a/backend/controllers/clubsController.js b/backend/controllers/clubsController.js
index 848e288..b8932ef 100644
--- a/backend/controllers/clubsController.js
+++ b/backend/controllers/clubsController.js
@@ -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);
diff --git a/backend/controllers/permissionController.js b/backend/controllers/permissionController.js
new file mode 100644
index 0000000..7686023
--- /dev/null
+++ b/backend/controllers/permissionController.js
@@ -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
+};
+
+
diff --git a/backend/middleware/authorizationMiddleware.js b/backend/middleware/authorizationMiddleware.js
new file mode 100644
index 0000000..e9524d4
--- /dev/null
+++ b/backend/middleware/authorizationMiddleware.js
@@ -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
+};
+
diff --git a/backend/migrations/add_permissions_to_user_club.sql b/backend/migrations/add_permissions_to_user_club.sql
new file mode 100644
index 0000000..75d488b
--- /dev/null
+++ b/backend/migrations/add_permissions_to_user_club.sql
@@ -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
+
+
diff --git a/backend/migrations/update_existing_user_club_permissions.sql b/backend/migrations/update_existing_user_club_permissions.sql
new file mode 100644
index 0000000..0eee5cb
--- /dev/null
+++ b/backend/migrations/update_existing_user_club_permissions.sql
@@ -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`;
+
+
diff --git a/backend/models/UserClub.js b/backend/models/UserClub.js
index e4f5898..b626fa4 100644
--- a/backend/models/UserClub.js
+++ b/backend/models/UserClub.js
@@ -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',
diff --git a/backend/routes/clubRoutes.js b/backend/routes/clubRoutes.js
index 024104a..b9c03c5 100644
--- a/backend/routes/clubRoutes.js
+++ b/backend/routes/clubRoutes.js
@@ -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;
diff --git a/backend/routes/diaryRoutes.js b/backend/routes/diaryRoutes.js
index dacfce2..8f75876 100644
--- a/backend/routes/diaryRoutes.js
+++ b/backend/routes/diaryRoutes.js
@@ -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;
diff --git a/backend/routes/matchRoutes.js b/backend/routes/matchRoutes.js
index de0f6d4..a2bd09e 100644
--- a/backend/routes/matchRoutes.js
+++ b/backend/routes/matchRoutes.js
@@ -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;
diff --git a/backend/routes/memberRoutes.js b/backend/routes/memberRoutes.js
index ecbab85..c6b9b7a 100644
--- a/backend/routes/memberRoutes.js
+++ b/backend/routes/memberRoutes.js
@@ -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;
diff --git a/backend/routes/myTischtennisRoutes.js b/backend/routes/myTischtennisRoutes.js
index 0e42c65..05f457b 100644
--- a/backend/routes/myTischtennisRoutes.js
+++ b/backend/routes/myTischtennisRoutes.js
@@ -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);
diff --git a/backend/routes/permissionRoutes.js b/backend/routes/permissionRoutes.js
new file mode 100644
index 0000000..b7e2bf0
--- /dev/null
+++ b/backend/routes/permissionRoutes.js
@@ -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;
+
diff --git a/backend/routes/predefinedActivityRoutes.js b/backend/routes/predefinedActivityRoutes.js
index ce74def..e590284 100644
--- a/backend/routes/predefinedActivityRoutes.js
+++ b/backend/routes/predefinedActivityRoutes.js
@@ -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;
diff --git a/backend/routes/teamRoutes.js b/backend/routes/teamRoutes.js
index ea3ee3c..52415d4 100644
--- a/backend/routes/teamRoutes.js
+++ b/backend/routes/teamRoutes.js
@@ -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;
\ No newline at end of file
diff --git a/backend/scripts/createTestUsers.js b/backend/scripts/createTestUsers.js
new file mode 100644
index 0000000..ea16bfa
--- /dev/null
+++ b/backend/scripts/createTestUsers.js
@@ -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);
+});
+
+
diff --git a/backend/scripts/migratePermissions.js b/backend/scripts/migratePermissions.js
new file mode 100644
index 0000000..96d383b
--- /dev/null
+++ b/backend/scripts/migratePermissions.js
@@ -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);
+});
+
+
diff --git a/backend/scripts/quickFixOwner.js b/backend/scripts/quickFixOwner.js
new file mode 100644
index 0000000..90d25ff
--- /dev/null
+++ b/backend/scripts/quickFixOwner.js
@@ -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);
+});
+
diff --git a/backend/server.js b/backend/server.js
index 67d8805..1aadafe 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -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')));
diff --git a/backend/services/clubService.js b/backend/services/clubService.js
index 0b1ee26..e55d7f6 100644
--- a/backend/services/clubService.js
+++ b/backend/services/clubService.js
@@ -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) {
diff --git a/backend/services/permissionService.js b/backend/services/permissionService.js
new file mode 100644
index 0000000..3c00aa5
--- /dev/null
+++ b/backend/services/permissionService.js
@@ -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();
+
diff --git a/backend/services/schedulerService.js b/backend/services/schedulerService.js
index ddc7bd9..9dd7270 100644
--- a/backend/services/schedulerService.js
+++ b/backend/services/schedulerService.js
@@ -11,7 +11,7 @@ class SchedulerService {
/**
* Start the scheduler
- */
+ */
start() {
if (this.isRunning) {
devLog('Scheduler is already running');
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index e9766ce..e9f114a 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -7,6 +7,28 @@
Trainingstagebuch
+
@@ -33,74 +55,57 @@
-
-
-
-
@@ -179,10 +184,25 @@ export default {
selectedClub: null,
sessionInterval: null,
logoUrl,
+ userDropdownOpen: false,
};
},
computed: {
- ...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'sidebarCollapsed']),
+ ...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'sidebarCollapsed', 'username', 'hasPermission', 'isClubOwner', 'userRole']),
+ canManageApprovals() {
+ // Nur anzeigen, wenn Permissions geladen sind UND Berechtigung vorhanden
+ if (!this.currentClub) return false;
+
+ // Owner oder Admin können Freigaben verwalten
+ return this.isClubOwner || this.userRole === 'admin' || this.hasPermission('approvals', 'read');
+ },
+ canManagePermissions() {
+ // Nur anzeigen, wenn Permissions geladen sind UND Berechtigung vorhanden
+ if (!this.currentClub) return false;
+
+ // Owner oder Admin können Berechtigungen verwalten
+ return this.isClubOwner || this.userRole === 'admin' || this.hasPermission('permissions', 'read');
+ },
},
watch: {
selectedClub(newVal) {
@@ -211,6 +231,16 @@ export default {
},
},
methods: {
+ toggleUserDropdown(event) {
+ event.stopPropagation();
+ this.userDropdownOpen = !this.userDropdownOpen;
+ },
+ handleClickOutside(event) {
+ const userMenu = event.target.closest('.user-menu');
+ if (!userMenu && this.userDropdownOpen) {
+ this.userDropdownOpen = false;
+ }
+ },
// Dialog Helper Methods
async showInfo(title, message, details = '', type = 'info') {
this.infoDialog = {
@@ -286,6 +316,9 @@ export default {
}
},
async mounted() {
+ // Click-outside handler für User-Dropdown
+ document.addEventListener('click', this.handleClickOutside);
+
// Nur Daten laden, wenn der Benutzer authentifiziert ist
if (this.isAuthenticated) {
try {
@@ -304,6 +337,7 @@ export default {
},
beforeUnmount() {
clearInterval(this.sessionInterval);
+ document.removeEventListener('click', this.handleClickOutside);
}
};
@@ -325,6 +359,10 @@ export default {
position: relative;
z-index: 1000;
flex-shrink: 0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 1.5rem;
}
.header-content {
@@ -344,6 +382,105 @@ export default {
/* Schriftgröße bleibt wie in der main.scss definiert */
}
+.user-menu {
+ position: relative;
+}
+
+.user-info {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ background-color: rgba(255, 255, 255, 0.1);
+ border-radius: 20px;
+ font-size: 0.9rem;
+ color: white;
+ border: none;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+
+.user-info:hover {
+ background-color: rgba(255, 255, 255, 0.2);
+}
+
+.user-icon {
+ font-size: 1.2rem;
+}
+
+.user-email {
+ font-weight: 500;
+}
+
+.dropdown-arrow {
+ font-size: 0.7rem;
+ margin-left: 0.25rem;
+ transition: transform 0.2s ease;
+}
+
+.user-dropdown {
+ position: absolute;
+ top: calc(100% + 0.5rem);
+ right: 0;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ min-width: 200px;
+ overflow: hidden;
+ z-index: 10000;
+ animation: dropdownFadeIn 0.2s ease;
+}
+
+@keyframes dropdownFadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.dropdown-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ color: #333;
+ text-decoration: none;
+ background: none;
+ border: none;
+ width: 100%;
+ text-align: left;
+ cursor: pointer;
+ font-size: 0.9rem;
+ transition: background-color 0.15s ease;
+}
+
+.dropdown-item:hover {
+ background-color: #f5f5f5;
+}
+
+.dropdown-icon {
+ font-size: 1.1rem;
+}
+
+.dropdown-divider {
+ height: 1px;
+ background-color: #e0e0e0;
+ margin: 0.25rem 0;
+}
+
+.logout-item {
+ color: #dc3545;
+ font-weight: 500;
+}
+
+.logout-item:hover {
+ background-color: #fff5f5;
+}
+
.home-link {
display: inline-flex;
align-items: center;
@@ -699,6 +836,15 @@ export default {
font-size: 1rem;
}
+ .user-info {
+ font-size: 0.75rem;
+ padding: 0.3rem 0.6rem;
+ }
+
+ .user-email {
+ display: none; /* Nur Icon auf mobile */
+ }
+
.sidebar-content {
padding: 0.5rem;
}
diff --git a/frontend/src/composables/usePermissions.js b/frontend/src/composables/usePermissions.js
new file mode 100644
index 0000000..8900388
--- /dev/null
+++ b/frontend/src/composables/usePermissions.js
@@ -0,0 +1,66 @@
+import { computed } from 'vue';
+import { useStore } from 'vuex';
+
+/**
+ * Composable for permission checks in Vue components
+ */
+export function usePermissions() {
+ const store = useStore();
+
+ const permissions = computed(() => store.getters.currentPermissions);
+ const isOwner = computed(() => store.getters.isClubOwner);
+ const userRole = computed(() => store.getters.userRole);
+
+ /**
+ * Check if user has specific permission
+ * @param {string} resource - Resource name (diary, members, teams, etc.)
+ * @param {string} action - Action type (read, write, delete)
+ * @returns {boolean}
+ */
+ const can = (resource, action = 'read') => {
+ return store.getters.hasPermission(resource, action);
+ };
+
+ /**
+ * Check if user can read
+ */
+ const canRead = (resource) => can(resource, 'read');
+
+ /**
+ * Check if user can write
+ */
+ const canWrite = (resource) => can(resource, 'write');
+
+ /**
+ * Check if user can delete
+ */
+ const canDelete = (resource) => can(resource, 'delete');
+
+ /**
+ * Check if user is admin (owner or admin role)
+ */
+ const isAdmin = computed(() => {
+ return isOwner.value || userRole.value === 'admin';
+ });
+
+ /**
+ * Check if user has specific role
+ */
+ const hasRole = (role) => {
+ return userRole.value === role;
+ };
+
+ return {
+ permissions,
+ isOwner,
+ isAdmin,
+ userRole,
+ can,
+ canRead,
+ canWrite,
+ canDelete,
+ hasRole
+ };
+}
+
+
diff --git a/frontend/src/directives/permissions.js b/frontend/src/directives/permissions.js
new file mode 100644
index 0000000..7541bad
--- /dev/null
+++ b/frontend/src/directives/permissions.js
@@ -0,0 +1,198 @@
+/**
+ * Vue directive for permission-based element visibility
+ * Usage: v-can:resource.action or v-can="'resource.action'"
+ *
+ * Examples:
+ *
+ *
+ * ...
+ */
+
+const checkPermission = (el, binding, vnode) => {
+ // Safely access store
+ if (!vnode.appContext || !vnode.appContext.config || !vnode.appContext.config.globalProperties) {
+ // Hide by default if store not available (deny by default)
+ el.style.display = 'none';
+ return;
+ }
+
+ const store = vnode.appContext.config.globalProperties.$store;
+
+ if (!store) {
+ // Hide by default if store not found (deny by default)
+ el.style.display = 'none';
+ return;
+ }
+
+ let resource, action;
+
+ // Parse directive value
+ if (typeof binding.value === 'string') {
+ // v-can="'diary.write'"
+ [resource, action] = binding.value.split('.');
+ } else if (binding.arg) {
+ // v-can:diary.write
+ resource = binding.arg;
+ action = Object.keys(binding.modifiers)[0] || 'read';
+ } else {
+ console.warn('v-can directive requires resource and action');
+ el.style.display = 'none';
+ return;
+ }
+
+ const hasPermission = store.getters.hasPermission(resource, action);
+
+ if (hasPermission) {
+ el.style.display = '';
+ } else {
+ el.style.display = 'none';
+ }
+};
+
+export const canDirective = {
+ mounted(el, binding, vnode) {
+ // Initial check
+ checkPermission(el, binding, vnode);
+
+ // Set up watcher for permissions changes
+ if (vnode.appContext && vnode.appContext.config && vnode.appContext.config.globalProperties) {
+ const store = vnode.appContext.config.globalProperties.$store;
+ if (store) {
+ // Watch both permissions and currentClub
+ el._permissionUnwatch = store.subscribe((mutation) => {
+ if (mutation.type === 'setPermissions' || mutation.type === 'setClub') {
+ checkPermission(el, binding, vnode);
+ }
+ });
+ }
+ }
+ },
+
+ updated(el, binding, vnode) {
+ checkPermission(el, binding, vnode);
+ },
+
+ unmounted(el) {
+ // Clean up watcher
+ if (el._permissionUnwatch) {
+ el._permissionUnwatch();
+ delete el._permissionUnwatch;
+ }
+ }
+};
+
+/**
+ * Directive for admin-only elements
+ * Usage: v-admin
+ */
+const checkAdmin = (el, vnode) => {
+ if (!vnode.appContext || !vnode.appContext.config || !vnode.appContext.config.globalProperties) {
+ el.style.display = 'none';
+ return;
+ }
+
+ const store = vnode.appContext.config.globalProperties.$store;
+
+ if (!store) {
+ el.style.display = 'none';
+ return;
+ }
+
+ const isOwner = store.getters.isClubOwner;
+ const role = store.getters.userRole;
+
+ if (isOwner || role === 'admin') {
+ el.style.display = '';
+ } else {
+ el.style.display = 'none';
+ }
+};
+
+export const adminDirective = {
+ mounted(el, binding, vnode) {
+ checkAdmin(el, vnode);
+
+ if (vnode.appContext && vnode.appContext.config && vnode.appContext.config.globalProperties) {
+ const store = vnode.appContext.config.globalProperties.$store;
+ if (store) {
+ el._adminUnwatch = store.subscribe((mutation) => {
+ if (mutation.type === 'setPermissions' || mutation.type === 'setClub') {
+ checkAdmin(el, vnode);
+ }
+ });
+ }
+ }
+ },
+
+ updated(el, binding, vnode) {
+ checkAdmin(el, vnode);
+ },
+
+ unmounted(el) {
+ if (el._adminUnwatch) {
+ el._adminUnwatch();
+ delete el._adminUnwatch;
+ }
+ }
+};
+
+/**
+ * Directive for owner-only elements
+ * Usage: v-owner
+ */
+const checkOwner = (el, vnode) => {
+ if (!vnode.appContext || !vnode.appContext.config || !vnode.appContext.config.globalProperties) {
+ el.style.display = 'none';
+ return;
+ }
+
+ const store = vnode.appContext.config.globalProperties.$store;
+
+ if (!store) {
+ el.style.display = 'none';
+ return;
+ }
+
+ const isOwner = store.getters.isClubOwner;
+
+ if (isOwner) {
+ el.style.display = '';
+ } else {
+ el.style.display = 'none';
+ }
+};
+
+export const ownerDirective = {
+ mounted(el, binding, vnode) {
+ checkOwner(el, vnode);
+
+ if (vnode.appContext && vnode.appContext.config && vnode.appContext.config.globalProperties) {
+ const store = vnode.appContext.config.globalProperties.$store;
+ if (store) {
+ el._ownerUnwatch = store.subscribe((mutation) => {
+ if (mutation.type === 'setPermissions' || mutation.type === 'setClub') {
+ checkOwner(el, vnode);
+ }
+ });
+ }
+ }
+ },
+
+ updated(el, binding, vnode) {
+ checkOwner(el, vnode);
+ },
+
+ unmounted(el) {
+ if (el._ownerUnwatch) {
+ el._ownerUnwatch();
+ delete el._ownerUnwatch;
+ }
+ }
+};
+
+export default {
+ can: canDirective,
+ admin: adminDirective,
+ owner: ownerDirective
+};
+
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 931f116..341a087 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -4,9 +4,16 @@ import router from './router';
import store from './store';
import '@/assets/css/main.scss';
import './assets/css/vue-multiselect.css';
+import permissionDirectives from './directives/permissions.js';
const app = createApp(App);
app.config.devtools = true;
+
+// Register permission directives
+app.directive('can', permissionDirectives.can);
+app.directive('admin', permissionDirectives.admin);
+app.directive('owner', permissionDirectives.owner);
+
app
.use(router)
.use(store)
diff --git a/frontend/src/router.js b/frontend/src/router.js
index 264802f..d38a887 100644
--- a/frontend/src/router.js
+++ b/frontend/src/router.js
@@ -15,6 +15,7 @@ import PredefinedActivities from './views/PredefinedActivities.vue';
import OfficialTournaments from './views/OfficialTournaments.vue';
import MyTischtennisAccount from './views/MyTischtennisAccount.vue';
import TeamManagementView from './views/TeamManagementView.vue';
+import PermissionsView from './views/PermissionsView.vue';
import Impressum from './views/Impressum.vue';
import Datenschutz from './views/Datenschutz.vue';
@@ -35,6 +36,7 @@ const routes = [
{ path: '/official-tournaments', component: OfficialTournaments },
{ path: '/mytischtennis-account', component: MyTischtennisAccount },
{ path: '/team-management', component: TeamManagementView },
+ { path: '/permissions', component: PermissionsView },
{ path: '/impressum', component: Impressum },
{ path: '/datenschutz', component: Datenschutz },
];
diff --git a/frontend/src/store.js b/frontend/src/store.js
index 8391a3a..dcdfc06 100644
--- a/frontend/src/store.js
+++ b/frontend/src/store.js
@@ -14,6 +14,13 @@ const store = createStore({
this.clubs = [];
}
})(),
+ permissions: (() => {
+ try {
+ return JSON.parse(localStorage.getItem('clubPermissions')) || {};
+ } catch (e) {
+ return {};
+ }
+ })(), // { clubId: { role, isOwner, permissions: {...} } }
dialogs: [], // Array von offenen Dialogen
dialogCounter: 0, // Zähler für eindeutige Dialog-IDs
sidebarCollapsed: (() => {
@@ -44,6 +51,17 @@ const store = createStore({
state.clubs = clubs;
localStorage.setItem('clubs', JSON.stringify(clubs));
},
+ setPermissions(state, { clubId, permissions }) {
+ state.permissions = {
+ ...state.permissions,
+ [clubId]: permissions
+ };
+ localStorage.setItem('clubPermissions', JSON.stringify(state.permissions));
+ },
+ clearPermissions(state) {
+ state.permissions = {};
+ localStorage.removeItem('clubPermissions');
+ },
setSidebarCollapsed(state, collapsed) {
state.sidebarCollapsed = collapsed;
localStorage.setItem('sidebarCollapsed', collapsed.toString());
@@ -105,12 +123,33 @@ const store = createStore({
logout({ commit }) {
commit('clearToken');
commit('clearUsername');
+ commit('clearPermissions');
router.push('/login'); // Leitet den Benutzer zur Login-Seite um
// window.location.reload() entfernt, um Endlos-Neuladeschleife zu verhindern
},
- setCurrentClub({ commit }, club) {
+ async setCurrentClub({ commit, dispatch }, club) {
commit('setClub', club);
+ // Load permissions for this club
+ await dispatch('loadPermissions', club);
+ },
+
+ async loadPermissions({ commit }, clubId) {
+ try {
+ const response = await apiClient.get(`/permissions/${clubId}`);
+ commit('setPermissions', { clubId, permissions: response.data });
+ } catch (error) {
+ console.error('Error loading permissions:', error);
+ // Set default permissions (read-only)
+ commit('setPermissions', {
+ clubId,
+ permissions: {
+ role: 'member',
+ isOwner: false,
+ permissions: {}
+ }
+ });
+ }
},
setClubs({ commit }, clubs) {
commit('setClubsMutation', clubs);
@@ -146,6 +185,31 @@ const store = createStore({
const club = state.clubs.find(club => club.id === parseInt(state.currentClub));
return club ? club.name : '';
},
+ // Permission getters
+ currentPermissions: state => {
+ if (!state.currentClub) return null;
+ return state.permissions[state.currentClub] || null;
+ },
+ hasPermission: (state) => (resource, action) => {
+ if (!state.currentClub) return false;
+ const perms = state.permissions[state.currentClub];
+ if (!perms) return false;
+ if (perms.isOwner) return true;
+ if (resource === 'mytischtennis') return true; // MyTischtennis für alle
+ const resourcePerms = perms.permissions[resource];
+ if (!resourcePerms) return false;
+ return resourcePerms[action] === true;
+ },
+ isClubOwner: state => {
+ if (!state.currentClub) return false;
+ const perms = state.permissions[state.currentClub];
+ return perms?.isOwner || false;
+ },
+ userRole: state => {
+ if (!state.currentClub) return null;
+ const perms = state.permissions[state.currentClub];
+ return perms?.role || null; // null wenn nicht geladen, nicht 'member'
+ },
// Dialog-Getters
dialogs: state => state.dialogs,
minimizedDialogs: state => state.dialogs.filter(dialog => dialog.isMinimized),
diff --git a/frontend/src/views/PendingApprovalsView.vue b/frontend/src/views/PendingApprovalsView.vue
index 8cd3249..64a72f2 100644
--- a/frontend/src/views/PendingApprovalsView.vue
+++ b/frontend/src/views/PendingApprovalsView.vue
@@ -124,7 +124,12 @@ export default {
const response = await apiClient.get(`/clubs/pending/${this.currentClub}`);
this.pendingUsers = response.data.map(entry => entry.user);
} catch (error) {
- this.showInfo('Fehler', 'Fehler beim Laden der ausstehenden Anfragen', '', 'error');
+ if (error.response?.status === 403) {
+ await this.showInfo('Keine Berechtigung', 'Sie haben keine Berechtigung, Freigaben zu verwalten.', 'Nur Administratoren können Mitgliedsanfragen bearbeiten.', 'error');
+ this.$router.push('/');
+ } else {
+ this.showInfo('Fehler', 'Fehler beim Laden der ausstehenden Anfragen', error.response?.data?.error || '', 'error');
+ }
}
},
async approveUser(userId) {
diff --git a/frontend/src/views/PermissionsView.vue b/frontend/src/views/PermissionsView.vue
new file mode 100644
index 0000000..19bd988
--- /dev/null
+++ b/frontend/src/views/PermissionsView.vue
@@ -0,0 +1,631 @@
+
+
+
+
+
Lade Mitglieder...
+
{{ error }}
+
+
+
+
Verfügbare Rollen
+
+
+
{{ role.label }}
+
{{ role.description }}
+
+
+
+
+
+
+
Clubmitglieder
+
+
+
+ | Email |
+ Rolle |
+ Status |
+ Aktionen |
+
+
+
+
+ | {{ member.user?.email || 'N/A' }} |
+
+
+
+ {{ getRoleLabel(member.role) }}
+ 👑 Ersteller
+
+ |
+
+
+ ✓ Aktiv
+
+
+ ✗ Deaktiviert
+
+ |
+
+
+ —
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Basis-Rolle: {{ getRoleLabel(selectedMember.role) }}
+ Hier können Sie individuelle Anpassungen vornehmen.
+
+
+
+
+
{{ resource.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+