b05e074456
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local> Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local> Reviewed-on: #11
344 lines
9.7 KiB
PHP
344 lines
9.7 KiB
PHP
<?php
|
|
|
|
namespace Modules\Auth\Entities;
|
|
|
|
use Illuminate\Auth\MustVerifyEmail as MustVerifyEmailTrait;
|
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
|
use Illuminate\Notifications\Notifiable;
|
|
use Illuminate\Support\Str;
|
|
use Laravel\Sanctum\HasApiTokens;
|
|
use Spatie\Activitylog\LogOptions;
|
|
use Spatie\Activitylog\Traits\LogsActivity;
|
|
use Spatie\Permission\Contracts\Permission;
|
|
use Spatie\Permission\PermissionRegistrar;
|
|
use Spatie\Permission\Traits\HasPermissions;
|
|
use Spatie\Permission\Traits\HasRoles;
|
|
use App\Services\ActiveRoleService;
|
|
use Lab404\Impersonate\Models\Impersonate;
|
|
use Modules\Role\Entities\Role;
|
|
use Modules\User\Entities\Address;
|
|
use Modules\User\Entities\BankDetail;
|
|
use Modules\User\Entities\Employment;
|
|
use Modules\Auth\Notifications\VerifyEmailNotification;
|
|
use Modules\User\Entities\Heir;
|
|
|
|
class User extends Authenticatable implements MustVerifyEmail
|
|
{
|
|
/** @use HasFactory<\Modules\Auth\Database\Factories\UserFactory> */
|
|
use HasApiTokens, HasFactory, HasPermissions, HasRoles, HasUuids, Impersonate, LogsActivity, MustVerifyEmailTrait, Notifiable, SoftDeletes;
|
|
|
|
protected $table = 'users';
|
|
|
|
/**
|
|
* The attributes that are mass assignable.
|
|
*
|
|
* @var list<string>
|
|
*/
|
|
protected $fillable = [
|
|
'name',
|
|
'email',
|
|
'password',
|
|
'ic_number',
|
|
'position',
|
|
'phone_number',
|
|
'phone_verified_at',
|
|
'image_url',
|
|
'status',
|
|
'two_factor_secret',
|
|
'two_factor_recovery_codes',
|
|
'two_factor_confirmed_at',
|
|
'gender',
|
|
'marriage_status',
|
|
'member_number',
|
|
'member_type',
|
|
'public_profile_token',
|
|
'join_date',
|
|
'leave_date',
|
|
'birth_date',
|
|
'birth_place',
|
|
'onboarding_completed_at',
|
|
];
|
|
|
|
/**
|
|
* The attributes that should be hidden for serialization.
|
|
*
|
|
* @var list<string>
|
|
*/
|
|
protected $hidden = [
|
|
'password',
|
|
'remember_token',
|
|
'two_factor_secret',
|
|
'two_factor_recovery_codes',
|
|
'two_factor_confirmed_at',
|
|
'public_profile_token',
|
|
];
|
|
|
|
protected static function booted(): void
|
|
{
|
|
static::creating(function (User $user) {
|
|
$expiryDays = self::publicProfileTokenTtlDays();
|
|
|
|
if (empty($user->public_profile_token)) {
|
|
$user->public_profile_token = Str::random(48);
|
|
}
|
|
|
|
if (empty($user->public_profile_token_expires_at)) {
|
|
$user->public_profile_token_expires_at = now()->addDays($expiryDays);
|
|
}
|
|
});
|
|
}
|
|
|
|
public static function publicProfileTokenTtlDays(): int
|
|
{
|
|
return (int) config('user.public_profile_token_ttl_days', 90);
|
|
}
|
|
|
|
public function isPublicProfileTokenExpired(): bool
|
|
{
|
|
return $this->public_profile_token_expires_at !== null
|
|
&& $this->public_profile_token_expires_at->isPast();
|
|
}
|
|
|
|
public function ensurePublicProfileToken(): string
|
|
{
|
|
$expiryDays = self::publicProfileTokenTtlDays();
|
|
|
|
if (blank($this->public_profile_token) || $this->isPublicProfileTokenExpired()) {
|
|
$this->forceFill([
|
|
'public_profile_token' => Str::random(48),
|
|
'public_profile_token_expires_at' => now()->addDays($expiryDays),
|
|
])->save();
|
|
|
|
return $this->public_profile_token;
|
|
}
|
|
|
|
$this->forceFill([
|
|
'public_profile_token_expires_at' => now()->addDays($expiryDays),
|
|
])->save();
|
|
|
|
return $this->public_profile_token;
|
|
}
|
|
|
|
/**
|
|
* Get the attributes that should be cast.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'email_verified_at' => 'datetime',
|
|
'phone_verified_at' => 'datetime',
|
|
'password' => 'hashed',
|
|
'status' => 'string',
|
|
'gender' => 'string',
|
|
'marriage_status' => 'string',
|
|
'member_number' => 'integer',
|
|
'member_type' => 'string',
|
|
'public_profile_token_expires_at' => 'datetime',
|
|
'join_date' => 'date',
|
|
'leave_date' => 'date',
|
|
'birth_date' => 'date',
|
|
'birth_place' => 'string',
|
|
'onboarding_completed_at' => 'datetime',
|
|
];
|
|
}
|
|
|
|
|
|
/**
|
|
* Role used for permission checks in the current session (Sanctum token).
|
|
*/
|
|
public function activeRole(): ?Role
|
|
{
|
|
return ActiveRoleService::getActiveRole($this);
|
|
}
|
|
|
|
public function addresses()
|
|
{
|
|
return $this->hasMany(Address::class);
|
|
}
|
|
|
|
public function employments()
|
|
{
|
|
return $this->hasMany(Employment::class);
|
|
}
|
|
|
|
public function bankDetails()
|
|
{
|
|
return $this->hasMany(BankDetail::class);
|
|
}
|
|
|
|
public function heirs()
|
|
{
|
|
return $this->hasMany(Heir::class);
|
|
}
|
|
|
|
public function hasPermissionAction($action): bool
|
|
{
|
|
$role = $this->activeRole();
|
|
|
|
return $role && $role->permission_actions &&
|
|
in_array($action, $role->permission_actions, true);
|
|
}
|
|
|
|
public function getActivityLogOptions(): LogOptions
|
|
{
|
|
return LogOptions::defaults()
|
|
->logAll()
|
|
->logOnlyDirty()
|
|
->dontSubmitEmptyLogs()
|
|
->setDescriptionForEvent(fn (string $eventName) => "User {$this->name} was {$eventName}");
|
|
}
|
|
|
|
/**
|
|
* Override hasPermissionTo to bypass all permission checks for DEVELOPER role
|
|
*/
|
|
public function hasPermissionTo($permission, $guardName = null): bool
|
|
{
|
|
$activeRole = $this->activeRole();
|
|
|
|
if ($activeRole?->name === 'DEVELOPER') {
|
|
return true;
|
|
}
|
|
|
|
static $isCheckingPermission = false;
|
|
|
|
if ($isCheckingPermission) {
|
|
return false;
|
|
}
|
|
|
|
$isCheckingPermission = true;
|
|
|
|
try {
|
|
$permissionClass = app(PermissionRegistrar::class)->getPermissionClass();
|
|
|
|
if (is_string($permission)) {
|
|
$permission = $permissionClass::findByName($permission, $guardName ?? 'api');
|
|
}
|
|
|
|
if (is_int($permission)) {
|
|
$permission = $permissionClass::findById($permission, $guardName ?? 'api');
|
|
}
|
|
|
|
if (! $permission instanceof Permission) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->permissions->contains('id', $permission->id)) {
|
|
return true;
|
|
}
|
|
|
|
if (! $activeRole) {
|
|
return false;
|
|
}
|
|
|
|
$activeRole->loadMissing('permissions');
|
|
|
|
return $activeRole->permissions->contains('id', $permission->id);
|
|
} catch (\Exception $e) {
|
|
return false;
|
|
} finally {
|
|
$isCheckingPermission = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scope to exclude current user unless they have developer role
|
|
*/
|
|
public function scopeExcludeCurrentUserUnlessDeveloper($query, $currentUser = null)
|
|
{
|
|
$currentUser = $currentUser ?? auth()->user();
|
|
|
|
if ($currentUser && ! $currentUser->hasRole('DEVELOPER')) {
|
|
return $query->where('id', '!=', $currentUser->id);
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Scope to exclude users with DEVELOPER role unless current user is DEVELOPER
|
|
*/
|
|
public function scopeExcludeDevelopersUnlessDeveloper($query, $currentUser = null)
|
|
{
|
|
$currentUser = $currentUser ?? auth()->user();
|
|
|
|
if ($currentUser && ! $currentUser->hasRole('DEVELOPER')) {
|
|
return $query->whereDoesntHave('roles', function ($q) {
|
|
$q->where('name', 'DEVELOPER');
|
|
});
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Check if the user can impersonate another user
|
|
*/
|
|
public function canImpersonate(): bool
|
|
{
|
|
// Check permission instead of hardcoded roles
|
|
return $this->hasPermissionTo('menyamar pengguna');
|
|
}
|
|
|
|
/**
|
|
* Check if the user can be impersonated
|
|
*/
|
|
public function canBeImpersonated(): bool
|
|
{
|
|
// DEVELOPER cannot be impersonated by anyone
|
|
if ($this->hasRole('DEVELOPER')) {
|
|
return false;
|
|
}
|
|
|
|
// Check if the current user has permission to impersonate
|
|
$currentUser = auth()->user();
|
|
if (!$currentUser) {
|
|
return false;
|
|
}
|
|
|
|
// Users with permission can impersonate other users (except DEVELOPER)
|
|
return $currentUser->hasPermissionTo('menyamar pengguna');
|
|
}
|
|
|
|
public function sendEmailVerificationNotification(): void
|
|
{
|
|
$this->notify(new VerifyEmailNotification);
|
|
}
|
|
|
|
/**
|
|
* Check if user can login based on their status
|
|
*/
|
|
public function canLogin(): bool
|
|
{
|
|
return $this->status === 'active';
|
|
}
|
|
|
|
/**
|
|
* Whether credentials are valid for issuing a session (includes pending users awaiting admin activation).
|
|
*/
|
|
public function canAuthenticate(): bool
|
|
{
|
|
return in_array($this->status, ['active', 'pending'], true);
|
|
}
|
|
|
|
/**
|
|
* Get the login restriction message based on user status
|
|
*/
|
|
public function getLoginRestrictionMessage(): ?string
|
|
{
|
|
switch ($this->status) {
|
|
case 'pending':
|
|
return 'Akaun anda sedang menunggu pengaktifan dari pentadbir sistem. Sila hubungi pentadbir sistem.';
|
|
case 'inactive':
|
|
return 'Akaun anda tidak aktif. Sila hubungi pentadbir sistem.';
|
|
default:
|
|
return 'Akaun anda tidak dapat mengakses sistem. Sila hubungi pentadbir sistem.';
|
|
}
|
|
}
|
|
}
|