1. 主帳號不變量:storeAccount 的 is_admin 改補位(每家公司恰一主帳號)、destroyAccount 禁刪最後主帳號、roles 新增 is_company_admin 旗標保護公司管理員角色不被刪除 2. 帳號樹狀層級:users 新增 parent_id/level(主帳號 level 0、子帳號最多兩層),新增 visibleTo/canManageAccount,帳號清單與管理端點(accounts/update/destroy/toggle、機台授權選帳號、補貨指派)一律套層級可見範圍與越權防護 3. 角色指派子集:storeAccount/updateAccount 指派角色與帳號 Modal 下拉一律限制為「權限為操作者子集」,杜絕權限提升 4. 機台授權子集:MachinePermissionController 僅允許在操作者本身可授權機台範圍內增刪,保留範圍外既有授權,並修正 machine_access 全域 scope 對讀寫的干擾 5. 角色層級隔離:roles 新增 created_by,子帳號只看/只管/只能指派自己建立的角色(主帳號看全公司),edit/update/destroy 加 canBeManagedBy 守衛 6. 補強:MachineSetting 機台權限人員清單補租戶隔離、修正角色下拉 N+1、三語系新增提示字串 7. 測試:新增 AccountHierarchyTest/AccountSubsetGuardTest/RoleHierarchyTest,Admin 套件 55 測試 0 回歸 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
198 lines
4.8 KiB
PHP
198 lines
4.8 KiB
PHP
<?php
|
||
|
||
namespace App\Models\System;
|
||
|
||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||
use Illuminate\Notifications\Notifiable;
|
||
use Laravel\Sanctum\HasApiTokens;
|
||
|
||
use App\Traits\TenantScoped;
|
||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||
use Spatie\Permission\Traits\HasRoles;
|
||
|
||
class User extends Authenticatable
|
||
{
|
||
use HasApiTokens, HasFactory, Notifiable, HasRoles, TenantScoped, SoftDeletes;
|
||
|
||
/**
|
||
* The attributes that are mass assignable.
|
||
*
|
||
* @var array<int, string>
|
||
*/
|
||
protected $fillable = [
|
||
'company_id',
|
||
'parent_id',
|
||
'level',
|
||
'username',
|
||
'name',
|
||
'email',
|
||
'password',
|
||
'phone',
|
||
'avatar',
|
||
'role',
|
||
'status',
|
||
'is_admin',
|
||
];
|
||
|
||
/**
|
||
* The attributes that should be hidden for serialization.
|
||
*
|
||
* @var array<int, string>
|
||
*/
|
||
protected $hidden = [
|
||
'password',
|
||
'remember_token',
|
||
];
|
||
|
||
/**
|
||
* The attributes that should be cast.
|
||
*
|
||
* @var array<string, string>
|
||
*/
|
||
protected $casts = [
|
||
'email_verified_at' => 'datetime',
|
||
'password' => 'hashed',
|
||
'is_admin' => 'boolean',
|
||
'level' => 'integer',
|
||
];
|
||
|
||
/**
|
||
* 帳號層級上限:子帳號最多兩層(主帳號 level 0 → 子 level 1 → 孫 level 2,level 2 不可再建)。
|
||
*/
|
||
public const MAX_LEVEL = 2;
|
||
|
||
/**
|
||
* Get the login logs for the user.
|
||
*/
|
||
public function loginLogs()
|
||
{
|
||
return $this->hasMany(UserLoginLog::class);
|
||
}
|
||
|
||
/**
|
||
* Get the company that owns the user.
|
||
*/
|
||
public function company()
|
||
{
|
||
return $this->belongsTo(Company::class);
|
||
}
|
||
|
||
/**
|
||
* Get the machines assigned to the user.
|
||
*/
|
||
public function machines()
|
||
{
|
||
return $this->belongsToMany(\App\Models\Machine\Machine::class);
|
||
}
|
||
|
||
/**
|
||
* Check if the user is a system administrator.
|
||
*/
|
||
public function isSystemAdmin(): bool
|
||
{
|
||
return is_null($this->company_id);
|
||
}
|
||
|
||
/**
|
||
* Check if the user belongs to a tenant.
|
||
*/
|
||
public function isTenant(): bool
|
||
{
|
||
return !is_null($this->company_id);
|
||
}
|
||
|
||
/**
|
||
* 上層(建立者)帳號。
|
||
*/
|
||
public function parent()
|
||
{
|
||
return $this->belongsTo(self::class, 'parent_id');
|
||
}
|
||
|
||
/**
|
||
* 直接建立的下層帳號。
|
||
*/
|
||
public function children()
|
||
{
|
||
return $this->hasMany(self::class, 'parent_id');
|
||
}
|
||
|
||
/**
|
||
* 是否為該公司主帳號(租戶層級且 is_admin)。
|
||
*/
|
||
public function isMainAccount(): bool
|
||
{
|
||
return $this->isTenant() && $this->is_admin;
|
||
}
|
||
|
||
/**
|
||
* 是否為子帳號(租戶層級且非主帳號)。
|
||
*/
|
||
public function isSubAccount(): bool
|
||
{
|
||
return $this->isTenant() && !$this->is_admin;
|
||
}
|
||
|
||
/**
|
||
* 是否還能再建立下層子帳號(深度上限約束,系統管理員不受限)。
|
||
*/
|
||
public function canCreateSubAccount(): bool
|
||
{
|
||
return $this->isSystemAdmin() || $this->level < self::MAX_LEVEL;
|
||
}
|
||
|
||
/**
|
||
* 查詢範圍:限制為指定使用者「可見」的帳號。
|
||
* - 系統管理員:全部
|
||
* - 主帳號:同公司全部(安全網)
|
||
* - 子帳號:僅自己直接建立的下層帳號
|
||
*/
|
||
public function scopeVisibleTo($query, self $viewer)
|
||
{
|
||
if ($viewer->isSystemAdmin()) {
|
||
return $query;
|
||
}
|
||
$query->where($this->getTable() . '.company_id', $viewer->company_id);
|
||
if (!$viewer->is_admin) {
|
||
$query->where($this->getTable() . '.parent_id', $viewer->id);
|
||
}
|
||
return $query;
|
||
}
|
||
|
||
/**
|
||
* 操作者是否有權管理(編輯/刪除/停用)指定帳號。
|
||
* - 系統管理員:全部
|
||
* - 跨公司:禁止
|
||
* - 主帳號:同公司全部
|
||
* - 子帳號:僅自己直接建立的下層帳號
|
||
*/
|
||
public function canManageAccount(self $target): bool
|
||
{
|
||
if ($this->isSystemAdmin()) {
|
||
return true;
|
||
}
|
||
if ($target->company_id !== $this->company_id) {
|
||
return false;
|
||
}
|
||
if ($this->is_admin) {
|
||
return true;
|
||
}
|
||
return $target->parent_id === $this->id;
|
||
}
|
||
|
||
/**
|
||
* Get the URL for the user's avatar.
|
||
*/
|
||
public function getAvatarUrlAttribute(): string
|
||
{
|
||
if ($this->avatar) {
|
||
return \Illuminate\Support\Facades\Storage::disk('public')->url($this->avatar);
|
||
}
|
||
|
||
// Return a default UI Avatar if no avatar is set
|
||
return "https://ui-avatars.com/api/?name=" . urlencode($this->name) . "&color=7F9CF5&background=EBF4FF";
|
||
}
|
||
}
|