mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 13:32:43 +01:00
Add auditing
This commit is contained in:
committed by
Constantin Graf
parent
a01e1d6b0b
commit
156d2ff1a0
@@ -3,6 +3,7 @@ APP_ENV=local
|
||||
APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk=
|
||||
APP_DEBUG=true
|
||||
APP_URL=https://solidtime.test
|
||||
AUDITING_ENABLED=true
|
||||
|
||||
SUPER_ADMINS=admin@example.com
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
95
app/Filament/Resources/AuditResource.php
Normal file
95
app/Filament/Resources/AuditResource.php
Normal 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}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
13
app/Filament/Resources/AuditResource/Pages/CreateAudit.php
Normal file
13
app/Filament/Resources/AuditResource/Pages/CreateAudit.php
Normal 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;
|
||||
}
|
||||
18
app/Filament/Resources/AuditResource/Pages/ListAudits.php
Normal file
18
app/Filament/Resources/AuditResource/Pages/ListAudits.php
Normal 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 [];
|
||||
}
|
||||
}
|
||||
13
app/Filament/Resources/AuditResource/Pages/ViewAudit.php
Normal file
13
app/Filament/Resources/AuditResource/Pages/ViewAudit.php
Normal 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
33
app/Models/Audit.php
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"novadaemon/filament-pretty-json": "^2.2",
|
||||
"nwidart/laravel-modules": "^11.0.11",
|
||||
"owen-it/laravel-auditing": "^13.6",
|
||||
"pxlrbt/filament-environment-indicator": "^2.0",
|
||||
"spatie/temporary-directory": "^2.2",
|
||||
"stechstudio/filament-impersonate": "^3.8",
|
||||
|
||||
90
composer.lock
generated
90
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "398572ca3d095ca043c84e7dfb4f051a",
|
||||
"content-hash": "38a45676e6bb2159275c370648f7ca11",
|
||||
"packages": [
|
||||
{
|
||||
"name": "anourvalar/eloquent-serialize",
|
||||
@@ -6699,6 +6699,94 @@
|
||||
],
|
||||
"time": "2024-06-17T08:53:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "owen-it/laravel-auditing",
|
||||
"version": "v13.6.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/owen-it/laravel-auditing.git",
|
||||
"reference": "28ecd2d5cc05c3619f99af42611877f54371af20"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/owen-it/laravel-auditing/zipball/28ecd2d5cc05c3619f99af42611877f54371af20",
|
||||
"reference": "28ecd2d5cc05c3619f99af42611877f54371af20",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"illuminate/console": "^7.0|^8.0|^9.0|^10.0|^11.0",
|
||||
"illuminate/database": "^7.0|^8.0|^9.0|^10.0|^11.0",
|
||||
"illuminate/filesystem": "^7.0|^8.0|^9.0|^10.0|^11.0",
|
||||
"php": "^7.3|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/legacy-factories": "*",
|
||||
"mockery/mockery": "^1.0",
|
||||
"orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0",
|
||||
"phpunit/phpunit": "^9.6|^10.5|^11.0"
|
||||
},
|
||||
"suggest": {
|
||||
"irazasyed/larasupport": "Needed to publish the package configuration in Lumen"
|
||||
},
|
||||
"type": "package",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "v13-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"OwenIt\\Auditing\\AuditingServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"OwenIt\\Auditing\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Antério Vieira",
|
||||
"email": "anteriovieira@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Raphael França",
|
||||
"email": "raphaelfrancabsb@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Morten D. Hansen",
|
||||
"email": "morten@visia.dk"
|
||||
}
|
||||
],
|
||||
"description": "Audit changes of your Eloquent models in Laravel/Lumen",
|
||||
"homepage": "https://laravel-auditing.com",
|
||||
"keywords": [
|
||||
"Accountability",
|
||||
"Audit",
|
||||
"auditing",
|
||||
"changes",
|
||||
"eloquent",
|
||||
"history",
|
||||
"laravel",
|
||||
"log",
|
||||
"logging",
|
||||
"lumen",
|
||||
"observer",
|
||||
"record",
|
||||
"revision",
|
||||
"tracking"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/owen-it/laravel-auditing/issues",
|
||||
"source": "https://github.com/owen-it/laravel-auditing"
|
||||
},
|
||||
"time": "2024-06-26T20:56:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/constant_time_encoding",
|
||||
"version": "v2.7.0",
|
||||
|
||||
200
config/audit.php
Normal file
200
config/audit.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'enabled' => env('AUDITING_ENABLED', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit Implementation
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define which Audit model implementation should be used.
|
||||
|
|
||||
*/
|
||||
|
||||
'implementation' => OwenIt\Auditing\Models\Audit::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Morph prefix & Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define the morph prefix and authentication guards for the User resolver.
|
||||
|
|
||||
*/
|
||||
|
||||
'user' => [
|
||||
'morph_prefix' => 'user',
|
||||
'guards' => [
|
||||
'web',
|
||||
'api',
|
||||
],
|
||||
'resolver' => OwenIt\Auditing\Resolvers\UserResolver::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit Resolvers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define the IP Address, User Agent and URL resolver implementations.
|
||||
|
|
||||
*/
|
||||
'resolvers' => [
|
||||
'ip_address' => App\Extensions\Auditing\Resolvers\CustomIpAddressResolver::class,
|
||||
'user_agent' => OwenIt\Auditing\Resolvers\UserAgentResolver::class,
|
||||
'url' => OwenIt\Auditing\Resolvers\UrlResolver::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit Events
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The Eloquent events that trigger an Audit.
|
||||
|
|
||||
*/
|
||||
|
||||
'events' => [
|
||||
'created',
|
||||
'updated',
|
||||
'deleted',
|
||||
'restored',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Strict Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Enable the strict mode when auditing?
|
||||
|
|
||||
*/
|
||||
|
||||
'strict' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global exclude
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Have something you always want to exclude by default? - add it here.
|
||||
| Note that this is overwritten (not merged) with local exclude
|
||||
|
|
||||
*/
|
||||
|
||||
'exclude' => [],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Empty Values
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Should Audit records be stored when the recorded old_values & new_values
|
||||
| are both empty?
|
||||
|
|
||||
| Some events may be empty on purpose. Use allowed_empty_values to exclude
|
||||
| those from the empty values check. For example when auditing
|
||||
| model retrieved events which will never have new and old values.
|
||||
|
|
||||
|
|
||||
*/
|
||||
|
||||
'empty_values' => false,
|
||||
'allowed_empty_values' => [
|
||||
'retrieved',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allowed Array Values
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Should the array values be audited?
|
||||
|
|
||||
| By default, array values are not allowed. This is to prevent performance
|
||||
| issues when storing large amounts of data. You can override this by
|
||||
| setting allow_array_values to true.
|
||||
*/
|
||||
'allowed_array_values' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit Timestamps
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Should the created_at, updated_at and deleted_at timestamps be audited?
|
||||
|
|
||||
*/
|
||||
|
||||
'timestamps' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit Threshold
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify a threshold for the amount of Audit records a model can have.
|
||||
| Zero means no limit.
|
||||
|
|
||||
*/
|
||||
|
||||
'threshold' => 0,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default audit driver used to keep track of changes.
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => 'database',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit Driver Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Available audit drivers and respective configurations.
|
||||
|
|
||||
*/
|
||||
|
||||
'drivers' => [
|
||||
'database' => [
|
||||
'table' => 'audits',
|
||||
'connection' => null,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit Queue Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Available audit queue configurations.
|
||||
|
|
||||
*/
|
||||
|
||||
'queue' => [
|
||||
'enable' => false,
|
||||
'connection' => 'sync',
|
||||
'queue' => 'default',
|
||||
'delay' => 0,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit Console
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Whether console events should be audited (eg. php artisan db:seed).
|
||||
|
|
||||
*/
|
||||
|
||||
'console' => true,
|
||||
];
|
||||
71
database/factories/AuditFactory.php
Normal file
71
database/factories/AuditFactory.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Audit;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* @extends Factory<Audit>
|
||||
*/
|
||||
class AuditFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$morphPrefix = Config::get('audit.user.morph_prefix', 'user');
|
||||
|
||||
return [
|
||||
$morphPrefix.'_id' => function () {
|
||||
return User::factory()->create()->id;
|
||||
},
|
||||
$morphPrefix.'_type' => function () {
|
||||
return (new User())->getMorphClass();
|
||||
},
|
||||
'event' => 'updated',
|
||||
'auditable_id' => function () {
|
||||
return User::factory()->create()->getKey();
|
||||
},
|
||||
'auditable_type' => function () {
|
||||
return (new User())->getMorphClass();
|
||||
},
|
||||
'old_values' => [],
|
||||
'new_values' => [],
|
||||
'url' => $this->faker->url,
|
||||
'ip_address' => $this->faker->ipv4,
|
||||
'user_agent' => $this->faker->userAgent,
|
||||
'tags' => implode(',', $this->faker->words(4)),
|
||||
];
|
||||
}
|
||||
|
||||
public function auditUser(User $user): self
|
||||
{
|
||||
return $this->state(function (array $attributes) use ($user) {
|
||||
$morphPrefix = Config::get('audit.user.morph_prefix', 'user');
|
||||
|
||||
return [
|
||||
$morphPrefix.'_id' => $user->getKey(),
|
||||
$morphPrefix.'_type' => $user->getMorphClass(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function auditFor(Model $model): self
|
||||
{
|
||||
return $this->state(function (array $attributes) use ($model) {
|
||||
return [
|
||||
'auditable_id' => $model->getKey(),
|
||||
'auditable_type' => $model->getMorphClass(),
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateAuditsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$connection = config('audit.drivers.database.connection', config('database.default'));
|
||||
$table = config('audit.drivers.database.table', 'audits');
|
||||
|
||||
Schema::connection($connection)->create($table, function (Blueprint $table) {
|
||||
|
||||
$morphPrefix = config('audit.user.morph_prefix', 'user');
|
||||
|
||||
$table->bigIncrements('id');
|
||||
$table->string($morphPrefix.'_type')->nullable();
|
||||
$table->uuid($morphPrefix.'_id')->nullable();
|
||||
$table->string('event');
|
||||
$table->uuidMorphs('auditable');
|
||||
$table->json('old_values')->nullable();
|
||||
$table->json('new_values')->nullable();
|
||||
$table->text('url')->nullable();
|
||||
$table->ipAddress('ip_address')->nullable();
|
||||
$table->string('user_agent', 1023)->nullable();
|
||||
$table->string('tags')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index([$morphPrefix.'_id', $morphPrefix.'_type']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$connection = config('audit.drivers.database.connection', config('database.default'));
|
||||
$table = config('audit.drivers.database.table', 'audits');
|
||||
|
||||
Schema::connection($connection)->drop($table);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\Audit;
|
||||
use App\Models\Client;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
@@ -153,6 +154,7 @@ class DatabaseSeeder extends Seeder
|
||||
|
||||
private function deleteAll(): void
|
||||
{
|
||||
DB::table((new Audit())->getTable())->delete();
|
||||
DB::table((new TimeEntry())->getTable())->delete();
|
||||
DB::table((new Task())->getTable())->delete();
|
||||
DB::table((new Tag())->getTable())->delete();
|
||||
|
||||
58
tests/Unit/Filament/AuditResourceTest.php
Normal file
58
tests/Unit/Filament/AuditResourceTest.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Filament;
|
||||
|
||||
use App\Filament\Resources\AuditResource;
|
||||
use App\Models\Audit;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Livewire;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[UsesClass(AuditResource::class)]
|
||||
class AuditResourceTest extends FilamentTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Config::set('auth.super_admins', ['admin@example.com']);
|
||||
$user = User::factory()->withPersonalOrganization()->create([
|
||||
'email' => 'admin@example.com',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
}
|
||||
|
||||
public function test_can_list_audits(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$timeEntry = TimeEntry::factory()->forMember($user->member)->create();
|
||||
DB::table((new Audit())->getTable())->delete();
|
||||
$audits = Audit::factory()->auditFor($timeEntry)->auditUser($user->user)->createMany(5);
|
||||
|
||||
// Act
|
||||
$response = Livewire::test(AuditResource\Pages\ListAudits::class);
|
||||
|
||||
// Assert
|
||||
$response->assertSuccessful();
|
||||
$response->assertCanSeeTableRecords($audits);
|
||||
}
|
||||
|
||||
public function test_can_see_view_page_of_audit(): void
|
||||
{
|
||||
// Arrange
|
||||
DB::table((new Audit())->getTable())->delete();
|
||||
$audit = Audit::factory()->create();
|
||||
|
||||
// Act
|
||||
$response = Livewire::test(AuditResource\Pages\ViewAudit::class, ['record' => $audit->getKey()]);
|
||||
|
||||
// Assert
|
||||
$response->assertSuccessful();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user