feat: Privacy Guard - data compliance, consent, retention (#150)
Admin Privacy Dashboard (view=privacy): - Data subject requests list with approve/deny actions - Retention policies table with active status - Summary cards (pending, total, consent entries, policies) - Export user data as JSON download - ACL: core.admin only PrivacyModel: - createRequest/processRequest for export/delete/anonymize - exportUserData: profile, articles, action logs, tickets, replies, consent history, Community Builder profile - anonymizeUserData: replace PII, block account, clear logs - deleteUserData: full hard delete (anonymize first, then remove) - logConsent/getUserConsent: consent tracking - enforceRetentionPolicies: action_logs, waf_logs, sessions, inactive_users, closed_tickets (scheduled task ready) - getDashboardSummary Frontend Self-Service (/index.php?option=com_mokowaas&view=privacy): - Download My Data, Anonymize, Delete Account buttons - Request history table - Consent history table - Login required Database tables: - #__mokowaas_consent_log - #__mokowaas_data_requests - #__mokowaas_retention_policies (5 defaults) Submenu: MokoWaaS > Privacy Guard Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,4 +12,5 @@ COM_MOKOWAAS_MENU_UPDATES="Joomla Updates"
|
||||
COM_MOKOWAAS_MENU_CHECKIN="Global Check-in"
|
||||
COM_MOKOWAAS_MENU_TICKETS="Helpdesk"
|
||||
COM_MOKOWAAS_MENU_HTACCESS=".htaccess Maker"
|
||||
COM_MOKOWAAS_MENU_PRIVACY="Privacy Guard"
|
||||
COM_MOKOWAAS_MENU_CACHE="Cache Management"
|
||||
|
||||
@@ -85,3 +85,51 @@ INSERT IGNORE INTO `#__mokowaas_ticket_categories` (`id`, `title`, `alias`, `des
|
||||
(3, 'Feature Request', 'feature-request', 'Request a new feature or enhancement', 1440, 10080, 3),
|
||||
(4, 'Billing', 'billing', 'Billing, invoicing, and payment questions', 240, 1440, 4),
|
||||
(5, 'Urgent / Outage', 'urgent-outage', 'Site down or critical issue', 60, 240, 5);
|
||||
|
||||
--
|
||||
-- Privacy Guard Tables
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokowaas_consent_log` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT NOT NULL,
|
||||
`category` VARCHAR(50) NOT NULL,
|
||||
`action` ENUM('granted','revoked') NOT NULL,
|
||||
`ip_address` VARCHAR(45) NOT NULL DEFAULT '',
|
||||
`created` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user` (`user_id`),
|
||||
KEY `idx_category` (`category`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokowaas_data_requests` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT NOT NULL,
|
||||
`type` ENUM('export','delete','anonymize') NOT NULL,
|
||||
`status` ENUM('pending','processing','completed','denied') NOT NULL DEFAULT 'pending',
|
||||
`notes` TEXT,
|
||||
`processed_by` INT DEFAULT NULL,
|
||||
`created` DATETIME NOT NULL,
|
||||
`processed` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user` (`user_id`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokowaas_retention_policies` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`content_type` VARCHAR(100) NOT NULL,
|
||||
`retention_days` INT UNSIGNED NOT NULL DEFAULT 365,
|
||||
`action` ENUM('anonymize','delete','archive') NOT NULL DEFAULT 'anonymize',
|
||||
`enabled` TINYINT NOT NULL DEFAULT 1,
|
||||
`description` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Default retention policies
|
||||
INSERT IGNORE INTO `#__mokowaas_retention_policies` (`id`, `content_type`, `retention_days`, `action`, `enabled`, `description`) VALUES
|
||||
(1, 'action_logs', 90, 'delete', 1, 'Delete action log entries older than 90 days'),
|
||||
(2, 'waf_logs', 30, 'delete', 1, 'Delete WAF block logs older than 30 days'),
|
||||
(3, 'sessions', 7, 'delete', 1, 'Purge expired sessions older than 7 days'),
|
||||
(4, 'inactive_users', 730, 'anonymize', 0, 'Anonymize users inactive for 2 years (disabled by default)'),
|
||||
(5, 'closed_tickets', 365, 'anonymize', 0, 'Anonymize closed tickets older than 1 year (disabled by default)');
|
||||
|
||||
@@ -29,6 +29,7 @@ class DisplayController extends BaseController
|
||||
'htaccess' => 'mokowaas.htaccess',
|
||||
'tickets' => 'mokowaas.tickets',
|
||||
'ticket' => 'mokowaas.tickets',
|
||||
'privacy' => 'core.admin',
|
||||
];
|
||||
|
||||
public function display($cachable = false, $urlparams = [])
|
||||
@@ -268,6 +269,44 @@ class DisplayController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// Privacy Guard
|
||||
// ==================================================================
|
||||
|
||||
public function processDataRequest()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('core.admin'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
}
|
||||
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
|
||||
|
||||
$this->jsonResponse($model->processRequest(
|
||||
$input->getInt('request_id', 0),
|
||||
$input->getString('action', 'deny')
|
||||
));
|
||||
}
|
||||
|
||||
public function exportUserData()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('core.admin'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
}
|
||||
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
|
||||
|
||||
$this->jsonResponse($model->exportUserData(
|
||||
Factory::getApplication()->getInput()->getInt('user_id', 0)
|
||||
));
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// Importers
|
||||
// ==================================================================
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoWaaS\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
|
||||
class PrivacyModel extends BaseDatabaseModel
|
||||
{
|
||||
/**
|
||||
* Get all pending data requests.
|
||||
*/
|
||||
public function getDataRequests(string $filterStatus = ''): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('r') . '.*',
|
||||
$db->quoteName('u.name', 'user_name'),
|
||||
$db->quoteName('u.email', 'user_email'),
|
||||
$db->quoteName('u.username'),
|
||||
$db->quoteName('p.name', 'processed_by_name'),
|
||||
])
|
||||
->from($db->quoteName('#__mokowaas_data_requests', 'r'))
|
||||
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
|
||||
->leftJoin($db->quoteName('#__users', 'p') . ' ON p.id = r.processed_by');
|
||||
|
||||
if ($filterStatus)
|
||||
{
|
||||
$query->where($db->quoteName('r.status') . ' = ' . $db->quote($filterStatus));
|
||||
}
|
||||
|
||||
$query->order($db->quoteName('r.created') . ' DESC')->setLimit(50);
|
||||
$db->setQuery($query);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a data request (from admin or user self-service).
|
||||
*/
|
||||
public function createRequest(int $userId, string $type, string $notes = ''): array
|
||||
{
|
||||
$validTypes = ['export', 'delete', 'anonymize'];
|
||||
|
||||
if (!\in_array($type, $validTypes, true))
|
||||
{
|
||||
return ['success' => false, 'message' => 'Invalid request type.'];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$row = (object) [
|
||||
'user_id' => $userId,
|
||||
'type' => $type,
|
||||
'status' => 'pending',
|
||||
'notes' => $notes,
|
||||
'created' => Factory::getDate()->toSql(),
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokowaas_data_requests', $row, 'id');
|
||||
|
||||
return ['success' => true, 'message' => ucfirst($type) . ' request #' . $row->id . ' created.', 'id' => (int) $row->id];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
return ['success' => false, 'message' => 'Failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a data request (approve and execute).
|
||||
*/
|
||||
public function processRequest(int $requestId, string $action): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
try
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokowaas_data_requests'))
|
||||
->where($db->quoteName('id') . ' = ' . $requestId)
|
||||
);
|
||||
$request = $db->loadObject();
|
||||
|
||||
if (!$request)
|
||||
{
|
||||
return ['success' => false, 'message' => 'Request not found.'];
|
||||
}
|
||||
|
||||
if ($action === 'deny')
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokowaas_data_requests'))
|
||||
->set($db->quoteName('status') . ' = ' . $db->quote('denied'))
|
||||
->set($db->quoteName('processed_by') . ' = ' . (int) Factory::getApplication()->getIdentity()->id)
|
||||
->set($db->quoteName('processed') . ' = ' . $db->quote(Factory::getDate()->toSql()))
|
||||
->where($db->quoteName('id') . ' = ' . $requestId)
|
||||
)->execute();
|
||||
|
||||
return ['success' => true, 'message' => 'Request denied.'];
|
||||
}
|
||||
|
||||
// Mark as processing
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokowaas_data_requests'))
|
||||
->set($db->quoteName('status') . ' = ' . $db->quote('processing'))
|
||||
->where($db->quoteName('id') . ' = ' . $requestId)
|
||||
)->execute();
|
||||
|
||||
// Execute the request
|
||||
$result = null;
|
||||
|
||||
switch ($request->type)
|
||||
{
|
||||
case 'export':
|
||||
$result = $this->exportUserData((int) $request->user_id);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
$result = $this->deleteUserData((int) $request->user_id);
|
||||
break;
|
||||
|
||||
case 'anonymize':
|
||||
$result = $this->anonymizeUserData((int) $request->user_id);
|
||||
break;
|
||||
}
|
||||
|
||||
// Mark completed
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokowaas_data_requests'))
|
||||
->set($db->quoteName('status') . ' = ' . $db->quote('completed'))
|
||||
->set($db->quoteName('processed_by') . ' = ' . (int) Factory::getApplication()->getIdentity()->id)
|
||||
->set($db->quoteName('processed') . ' = ' . $db->quote(Factory::getDate()->toSql()))
|
||||
->where($db->quoteName('id') . ' = ' . $requestId)
|
||||
)->execute();
|
||||
|
||||
return $result ?? ['success' => true, 'message' => 'Request processed.'];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
return ['success' => false, 'message' => 'Processing failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all data for a user as a structured array.
|
||||
*/
|
||||
public function exportUserData(int $userId): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$data = ['user_id' => $userId, 'exported' => gmdate('Y-m-d\TH:i:s\Z')];
|
||||
|
||||
try
|
||||
{
|
||||
// User profile
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select(['id', 'name', 'username', 'email', 'registerDate', 'lastvisitDate', 'params'])
|
||||
->from($db->quoteName('#__users'))
|
||||
->where($db->quoteName('id') . ' = ' . $userId)
|
||||
);
|
||||
$data['profile'] = $db->loadObject();
|
||||
|
||||
// Content (articles)
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select(['id', 'title', 'alias', 'created', 'modified', 'hits'])
|
||||
->from($db->quoteName('#__content'))
|
||||
->where($db->quoteName('created_by') . ' = ' . $userId)
|
||||
);
|
||||
$data['articles'] = $db->loadObjectList() ?: [];
|
||||
|
||||
// Action logs
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select(['message', 'log_date', 'ip_address'])
|
||||
->from($db->quoteName('#__action_logs'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
->order('log_date DESC')
|
||||
->setLimit(100)
|
||||
);
|
||||
$data['action_logs'] = $db->loadObjectList() ?: [];
|
||||
|
||||
// Support tickets
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select(['id', 'subject', 'body', 'status', 'priority', 'created'])
|
||||
->from($db->quoteName('#__mokowaas_tickets'))
|
||||
->where($db->quoteName('created_by') . ' = ' . $userId)
|
||||
);
|
||||
$data['tickets'] = $db->loadObjectList() ?: [];
|
||||
|
||||
// Ticket replies
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select(['r.id', 'r.ticket_id', 'r.body', 'r.created'])
|
||||
->from($db->quoteName('#__mokowaas_ticket_replies', 'r'))
|
||||
->where($db->quoteName('r.user_id') . ' = ' . $userId)
|
||||
);
|
||||
$data['ticket_replies'] = $db->loadObjectList() ?: [];
|
||||
|
||||
// Consent log
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokowaas_consent_log'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
->order('created ASC')
|
||||
);
|
||||
$data['consent_history'] = $db->loadObjectList() ?: [];
|
||||
|
||||
// Community Builder profile (if table exists)
|
||||
try
|
||||
{
|
||||
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%comprofiler%'));
|
||||
|
||||
if ($db->loadResult())
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__comprofiler'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
);
|
||||
$data['community_builder'] = $db->loadObject();
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e) {}
|
||||
|
||||
return ['success' => true, 'message' => 'Data exported.', 'data' => $data];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
return ['success' => false, 'message' => 'Export failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymize a user's data (GDPR right to be forgotten — soft).
|
||||
*/
|
||||
public function anonymizeUserData(int $userId): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$now = Factory::getDate()->toSql();
|
||||
$anon = 'Anonymous User #' . $userId;
|
||||
|
||||
try
|
||||
{
|
||||
// Anonymize user record
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__users'))
|
||||
->set([
|
||||
$db->quoteName('name') . ' = ' . $db->quote($anon),
|
||||
$db->quoteName('username') . ' = ' . $db->quote('anon_' . $userId),
|
||||
$db->quoteName('email') . ' = ' . $db->quote('anon_' . $userId . '@deleted.local'),
|
||||
$db->quoteName('password') . ' = ' . $db->quote(''),
|
||||
$db->quoteName('block') . ' = 1',
|
||||
$db->quoteName('params') . ' = ' . $db->quote('{}'),
|
||||
])
|
||||
->where($db->quoteName('id') . ' = ' . $userId)
|
||||
)->execute();
|
||||
|
||||
// Anonymize article authorship
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__content'))
|
||||
->set($db->quoteName('created_by_alias') . ' = ' . $db->quote($anon))
|
||||
->where($db->quoteName('created_by') . ' = ' . $userId)
|
||||
)->execute();
|
||||
|
||||
// Delete action logs
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__action_logs'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
)->execute();
|
||||
|
||||
// Anonymize ticket replies
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokowaas_ticket_replies'))
|
||||
->set($db->quoteName('body') . ' = ' . $db->quote('[Content removed per data request]'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
)->execute();
|
||||
|
||||
// Community Builder
|
||||
try
|
||||
{
|
||||
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%comprofiler%'));
|
||||
|
||||
if ($db->loadResult())
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__comprofiler'))
|
||||
->set([
|
||||
$db->quoteName('firstname') . ' = ' . $db->quote('Anonymous'),
|
||||
$db->quoteName('lastname') . ' = ' . $db->quote('User'),
|
||||
$db->quoteName('middlename') . ' = ' . $db->quote(''),
|
||||
])
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
)->execute();
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e) {}
|
||||
|
||||
// Log the anonymization
|
||||
$this->logConsent($userId, 'account_anonymized', 'granted');
|
||||
|
||||
return ['success' => true, 'message' => 'User #' . $userId . ' data anonymized.'];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
return ['success' => false, 'message' => 'Anonymization failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user's data completely (hard delete).
|
||||
*/
|
||||
public function deleteUserData(int $userId): array
|
||||
{
|
||||
$result = $this->anonymizeUserData($userId);
|
||||
|
||||
if (!$result['success'])
|
||||
{
|
||||
return $result;
|
||||
}
|
||||
|
||||
$db = $this->getDatabase();
|
||||
|
||||
try
|
||||
{
|
||||
// Delete tickets and replies
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__mokowaas_tickets'))
|
||||
->where($db->quoteName('created_by') . ' = ' . $userId)
|
||||
);
|
||||
$ticketIds = $db->loadColumn() ?: [];
|
||||
|
||||
if (!empty($ticketIds))
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokowaas_ticket_replies'))
|
||||
->where($db->quoteName('ticket_id') . ' IN (' . implode(',', $ticketIds) . ')')
|
||||
)->execute();
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokowaas_tickets'))
|
||||
->where($db->quoteName('created_by') . ' = ' . $userId)
|
||||
)->execute();
|
||||
}
|
||||
|
||||
// Delete consent log
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokowaas_consent_log'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
)->execute();
|
||||
|
||||
// Delete user record
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__users'))
|
||||
->where($db->quoteName('id') . ' = ' . $userId)
|
||||
)->execute();
|
||||
|
||||
return ['success' => true, 'message' => 'User #' . $userId . ' data permanently deleted.'];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
return ['success' => false, 'message' => 'Deletion failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// Consent Management
|
||||
// ==================================================================
|
||||
|
||||
/**
|
||||
* Get consent status for a user.
|
||||
*/
|
||||
public function getUserConsent(int $userId): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokowaas_consent_log'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
->order($db->quoteName('created') . ' DESC')
|
||||
);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a consent action.
|
||||
*/
|
||||
public function logConsent(int $userId, string $category, string $action): void
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$row = (object) [
|
||||
'user_id' => $userId,
|
||||
'category' => $category,
|
||||
'action' => $action === 'revoked' ? 'revoked' : 'granted',
|
||||
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
|
||||
'created' => Factory::getDate()->toSql(),
|
||||
];
|
||||
$db->insertObject('#__mokowaas_consent_log', $row, 'id');
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// Retention Policy Enforcement
|
||||
// ==================================================================
|
||||
|
||||
/**
|
||||
* Get all retention policies.
|
||||
*/
|
||||
public function getRetentionPolicies(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokowaas_retention_policies'))
|
||||
->order($db->quoteName('id') . ' ASC')
|
||||
);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Run retention policy enforcement (called by scheduled task).
|
||||
*/
|
||||
public function enforceRetentionPolicies(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$results = ['policies_run' => 0, 'items_affected' => 0];
|
||||
$policies = $this->getRetentionPolicies();
|
||||
|
||||
foreach ($policies as $policy)
|
||||
{
|
||||
if (!(int) $policy->enabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$cutoff = Factory::getDate('-' . (int) $policy->retention_days . ' days')->toSql();
|
||||
$count = 0;
|
||||
|
||||
try
|
||||
{
|
||||
switch ($policy->content_type)
|
||||
{
|
||||
case 'action_logs':
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__action_logs'))
|
||||
->where($db->quoteName('log_date') . ' < ' . $db->quote($cutoff))
|
||||
)->execute();
|
||||
$count = $db->getAffectedRows();
|
||||
break;
|
||||
|
||||
case 'waf_logs':
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokowaas_waf_log'))
|
||||
->where($db->quoteName('created') . ' < ' . $db->quote($cutoff))
|
||||
)->execute();
|
||||
$count = $db->getAffectedRows();
|
||||
break;
|
||||
|
||||
case 'sessions':
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__session'))
|
||||
->where($db->quoteName('time') . ' < ' . (int) strtotime($cutoff))
|
||||
)->execute();
|
||||
$count = $db->getAffectedRows();
|
||||
break;
|
||||
|
||||
case 'closed_tickets':
|
||||
if ($policy->action === 'anonymize')
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokowaas_tickets'))
|
||||
->set($db->quoteName('body') . ' = ' . $db->quote('[Removed per retention policy]'))
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('closed'))
|
||||
->where($db->quoteName('closed') . ' < ' . $db->quote($cutoff))
|
||||
->where($db->quoteName('body') . ' != ' . $db->quote('[Removed per retention policy]'))
|
||||
)->execute();
|
||||
$count = $db->getAffectedRows();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'inactive_users':
|
||||
if ($policy->action === 'anonymize')
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__users'))
|
||||
->where($db->quoteName('lastvisitDate') . ' < ' . $db->quote($cutoff))
|
||||
->where($db->quoteName('lastvisitDate') . ' != ' . $db->quote('0000-00-00 00:00:00'))
|
||||
->where($db->quoteName('block') . ' = 0')
|
||||
->where($db->quoteName('username') . ' NOT LIKE ' . $db->quote('anon_%'))
|
||||
);
|
||||
$userIds = $db->loadColumn() ?: [];
|
||||
|
||||
foreach ($userIds as $uid)
|
||||
{
|
||||
$this->anonymizeUserData((int) $uid);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($count > 0)
|
||||
{
|
||||
$results['policies_run']++;
|
||||
$results['items_affected'] += $count;
|
||||
Log::add(\sprintf('Retention: %s — %d items affected', $policy->content_type, $count), Log::INFO, 'mokowaas');
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Retention policy error (' . $policy->content_type . '): ' . $e->getMessage(), Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get privacy dashboard summary counts.
|
||||
*/
|
||||
public function getDashboardSummary(): object
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
$summary = (object) [
|
||||
'pending_requests' => 0,
|
||||
'total_requests' => 0,
|
||||
'consent_entries' => 0,
|
||||
'policies_active' => 0,
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_data_requests WHERE status = ' . $db->quote('pending'));
|
||||
$summary->pending_requests = (int) $db->loadResult();
|
||||
|
||||
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_data_requests');
|
||||
$summary->total_requests = (int) $db->loadResult();
|
||||
|
||||
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_consent_log');
|
||||
$summary->consent_entries = (int) $db->loadResult();
|
||||
|
||||
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_retention_policies WHERE enabled = 1');
|
||||
$summary->policies_active = (int) $db->loadResult();
|
||||
}
|
||||
catch (\Throwable $e) {}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Privacy;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
protected $requests = [];
|
||||
protected $policies = [];
|
||||
protected $summary;
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
|
||||
|
||||
$filterStatus = Factory::getApplication()->getInput()->getString('filter_status', '');
|
||||
$this->requests = $model->getDataRequests($filterStatus);
|
||||
$this->policies = $model->getRetentionPolicies();
|
||||
$this->summary = $model->getDashboardSummary();
|
||||
|
||||
$this->addToolbar();
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title('Privacy Guard', 'lock');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
|
||||
$requests = $this->requests;
|
||||
$policies = $this->policies;
|
||||
$summary = $this->summary;
|
||||
$token = Session::getFormToken();
|
||||
|
||||
$statusBadge = [
|
||||
'pending' => 'bg-warning text-dark',
|
||||
'processing' => 'bg-info',
|
||||
'completed' => 'bg-success',
|
||||
'denied' => 'bg-secondary',
|
||||
];
|
||||
$typeBadge = [
|
||||
'export' => 'bg-primary',
|
||||
'delete' => 'bg-danger',
|
||||
'anonymize' => 'bg-warning text-dark',
|
||||
];
|
||||
?>
|
||||
|
||||
<div id="mokowaas-privacy">
|
||||
<!-- Summary cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center p-3">
|
||||
<span class="fw-bold fs-3 <?php echo $summary->pending_requests > 0 ? 'text-warning' : 'text-success'; ?>"><?php echo $summary->pending_requests; ?></span>
|
||||
<small class="text-muted">Pending Requests</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center p-3">
|
||||
<span class="fw-bold fs-3"><?php echo $summary->total_requests; ?></span>
|
||||
<small class="text-muted">Total Requests</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center p-3">
|
||||
<span class="fw-bold fs-3"><?php echo $summary->consent_entries; ?></span>
|
||||
<small class="text-muted">Consent Entries</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center p-3">
|
||||
<span class="fw-bold fs-3"><?php echo $summary->policies_active; ?></span>
|
||||
<small class="text-muted">Active Policies</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Data Requests -->
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong><span class="icon-user-shield"></span> Data Subject Requests</strong>
|
||||
<form method="get" class="d-inline">
|
||||
<input type="hidden" name="option" value="com_mokowaas">
|
||||
<input type="hidden" name="view" value="privacy">
|
||||
<select name="filter_status" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
|
||||
<option value="">All</option>
|
||||
<?php foreach (['pending','processing','completed','denied'] as $s): ?>
|
||||
<option value="<?php echo $s; ?>" <?php echo Factory::getApplication()->getInput()->getString('filter_status') === $s ? 'selected' : ''; ?>><?php echo ucfirst($s); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
<?php if (empty($requests)): ?>
|
||||
<div class="card-body text-center text-muted py-4">No data requests found.</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead><tr><th>#</th><th>User</th><th>Type</th><th>Status</th><th>Created</th><th>Processed</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $r): ?>
|
||||
<tr>
|
||||
<td><?php echo $r->id; ?></td>
|
||||
<td><?php echo $this->escape($r->user_name ?? ''); ?><br><small class="text-muted"><?php echo $this->escape($r->user_email ?? ''); ?></small></td>
|
||||
<td><span class="badge <?php echo $typeBadge[$r->type] ?? 'bg-secondary'; ?>"><?php echo ucfirst($r->type); ?></span></td>
|
||||
<td><span class="badge <?php echo $statusBadge[$r->status] ?? 'bg-secondary'; ?>"><?php echo ucfirst($r->status); ?></span></td>
|
||||
<td class="text-nowrap small"><?php echo HTMLHelper::_('date', $r->created, 'M d, Y H:i'); ?></td>
|
||||
<td class="text-nowrap small"><?php echo $r->processed ? HTMLHelper::_('date', $r->processed, 'M d, Y H:i') : '—'; ?></td>
|
||||
<td>
|
||||
<?php if ($r->status === 'pending'): ?>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-success btn-privacy-action" data-id="<?php echo $r->id; ?>" data-action="approve"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.processDataRequest&format=json'); ?>"
|
||||
data-token="<?php echo $token; ?>">Approve</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-privacy-action" data-id="<?php echo $r->id; ?>" data-action="deny"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.processDataRequest&format=json'); ?>"
|
||||
data-token="<?php echo $token; ?>">Deny</button>
|
||||
</div>
|
||||
<?php elseif ($r->status === 'completed' && $r->type === 'export'): ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary btn-export-download" data-user="<?php echo $r->user_id; ?>"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.exportUserData&format=json'); ?>"
|
||||
data-token="<?php echo $token; ?>">
|
||||
<span class="icon-download"></span> Download
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Retention Policies -->
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><strong><span class="icon-clock"></span> Retention Policies</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead><tr><th>Type</th><th>Days</th><th>Action</th><th>Active</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($policies as $p): ?>
|
||||
<tr>
|
||||
<td class="small"><?php echo $this->escape($p->content_type); ?></td>
|
||||
<td><?php echo $p->retention_days; ?></td>
|
||||
<td><span class="badge bg-secondary"><?php echo $p->action; ?></span></td>
|
||||
<td><?php echo (int) $p->enabled ? '<span class="text-success">Yes</span>' : '<span class="text-muted">No</span>'; ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Process request buttons
|
||||
document.querySelectorAll('.btn-privacy-action').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var el = this;
|
||||
var action = el.dataset.action;
|
||||
if (!confirm(action === 'approve' ? 'Approve and process this data request?' : 'Deny this request?')) return;
|
||||
el.disabled = true;
|
||||
var fd = new FormData();
|
||||
fd.append('request_id', el.dataset.id);
|
||||
fd.append('action', action);
|
||||
fd.append(el.dataset.token, '1');
|
||||
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
if (d.success) { Joomla.renderMessages({message:[d.message]}); location.reload(); }
|
||||
else { Joomla.renderMessages({error:[d.message]}); el.disabled = false; }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Export download
|
||||
document.querySelectorAll('.btn-export-download').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var el = this;
|
||||
var fd = new FormData();
|
||||
fd.append('user_id', el.dataset.user);
|
||||
fd.append(el.dataset.token, '1');
|
||||
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
if (d.success && d.data) {
|
||||
var blob = new Blob([JSON.stringify(d.data, null, 2)], {type:'application/json'});
|
||||
var a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'user-data-export-' + el.dataset.user + '.json';
|
||||
a.click();
|
||||
} else {
|
||||
Joomla.renderMessages({error:[d.message || 'Export failed']});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -32,6 +32,7 @@
|
||||
<menu link="option=com_mokowaas&view=extensions" img="class:puzzle-piece">COM_MOKOWAAS_MENU_EXTENSIONS</menu>
|
||||
<menu link="option=com_mokowaas&view=tickets" img="class:headphones">COM_MOKOWAAS_MENU_TICKETS</menu>
|
||||
<menu link="option=com_mokowaas&view=htaccess" img="class:file-code">COM_MOKOWAAS_MENU_HTACCESS</menu>
|
||||
<menu link="option=com_mokowaas&view=privacy" img="class:lock">COM_MOKOWAAS_MENU_PRIVACY</menu>
|
||||
<menu link="option=com_plugins&filter[folder]=system&filter[search]=mokowaas" img="class:power-off">COM_MOKOWAAS_MENU_PLUGINS</menu>
|
||||
<menu link="option=com_installer&view=update" img="class:refresh">COM_MOKOWAAS_MENU_UPDATES</menu>
|
||||
<menu link="option=com_checkin" img="class:check-square">COM_MOKOWAAS_MENU_CHECKIN</menu>
|
||||
|
||||
@@ -163,6 +163,26 @@ class DisplayController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a data privacy request from frontend.
|
||||
*/
|
||||
public function submitDataRequest()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
|
||||
if ($user->guest)
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Please log in.']);
|
||||
}
|
||||
|
||||
$type = Factory::getApplication()->getInput()->getString('type', '');
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
|
||||
|
||||
$this->jsonResponse($model->createRequest($user->id, $type, 'Submitted via self-service portal'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is support staff (can manage tickets beyond their own).
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoWaaS\Site\View\Privacy;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
protected $requests = [];
|
||||
protected $consent = [];
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
|
||||
if ($user->guest)
|
||||
{
|
||||
Factory::getApplication()->redirect(Route::_(
|
||||
'index.php?option=com_users&view=login&return=' . base64_encode('index.php?option=com_mokowaas&view=privacy'),
|
||||
false
|
||||
));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
|
||||
|
||||
// Get user's data requests
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokowaas_data_requests'))
|
||||
->where($db->quoteName('user_id') . ' = ' . (int) $user->id)
|
||||
->order($db->quoteName('created') . ' DESC');
|
||||
|
||||
try
|
||||
{
|
||||
$db->setQuery($query);
|
||||
$this->requests = $db->loadObjectList() ?: [];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->requests = [];
|
||||
}
|
||||
|
||||
// Get consent history
|
||||
try
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokowaas_consent_log'))
|
||||
->where($db->quoteName('user_id') . ' = ' . (int) $user->id)
|
||||
->order($db->quoteName('created') . ' DESC')
|
||||
->setLimit(20)
|
||||
);
|
||||
$this->consent = $db->loadObjectList() ?: [];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->consent = [];
|
||||
}
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
$requests = $this->requests;
|
||||
$consent = $this->consent;
|
||||
$token = Session::getFormToken();
|
||||
|
||||
$statusLabel = ['pending' => 'Pending', 'processing' => 'Processing', 'completed' => 'Completed', 'denied' => 'Denied'];
|
||||
$statusClass = ['pending' => 'warning', 'processing' => 'info', 'completed' => 'success', 'denied' => 'secondary'];
|
||||
?>
|
||||
|
||||
<div class="mokowaas-portal">
|
||||
<h2>My Privacy & Data</h2>
|
||||
<p class="text-muted">Manage your personal data, download your information, or request account deletion.</p>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<button type="button" class="btn btn-primary w-100 py-3 btn-data-request" data-type="export">
|
||||
<span class="icon-download d-block mb-1" style="font-size:1.5rem"></span>
|
||||
Download My Data
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<button type="button" class="btn btn-outline-warning w-100 py-3 btn-data-request" data-type="anonymize">
|
||||
<span class="icon-user-shield d-block mb-1" style="font-size:1.5rem"></span>
|
||||
Anonymize My Account
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<button type="button" class="btn btn-outline-danger w-100 py-3 btn-data-request" data-type="delete">
|
||||
<span class="icon-trash d-block mb-1" style="font-size:1.5rem"></span>
|
||||
Delete My Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- My requests -->
|
||||
<?php if (!empty($requests)): ?>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><strong>My Data Requests</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped mb-0">
|
||||
<thead><tr><th>Type</th><th>Status</th><th>Submitted</th><th>Processed</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $r): ?>
|
||||
<tr>
|
||||
<td><?php echo ucfirst($r->type); ?></td>
|
||||
<td><span class="badge bg-<?php echo $statusClass[$r->status] ?? 'secondary'; ?>"><?php echo $statusLabel[$r->status] ?? $r->status; ?></span></td>
|
||||
<td><?php echo HTMLHelper::_('date', $r->created, 'M d, Y H:i'); ?></td>
|
||||
<td><?php echo $r->processed ? HTMLHelper::_('date', $r->processed, 'M d, Y H:i') : '—'; ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Consent history -->
|
||||
<?php if (!empty($consent)): ?>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><strong>Consent History</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead><tr><th>Category</th><th>Action</th><th>Date</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($consent as $c): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars(ucwords(str_replace('_', ' ', $c->category))); ?></td>
|
||||
<td><span class="badge bg-<?php echo $c->action === 'granted' ? 'success' : 'secondary'; ?>"><?php echo ucfirst($c->action); ?></span></td>
|
||||
<td><?php echo HTMLHelper::_('date', $c->created, 'M d, Y H:i'); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.btn-data-request').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var type = this.dataset.type;
|
||||
var messages = {
|
||||
'export': 'Request a download of all your personal data?',
|
||||
'anonymize': 'Request your account to be anonymized? Your name, email, and personal details will be replaced. This cannot be undone.',
|
||||
'delete': 'Request permanent deletion of your account and all data? This cannot be undone.'
|
||||
};
|
||||
if (!confirm(messages[type] || 'Submit this request?')) return;
|
||||
this.disabled = true;
|
||||
var fd = new FormData();
|
||||
fd.append('type', type);
|
||||
fd.append('<?php echo $token; ?>', '1');
|
||||
fetch('<?php echo Route::_("index.php?option=com_mokowaas&task=display.submitDataRequest&format=json"); ?>', {
|
||||
method: 'POST', body: fd, headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.success) { alert(d.message); location.reload(); }
|
||||
else { alert(d.message || 'Failed.'); }
|
||||
})
|
||||
.catch(function() { alert('Network error.'); })
|
||||
.finally(function() { btn.disabled = false; });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user