380 lines
13 KiB
MySQL
380 lines
13 KiB
MySQL
|
|
-- ============================================================================
|
||
|
|
-- 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;
|
||
|
|
|
||
|
|
*/
|