shinkan-jinkendo/backend/migrations/039_club_membership_rbac.sql
Lars e69aca51f6
Some checks failed
Deploy Development / deploy (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Has been cancelled
fix: update logging messages for database operations and version bump
- Changed logging output for PostgreSQL readiness, schema loading, and migration status to a consistent format using [OK], [FAIL], and [WARN].
- Updated application version to 0.8.14 and modified changelog to reflect recent changes, including a fix for co-trainer backfill logic in the database migration.
- Enhanced error handling messages for better clarity during migration processes.
2026-05-05 16:18:42 +02:00

116 lines
4.2 KiB
SQL

-- Migration 039: Vereins-Mitgliedschaft, Rollen pro Verein, aktiver Vereinskontext (Multi-Tenancy Phase 1)
-- Erstellt: 2026-05-05
-- Mitgliedschaft Profil ↔ Verein
CREATE TABLE club_members (
id SERIAL PRIMARY KEY,
profile_id INT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(profile_id, club_id)
);
CREATE INDEX idx_club_members_profile ON club_members(profile_id);
CREATE INDEX idx_club_members_club ON club_members(club_id);
CREATE INDEX idx_club_members_status ON club_members(status);
-- Rollen pro Mitgliedschaft (role_code: club_admin, trainer, division_lead, content_editor)
CREATE TABLE club_member_roles (
id SERIAL PRIMARY KEY,
club_member_id INT NOT NULL REFERENCES club_members(id) ON DELETE CASCADE,
role_code VARCHAR(50) NOT NULL,
division_id INT REFERENCES divisions(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE (club_member_id, role_code)
);
CREATE INDEX idx_club_member_roles_member ON club_member_roles(club_member_id);
CREATE INDEX idx_club_member_roles_code ON club_member_roles(role_code);
-- Hauptverwalter:in (fachliche Referenz; Rechte über club_member_roles.club_admin)
ALTER TABLE clubs ADD COLUMN IF NOT EXISTS primary_admin_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL;
CREATE INDEX idx_clubs_primary_admin ON clubs(primary_admin_profile_id);
-- Persistenz gewählter aktiver Verein (UI); Request-Header kann überschreiben
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS active_club_id INT REFERENCES clubs(id) ON DELETE SET NULL;
CREATE INDEX idx_profiles_active_club ON profiles(active_club_id);
-- ── Backfill: Trainer/Co-Trainer aus Trainingsgruppen → Mitgliedschaft + Rolle trainer
INSERT INTO club_members (profile_id, club_id, status)
SELECT DISTINCT t.trainer_id, t.club_id, 'active'
FROM training_groups t
WHERE t.trainer_id IS NOT NULL
ON CONFLICT (profile_id, club_id) DO NOTHING;
INSERT INTO club_members (profile_id, club_id, status)
SELECT DISTINCT elem::int, t.club_id, 'active'
FROM training_groups t,
LATERAL jsonb_array_elements_text(t.co_trainer_ids) AS elem
WHERE jsonb_typeof(t.co_trainer_ids) = 'array'
AND jsonb_array_length(t.co_trainer_ids) > 0
ON CONFLICT (profile_id, club_id) DO NOTHING;
INSERT INTO club_member_roles (club_member_id, role_code)
SELECT cm.id, 'trainer'
FROM club_members cm
WHERE NOT EXISTS (
SELECT 1 FROM club_member_roles r
WHERE r.club_member_id = cm.id AND r.role_code = 'trainer'
);
-- Pro Verein: kleinste trainer_id einer Gruppe als primärer Admin (Pilot / CURR-008-Analog)
UPDATE clubs c
SET primary_admin_profile_id = sub.pid
FROM (
SELECT DISTINCT ON (club_id)
club_id,
trainer_id AS pid
FROM training_groups
WHERE trainer_id IS NOT NULL
ORDER BY club_id, id ASC
) sub
WHERE c.id = sub.club_id
AND c.primary_admin_profile_id IS NULL;
INSERT INTO club_member_roles (club_member_id, role_code)
SELECT cm.id, 'club_admin'
FROM club_members cm
INNER JOIN clubs cl ON cl.id = cm.club_id AND cl.primary_admin_profile_id = cm.profile_id
WHERE NOT EXISTS (
SELECT 1 FROM club_member_roles r
WHERE r.club_member_id = cm.id AND r.role_code = 'club_admin'
);
-- Globale Admins: in jedem bestehenden Verein als club_admin spiegeln (volle Plattform-Sicht)
INSERT INTO club_members (profile_id, club_id, status)
SELECT p.id, c.id, 'active'
FROM profiles p
CROSS JOIN clubs c
WHERE p.role IN ('admin', 'superadmin')
ON CONFLICT (profile_id, club_id) DO NOTHING;
INSERT INTO club_member_roles (club_member_id, role_code)
SELECT cm.id, 'club_admin'
FROM club_members cm
INNER JOIN profiles p ON p.id = cm.profile_id AND p.role IN ('admin', 'superadmin')
WHERE NOT EXISTS (
SELECT 1 FROM club_member_roles r
WHERE r.club_member_id = cm.id AND r.role_code = 'club_admin'
);
-- Default aktiver Verein: einziger Verein des Nutzers
UPDATE profiles p
SET active_club_id = sub.only_club
FROM (
SELECT profile_id, MIN(club_id) AS only_club
FROM club_members
WHERE status = 'active'
GROUP BY profile_id
HAVING COUNT(DISTINCT club_id) = 1
) sub
WHERE p.id = sub.profile_id AND (p.active_club_id IS NULL);