From f94860aacd6200fb24c9e7431eb379a368cb392d Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 10 Jul 2026 00:35:44 +0000
Subject: [PATCH] =Refactored CredentialsManager.php to utilize a CustomTable instance instead

---
 inc/integrations/Integrations.php       |  161 +++++++++++---------------
 inc/registrar/Fields.php                |   16 ++
 inc/integrations/CredentialsManager.php |  125 ++++++++------------
 3 files changed, 135 insertions(+), 167 deletions(-)

diff --git a/inc/integrations/CredentialsManager.php b/inc/integrations/CredentialsManager.php
index 66a982e..a3967a2 100644
--- a/inc/integrations/CredentialsManager.php
+++ b/inc/integrations/CredentialsManager.php
@@ -1,6 +1,8 @@
 <?php
 namespace JVBase\integrations;
 
+use JVBase\managers\CustomTable;
+
 if (!defined('ABSPATH')) {
 	exit;
 }
@@ -9,12 +11,39 @@
 {
 	private static ?CredentialsManager $instance = null;
 	private string $encryption_key;
-	private string $option_prefix = 'jvb_integration_';
-	private string $user_meta_prefix = 'jvb_integration_';
+	private CustomTable $table;
 
 	private function __construct()
 	{
-		$this->encryption_key = $this->getEncryptionKey();
+		$this->getEncryptionKey();
+		$this->defineTable();
+	}
+
+	protected function defineTable():void
+	{
+		$table = CustomTable::for('integrations');
+		$table->setColumns([
+			'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+			'user_id'		=> $table->getUserIDType().' NOT NULL',
+			'integration'	=> "ENUM('bluesky','cloudflare','facebook','google-maps','gmb','helcim','instagram','postmark','square','umami') NOT NULL",
+			'credentials'	=> 'varchar(255) DEFAULT NULL',
+			'created_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP',
+			'updated_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
+		]);
+
+		$table->setKeys([
+			['key' => 'PRIMARY', 'value' => '(`id`)'],
+			['key' => 'UNIQUE', 'value' => '`user_integration` (`user_id`, `integration`)'],
+			'`user` (`user_id`)',
+		]);
+
+		$base = BASE;
+		$table->setConstraints([
+			"CONSTRAINT `{$base}integrations_user` FOREIGN KEY (`user_id`)
+			REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
+		]);
+		$table->defineTable();
+		$this->table = $table;
 	}
 
 	public static function getInstance(): CredentialsManager
@@ -30,18 +59,11 @@
 	 */
 	public function storeCredentials(string $service, array $credentials, ?int $userID = null): bool
 	{
-		$credentials['updated_at'] = time();
-		if ($userID) {
-			return $this->storeUserCredentials($service, $credentials, $userID);
-		}
-		$encrypted_data = $this->encrypt(json_encode($credentials));
-		return update_option($this->option_prefix . $service, $encrypted_data);
-	}
-
-	public function storeUserCredentials(string $service, array $credentials, int $userID): bool
-	{
-		$encrypted_data = $this->encrypt(json_encode($credentials));
-		return update_user_meta($userID, $this->user_meta_prefix . $service, $encrypted_data);
+		$find = $this->findBy($service, $userID);
+		$update = [
+			'credentials'	=> $this->encrypt(json_encode($credentials))
+		];
+		return (bool)$this->table->findOrCreate($find, $update);
 	}
 
 	/**
@@ -49,30 +71,13 @@
 	 */
 	public function getCredentials(string $service, ?int $userID = null): array
 	{
-		if ($userID) {
-			return $this->getUserCredentials($service, $userID);
-		}
-
-		$encrypted_data = get_option($this->option_prefix . $service, '');
-
-		if (empty($encrypted_data)) {
+		$find = $this->findBy($service, $userID);
+		$credentials = $this->table->pluck('credentials', $find);
+		if (empty($credentials)) {
 			return [];
 		}
-
-		$decrypted_data = $this->decrypt($encrypted_data);
-		return $decrypted_data ? json_decode($decrypted_data, true) : [];
-	}
-
-	public function getUserCredentials(string $service, int $userID): array
-	{
-		$encrypted_data = get_user_meta($userID, $this->user_meta_prefix . $service, true);
-
-		if (empty($encrypted_data)) {
-			return [];
-		}
-
-		$decrypted_data = $this->decrypt($encrypted_data);
-		return $decrypted_data ? json_decode($decrypted_data, true) : [];
+		$decrypted_data = $this->decrypt($credentials[0]);
+		return empty($decrypted_data) ? [] : json_decode($decrypted_data, true);
 	}
 
 	/**
@@ -80,16 +85,16 @@
 	 */
 	public function deleteCredentials(string $service, ?int $userID = null): bool
 	{
-
-		if ($userID) {
-			return $this->deleteUserCredentials($service, $userID);
-		}
-		return delete_option($this->option_prefix . $service);
+		$find = $this->findBy($service, $userID);
+		return $this->table->delete($find);
 	}
 
-	public function deleteUserCredentials(string $service, int $userID): bool
+	protected function findBy(string $service, ?int $userID = null):array
 	{
-		return delete_user_meta($userID, $this->user_meta_prefix . $service);
+		return [
+			'user_id'	=> is_null($userID) ? 0 : $userID,
+			'integration'=> $service
+		];
 	}
 
 	/**
@@ -97,31 +102,15 @@
 	 */
 	public function hasCredentials(string $service, ?int $userID = null): bool
 	{
-		if ($userID) {
-			return $this->hasUserCredentials($service, $userID);
-		}
-		return !empty(get_option($this->option_prefix . $service, ''));
-	}
-
-	public function hasUserCredentials(string $service, int $userID): bool
-	{
-		$encrypted_data = get_user_meta($userID, $this->user_meta_prefix . $service, true);
-		return !empty($encrypted_data);
+		return !empty($this->getCredentials($service, $userID));
 	}
 
 	/**
 	 * Get or create encryption key
 	 */
-	private function getEncryptionKey(): string
+	private function getEncryptionKey():void
 	{
-		$key = get_option('jvb_encryption_key');
-
-		if (!$key) {
-			$key = base64_encode(random_bytes(32));
-			update_option('jvb_encryption_key', $key);
-		}
-
-		return base64_decode($key);
+		$this->encryption_key = JVB_KEY;
 	}
 
 	/**
@@ -151,14 +140,6 @@
 	 */
 	public function getUsersWithCredentials(string $service): array
 	{
-		global $wpdb;
-
-		$meta_key = $this->user_meta_prefix . $service;
-		$results = $wpdb->get_results($wpdb->prepare(
-			"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s",
-			$meta_key
-		));
-
-		return wp_list_pluck($results, 'user_id');
+		return $this->table->pluck('user_id', ['integration' => $service]);
 	}
 }
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index f7ce033..6f6232c 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -2312,93 +2312,6 @@
 
 	}
 
