-- Migration: 035_add_role_system.sql -- Description: Add hierarchical role system for granular access control -- Author: Claude Code -- Date: 2026-02-01 -- -- Role hierarchy: -- UNAFFILIATED (10) - Firma spoza Izby - tylko publiczne profile -- MEMBER (20) - Członek Norda bez firmy - pełny dostęp do treści -- EMPLOYEE (30) - Pracownik firmy członkowskiej - edycja danych firmy -- MANAGER (40) - Kadra zarządzająca - pełna kontrola firmy + użytkownicy -- OFFICE_MANAGER (50) - Kierownik biura Norda - panel admina -- ADMIN (100) - Administrator portalu - pełne prawa -- -- Company roles: -- NONE (0) - Brak powiązania z firmą -- VIEWER (10) - Może przeglądać dashboard firmy -- EMPLOYEE (20) - Może edytować dane firmy -- MANAGER (30) - Pełna kontrola + zarządzanie użytkownikami -- ============================================================ -- STEP 1: Add new columns -- ============================================================ -- Add role column with default UNAFFILIATED ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR(20) DEFAULT 'UNAFFILIATED' NOT NULL; -- Add company_role column with default NONE ALTER TABLE users ADD COLUMN IF NOT EXISTS company_role VARCHAR(20) DEFAULT 'NONE' NOT NULL; -- Add index for role lookups CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); -- ============================================================ -- STEP 2: Migrate existing admins -- ============================================================ UPDATE users SET role = 'ADMIN' WHERE is_admin = TRUE AND (role IS NULL OR role = 'UNAFFILIATED'); -- ============================================================ -- STEP 3: Auto-detect MANAGER role from KRS data -- Users linked to company management (zarząd) get MANAGER role -- ============================================================ UPDATE users u SET role = 'MANAGER', company_role = 'MANAGER' WHERE u.company_id IS NOT NULL AND u.role NOT IN ('ADMIN', 'OFFICE_MANAGER') AND EXISTS ( SELECT 1 FROM company_people cp WHERE cp.person_id = u.person_id AND cp.company_id = u.company_id AND cp.role_category = 'zarzad' ); -- ============================================================ -- STEP 4: Set EMPLOYEE role for users with company but no KRS link -- ============================================================ UPDATE users SET role = 'EMPLOYEE', company_role = 'EMPLOYEE' WHERE company_id IS NOT NULL AND role NOT IN ('ADMIN', 'OFFICE_MANAGER', 'MANAGER'); -- ============================================================ -- STEP 5: Set MEMBER role for Norda members without company -- ============================================================ UPDATE users SET role = 'MEMBER' WHERE is_norda_member = TRUE AND company_id IS NULL AND role = 'UNAFFILIATED'; -- ============================================================ -- STEP 6: Create user_company_permissions table for delegation -- ============================================================ CREATE TABLE IF NOT EXISTS user_company_permissions ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, company_id INTEGER NOT NULL REFERENCES companies(id) ON DELETE CASCADE, -- Content editing permissions can_edit_description BOOLEAN DEFAULT TRUE, -- Opis firmy, historia, wartości can_edit_services BOOLEAN DEFAULT TRUE, -- Usługi, kompetencje, technologie can_edit_contacts BOOLEAN DEFAULT FALSE, -- Email, telefon, adres can_edit_social BOOLEAN DEFAULT FALSE, -- Social media, strona www -- Feature permissions can_manage_classifieds BOOLEAN DEFAULT TRUE, -- B2B ogłoszenia w imieniu firmy can_post_forum BOOLEAN DEFAULT TRUE, -- Posty na forum w imieniu firmy can_view_analytics BOOLEAN DEFAULT FALSE, -- Statystyki firmy, wyświetlenia -- Audit trail granted_by_id INTEGER REFERENCES users(id), granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Unique constraint: one permission record per user-company pair CONSTRAINT uq_user_company_permissions UNIQUE (user_id, company_id) ); -- Indexes for lookups CREATE INDEX IF NOT EXISTS idx_user_company_permissions_user ON user_company_permissions(user_id); CREATE INDEX IF NOT EXISTS idx_user_company_permissions_company ON user_company_permissions(company_id); -- Grant permissions to application user GRANT ALL ON TABLE user_company_permissions TO nordabiz_app; GRANT USAGE, SELECT ON SEQUENCE user_company_permissions_id_seq TO nordabiz_app; -- ============================================================ -- STEP 7: Create default permissions for existing EMPLOYEE users -- ============================================================ INSERT INTO user_company_permissions (user_id, company_id, can_edit_description, can_edit_services) SELECT id, company_id, TRUE, TRUE FROM users WHERE role = 'EMPLOYEE' AND company_id IS NOT NULL ON CONFLICT (user_id, company_id) DO NOTHING; -- ============================================================ -- STEP 8: Verify migration results -- ============================================================ -- Log migration statistics (view in PostgreSQL logs) DO $$ DECLARE admin_count INTEGER; office_mgr_count INTEGER; manager_count INTEGER; employee_count INTEGER; member_count INTEGER; unaffiliated_count INTEGER; permissions_count INTEGER; BEGIN SELECT COUNT(*) INTO admin_count FROM users WHERE role = 'ADMIN'; SELECT COUNT(*) INTO office_mgr_count FROM users WHERE role = 'OFFICE_MANAGER'; SELECT COUNT(*) INTO manager_count FROM users WHERE role = 'MANAGER'; SELECT COUNT(*) INTO employee_count FROM users WHERE role = 'EMPLOYEE'; SELECT COUNT(*) INTO member_count FROM users WHERE role = 'MEMBER'; SELECT COUNT(*) INTO unaffiliated_count FROM users WHERE role = 'UNAFFILIATED'; SELECT COUNT(*) INTO permissions_count FROM user_company_permissions; RAISE NOTICE 'Role migration complete:'; RAISE NOTICE ' ADMIN: %', admin_count; RAISE NOTICE ' OFFICE_MANAGER: %', office_mgr_count; RAISE NOTICE ' MANAGER: %', manager_count; RAISE NOTICE ' EMPLOYEE: %', employee_count; RAISE NOTICE ' MEMBER: %', member_count; RAISE NOTICE ' UNAFFILIATED: %', unaffiliated_count; RAISE NOTICE ' Delegated permissions records: %', permissions_count; END $$; -- ============================================================ -- VERIFICATION QUERIES (run manually to verify) -- ============================================================ -- Check for any users with NULL role (should be 0) -- SELECT COUNT(*) FROM users WHERE role IS NULL; -- View role distribution -- SELECT role, COUNT(*) as count FROM users GROUP BY role ORDER BY count DESC; -- View users with company but no company_role -- SELECT id, email, company_id, role, company_role -- FROM users -- WHERE company_id IS NOT NULL AND company_role = 'NONE'; -- Check KRS-linked managers -- SELECT u.id, u.email, u.role, u.company_role, c.name as company_name -- FROM users u -- JOIN companies c ON u.company_id = c.id -- WHERE u.role = 'MANAGER' -- LIMIT 10;