kontenhumas-be/docs/migrations/001_add_multi_client_suppor...

380 lines
13 KiB
MySQL
Raw Normal View History

2025-09-30 13:34:56 +00:00
-- ============================================================================
-- 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;
*/