-	protected function getTermFieldMapping(string $taxonomy): array
-	{
-		return apply_filters(
-			"jvb_{$this->service_name}_term_field_mapping",
-			[],
-			$taxonomy,
-			$this
-		);
-	}
-
-	/**
-	 * Share post to external service (override in child classes)
-	 */
-	protected function sharePost(WP_Post $post): ?string
-	{
-		throw new Exception('sharePost must be implemented by child class');
-	}
-
-	/**
-	 * Update shared post on external service (override in child classes)
-	 */
-	protected function updateSharedPost(WP_Post $post, string $external_id): void
-	{
-		throw new Exception('updateSharedPost must be implemented by child class');
-	}
-
-	/**
-	 * Remove post from external service (override in child classes)
-	 */
-	protected function unsharePost(WP_Post $post, string $external_id): void
-	{
-		throw new Exception('unsharePost must be implemented by child class');
-	}
-
-	/**
-	 * Get sync capabilities
-	 */
-	public function getSyncCapabilities(): array
-	{
-		return $this->canSync;
-	}
-
-	/**
-	 * Check if a specific sync capability is supported
-	 */
-	public function supportsSyncCapability(string $capability): bool
-	{
-		return $this->canSync[$capability] ?? false;
-	}
-
-	/**
-	 * Get posts shared to this service
-	 */
-	public function getSharedPosts(array $args = []): array
-	{
-		$defaults = [
-			'post_type' => $this->syncPostTypes,
-			'meta_query' => [
-				[
-					'key' => BASE."_{$this->service_name}_item_id",
-					'compare' => 'EXISTS'
-				]
-			],
-			'posts_per_page' => -1
-		];
-
-		$args = wp_parse_args($args, $defaults);
-		return get_posts($args);
-	}
-
-	/**
-	 * Check if post is shared to this service
-	 */
-	public function isPostShared(int $postID): bool
-	{
-		$external_id = get_post_meta($postID, BASE."_{$this->service_name}_item_id", true);
-		return $external_id !== '';
-	}
-
-	/**
-	 * Get external ID for a post
-	 */
-	public function getPostExternalId(int $postID): ?string
-	{
-		$external_id = get_post_meta($postID, BASE."_{$this->service_name}_item_id", true);
-		return $external_id ?: null;
-	}
 
 	/*******************************************************************
 	 * UTILITIES
@@ -2793,14 +2706,6 @@
 		return strtolower($this->service_name);
 	}
 
-	/**
-	 * Refresh credentials from storage and re-initialize
-	 */
-	public function refreshCredentials(): void
-	{
-		$this->credentials = [];
-		$this->ensureInitialized();
-	}
 
 	/**
 	 * Enhanced webhook handling with common patterns
@@ -4001,8 +3906,74 @@
 	/*********************************************************************************
 	 * Syncing WP items from Service
 	*********************************************************************************/
