Add auditing

This commit is contained in:
Constantin Graf
2024-09-03 14:07:13 +02:00
committed by Constantin Graf
parent a01e1d6b0b
commit 156d2ff1a0
26 changed files with 732 additions and 16 deletions

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Extensions\Auditing\Resolvers;
use Illuminate\Support\Facades\Request;
use OwenIt\Auditing\Contracts\Auditable;
use OwenIt\Auditing\Contracts\Resolver;
class CustomIpAddressResolver implements Resolver
{
private static function anonymizeIpAddress(string $ipAddress): string
{
/** @source https://stackoverflow.com/a/48777412 */
return preg_replace(
['/\.\d*$/', '/[\da-f]*:[\da-f]*$/'],
['.0', '0:0'],
$ipAddress
);
}
public static function resolve(Auditable $auditable): string
{
$ip = $auditable->preloadedResolverData['ip_address'] ?? Request::ip();
if ($ip !== null) {
$ip = self::anonymizeIpAddress($ip);
}
return $ip;
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\AuditResource\Pages;
use App\Models\Audit;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use Novadaemon\FilamentPrettyJson\PrettyJson;
class AuditResource extends Resource
{
protected static ?string $model = Audit::class;
protected static ?string $navigationIcon = 'heroicon-o-archive-box';
protected static ?string $navigationGroup = 'System';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('user_type')
->maxLength(255),
Forms\Components\TextInput::make('user_id'),
Forms\Components\TextInput::make('event')
->required()
->maxLength(255),
Forms\Components\TextInput::make('auditable_type')
->required()
->maxLength(255),
Forms\Components\TextInput::make('auditable_id')
->required(),
PrettyJson::make('old_values'),
PrettyJson::make('new_values'),
Forms\Components\Textarea::make('url'),
Forms\Components\TextInput::make('ip_address'),
Forms\Components\TextInput::make('user_agent')
->maxLength(1023),
Forms\Components\TextInput::make('tags')
->maxLength(255),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('user.name'),
Tables\Columns\TextColumn::make('event'),
Tables\Columns\TextColumn::make('auditable_type'),
Tables\Columns\TextColumn::make('auditable_id'),
IconColumn::make('was_command')
->getStateUsing(fn (Audit $record) => Str::startsWith($record->url, 'artisan '))
->boolean(),
Tables\Columns\TextColumn::make('created_at')
->sortable()
->dateTime(),
Tables\Columns\TextColumn::make('updated_at')
->sortable()
->dateTime(),
])
->filters([
//
])
->actions([
Tables\Actions\ViewAction::make(),
])
->bulkActions([
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListAudits::route('/'),
'create' => Pages\CreateAudit::route('/create'),
'view' => Pages\ViewAudit::route('/{record}'),
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\AuditResource\Pages;
use App\Filament\Resources\AuditResource;
use Filament\Resources\Pages\CreateRecord;
class CreateAudit extends CreateRecord
{
protected static string $resource = AuditResource::class;
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\AuditResource\Pages;
use App\Filament\Resources\AuditResource;
use Filament\Resources\Pages\ListRecords;
class ListAudits extends ListRecords
{
protected static string $resource = AuditResource::class;
protected function getHeaderActions(): array
{
return [];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\AuditResource\Pages;
use App\Filament\Resources\AuditResource;
use Filament\Resources\Pages\ViewRecord;
class ViewAudit extends ViewRecord
{
protected static string $resource = AuditResource::class;
}

33
app/Models/Audit.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Database\Factories\AuditFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Carbon;
use OwenIt\Auditing\Models\Audit as PackageAuditModel;
/**
* @property int $id
* @property string|null $user_type
* @property string|null $user_id
* @property string $event
* @property string $auditable_type
* @property string $auditable_id
* @property array|null $old_values
* @property array|null $new_values
* @property string|null $url
* @property string|null $ip_address
* @property string|null $user_agent
* @property string|null $tags
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
*
* @method static AuditFactory factory()
*/
class Audit extends PackageAuditModel
{
use HasFactory;
}

View File

@@ -12,6 +12,8 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/**
* @property string $id
@@ -25,8 +27,9 @@ use Illuminate\Support\Carbon;
*
* @method static ClientFactory factory()
*/
class Client extends Model
class Client extends Model implements AuditableContract
{
use Auditable;
use HasFactory;
use HasUuids;

View File

@@ -11,6 +11,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon;
use Laravel\Jetstream\Membership as JetstreamMembership;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/**
* @property string $id
@@ -25,8 +27,9 @@ use Laravel\Jetstream\Membership as JetstreamMembership;
*
* @method static MemberFactory factory()
*/
class Member extends JetstreamMembership
class Member extends JetstreamMembership implements AuditableContract
{
use Auditable;
use HasFactory;
use HasUuids;

View File

@@ -18,6 +18,8 @@ use Laravel\Jetstream\Events\TeamDeleted;
use Laravel\Jetstream\Events\TeamUpdated;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\Team as JetstreamTeam;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/**
* @property string $id
@@ -37,8 +39,9 @@ use Laravel\Jetstream\Team as JetstreamTeam;
* @method HasMany<OrganizationInvitation> teamInvitations()
* @method static OrganizationFactory factory()
*/
class Organization extends JetstreamTeam
class Organization extends JetstreamTeam implements AuditableContract
{
use Auditable;
use HasFactory;
use HasUuids;

View File

@@ -11,6 +11,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\TeamInvitation as JetstreamTeamInvitation;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/**
* @property string $id
@@ -23,8 +25,9 @@ use Laravel\Jetstream\TeamInvitation as JetstreamTeamInvitation;
*
* @method static OrganizationInvitationFactory factory()
*/
class OrganizationInvitation extends JetstreamTeamInvitation
class OrganizationInvitation extends JetstreamTeamInvitation implements AuditableContract
{
use Auditable;
use HasFactory;
use HasUuids;

View File

@@ -14,6 +14,8 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/**
* @property string $id
@@ -36,8 +38,9 @@ use Illuminate\Support\Carbon;
* @method Builder<Project> visibleByEmployee(User $user)
* @method static ProjectFactory factory()
*/
class Project extends Model
class Project extends Model implements AuditableContract
{
use Auditable;
use HasFactory;
use HasUuids;

View File

@@ -11,6 +11,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/**
* @property string $id
@@ -27,8 +29,9 @@ use Illuminate\Support\Carbon;
* @method static Builder<ProjectMember> whereBelongsToOrganization(Organization $organization)
* @method static ProjectMemberFactory factory()
*/
class ProjectMember extends Model
class ProjectMember extends Model implements AuditableContract
{
use Auditable;
use HasFactory;
use HasUuids;

View File

@@ -10,6 +10,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/**
* @property string $id
@@ -21,8 +23,9 @@ use Illuminate\Support\Carbon;
*
* @method static TagFactory factory()
*/
class Tag extends Model
class Tag extends Model implements AuditableContract
{
use Auditable;
use HasFactory;
use HasUuids;

View File

@@ -14,6 +14,8 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/**
* @property string $id
@@ -30,8 +32,9 @@ use Illuminate\Support\Carbon;
*
* @method static TaskFactory factory()
*/
class Task extends Model
class Task extends Model implements AuditableContract
{
use Auditable;
use HasFactory;
use HasUuids;

View File

@@ -14,6 +14,8 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use Korridor\LaravelComputedAttributes\ComputedAttributes;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/**
* @property string $id
@@ -43,8 +45,9 @@ use Korridor\LaravelComputedAttributes\ComputedAttributes;
* @method Builder<TimeEntry> hasTag(Tag $tag)
* @method static TimeEntryFactory factory()
*/
class TimeEntry extends Model
class TimeEntry extends Model implements AuditableContract
{
use Auditable;
use ComputedAttributes;
use HasFactory;
use HasUuids;

View File

@@ -25,6 +25,8 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Jetstream\HasTeams;
use Laravel\Passport\HasApiTokens;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/**
* @property string $id
@@ -53,8 +55,9 @@ use Laravel\Passport\HasApiTokens;
* @method Builder<User> belongsToOrganization(Organization $organization)
* @method Builder<User> active()
*/
class User extends Authenticatable implements FilamentUser, MustVerifyEmail
class User extends Authenticatable implements AuditableContract, FilamentUser, MustVerifyEmail
{
use Auditable;
use HasApiTokens;
use HasFactory;
use HasProfilePhoto;

View File

@@ -5,10 +5,12 @@ declare(strict_types=1);
namespace App\Providers;
use App\Models\Client;
use App\Models\FailedJob;
use App\Models\Member;
use App\Models\Organization;
use App\Models\OrganizationInvitation;
use App\Models\Project;
use App\Models\ProjectMember;
use App\Models\Tag;
use App\Models\Task;
use App\Models\TimeEntry;
@@ -56,15 +58,17 @@ class AppServiceProvider extends ServiceProvider
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
Model::preventAccessingMissingAttributes(! $this->app->isProduction());
Relation::enforceMorphMap([
'client' => Client::class,
'failed-job' => FailedJob::class,
'membership' => Member::class,
'organization' => Organization::class,
'organization-invitation' => OrganizationInvitation::class,
'user' => User::class,
'time-entry' => TimeEntry::class,
'project' => Project::class,
'task' => Task::class,
'client' => Client::class,
'project-member' => ProjectMember::class,
'tag' => Tag::class,
'task' => Task::class,
'time-entry' => TimeEntry::class,
'user' => User::class,
]);
Model::unguard();

View File

@@ -86,7 +86,13 @@ class DeletionService
'currentOrganization',
])
->get();
$organization->users()->sync([]);
$members = Member::query()
->whereBelongsTo($organization, 'organization')
->get();
foreach ($members as $member) {
$member->delete();
}
// Make sure all users have at least one organization and delete placeholders
foreach ($users as $user) {