add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks - Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists - Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services - Add Microsoft OIDC SSO, API token management, and user lifecycle features - Add swagger-ui vendor integration and OpenAPI spec - Add production Docker setup and bin/ scripts - Update composer dependencies, config, templates, and frontend assets throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
297
lib/Repository/Audit/UserLifecycleAuditRepository.php
Normal file
297
lib/Repository/Audit/UserLifecycleAuditRepository.php
Normal file
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository\Audit;
|
||||
|
||||
use MintyPHP\DB;
|
||||
use MintyPHP\Repository\Support\RepoQuery;
|
||||
|
||||
class UserLifecycleAuditRepository
|
||||
{
|
||||
public static function create(array $row): int|false
|
||||
{
|
||||
$id = DB::insert(
|
||||
'insert into user_lifecycle_audit_log (
|
||||
run_uuid, action, trigger_type, status, reason_code,
|
||||
policy_deactivate_days, policy_delete_days,
|
||||
actor_user_id, target_user_id, target_user_uuid, target_user_email,
|
||||
snapshot_enc, snapshot_version, created_at
|
||||
) values (?,?,?,?,?,?,?,?,?,?,?,?,?,NOW())',
|
||||
(string) ($row['run_uuid'] ?? ''),
|
||||
(string) ($row['action'] ?? ''),
|
||||
(string) ($row['trigger_type'] ?? ''),
|
||||
(string) ($row['status'] ?? ''),
|
||||
$row['reason_code'] ?? null,
|
||||
(string) ((int) ($row['policy_deactivate_days'] ?? 0)),
|
||||
(string) ((int) ($row['policy_delete_days'] ?? 0)),
|
||||
$row['actor_user_id'] !== null ? (string) ((int) $row['actor_user_id']) : null,
|
||||
$row['target_user_id'] !== null ? (string) ((int) $row['target_user_id']) : null,
|
||||
$row['target_user_uuid'] ?? null,
|
||||
$row['target_user_email'] ?? null,
|
||||
$row['snapshot_enc'] ?? null,
|
||||
(string) ((int) ($row['snapshot_version'] ?? 1))
|
||||
);
|
||||
return $id ? (int) $id : false;
|
||||
}
|
||||
|
||||
public static function updateStatus(int $id, string $status, ?string $reasonCode = null): bool
|
||||
{
|
||||
if ($id <= 0) {
|
||||
return false;
|
||||
}
|
||||
$status = trim(strtolower($status));
|
||||
if (!in_array($status, ['success', 'skipped', 'failed'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$updated = DB::update(
|
||||
'update user_lifecycle_audit_log set status = ?, reason_code = ? where id = ?',
|
||||
$status,
|
||||
$reasonCode,
|
||||
(string) $id
|
||||
);
|
||||
return $updated !== false;
|
||||
}
|
||||
|
||||
public static function listPaged(array $filters): array
|
||||
{
|
||||
$search = trim((string) ($filters['search'] ?? ''));
|
||||
$action = strtolower(trim((string) ($filters['action'] ?? '')));
|
||||
$status = strtolower(trim((string) ($filters['status'] ?? '')));
|
||||
$triggerType = strtolower(trim((string) ($filters['trigger_type'] ?? '')));
|
||||
$createdFrom = trim((string) ($filters['created_from'] ?? ''));
|
||||
$createdTo = trim((string) ($filters['created_to'] ?? ''));
|
||||
|
||||
[$limit, $offset] = RepoQuery::sanitizeLimitOffset($filters, 20, 1, 200, 0);
|
||||
[$order, $dir] = RepoQuery::sanitizeOrder(
|
||||
$filters,
|
||||
['id', 'created_at', 'action', 'trigger_type', 'status'],
|
||||
'created_at',
|
||||
'desc'
|
||||
);
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
RepoQuery::addLikeFilter(
|
||||
$where,
|
||||
$params,
|
||||
[
|
||||
'user_lifecycle_audit_log.run_uuid',
|
||||
'user_lifecycle_audit_log.target_user_uuid',
|
||||
'user_lifecycle_audit_log.target_user_email',
|
||||
'user_lifecycle_audit_log.reason_code',
|
||||
],
|
||||
$search
|
||||
);
|
||||
if (in_array($action, ['deactivate', 'delete', 'restore'], true)) {
|
||||
$where[] = 'user_lifecycle_audit_log.action = ?';
|
||||
$params[] = $action;
|
||||
}
|
||||
if (in_array($status, ['success', 'skipped', 'failed'], true)) {
|
||||
$where[] = 'user_lifecycle_audit_log.status = ?';
|
||||
$params[] = $status;
|
||||
}
|
||||
if (in_array($triggerType, ['manual', 'cron', 'system'], true)) {
|
||||
$where[] = 'user_lifecycle_audit_log.trigger_type = ?';
|
||||
$params[] = $triggerType;
|
||||
}
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $createdFrom)) {
|
||||
$where[] = 'user_lifecycle_audit_log.created_at >= ?';
|
||||
$params[] = $createdFrom . ' 00:00:00';
|
||||
}
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $createdTo)) {
|
||||
$where[] = 'user_lifecycle_audit_log.created_at <= ?';
|
||||
$params[] = $createdTo . ' 23:59:59';
|
||||
}
|
||||
|
||||
$whereSql = $where ? (' where ' . implode(' and ', $where)) : '';
|
||||
$fromSql = ' from user_lifecycle_audit_log ' .
|
||||
'left join users actor_user on actor_user.id = user_lifecycle_audit_log.actor_user_id ' .
|
||||
'left join users restored_by_user on restored_by_user.id = user_lifecycle_audit_log.restored_by_user_id ' .
|
||||
'left join users restored_user on restored_user.id = user_lifecycle_audit_log.restored_user_id ';
|
||||
|
||||
$total = (int) (DB::selectValue('select count(*)' . $fromSql . $whereSql, ...$params) ?? 0);
|
||||
|
||||
$rows = DB::select(
|
||||
'select
|
||||
user_lifecycle_audit_log.id,
|
||||
user_lifecycle_audit_log.run_uuid,
|
||||
user_lifecycle_audit_log.action,
|
||||
user_lifecycle_audit_log.trigger_type,
|
||||
user_lifecycle_audit_log.status,
|
||||
user_lifecycle_audit_log.reason_code,
|
||||
user_lifecycle_audit_log.policy_deactivate_days,
|
||||
user_lifecycle_audit_log.policy_delete_days,
|
||||
user_lifecycle_audit_log.actor_user_id,
|
||||
user_lifecycle_audit_log.target_user_id,
|
||||
user_lifecycle_audit_log.target_user_uuid,
|
||||
user_lifecycle_audit_log.target_user_email,
|
||||
user_lifecycle_audit_log.snapshot_version,
|
||||
user_lifecycle_audit_log.restored_at,
|
||||
user_lifecycle_audit_log.restored_by_user_id,
|
||||
user_lifecycle_audit_log.restored_user_id,
|
||||
user_lifecycle_audit_log.created_at,
|
||||
actor_user.uuid,
|
||||
actor_user.display_name,
|
||||
actor_user.email,
|
||||
restored_by_user.uuid,
|
||||
restored_by_user.display_name,
|
||||
restored_by_user.email,
|
||||
restored_user.uuid,
|
||||
restored_user.display_name,
|
||||
restored_user.email
|
||||
' . $fromSql . $whereSql .
|
||||
sprintf(' order by user_lifecycle_audit_log.`%s` %s limit ? offset ?', $order, $dir),
|
||||
...array_merge($params, [(string) $limit, (string) $offset])
|
||||
);
|
||||
|
||||
$normalized = [];
|
||||
if (is_array($rows)) {
|
||||
foreach ($rows as $row) {
|
||||
$item = self::normalizeRow($row, false);
|
||||
if ($item !== null) {
|
||||
$normalized[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['total' => $total, 'rows' => $normalized];
|
||||
}
|
||||
|
||||
public static function find(int $id): ?array
|
||||
{
|
||||
if ($id <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = DB::selectOne(
|
||||
'select
|
||||
user_lifecycle_audit_log.id,
|
||||
user_lifecycle_audit_log.run_uuid,
|
||||
user_lifecycle_audit_log.action,
|
||||
user_lifecycle_audit_log.trigger_type,
|
||||
user_lifecycle_audit_log.status,
|
||||
user_lifecycle_audit_log.reason_code,
|
||||
user_lifecycle_audit_log.policy_deactivate_days,
|
||||
user_lifecycle_audit_log.policy_delete_days,
|
||||
user_lifecycle_audit_log.actor_user_id,
|
||||
user_lifecycle_audit_log.target_user_id,
|
||||
user_lifecycle_audit_log.target_user_uuid,
|
||||
user_lifecycle_audit_log.target_user_email,
|
||||
user_lifecycle_audit_log.snapshot_enc,
|
||||
user_lifecycle_audit_log.snapshot_version,
|
||||
user_lifecycle_audit_log.restored_at,
|
||||
user_lifecycle_audit_log.restored_by_user_id,
|
||||
user_lifecycle_audit_log.restored_user_id,
|
||||
user_lifecycle_audit_log.created_at,
|
||||
actor_user.uuid,
|
||||
actor_user.display_name,
|
||||
actor_user.email,
|
||||
restored_by_user.uuid,
|
||||
restored_by_user.display_name,
|
||||
restored_by_user.email,
|
||||
restored_user.uuid,
|
||||
restored_user.display_name,
|
||||
restored_user.email
|
||||
from user_lifecycle_audit_log
|
||||
left join users actor_user on actor_user.id = user_lifecycle_audit_log.actor_user_id
|
||||
left join users restored_by_user on restored_by_user.id = user_lifecycle_audit_log.restored_by_user_id
|
||||
left join users restored_user on restored_user.id = user_lifecycle_audit_log.restored_user_id
|
||||
where user_lifecycle_audit_log.id = ?
|
||||
limit 1',
|
||||
(string) $id
|
||||
);
|
||||
|
||||
return self::normalizeRow($row, true);
|
||||
}
|
||||
|
||||
public static function findDeleteEventForRestore(int $id, bool $forUpdate = false): ?array
|
||||
{
|
||||
if ($id <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$query = 'select
|
||||
id, run_uuid, action, trigger_type, status, reason_code,
|
||||
policy_deactivate_days, policy_delete_days, actor_user_id,
|
||||
target_user_id, target_user_uuid, target_user_email,
|
||||
snapshot_enc, snapshot_version, restored_at,
|
||||
restored_by_user_id, restored_user_id, created_at
|
||||
from user_lifecycle_audit_log
|
||||
where id = ?
|
||||
and action = \'delete\'
|
||||
and status = \'success\'
|
||||
limit 1';
|
||||
if ($forUpdate) {
|
||||
$query .= ' for update';
|
||||
}
|
||||
|
||||
$row = DB::selectOne($query, (string) $id);
|
||||
if (!is_array($row)) {
|
||||
return null;
|
||||
}
|
||||
$item = $row['user_lifecycle_audit_log'] ?? $row;
|
||||
return is_array($item) ? $item : null;
|
||||
}
|
||||
|
||||
public static function markRestored(int $id, int $restoredBy, int $restoredUserId): bool
|
||||
{
|
||||
if ($id <= 0 || $restoredBy <= 0 || $restoredUserId <= 0) {
|
||||
return false;
|
||||
}
|
||||
$updated = DB::update(
|
||||
'update user_lifecycle_audit_log
|
||||
set restored_at = NOW(),
|
||||
restored_by_user_id = ?,
|
||||
restored_user_id = ?
|
||||
where id = ? and restored_at is null',
|
||||
(string) $restoredBy,
|
||||
(string) $restoredUserId,
|
||||
(string) $id
|
||||
);
|
||||
return (int) $updated > 0;
|
||||
}
|
||||
|
||||
public static function purgeOlderThanDays(int $days): int
|
||||
{
|
||||
if ($days <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$cutoff = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))
|
||||
->modify('-' . $days . ' days')
|
||||
->format('Y-m-d H:i:s');
|
||||
$deleted = DB::delete('delete from user_lifecycle_audit_log where created_at < ?', $cutoff);
|
||||
return is_int($deleted) ? $deleted : 0;
|
||||
}
|
||||
|
||||
private static function normalizeRow(mixed $row, bool $includeSnapshot): ?array
|
||||
{
|
||||
if (!is_array($row)) {
|
||||
return null;
|
||||
}
|
||||
$item = $row['user_lifecycle_audit_log'] ?? [];
|
||||
if (!is_array($item) || !isset($item['id'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$actor = is_array($row['actor_user'] ?? null) ? $row['actor_user'] : [];
|
||||
$restoredBy = is_array($row['restored_by_user'] ?? null) ? $row['restored_by_user'] : [];
|
||||
$restoredUser = is_array($row['restored_user'] ?? null) ? $row['restored_user'] : [];
|
||||
|
||||
$item['actor_user_uuid'] = (string) ($actor['uuid'] ?? '');
|
||||
$item['actor_user_display_name'] = (string) ($actor['display_name'] ?? '');
|
||||
$item['actor_user_email'] = (string) ($actor['email'] ?? '');
|
||||
$item['restored_by_user_uuid'] = (string) ($restoredBy['uuid'] ?? '');
|
||||
$item['restored_by_user_display_name'] = (string) ($restoredBy['display_name'] ?? '');
|
||||
$item['restored_by_user_email'] = (string) ($restoredBy['email'] ?? '');
|
||||
$item['restored_user_uuid'] = (string) ($restoredUser['uuid'] ?? '');
|
||||
$item['restored_user_display_name'] = (string) ($restoredUser['display_name'] ?? '');
|
||||
$item['restored_user_email'] = (string) ($restoredUser['email'] ?? '');
|
||||
|
||||
if (!$includeSnapshot) {
|
||||
unset($item['snapshot_enc']);
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user