+	/**
+	 * @param array $itemData
+	 * @return bool
+	 * @throws Exception
+	 */
 	protected function createItemFromService(array $itemData):bool
 	{
+		throw new Exception('createItemFromService must be implemented by child class '.$this->service_name);
+	}
 
+	/**
+	 * @param array $itemData
+	 * @return bool
+	 * @throws Exception
+	 */
+	protected function updateItemFromService(array $itemData):bool
+	{
+		throw new Exception('updateItemFromService must be implemented by child class '.$this->service_name);
+	}
+
+	/**
+	 * @param array $itemData
+	 * @return bool
+	 * @throws Exception
+	 */
+	protected function deleteItemFromService(array $itemData):bool
+	{
+		throw new Exception('deleteItemFromService must be implemented by child class '.$this->service_name);
+	}
+
+	/****************************************************************
+	 * CONNECT to a user's integration
+	****************************************************************/
+	protected array $connections = [];
+	public function connectAs(?int $userID = null):bool
+	{
+		if (!$this->userCanConnect($userID)) {
+			return false;
+		}
+		$this->cleanup();
+		$this->userID = $userID;
+		$this->ensureInitialized();
+		return true;
+	}
+
+	public function userCanConnect(?int $userID):bool
+	{
+		$user = get_userdata($userID);
+		if (!$user || is_wp_error($user)) {
+			return false;
+		}
+		if (current_user_can('manage_options')) {
+			return true;
+		}
+		$role = jvbUserRole($userID);
+		$registrar = Registrar::getInstance($role);
+		if (!$registrar || !$registrar->hasIntegration($this->service_name)) {
+			return false;
+		}
+
+		return (bool) Meta::forUser($userID)->get('integration_'.$this->service_name.'_connected');
+	}
+
+	protected function cleanup():void
+	{
+		$this->userID = false;
+		$this->credentials = [];
+		$this->request_history = [];
+		$this->resetTokenRefreshFlag();
 	}
 }
diff --git a/inc/registrar/Fields.php b/inc/registrar/Fields.php
index c8a2d95..f9668f2 100644
--- a/inc/registrar/Fields.php
+++ b/inc/registrar/Fields.php
@@ -36,6 +36,9 @@
 				break;
 			case 'user':
 				$this->addUserFields();
+				if ($registrar->hasAnyIntegrations()) {
+					$this->addIntegrationSetupField();
+				}
 				break;
 		}
 	}
@@ -458,4 +461,17 @@
 	{
 		return (new self())->defaultUserFields();
 	}
+
+	protected function addIntegrationSetupField():void
+	{
+		$integrations = $this->registrar->getIntegrations();
+		foreach (array_keys($integrations) as $integration) {
+			$this->addField('integration_'.$integration.'_connected', [
+				'type'	=> 'true_false',
+				'hidden'	=> true,
+				'label'		=> 'Connected to '.$integration,
+				'default'	=> false,
+			]);
+		}
+	}
 }

--
Gitblit v1.10.0