-- ============================================================================ -- Migration: Add Multi-Client Access & Client Hierarchy Support -- Version: 1.0 -- Date: 2025-09-30 -- Description: Menambahkan support untuk parent-child clients dan -- multi-client access per user -- ============================================================================ -- ---------------------------------------------------------------------------- -- STEP 1: Update Clients Table -- ---------------------------------------------------------------------------- -- Add new columns to clients table ALTER TABLE clients ADD COLUMN IF NOT EXISTS description TEXT, ADD COLUMN IF NOT EXISTS client_type VARCHAR DEFAULT 'sub_client', ADD COLUMN IF NOT EXISTS parent_client_id UUID, ADD COLUMN IF NOT EXISTS settings JSONB, ADD COLUMN IF NOT EXISTS max_users INT4, ADD COLUMN IF NOT EXISTS max_storage INT8; -- Add index for parent_client_id CREATE INDEX IF NOT EXISTS idx_clients_parent_client_id ON clients(parent_client_id); -- Add foreign key constraint ALTER TABLE clients ADD CONSTRAINT fk_clients_parent_client FOREIGN KEY (parent_client_id) REFERENCES clients(id) ON DELETE SET NULL; -- Update existing clients to 'standalone' type UPDATE clients SET client_type = 'standalone' WHERE client_type IS NULL OR client_type = ''; COMMENT ON COLUMN clients.client_type IS 'parent_client, sub_client, or standalone'; COMMENT ON COLUMN clients.parent_client_id IS 'Reference to parent client for hierarchy'; COMMENT ON COLUMN clients.settings IS 'JSONB custom settings for the client'; -- ---------------------------------------------------------------------------- -- STEP 2: Update Users Table -- ---------------------------------------------------------------------------- -- Add super admin flag ALTER TABLE users ADD COLUMN IF NOT EXISTS is_super_admin BOOLEAN DEFAULT FALSE; -- Create index for super admin lookups CREATE INDEX IF NOT EXISTS idx_users_is_super_admin ON users(is_super_admin) WHERE is_super_admin = TRUE; COMMENT ON COLUMN users.is_super_admin IS 'Platform super admin with access to all clients'; COMMENT ON COLUMN users.client_id IS 'Primary/default client for the user'; -- ---------------------------------------------------------------------------- -- STEP 3: Create UserClientAccess Table (Many-to-Many) -- ---------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS user_client_access ( id SERIAL PRIMARY KEY, user_id INT4 NOT NULL, client_id UUID NOT NULL, -- Access control access_type VARCHAR DEFAULT 'read', can_manage BOOLEAN DEFAULT FALSE, can_delegate BOOLEAN DEFAULT FALSE, include_sub_clients BOOLEAN DEFAULT FALSE, -- Audit granted_by_id INT4, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), -- Constraints CONSTRAINT fk_user_client_access_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT fk_user_client_access_client FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE, CONSTRAINT fk_user_client_access_granted_by FOREIGN KEY (granted_by_id) REFERENCES users(id) ON DELETE SET NULL, -- Prevent duplicate access entries CONSTRAINT uk_user_client_access_unique UNIQUE (user_id, client_id) ); -- Create indexes for performance CREATE INDEX IF NOT EXISTS idx_user_client_access_user_id ON user_client_access(user_id); CREATE INDEX IF NOT EXISTS idx_user_client_access_client_id ON user_client_access(client_id); CREATE INDEX IF NOT EXISTS idx_user_client_access_user_client ON user_client_access(user_id, client_id); CREATE INDEX IF NOT EXISTS idx_user_client_access_active ON user_client_access(user_id, is_active) WHERE is_active = TRUE; -- Add comments COMMENT ON TABLE user_client_access IS 'Many-to-many relationship between users and clients for multi-tenant access'; COMMENT ON COLUMN user_client_access.access_type IS 'Access level: read, write, admin, or owner'; COMMENT ON COLUMN user_client_access.can_manage IS 'Can manage client settings'; COMMENT ON COLUMN user_client_access.can_delegate IS 'Can grant access to other users'; COMMENT ON COLUMN user_client_access.include_sub_clients IS 'Automatically grant access to all sub-clients'; -- ---------------------------------------------------------------------------- -- STEP 4: Data Migration (Optional but Recommended) -- ---------------------------------------------------------------------------- -- Create UserClientAccess entries for existing users with client_id -- This is optional - only if you want existing users to have explicit access records INSERT INTO user_client_access ( user_id, client_id, access_type, can_manage, is_active, created_at, updated_at ) SELECT id as user_id, client_id, 'admin' as access_type, -- Default existing users as admin TRUE as can_manage, TRUE as is_active, NOW(), NOW() FROM users WHERE client_id IS NOT NULL AND (is_super_admin IS NULL OR is_super_admin = FALSE) AND NOT EXISTS ( SELECT 1 FROM user_client_access uca WHERE uca.user_id = users.id AND uca.client_id = users.client_id ); -- ---------------------------------------------------------------------------- -- STEP 5: Create Helper Functions (Optional) -- ---------------------------------------------------------------------------- -- Function to get all accessible client IDs for a user (including sub-clients) CREATE OR REPLACE FUNCTION get_user_accessible_client_ids(p_user_id INT4) RETURNS TABLE(client_id UUID) AS $$ BEGIN RETURN QUERY WITH RECURSIVE client_hierarchy AS ( -- Base case: direct client access SELECT uca.client_id, uca.include_sub_clients FROM user_client_access uca WHERE uca.user_id = p_user_id AND uca.is_active = TRUE UNION -- User's primary client SELECT u.client_id, FALSE as include_sub_clients FROM users u WHERE u.id = p_user_id AND u.client_id IS NOT NULL UNION -- Recursive case: sub-clients if include_sub_clients is true SELECT c.id, FALSE FROM clients c INNER JOIN client_hierarchy ch ON c.parent_client_id = ch.client_id WHERE ch.include_sub_clients = TRUE AND c.is_active = TRUE ) SELECT DISTINCT ch.client_id FROM client_hierarchy ch WHERE ch.client_id IS NOT NULL; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION get_user_accessible_client_ids IS 'Returns all client IDs accessible by a user including sub-clients'; -- Function to check if client is a parent (has sub-clients) CREATE OR REPLACE FUNCTION is_parent_client(p_client_id UUID) RETURNS BOOLEAN AS $$ DECLARE v_count INT; BEGIN SELECT COUNT(*) INTO v_count FROM clients WHERE parent_client_id = p_client_id AND is_active = TRUE; RETURN v_count > 0; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION is_parent_client IS 'Check if a client has sub-clients'; -- Function to get all sub-client IDs recursively CREATE OR REPLACE FUNCTION get_sub_client_ids(p_parent_client_id UUID) RETURNS TABLE(client_id UUID) AS $$ BEGIN RETURN QUERY WITH RECURSIVE sub_clients AS ( -- Base case: direct children SELECT id as client_id FROM clients WHERE parent_client_id = p_parent_client_id AND is_active = TRUE UNION -- Recursive case: children of children SELECT c.id FROM clients c INNER JOIN sub_clients sc ON c.parent_client_id = sc.client_id WHERE c.is_active = TRUE ) SELECT sc.client_id FROM sub_clients sc; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION get_sub_client_ids IS 'Get all sub-client IDs recursively for a parent client'; -- ---------------------------------------------------------------------------- -- STEP 6: Create Views for Common Queries (Optional) -- ---------------------------------------------------------------------------- -- View: User Client Access with Details CREATE OR REPLACE VIEW v_user_client_access_details AS SELECT uca.id, uca.user_id, u.username, u.fullname, u.email, uca.client_id, c.name as client_name, c.client_type, c.parent_client_id, pc.name as parent_client_name, uca.access_type, uca.can_manage, uca.can_delegate, uca.include_sub_clients, uca.granted_by_id, gb.username as granted_by_username, uca.is_active, uca.created_at, uca.updated_at FROM user_client_access uca INNER JOIN users u ON uca.user_id = u.id INNER JOIN clients c ON uca.client_id = c.id LEFT JOIN clients pc ON c.parent_client_id = pc.id LEFT JOIN users gb ON uca.granted_by_id = gb.id; COMMENT ON VIEW v_user_client_access_details IS 'User client access with user and client details'; -- View: Client Hierarchy CREATE OR REPLACE VIEW v_client_hierarchy AS SELECT c.id, c.name, c.description, c.client_type, c.parent_client_id, pc.name as parent_client_name, (SELECT COUNT(*) FROM clients WHERE parent_client_id = c.id AND is_active = TRUE) as sub_client_count, c.max_users, c.max_storage, (SELECT COUNT(*) FROM users WHERE client_id = c.id AND is_active = TRUE) as current_users, c.is_active, c.created_at, c.updated_at FROM clients c LEFT JOIN clients pc ON c.parent_client_id = pc.id; COMMENT ON VIEW v_client_hierarchy IS 'Client hierarchy with parent info and statistics'; -- ---------------------------------------------------------------------------- -- STEP 7: Create Triggers (Optional - for audit/auto-update) -- ---------------------------------------------------------------------------- -- Trigger to update updated_at on user_client_access CREATE OR REPLACE FUNCTION update_user_client_access_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_update_user_client_access_timestamp ON user_client_access; CREATE TRIGGER trg_update_user_client_access_timestamp BEFORE UPDATE ON user_client_access FOR EACH ROW EXECUTE FUNCTION update_user_client_access_timestamp(); -- ---------------------------------------------------------------------------- -- STEP 8: Grant Permissions (Adjust based on your DB user) -- ---------------------------------------------------------------------------- -- Grant permissions to application user (adjust user name as needed) -- GRANT SELECT, INSERT, UPDATE, DELETE ON user_client_access TO your_app_user; -- GRANT USAGE, SELECT ON SEQUENCE user_client_access_id_seq TO your_app_user; -- GRANT SELECT ON v_user_client_access_details TO your_app_user; -- GRANT SELECT ON v_client_hierarchy TO your_app_user; -- ---------------------------------------------------------------------------- -- VERIFICATION QUERIES -- ---------------------------------------------------------------------------- -- Verify clients table structure SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_name = 'clients' ORDER BY ordinal_position; -- Verify users table has is_super_admin SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'is_super_admin'; -- Verify user_client_access table SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_name = 'user_client_access' ORDER BY ordinal_position; -- Check indexes SELECT indexname, tablename, indexdef FROM pg_indexes WHERE tablename IN ('clients', 'users', 'user_client_access') ORDER BY tablename, indexname; -- ============================================================================ -- ROLLBACK SCRIPT (Use with caution!) -- ============================================================================ /* -- To rollback this migration (WARNING: Will lose data!) -- Drop functions DROP FUNCTION IF EXISTS get_user_accessible_client_ids(INT4); DROP FUNCTION IF EXISTS is_parent_client(UUID); DROP FUNCTION IF EXISTS get_sub_client_ids(UUID); DROP FUNCTION IF EXISTS update_user_client_access_timestamp(); -- Drop views DROP VIEW IF EXISTS v_user_client_access_details; DROP VIEW IF EXISTS v_client_hierarchy; -- Drop table DROP TABLE IF EXISTS user_client_access; -- Remove columns from users ALTER TABLE users DROP COLUMN IF EXISTS is_super_admin; -- Remove columns from clients ALTER TABLE clients DROP COLUMN IF EXISTS description; ALTER TABLE clients DROP COLUMN IF EXISTS client_type; ALTER TABLE clients DROP COLUMN IF EXISTS parent_client_id; ALTER TABLE clients DROP COLUMN IF EXISTS settings; ALTER TABLE clients DROP COLUMN IF EXISTS max_users; ALTER TABLE clients DROP COLUMN IF EXISTS max_storage; */