mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-19 18:15:08 +00:00
7646e99f33
Every list in /api/v1 answers "what is there now". Nothing answered "what happened", and for the two events people most want to act on there was nowhere to look at all. Sharing a file writes an assignment row and never touches the file, so no amount of polling /files?updated_since= will ever show a share. A download is recorded only in the activity log. So the most requested automations for a file-sharing product — tell me when a client gets a file, tell me when they open it — were not possible to build. GET /api/v1/activity is one feed rather than one endpoint per event, because the log already records every one of them and a caller filtering by action gets whatever the application grows later without waiting for us to expose it. It reuses what already exists: view_actions_log is the permission the activity screen uses, and ActivityLogScope narrows the rows the same way, so a staff member limited to their assigned clients cannot read the whole installation's log through a token when the screen would not show it. Two deliberate limits. Class names never reach the wire — subject.type is a stable public string, or moving a model between namespaces would be a breaking change to a frozen contract. And no ip_address, though the column exists and the screen shows it: a person looking at a log has decided to look, where an integration streams every row to somebody else's servers by default. PollingQuery grew an optional column so it can walk a table that is appended to rather than edited. The parameter stays updated_since everywhere, because on an append-only log the two timestamps are the same thing and one shape learned once is worth more than a second name.
94 lines
3.3 KiB
PHP
94 lines
3.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Audit\Http\Resources\Api;
|
|
|
|
use App\Modules\Audit\ActivityLog;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Resources\Json\JsonResource;
|
|
|
|
/**
|
|
* One entry in the activity log, as an integration reads it.
|
|
*
|
|
* Deliberately not ActivityPresenter's shape. That one exists to render a
|
|
* sentence, so it hands back a template and the words to slot into it —
|
|
* right for a screen, useless to a caller that wants to branch on what
|
|
* happened. Here the action is a key, the subject is an object, and the
|
|
* specifics stay in `context`.
|
|
*
|
|
* @mixin ActivityLog
|
|
*/
|
|
class ActivityResource extends JsonResource
|
|
{
|
|
/**
|
|
* Class names are internal structure and must never reach the wire:
|
|
* moving a model between namespaces would otherwise be a breaking API
|
|
* change, and `/api/v1` is a frozen contract. These strings are the
|
|
* contract instead — add to this map when a new kind of thing becomes
|
|
* a subject, and never rename an entry in it.
|
|
*
|
|
* @var array<class-string, string>
|
|
*/
|
|
private const SUBJECTS = [
|
|
\App\Models\User::class => 'user',
|
|
\App\Modules\Files\Models\File::class => 'file',
|
|
\App\Modules\Files\Models\Folder::class => 'folder',
|
|
\App\Modules\Files\Models\Category::class => 'category',
|
|
\App\Modules\Groups\Models\Group::class => 'group',
|
|
\App\Modules\Identity\Models\Role::class => 'role',
|
|
\App\Modules\Clients\Models\ClientCustomField::class => 'client_custom_field',
|
|
];
|
|
|
|
/**
|
|
* The map, for the controller's reverse lookup.
|
|
*
|
|
* @return array<class-string, string>
|
|
*/
|
|
public static function subjects(): array
|
|
{
|
|
return self::SUBJECTS;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function toArray(Request $request): array
|
|
{
|
|
return [
|
|
'id' => $this->id,
|
|
'action' => $this->action->value,
|
|
'created_at' => $this->created_at->toIso8601String(),
|
|
|
|
// Snapshots, not joins. The actor may since have been deleted,
|
|
// and the entry still has to say who it was.
|
|
'actor' => $this->actor_id === null && $this->actor_name === null ? null : [
|
|
'id' => $this->actor_id,
|
|
'name' => $this->actor_name,
|
|
'type' => $this->actor_type,
|
|
],
|
|
|
|
// How it arrived: a person in the browser, an integration, a
|
|
// visitor with no account, or the installation itself.
|
|
'origin' => $this->origin->value,
|
|
|
|
'subject' => $this->subject_type === null ? null : [
|
|
'type' => self::SUBJECTS[$this->subject_type] ?? 'other',
|
|
'id' => $this->subject_id,
|
|
'name' => $this->subject_name,
|
|
],
|
|
|
|
// Whatever the action recorded beyond its subject — who a file
|
|
// was shared with, how many files a cascade removed. Shape
|
|
// varies by action and is documented per action rather than
|
|
// here.
|
|
'context' => $this->context ?? [],
|
|
|
|
// ip_address is deliberately absent. It is stored for some
|
|
// actions and shown on the activity screen, but handing a
|
|
// client's IP to an automation tool is a privacy expansion
|
|
// with no matching use — see docs/api-todo.md.
|
|
];
|
|
}
|
|
}
|