First version of the security module

This commit is contained in:
2026-06-22 13:19:05 +02:00
parent 0392043ee3
commit 498afc7840
64 changed files with 7581 additions and 4 deletions

View File

@@ -0,0 +1,213 @@
<?php
namespace MintyPHP\Module\Security\Repository;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
/**
* SQL access for security_check_templates. Tenant-scoped, prepared statements only.
*/
class SecurityCheckTemplateRepository
{
/**
* @param array<string, mixed> $data
* @return int|null Inserted ID or null on failure
*/
public function insert(int $tenantId, array $data): ?int
{
$result = DB::insert(
'INSERT INTO security_check_templates'
. ' (tenant_id, product_code, product_name, tech_schema_json, active, version, created_by, updated_by)'
. ' VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
(string) $tenantId,
(string) ($data['product_code'] ?? ''),
(string) ($data['product_name'] ?? ''),
$data['tech_schema_json'] ?? null,
(string) ((int) ($data['active'] ?? 1)),
(string) ((int) ($data['version'] ?? 1)),
(string) ((int) ($data['created_by'] ?? 0)),
(string) ((int) ($data['created_by'] ?? 0))
);
return $result === false ? null : (int) $result;
}
public function findById(int $tenantId, int $id): ?array
{
if ($id <= 0) {
return null;
}
$row = DB::selectOne(
'SELECT t.*, uc.display_name AS created_by_name, uu.display_name AS updated_by_name'
. ' FROM security_check_templates t'
. ' LEFT JOIN users uc ON uc.id = t.created_by'
. ' LEFT JOIN users uu ON uu.id = t.updated_by'
. ' WHERE t.id = ? AND t.tenant_id = ? LIMIT 1',
(string) $id,
(string) $tenantId
);
return $this->normalizeRow($row);
}
public function findByCode(int $tenantId, string $code): ?array
{
$code = trim($code);
if ($code === '') {
return null;
}
$row = DB::selectOne(
'SELECT * FROM security_check_templates WHERE tenant_id = ? AND product_code = ? LIMIT 1',
(string) $tenantId,
$code
);
return $this->normalizeRow($row);
}
public function codeExists(int $tenantId, string $code, int $exceptId = 0): bool
{
$code = trim($code);
if ($code === '') {
return false;
}
$count = (int) (DB::selectValue(
'SELECT COUNT(*) FROM security_check_templates WHERE tenant_id = ? AND product_code = ? AND id <> ?',
(string) $tenantId,
$code,
(string) $exceptId
) ?? 0);
return $count > 0;
}
/**
* Active templates for the wizard product picker.
*
* @return list<array<string, mixed>>
*/
public function listActive(int $tenantId): array
{
$rows = DB::select(
'SELECT id, product_code, product_name, tech_schema_json FROM security_check_templates'
. ' WHERE tenant_id = ? AND active = 1 ORDER BY product_name ASC',
(string) $tenantId
);
$normalized = [];
if (is_array($rows)) {
foreach ($rows as $row) {
$item = $this->normalizeRow($row);
if ($item !== null) {
$normalized[] = $item;
}
}
}
return $normalized;
}
/**
* @param array<string, mixed> $filters
* @return array{total: int, rows: list<array<string, mixed>>}
*/
public function listPaged(int $tenantId, array $filters): array
{
$search = trim((string) ($filters['search'] ?? ''));
$active = trim((string) ($filters['active'] ?? ''));
[$limit, $offset] = RepoQuery::sanitizeLimitOffset($filters, 20, 1, 100, 0);
[$order, $dir] = RepoQuery::sanitizeOrder(
$filters,
['id', 'product_code', 'product_name', 'active', 'version', 'updated_at', 'created_at'],
'product_name',
'asc'
);
$where = ['t.tenant_id = ?'];
$params = [(string) $tenantId];
RepoQuery::addLikeFilter($where, $params, ['t.product_code', 't.product_name'], $search);
if ($active === '1' || $active === '0') {
$where[] = 't.active = ?';
$params[] = $active;
}
$whereSql = ' WHERE ' . implode(' AND ', $where);
$total = (int) (DB::selectValue('SELECT COUNT(*) FROM security_check_templates t' . $whereSql, ...$params) ?? 0);
$rows = DB::select(
'SELECT t.*, uu.display_name AS updated_by_name'
. ' FROM security_check_templates t'
. ' LEFT JOIN users uu ON uu.id = t.updated_by'
. $whereSql
. sprintf(' ORDER BY t.`%s` %s LIMIT ? OFFSET ?', $order, $dir),
...array_merge($params, [(string) $limit, (string) $offset])
);
$normalized = [];
if (is_array($rows)) {
foreach ($rows as $row) {
$item = $this->normalizeRow($row);
if ($item !== null) {
$normalized[] = $item;
}
}
}
return ['total' => $total, 'rows' => $normalized];
}
public function updateMeta(int $tenantId, int $id, string $name, bool $active, int $updatedBy): bool
{
$result = DB::update(
'UPDATE security_check_templates SET product_name = ?, active = ?, updated_by = ? WHERE id = ? AND tenant_id = ?',
$name,
(string) ($active ? 1 : 0),
(string) $updatedBy,
(string) $id,
(string) $tenantId
);
return $result !== false;
}
public function updateSchema(int $tenantId, int $id, string $schemaJson, int $version, int $updatedBy): bool
{
$result = DB::update(
'UPDATE security_check_templates SET tech_schema_json = ?, version = ?, updated_by = ? WHERE id = ? AND tenant_id = ?',
$schemaJson,
(string) $version,
(string) $updatedBy,
(string) $id,
(string) $tenantId
);
return $result !== false;
}
private function normalizeRow(mixed $row): ?array
{
if (!is_array($row)) {
return null;
}
// JOIN queries return nested arrays keyed by table name plus scalar aliased
// columns — merge sub-arrays and copy scalars (same convention as core repos).
$item = [];
foreach ($row as $key => $value) {
if (is_array($value)) {
$item = array_merge($item, $value);
} else {
$item[$key] = $value;
}
}
return isset($item['id']) ? $item : null;
}
}