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:
2026-02-22 15:27:35 +01:00
parent 3eb9cc0ac4
commit 25370a1a55
389 changed files with 40506 additions and 8071 deletions

View File

@@ -0,0 +1,223 @@
<?php
namespace MintyPHP\Repository\Audit;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
class ApiAuditLogRepository
{
public static function create(array $data): int|false
{
$id = DB::insert(
'insert into api_audit_log (
request_id, method, path, query_json, status_code, duration_ms, error_code,
user_id, tenant_id, api_token_id, token_tenant_id, ip, user_agent, created_at
) values (?,?,?,?,?,?,?,?,?,?,?,?,?,NOW())',
(string) ($data['request_id'] ?? ''),
(string) ($data['method'] ?? ''),
(string) ($data['path'] ?? ''),
$data['query_json'] ?? null,
(string) ((int) ($data['status_code'] ?? 0)),
$data['duration_ms'] !== null ? (string) ((int) $data['duration_ms']) : null,
$data['error_code'] ?? null,
$data['user_id'] !== null ? (string) ((int) $data['user_id']) : null,
$data['tenant_id'] !== null ? (string) ((int) $data['tenant_id']) : null,
$data['api_token_id'] !== null ? (string) ((int) $data['api_token_id']) : null,
$data['token_tenant_id'] !== null ? (string) ((int) $data['token_tenant_id']) : null,
$data['ip'] ?? null,
$data['user_agent'] ?? null
);
return $id ? (int) $id : false;
}
public static function listPaged(array $filters): array
{
$search = trim((string) ($filters['search'] ?? ''));
$status = strtolower(trim((string) ($filters['status'] ?? '')));
$method = strtoupper(trim((string) ($filters['method'] ?? '')));
$createdFrom = trim((string) ($filters['created_from'] ?? ''));
$createdTo = trim((string) ($filters['created_to'] ?? ''));
$tenantId = (int) ($filters['tenant_id'] ?? 0);
$userId = (int) ($filters['user_id'] ?? 0);
[$limit, $offset] = RepoQuery::sanitizeLimitOffset($filters, 20, 1, 200, 0);
[$order, $dir] = RepoQuery::sanitizeOrder(
$filters,
['id', 'created_at', 'status_code', 'duration_ms', 'method', 'path'],
'created_at',
'desc'
);
$where = [];
$params = [];
RepoQuery::addLikeFilter(
$where,
$params,
['api_audit_log.path', 'api_audit_log.request_id', 'api_audit_log.ip', 'api_audit_log.error_code'],
$search
);
if (in_array($status, ['2xx', '4xx', '5xx'], true)) {
$rangeStart = (int) $status[0] * 100;
$where[] = 'api_audit_log.status_code between ? and ?';
$params[] = (string) $rangeStart;
$params[] = (string) ($rangeStart + 99);
} elseif ($status !== '' && ctype_digit($status)) {
$statusCode = (int) $status;
if ($statusCode >= 100 && $statusCode <= 599) {
$where[] = 'api_audit_log.status_code = ?';
$params[] = (string) $statusCode;
}
}
if (in_array($method, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], true)) {
$where[] = 'api_audit_log.method = ?';
$params[] = $method;
}
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $createdFrom)) {
$where[] = 'api_audit_log.created_at >= ?';
$params[] = $createdFrom . ' 00:00:00';
}
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $createdTo)) {
$where[] = 'api_audit_log.created_at <= ?';
$params[] = $createdTo . ' 23:59:59';
}
if ($tenantId > 0) {
$where[] = 'api_audit_log.tenant_id = ?';
$params[] = (string) $tenantId;
}
if ($userId > 0) {
$where[] = 'api_audit_log.user_id = ?';
$params[] = (string) $userId;
}
$whereSql = $where ? (' where ' . implode(' and ', $where)) : '';
$fromSql = ' from api_audit_log ' .
'left join users on users.id = api_audit_log.user_id ' .
'left join tenants on tenants.id = api_audit_log.tenant_id ';
$total = (int) (DB::selectValue('select count(*)' . $fromSql . $whereSql, ...$params) ?? 0);
$rows = DB::select(
'select
api_audit_log.id,
api_audit_log.request_id,
api_audit_log.method,
api_audit_log.path,
api_audit_log.query_json,
api_audit_log.status_code,
api_audit_log.duration_ms,
api_audit_log.error_code,
api_audit_log.user_id,
api_audit_log.tenant_id,
api_audit_log.api_token_id,
api_audit_log.token_tenant_id,
api_audit_log.ip,
api_audit_log.user_agent,
api_audit_log.created_at,
users.id,
users.uuid,
users.display_name,
users.email,
tenants.id,
tenants.uuid,
tenants.description
' . $fromSql . $whereSql .
sprintf(' order by api_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);
if ($item !== null) {
$normalized[] = $item;
}
}
}
return [
'total' => $total,
'rows' => $normalized,
];
}
public static function find(int $id): ?array
{
$row = DB::selectOne(
'select
api_audit_log.id,
api_audit_log.request_id,
api_audit_log.method,
api_audit_log.path,
api_audit_log.query_json,
api_audit_log.status_code,
api_audit_log.duration_ms,
api_audit_log.error_code,
api_audit_log.user_id,
api_audit_log.tenant_id,
api_audit_log.api_token_id,
api_audit_log.token_tenant_id,
api_audit_log.ip,
api_audit_log.user_agent,
api_audit_log.created_at,
users.id,
users.uuid,
users.display_name,
users.email,
tenants.id,
tenants.uuid,
tenants.description
from api_audit_log
left join users on users.id = api_audit_log.user_id
left join tenants on tenants.id = api_audit_log.tenant_id
where api_audit_log.id = ?
limit 1',
(string) $id
);
return self::normalizeRow($row);
}
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 api_audit_log where created_at < ?', $cutoff);
return is_int($deleted) ? $deleted : 0;
}
private static function normalizeRow(mixed $row): ?array
{
if (!is_array($row)) {
return null;
}
$log = $row['api_audit_log'] ?? [];
if (!is_array($log) || !isset($log['id'])) {
return null;
}
$user = is_array($row['users'] ?? null) ? $row['users'] : [];
$tenant = is_array($row['tenants'] ?? null) ? $row['tenants'] : [];
$log['user_uuid'] = (string) ($user['uuid'] ?? '');
$log['user_display_name'] = (string) ($user['display_name'] ?? '');
$log['user_email'] = (string) ($user['email'] ?? '');
$log['tenant_uuid'] = (string) ($tenant['uuid'] ?? '');
$log['tenant_description'] = (string) ($tenant['description'] ?? '');
return $log;
}
}