Fix support API audit logging
This commit is contained in:
@@ -8,6 +8,7 @@ use App\Http\Requests\Support\Resources\SupportResourceFormRequest;
|
|||||||
use App\Http\Requests\Support\SupportResourceRequest;
|
use App\Http\Requests\Support\SupportResourceRequest;
|
||||||
use App\Jobs\GenerateDataExport;
|
use App\Jobs\GenerateDataExport;
|
||||||
use App\Models\DataExport;
|
use App\Models\DataExport;
|
||||||
|
use App\Services\Audit\SuperAdminAuditLogger;
|
||||||
use App\Support\ApiError;
|
use App\Support\ApiError;
|
||||||
use App\Support\SupportApiAuthorizer;
|
use App\Support\SupportApiAuthorizer;
|
||||||
use App\Support\SupportApiRegistry;
|
use App\Support\SupportApiRegistry;
|
||||||
@@ -77,7 +78,7 @@ class SupportResourceController extends Controller
|
|||||||
|
|
||||||
public function store(SupportResourceRequest $request, string $resource): JsonResponse
|
public function store(SupportResourceRequest $request, string $resource): JsonResponse
|
||||||
{
|
{
|
||||||
if ($response = SupportApiAuthorizer::authorizeResource($request, $resource, 'write')) {
|
if ($response = SupportApiAuthorizer::authorizeAnyAbility($request, SupportApiRegistry::abilitiesFor($resource, 'write'), 'write')) {
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +111,14 @@ class SupportResourceController extends Controller
|
|||||||
|
|
||||||
$record = $modelClass::query()->create($payload);
|
$record = $modelClass::query()->create($payload);
|
||||||
|
|
||||||
|
app(SuperAdminAuditLogger::class)->record(
|
||||||
|
SupportApiRegistry::auditAction($resource, 'created'),
|
||||||
|
$record,
|
||||||
|
SuperAdminAuditLogger::fieldsMetadata($payload),
|
||||||
|
actor: $request->user(),
|
||||||
|
source: static::class
|
||||||
|
);
|
||||||
|
|
||||||
if ($resource === 'data-exports') {
|
if ($resource === 'data-exports') {
|
||||||
GenerateDataExport::dispatch($record->id);
|
GenerateDataExport::dispatch($record->id);
|
||||||
}
|
}
|
||||||
@@ -121,7 +130,7 @@ class SupportResourceController extends Controller
|
|||||||
|
|
||||||
public function update(SupportResourceRequest $request, string $resource, string $record): JsonResponse
|
public function update(SupportResourceRequest $request, string $resource, string $record): JsonResponse
|
||||||
{
|
{
|
||||||
if ($response = SupportApiAuthorizer::authorizeResource($request, $resource, 'write')) {
|
if ($response = SupportApiAuthorizer::authorizeAnyAbility($request, SupportApiRegistry::abilitiesFor($resource, 'write'), 'write')) {
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +157,14 @@ class SupportResourceController extends Controller
|
|||||||
$model->fill($payload);
|
$model->fill($payload);
|
||||||
$model->save();
|
$model->save();
|
||||||
|
|
||||||
|
app(SuperAdminAuditLogger::class)->record(
|
||||||
|
SupportApiRegistry::auditAction($resource, 'updated'),
|
||||||
|
$model,
|
||||||
|
SuperAdminAuditLogger::fieldsMetadata($payload),
|
||||||
|
actor: $request->user(),
|
||||||
|
source: static::class
|
||||||
|
);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'data' => $model->refresh(),
|
'data' => $model->refresh(),
|
||||||
]);
|
]);
|
||||||
@@ -155,7 +172,7 @@ class SupportResourceController extends Controller
|
|||||||
|
|
||||||
public function destroy(Request $request, string $resource, string $record): JsonResponse
|
public function destroy(Request $request, string $resource, string $record): JsonResponse
|
||||||
{
|
{
|
||||||
if ($response = SupportApiAuthorizer::authorizeResource($request, $resource, 'write')) {
|
if ($response = SupportApiAuthorizer::authorizeAnyAbility($request, SupportApiRegistry::abilitiesFor($resource, 'write'), 'write')) {
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +188,14 @@ class SupportResourceController extends Controller
|
|||||||
|
|
||||||
$model->delete();
|
$model->delete();
|
||||||
|
|
||||||
|
app(SuperAdminAuditLogger::class)->record(
|
||||||
|
SupportApiRegistry::auditAction($resource, 'deleted'),
|
||||||
|
$model,
|
||||||
|
SuperAdminAuditLogger::fieldsMetadata([]),
|
||||||
|
actor: $request->user(),
|
||||||
|
source: static::class
|
||||||
|
);
|
||||||
|
|
||||||
return response()->json(['ok' => true]);
|
return response()->json(['ok' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,11 +93,15 @@ class SuperAdminAuditLogger
|
|||||||
return $panel->getId() === 'superadmin';
|
return $panel->getId() === 'superadmin';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request()->is('super-admin*') || request()->is('api/v1/support*')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (app()->runningInConsole()) {
|
if (app()->runningInConsole()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return request()->is('super-admin*');
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -48,4 +48,39 @@ class SupportApiAuthorizer
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $abilities
|
||||||
|
*/
|
||||||
|
public static function authorizeAnyAbility(Request $request, array $abilities, string $actionLabel = 'resource'): ?JsonResponse
|
||||||
|
{
|
||||||
|
if ($abilities === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $request->user()?->currentAccessToken();
|
||||||
|
|
||||||
|
if (! $token) {
|
||||||
|
return ApiError::response(
|
||||||
|
'unauthenticated',
|
||||||
|
'Unauthenticated',
|
||||||
|
'Missing access token for support request.',
|
||||||
|
401
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($abilities as $ability) {
|
||||||
|
if ($token->can($ability)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiError::response(
|
||||||
|
'forbidden',
|
||||||
|
'Forbidden',
|
||||||
|
"Missing required ability for support {$actionLabel}.",
|
||||||
|
403,
|
||||||
|
['required' => $abilities]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,23 @@ class SupportApiRegistry
|
|||||||
return (bool) ($config['read_only'] ?? false);
|
return (bool) ($config['read_only'] ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function auditAction(string $resource, string $operation): string
|
||||||
|
{
|
||||||
|
$config = self::get($resource);
|
||||||
|
|
||||||
|
$action = null;
|
||||||
|
|
||||||
|
if ($config && is_array($config['audit'] ?? null)) {
|
||||||
|
$action = $config['audit'][$operation] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($action) && $action !== '') {
|
||||||
|
return $action;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resource.'.'.$operation;
|
||||||
|
}
|
||||||
|
|
||||||
public static function allowsMutation(string $resource, string $action): bool
|
public static function allowsMutation(string $resource, string $action): bool
|
||||||
{
|
{
|
||||||
if (self::isReadOnly($resource)) {
|
if (self::isReadOnly($resource)) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
openapi: 3.1.0
|
openapi: 3.1.0
|
||||||
info:
|
info:
|
||||||
title: Fotospiel Support API
|
title: Fotospiel Support API
|
||||||
version: 1.0.0
|
version: 1.1.0
|
||||||
description: Support-only management API for super admins.
|
description: Support-only management API for super admins.
|
||||||
servers:
|
servers:
|
||||||
- url: /api/v1
|
- url: /api/v1
|
||||||
@@ -50,6 +50,143 @@ components:
|
|||||||
data:
|
data:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
|
SupportError:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
meta:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
SupportEventPayload:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
de: { type: string }
|
||||||
|
en: { type: string }
|
||||||
|
description:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
de: { type: string }
|
||||||
|
en: { type: string }
|
||||||
|
slug: { type: string }
|
||||||
|
date: { type: string, format: date-time }
|
||||||
|
location: { type: string, nullable: true }
|
||||||
|
max_participants: { type: integer, nullable: true }
|
||||||
|
event_type_id: { type: integer, nullable: true }
|
||||||
|
default_locale: { type: string }
|
||||||
|
is_active: { type: boolean }
|
||||||
|
status: { type: string, enum: [draft, published, archived] }
|
||||||
|
settings: { type: object, additionalProperties: true }
|
||||||
|
join_link_enabled: { type: boolean }
|
||||||
|
photo_upload_enabled: { type: boolean }
|
||||||
|
task_checklist_enabled: { type: boolean }
|
||||||
|
SupportPhotoPayload:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status: { type: string, enum: [pending, approved, rejected, hidden] }
|
||||||
|
moderation_notes: { type: string, nullable: true }
|
||||||
|
is_featured: { type: boolean }
|
||||||
|
emotion_id: { type: integer, nullable: true }
|
||||||
|
task_id: { type: integer, nullable: true }
|
||||||
|
SupportBlogPostPayload:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
blog_category_id: { type: integer }
|
||||||
|
slug: { type: string }
|
||||||
|
banner: { type: string, nullable: true }
|
||||||
|
published_at: { type: string, format: date-time, nullable: true }
|
||||||
|
is_published: { type: boolean }
|
||||||
|
title:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
de: { type: string }
|
||||||
|
en: { type: string }
|
||||||
|
content:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
de: { type: string }
|
||||||
|
en: { type: string }
|
||||||
|
excerpt:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
de: { type: string }
|
||||||
|
en: { type: string }
|
||||||
|
meta_title:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
de: { type: string }
|
||||||
|
en: { type: string }
|
||||||
|
meta_description:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
de: { type: string }
|
||||||
|
en: { type: string }
|
||||||
|
SupportEmotionPayload:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
de: { type: string }
|
||||||
|
en: { type: string }
|
||||||
|
description:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
de: { type: string }
|
||||||
|
en: { type: string }
|
||||||
|
icon: { type: string, nullable: true }
|
||||||
|
color: { type: string, nullable: true }
|
||||||
|
sort_order: { type: integer }
|
||||||
|
is_active: { type: boolean }
|
||||||
|
tenant_id: { type: integer, nullable: true }
|
||||||
|
SupportTaskPayload:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
emotion_id: { type: integer }
|
||||||
|
event_type_id: { type: integer, nullable: true }
|
||||||
|
title:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
de: { type: string }
|
||||||
|
en: { type: string }
|
||||||
|
description:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
de: { type: string }
|
||||||
|
en: { type: string }
|
||||||
|
example_text:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
de: { type: string }
|
||||||
|
en: { type: string }
|
||||||
|
difficulty: { type: string, enum: [easy, medium, hard] }
|
||||||
|
sort_order: { type: integer }
|
||||||
|
is_active: { type: boolean }
|
||||||
paths:
|
paths:
|
||||||
/support/auth/token:
|
/support/auth/token:
|
||||||
post:
|
post:
|
||||||
@@ -62,14 +199,11 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
login:
|
login: { type: string }
|
||||||
type: string
|
password: { type: string }
|
||||||
password:
|
|
||||||
type: string
|
|
||||||
abilities:
|
abilities:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items: { type: string }
|
||||||
type: string
|
|
||||||
required: [login, password]
|
required: [login, password]
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -95,8 +229,7 @@ paths:
|
|||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
abilities:
|
abilities:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items: { type: string }
|
||||||
type: string
|
|
||||||
/support/auth/logout:
|
/support/auth/logout:
|
||||||
post:
|
post:
|
||||||
summary: Revoke current support token
|
summary: Revoke current support token
|
||||||
@@ -109,8 +242,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
ok:
|
ok: { type: boolean }
|
||||||
type: boolean
|
|
||||||
/support/settings/guest-policy:
|
/support/settings/guest-policy:
|
||||||
get:
|
get:
|
||||||
summary: Get guest policy settings
|
summary: Get guest policy settings
|
||||||
@@ -175,11 +307,9 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: tenant
|
name: tenant
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200': { description: Activated }
|
||||||
description: Activated
|
|
||||||
/support/tenants/{tenant}/actions/deactivate:
|
/support/tenants/{tenant}/actions/deactivate:
|
||||||
post:
|
post:
|
||||||
summary: Deactivate tenant
|
summary: Deactivate tenant
|
||||||
@@ -188,11 +318,9 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: tenant
|
name: tenant
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200': { description: Deactivated }
|
||||||
description: Deactivated
|
|
||||||
/support/tenants/{tenant}/actions/suspend:
|
/support/tenants/{tenant}/actions/suspend:
|
||||||
post:
|
post:
|
||||||
summary: Suspend tenant
|
summary: Suspend tenant
|
||||||
@@ -201,11 +329,9 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: tenant
|
name: tenant
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200': { description: Suspended }
|
||||||
description: Suspended
|
|
||||||
/support/tenants/{tenant}/actions/unsuspend:
|
/support/tenants/{tenant}/actions/unsuspend:
|
||||||
post:
|
post:
|
||||||
summary: Unsuspend tenant
|
summary: Unsuspend tenant
|
||||||
@@ -214,11 +340,9 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: tenant
|
name: tenant
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200': { description: Unsuspended }
|
||||||
description: Unsuspended
|
|
||||||
/support/tenants/{tenant}/actions/schedule-deletion:
|
/support/tenants/{tenant}/actions/schedule-deletion:
|
||||||
post:
|
post:
|
||||||
summary: Schedule tenant deletion
|
summary: Schedule tenant deletion
|
||||||
@@ -227,8 +351,7 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: tenant
|
name: tenant
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@@ -236,15 +359,11 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
pending_deletion_at:
|
pending_deletion_at: { type: string, format: date-time }
|
||||||
type: string
|
send_warning: { type: boolean }
|
||||||
format: date-time
|
|
||||||
send_warning:
|
|
||||||
type: boolean
|
|
||||||
required: [pending_deletion_at]
|
required: [pending_deletion_at]
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200': { description: Scheduled }
|
||||||
description: Scheduled
|
|
||||||
/support/tenants/{tenant}/actions/cancel-deletion:
|
/support/tenants/{tenant}/actions/cancel-deletion:
|
||||||
post:
|
post:
|
||||||
summary: Cancel tenant deletion
|
summary: Cancel tenant deletion
|
||||||
@@ -253,11 +372,9 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: tenant
|
name: tenant
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200': { description: Cancelled }
|
||||||
description: Cancelled
|
|
||||||
/support/tenants/{tenant}/actions/anonymize:
|
/support/tenants/{tenant}/actions/anonymize:
|
||||||
post:
|
post:
|
||||||
summary: Anonymize tenant
|
summary: Anonymize tenant
|
||||||
@@ -266,11 +383,9 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: tenant
|
name: tenant
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200': { description: Anonymize requested }
|
||||||
description: Anonymize requested
|
|
||||||
/support/tenants/{tenant}/actions/add-package:
|
/support/tenants/{tenant}/actions/add-package:
|
||||||
post:
|
post:
|
||||||
summary: Add package to tenant
|
summary: Add package to tenant
|
||||||
@@ -279,8 +394,7 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: tenant
|
name: tenant
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@@ -288,17 +402,12 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
package_id:
|
package_id: { type: integer }
|
||||||
type: integer
|
expires_at: { type: string, format: date-time }
|
||||||
expires_at:
|
reason: { type: string }
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
reason:
|
|
||||||
type: string
|
|
||||||
required: [package_id]
|
required: [package_id]
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200': { description: Package added }
|
||||||
description: Package added
|
|
||||||
/support/tenants/{tenant}/actions/update-limits:
|
/support/tenants/{tenant}/actions/update-limits:
|
||||||
post:
|
post:
|
||||||
summary: Update tenant limits
|
summary: Update tenant limits
|
||||||
@@ -307,8 +416,7 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: tenant
|
name: tenant
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@@ -316,16 +424,12 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
max_photos_per_event:
|
max_photos_per_event: { type: integer }
|
||||||
type: integer
|
max_storage_mb: { type: integer }
|
||||||
max_storage_mb:
|
note: { type: string }
|
||||||
type: integer
|
|
||||||
note:
|
|
||||||
type: string
|
|
||||||
required: [max_photos_per_event, max_storage_mb]
|
required: [max_photos_per_event, max_storage_mb]
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200': { description: Limits updated }
|
||||||
description: Limits updated
|
|
||||||
/support/tenants/{tenant}/actions/update-subscription-expires-at:
|
/support/tenants/{tenant}/actions/update-subscription-expires-at:
|
||||||
post:
|
post:
|
||||||
summary: Update tenant subscription expiry
|
summary: Update tenant subscription expiry
|
||||||
@@ -334,8 +438,7 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: tenant
|
name: tenant
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@@ -343,14 +446,10 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
subscription_expires_at:
|
subscription_expires_at: { type: string, format: date-time }
|
||||||
type: string
|
note: { type: string }
|
||||||
format: date-time
|
|
||||||
note:
|
|
||||||
type: string
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200': { description: Subscription expiry updated }
|
||||||
description: Subscription expiry updated
|
|
||||||
/support/tenants/{tenant}/actions/set-grace-period:
|
/support/tenants/{tenant}/actions/set-grace-period:
|
||||||
post:
|
post:
|
||||||
summary: Set tenant grace period
|
summary: Set tenant grace period
|
||||||
@@ -359,8 +458,7 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: tenant
|
name: tenant
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@@ -368,15 +466,11 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
grace_period_ends_at:
|
grace_period_ends_at: { type: string, format: date-time }
|
||||||
type: string
|
note: { type: string }
|
||||||
format: date-time
|
|
||||||
note:
|
|
||||||
type: string
|
|
||||||
required: [grace_period_ends_at]
|
required: [grace_period_ends_at]
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200': { description: Grace period set }
|
||||||
description: Grace period set
|
|
||||||
/support/tenants/{tenant}/actions/clear-grace-period:
|
/support/tenants/{tenant}/actions/clear-grace-period:
|
||||||
post:
|
post:
|
||||||
summary: Clear tenant grace period
|
summary: Clear tenant grace period
|
||||||
@@ -385,11 +479,9 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: tenant
|
name: tenant
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200': { description: Grace period cleared }
|
||||||
description: Grace period cleared
|
|
||||||
/support/{resource}:
|
/support/{resource}:
|
||||||
get:
|
get:
|
||||||
summary: List support resource
|
summary: List support resource
|
||||||
@@ -400,46 +492,16 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum: [tenants, users, events, event-types, photos, event-purchases, purchases, purchase-histories, packages, package-addons, tenant-packages, coupons, gift-vouchers, tenant-feedback, tenant-announcements, media-storage-targets, retention-overrides, data-exports, photobooth-settings, legal-pages, blog-categories, blog-posts, emotions, tasks, task-collections, super-admin-action-logs, infrastructure-action-logs]
|
||||||
- tenants
|
|
||||||
- users
|
|
||||||
- events
|
|
||||||
- event-types
|
|
||||||
- photos
|
|
||||||
- event-purchases
|
|
||||||
- purchases
|
|
||||||
- purchase-histories
|
|
||||||
- packages
|
|
||||||
- package-addons
|
|
||||||
- tenant-packages
|
|
||||||
- coupons
|
|
||||||
- gift-vouchers
|
|
||||||
- tenant-feedback
|
|
||||||
- tenant-announcements
|
|
||||||
- media-storage-targets
|
|
||||||
- retention-overrides
|
|
||||||
- data-exports
|
|
||||||
- photobooth-settings
|
|
||||||
- legal-pages
|
|
||||||
- blog-categories
|
|
||||||
- blog-posts
|
|
||||||
- emotions
|
|
||||||
- tasks
|
|
||||||
- task-collections
|
|
||||||
- super-admin-action-logs
|
|
||||||
- infrastructure-action-logs
|
|
||||||
- in: query
|
- in: query
|
||||||
name: search
|
name: search
|
||||||
schema:
|
schema: { type: string }
|
||||||
type: string
|
|
||||||
- in: query
|
- in: query
|
||||||
name: sort
|
name: sort
|
||||||
schema:
|
schema: { type: string }
|
||||||
type: string
|
|
||||||
- in: query
|
- in: query
|
||||||
name: per_page
|
name: per_page
|
||||||
schema:
|
schema: { type: integer }
|
||||||
type: integer
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Resource list
|
description: Resource list
|
||||||
@@ -454,14 +516,16 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: resource
|
name: resource
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: string }
|
||||||
type: string
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/SupportResourcePayload'
|
oneOf:
|
||||||
|
- $ref: '#/components/schemas/SupportBlogPostPayload'
|
||||||
|
- $ref: '#/components/schemas/SupportEmotionPayload'
|
||||||
|
- $ref: '#/components/schemas/SupportTaskPayload'
|
||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
description: Resource created
|
description: Resource created
|
||||||
@@ -477,13 +541,11 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: resource
|
name: resource
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: string }
|
||||||
type: string
|
|
||||||
- in: path
|
- in: path
|
||||||
name: record
|
name: record
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: string }
|
||||||
type: string
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Resource
|
description: Resource
|
||||||
@@ -498,19 +560,22 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: resource
|
name: resource
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: string }
|
||||||
type: string
|
|
||||||
- in: path
|
- in: path
|
||||||
name: record
|
name: record
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: string }
|
||||||
type: string
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/SupportResourcePayload'
|
oneOf:
|
||||||
|
- $ref: '#/components/schemas/SupportEventPayload'
|
||||||
|
- $ref: '#/components/schemas/SupportPhotoPayload'
|
||||||
|
- $ref: '#/components/schemas/SupportBlogPostPayload'
|
||||||
|
- $ref: '#/components/schemas/SupportEmotionPayload'
|
||||||
|
- $ref: '#/components/schemas/SupportTaskPayload'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Resource updated
|
description: Resource updated
|
||||||
@@ -525,13 +590,11 @@ paths:
|
|||||||
- in: path
|
- in: path
|
||||||
name: resource
|
name: resource
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: string }
|
||||||
type: string
|
|
||||||
- in: path
|
- in: path
|
||||||
name: record
|
name: record
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema: { type: string }
|
||||||
type: string
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Resource deleted
|
description: Resource deleted
|
||||||
@@ -540,5 +603,4 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
ok:
|
ok: { type: boolean }
|
||||||
type: boolean
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace Tests\Feature\Support;
|
|||||||
|
|
||||||
use App\Models\BlogCategory;
|
use App\Models\BlogCategory;
|
||||||
use App\Models\Photo;
|
use App\Models\Photo;
|
||||||
|
use App\Models\SuperAdminActionLog;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
@@ -130,4 +131,25 @@ class SupportApiTest extends TestCase
|
|||||||
$response->assertStatus(422)
|
$response->assertStatus(422)
|
||||||
->assertJsonValidationErrors(['title', 'content']);
|
->assertJsonValidationErrors(['title', 'content']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_support_update_logs_audit_entry(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'role' => 'super_admin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
|
||||||
|
Sanctum::actingAs($user, ['support-admin', 'support:write']);
|
||||||
|
|
||||||
|
$response = $this->patchJson('/api/v1/support/tenants/'.$tenant->id, [
|
||||||
|
'data' => [
|
||||||
|
'slug' => 'support-updated',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
$this->assertTrue(SuperAdminActionLog::query()->where('action', 'tenants.updated')->exists());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user