feat: add app and database modules

This commit is contained in:
2026-05-21 16:05:11 +07:00
parent 37b7e783f5
commit fad70d096b
212 changed files with 23901 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
<?php
/**
* ============================================================
*
* @project biiproject
*
* @author Andika Debi Putra
*
* @email andikadebiputra@gmail.com
*
* @website https://biiproject.com
*
* @copyright Copyright (c) 2026 Andika Debi Putra
* @license Proprietary - All Rights Reserved
*
* @version 1.0.0
*
* @created 2026-05-01
* ============================================================
*/
namespace App\Models\AI;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class AiUsageLog extends Model
{
use Prunable;
protected $fillable = [
'user_id',
'provider',
'model',
'prompt',
'response',
'prompt_tokens',
'completion_tokens',
'total_tokens',
'estimated_cost',
'status',
'error_message',
'metadata',
];
protected $casts = [
'metadata' => 'array',
'estimated_cost' => 'decimal:6',
];
/**
* Get the prunable model query.
*/
public function prunable()
{
return static::where('created_at', '<=', now()->subMonths(3));
}
public function user()
{
return $this->belongsTo(User::class);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class AiHealingLog extends Model
{
use \Illuminate\Database\Eloquent\Factories\HasFactory, Prunable;
protected $fillable = [
'error_type',
'error_message',
'stack_trace',
'ai_diagnosis',
'original_code',
'fixed_code',
'action_taken',
'status',
];
protected $casts = [
'stack_trace' => 'encrypted',
'original_code' => 'encrypted',
'fixed_code' => 'encrypted',
];
public function prunable(): Builder
{
return self::query()->where('created_at', '<=', now()->subDays(90));
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DashboardWidgetPreference extends Model
{
protected $fillable = ['user_id', 'widget_key', 'visible', 'sort_order'];
protected $casts = ['visible' => 'boolean'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Return merged widget config: defaults overlaid with user prefs.
*
* @return array<string, array{label: string, visible: bool, sort_order: int, permission: string|null}>
*/
public static function forUser(int $userId): array
{
$defaults = self::defaults();
$prefs = self::where('user_id', $userId)
->get()
->keyBy('widget_key');
foreach ($defaults as $key => &$widget) {
if ($prefs->has($key)) {
$widget['visible'] = $prefs[$key]->visible;
$widget['sort_order'] = $prefs[$key]->sort_order;
}
}
uasort($defaults, fn ($a, $b) => $a['sort_order'] <=> $b['sort_order']);
return $defaults;
}
/**
* All available widgets with their defaults.
*/
public static function defaults(): array
{
return [
'cpu' => ['label' => 'CPU Load', 'visible' => true, 'sort_order' => 1, 'permission' => null],
'ram' => ['label' => 'Memory', 'visible' => true, 'sort_order' => 2, 'permission' => null],
'disk' => ['label' => 'Storage', 'visible' => true, 'sort_order' => 3, 'permission' => null],
'live_users' => ['label' => 'Live Users', 'visible' => true, 'sort_order' => 4, 'permission' => null],
'queues' => ['label' => 'Queue Stats', 'visible' => true, 'sort_order' => 5, 'permission' => null],
'activity_feed' => ['label' => 'Activity Feed', 'visible' => true, 'sort_order' => 6, 'permission' => 'view health and logs'],
'ai_insight' => ['label' => 'AI Security Insight','visible' => true, 'sort_order' => 7, 'permission' => 'view ai log analysis'],
];
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DeviceToken extends Model
{
protected $fillable = [
'user_id',
'token',
'platform',
'device_name',
'app_version',
'last_used_at',
];
protected $casts = [
'last_used_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* ============================================================
*
* @project biiproject
*
* @author Andika Debi Putra
*
* @email andikadebiputra@gmail.com
*
* @website https://biiproject.com
*
* @copyright Copyright (c) 2026 Andika Debi Putra
* @license Proprietary - All Rights Reserved
*
* @version 1.0.0
*
* @created 2026-05-01
* ============================================================
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class MobileErrorLog extends Model
{
use Prunable;
public $timestamps = false;
/**
* Get the prunable model query.
*/
public function prunable(): Builder
{
return self::query()->where('occurred_at', '<=', now()->subDays(90));
}
protected $fillable = [
'user_id',
'error_type',
'message',
'stack_trace',
'platform',
'app_version',
'device_info',
'occurred_at',
];
protected $casts = [
'device_info' => 'array',
'occurred_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* ============================================================
*
* @project biiproject
*
* @author Andika Debi Putra
*
* @email andikadebiputra@gmail.com
*
* @website https://biiproject.com
*
* @copyright Copyright (c) 2026 Andika Debi Putra
* @license Proprietary - All Rights Reserved
*
* @version 1.0.0
*
* @created 2026-05-01
* ============================================================
*
* Unauthorized copying, modification, distribution, or use
* of this file is strictly prohibited without prior written
* permission from the author.
* ============================================================
*/
namespace App\Models;
use App\Services\MobileConfig\MobileConfigService;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
class MobileSetting extends Model
{
use LogsActivity;
protected $fillable = [
'key',
'value',
'group',
'type',
];
/**
* Spatie Activitylog configuration
*/
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->useLogName('mobile-config')
->logOnly(['key', 'value', 'group', 'type'])
->logOnlyDirty()
->dontSubmitEmptyLogs();
}
protected static function booted()
{
static::saved(fn () => MobileConfigService::clearCacheStatic());
static::deleted(fn () => MobileConfigService::clearCacheStatic());
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class MobileSyncLog extends Model
{
use Prunable;
public $timestamps = false;
/**
* Get the prunable model query.
*/
public function prunable(): Builder
{
return self::query()->where('synced_at', '<=', now()->subDays(30));
}
protected $fillable = [
'user_id',
'platform',
'device_model',
'app_version',
'ip_address',
'synced_at',
];
protected $casts = [
'synced_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
use Illuminate\Database\Eloquent\SoftDeletes;
class Notification extends Model
{
use Prunable, SoftDeletes;
protected $table = 'system_notifications';
protected $fillable = [
'title',
'message',
'recipient',
'type',
'read_at',
'created_by',
];
protected $casts = [
'read_at' => 'datetime',
];
/**
* Get the user who created the notification.
*/
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* Users who have interacted with this notification (read/deleted).
*/
public function users()
{
return $this->belongsToMany(User::class, 'notification_user')
->withPivot('read_at', 'deleted_at')
->withTimestamps();
}
/**
* Get the prunable model query.
* Auto-delete notifications older than 30 days.
*/
public function prunable(): Builder
{
return self::query()->where('created_at', '<=', now()->subDays(30));
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class OtpCode extends Model
{
use Prunable;
protected $fillable = [
'identifier',
'code',
'expires_at',
'verified_at',
];
protected $casts = [
'expires_at' => 'datetime',
'verified_at' => 'datetime',
];
public function prunable(): Builder
{
return self::query()->where('expires_at', '<', now());
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class PasswordHistory extends Model
{
use Prunable;
protected $fillable = ['user_id', 'password'];
public function prunable(): Builder
{
return self::query()->where('created_at', '<=', now()->subDays(365));
}
public function user()
{
return $this->belongsTo(User::class);
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Auth;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Permission\Models\Permission as SpatiePermission;
class Permission extends SpatiePermission
{
use HasFactory, LogsActivity, SoftDeletes;
/**
* Activity log configuration
*/
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->useLogName('permission-management')
->logOnly(['name', 'guard_name', 'is_active'])
->logOnlyDirty()
->dontSubmitEmptyLogs();
}
/**
* Fillable attributes
*/
protected $fillable = [
'name',
'scope',
'guard_name',
'is_active',
'created_by',
'updated_by',
];
/**
* Casting
*/
protected $casts = [
'is_active' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
/**
* Boot model to automatically set created_by & updated_by
*/
protected static function boot()
{
parent::boot();
}
/**
* Audit trail relations
*/
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}
@@ -0,0 +1,85 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Auth;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Permission\Models\Permission as SpatiePermission;
class Permission extends SpatiePermission
{
use HasFactory, LogsActivity, SoftDeletes;
/**
* Activity log configuration
*/
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->useLogName('permission-management')
->logOnly(['name', 'guard_name', 'is_active'])
->logOnlyDirty()
->dontSubmitEmptyLogs();
}
/**
* Ketika permission diupdate
*/
public function updated(\App\Models\Permission $permission)
{
\Illuminate\Support\Facades\Cache::forget("permission_status:{$permission->name}");
}
/**
* Ketika permission didelete (termasuk soft delete)
*/
public function deleted(\App\Models\Permission $permission)
{
\Illuminate\Support\Facades\Cache::forget("permission_status:{$permission->name}");
}
/**
* Fillable attributes
*/
protected $fillable = [
'name',
'guard_name',
'is_active',
'created_by',
'updated_by',
];
/**
* Casting
*/
protected $casts = [
'is_active' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
/**
* Boot model to automatically set created_by & updated_by
*/
protected static function boot()
{
parent::boot();
}
/**
* Audit trail relations
*/
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Auth;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Permission\Models\Role as SpatieRole;
class Role extends SpatieRole
{
use HasFactory, LogsActivity, SoftDeletes;
/**
* Activity log configuration for Role
*/
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->useLogName('role-management') // module log category
->logOnly(['name', 'guard_name', 'is_active']) // fields to monitor
->logOnlyDirty() // only log if changed
->dontSubmitEmptyLogs(); // skip if no changes
}
protected $fillable = [
'name',
'guard_name',
'is_active',
'created_by',
'updated_by',
];
protected $casts = [
'is_active' => 'boolean',
];
/**
* Boot model to automatically set created_by & updated_by
*/
protected static function boot()
{
parent::boot();
// Automatically set created_by
static::creating(function ($model) {
if (Auth::check()) {
$model->created_by = Auth::id();
}
});
// Automatically set updated_by (including soft deletes)
static::updating(function ($model) {
if (Auth::check()) {
$model->updated_by = Auth::id();
}
});
static::deleting(function ($model) {
if (Auth::check()) {
$model->updated_by = Auth::id();
$model->saveQuietly(); // save without triggering infinite update log
}
});
}
/**
* Audit Trail Relationships
*/
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* ============================================================
*
* @project biiproject
*
* @author Andika Debi Putra
*
* @email andikadebiputra@gmail.com
*
* @website https://biiproject.com
*
* @copyright Copyright (c) 2026 Andika Debi Putra
* @license Proprietary - All Rights Reserved
*
* @version 1.0.0
*
* @created 2026-05-01
* ============================================================
*
* Unauthorized copying, modification, distribution, or use
* of this file is strictly prohibited without prior written
* permission from the author.
* ============================================================
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
class SystemSetting extends Model
{
use LogsActivity;
/**
* Spatie Activitylog configuration
*/
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->useLogName('system-config')
->logOnly(['key', 'value', 'group'])
->logOnlyDirty()
->dontSubmitEmptyLogs();
}
protected $fillable = [
'key',
'value',
'type',
'group',
'is_public',
'description',
'created_by',
'updated_by',
];
protected $casts = [
'is_public' => 'boolean',
];
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SystemSettingRevision extends Model
{
public $timestamps = false;
protected $fillable = [
'system_setting_id',
'key',
'old_value',
'new_value',
'changed_by',
'changed_ip',
'changed_agent',
'created_at',
];
}
+27
View File
@@ -0,0 +1,27 @@
<?php
// MOBILE APPS
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Transaction extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'title',
'amount',
'type',
'category',
'icon',
];
public function user()
{
return $this->belongsTo(User::class);
}
}
+214
View File
@@ -0,0 +1,214 @@
<?php
/**
* ============================================================
*
* @project biiproject
*
* @author Andika Debi Putra
*
* @email andikadebiputra@gmail.com
*
* @website https://biiproject.com
*
* @copyright Copyright (c) 2026 Andika Debi Putra
* @license Proprietary - All Rights Reserved
*
* @version 1.0.0
*
* @created 2026-05-01
* ============================================================
*
* Unauthorized copying, modification, distribution, or use
* of this file is strictly prohibited without prior written
* permission from the author.
* ============================================================
*/
namespace App\Models;
use App\Notifications\Auth\ResetPasswordNotification;
use App\Notifications\Auth\VerifyEmailNotification;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable as WebAuthnContract;
use Laragear\WebAuthn\WebAuthnAuthentication;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable implements HasMedia, WebAuthnContract
{
use HasApiTokens, HasFactory, HasRoles, InteractsWithMedia, LogsActivity, Notifiable, SoftDeletes, WebAuthnAuthentication;
protected string $guard_name = 'web';
/**
* Spatie Activitylog configuration
*/
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->useLogName('user-management')
->logOnly(['name', 'username', 'email', 'phone_number', 'is_active'])
->logOnlyDirty()
->dontSubmitEmptyLogs();
}
/**
* The attributes that are mass assignable.
*/
protected $fillable = [
'name',
'username',
'email',
'phone_number',
'password',
'google_id',
'facebook_id',
'github_id',
'is_active',
'password_changed_at',
'last_session_id',
'created_by',
'updated_by',
];
/**
* Hidden fields
*/
protected $hidden = [
'password',
'remember_token',
'media', // Hide raw media relation
];
/**
* Appended attributes
*/
protected $appends = ['avatar'];
/**
* Avatar Accessor (using Media Library)
*/
public function getAvatarAttribute(): ?string
{
return $this->getFirstMediaUrl('avatar') ?: null;
}
/**
* The attributes that should be cast.
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_active' => 'boolean',
'password_changed_at' => 'datetime',
];
/**
* Password History Relation
*/
public function passwordHistories()
{
return $this->hasMany(PasswordHistory::class);
}
/**
* Trusted Devices Relation
*/
public function trustedDevices()
{
return $this->hasMany(UserTrustedDevice::class);
}
/**
* ==========================================
* DISABLE RESET PASSWORD FOR SOCIAL USERS
* ==========================================
*/
public function sendPasswordResetNotification($token)
{
// ❌ OAuth user → BLOCK reset password
if ($this->google_id || $this->facebook_id || $this->github_id) {
return;
}
// ✅ User manual → normal
$this->notify(new ResetPasswordNotification($token));
}
/**
* Use our branded email verification template.
*/
public function sendEmailVerificationNotification()
{
$this->notify(new VerifyEmailNotification);
}
/**
* Helpers
*/
public function isSocialUser(): bool
{
return ! is_null($this->google_id) || ! is_null($this->facebook_id) || ! is_null($this->github_id);
}
/**
* Consents Relation
*/
public function consents()
{
return $this->hasMany(UserConsent::class);
}
/**
* Check if user has agreed to the current version of a document.
*/
public function hasAgreedToCurrentLegal(string $type): bool
{
// Settings use 'pdp' prefix for Privacy Policy, while code uses 'privacy' type
$keyPrefix = ($type === 'privacy' || $type === 'pdp') ? 'pdp' : $type;
$currentVersion = (int) get_setting("{$keyPrefix}_document_version", 1);
return $this->consents()
->where(function ($q) use ($type) {
$q->where('consent_type', $type)
->orWhere('consent_type', 'privacy')
->orWhere('consent_type', 'pdp');
})
->where('version_id', '>=', $currentVersion)
->exists();
}
/**
* Notifications this user has interacted with (including personal read/deleted status).
*/
public function broadcastNotifications()
{
return $this->belongsToMany(Notification::class, 'system_notification_user')
->withPivot('read_at', 'deleted_at')
->withTimestamps();
}
/**
* Get the user who created this record.
*/
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* Get the user who last updated this record.
*/
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserConsent extends Model
{
use HasFactory;
/**
* Disable standard timestamps since we only use created_at.
*/
public $timestamps = false;
/**
* The attributes that are mass assignable.
*/
protected $fillable = [
'user_id',
'consent_type',
'version_id',
'ip_address',
'user_agent',
];
/**
* Relationship: User
*/
public function user()
{
return $this->belongsTo(User::class);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class UserTrustedDevice extends Model
{
use Prunable;
protected $fillable = ['user_id', 'device_id', 'token', 'expires_at'];
protected $casts = [
'expires_at' => 'datetime',
];
public function prunable(): Builder
{
return self::query()->where('expires_at', '<', now());
}
public function user()
{
return $this->belongsTo(User::class);
}
}