diff --git a/.agents/skills/api-technical-specs/SKILL.md b/.agents/skills/api-technical-specs/SKILL.md index 604d4fe..b265fb7 100644 --- a/.agents/skills/api-technical-specs/SKILL.md +++ b/.agents/skills/api-technical-specs/SKILL.md @@ -51,6 +51,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 | success | Boolean | 請求是否成功 | true | | code | Integer | 業務狀態碼 | 200 | | message | String | 驗證結果 (Success 或 Failed) | Success | +| identity | String | **登入者身分**:`system` 系統登入者 / `company` 公司帳號 / `staff` 一般人員。僅驗證成功時回傳 | system | | token | String | **臨時身份認證 Token** (可選,供未來擴充使用) | 1\|abcdefg... | --- diff --git a/app/Http/Controllers/Api/V1/App/MachineAuthController.php b/app/Http/Controllers/Api/V1/App/MachineAuthController.php index 8703431..6f83dc8 100644 --- a/app/Http/Controllers/Api/V1/App/MachineAuthController.php +++ b/app/Http/Controllers/Api/V1/App/MachineAuthController.php @@ -59,16 +59,22 @@ class MachineAuthController extends Controller } // 5. RBAC 權限驗證 (遵循多租戶與機台授權規範) + // $identity: 登入者身分 (system:系統登入者 / company:公司帳號 / staff:一般人員) + // 僅在授權通過時賦值,確保未授權者不洩漏身分 $isAuthorized = false; + $identity = null; if ($user->isSystemAdmin()) { $isAuthorized = true; + $identity = 'system'; } elseif ($user->is_admin) { if ($machine->company_id === $user->company_id) { $isAuthorized = true; + $identity = 'company'; } } else { if ($user->machines()->where('machine_id', $machine->id)->exists()) { $isAuthorized = true; + $identity = 'staff'; } } @@ -120,6 +126,7 @@ class MachineAuthController extends Controller 'success' => true, 'code' => 200, 'message' => 'Success', + 'identity' => $identity, 'token' => $user->createToken('technician-setup', ['*'], now()->addHours(8))->plainTextToken ]); } diff --git a/config/api-docs.php b/config/api-docs.php index 5c84d45..8d44342 100644 --- a/config/api-docs.php +++ b/config/api-docs.php @@ -55,6 +55,11 @@ return [ 'description' => '回應訊息', 'example' => 'Success' ], + 'identity' => [ + 'type' => 'string', + 'description' => '登入者身分:system 系統登入者 / company 公司帳號 / staff 一般人員 (僅驗證成功時回傳)', + 'example' => 'system' + ], 'token' => [ 'type' => 'string', 'description' => '臨時身份認證 Token', @@ -70,6 +75,7 @@ return [ 'success' => true, 'code' => 200, 'message' => 'Success', + 'identity' => 'system', 'token' => '1|abcdefg...' ], ], diff --git a/tests/Feature/Api/V1/MachineAuthB000Test.php b/tests/Feature/Api/V1/MachineAuthB000Test.php new file mode 100644 index 0000000..be8114b --- /dev/null +++ b/tests/Feature/Api/V1/MachineAuthB000Test.php @@ -0,0 +1,142 @@ +company = Company::create([ + 'name' => 'Test Company', + ]); + + $this->apiToken = 'test-machine-api-token-123456'; + + $this->machine = Machine::factory()->create([ + 'company_id' => $this->company->id, + 'api_token' => $this->apiToken, + ]); + } + + private function loginAs(string $account, string $password = 'password') + { + return $this->withHeader('Authorization', "Bearer {$this->apiToken}") + ->postJson('/api/v1/app/admin/login/B000', [ + 'Su_Account' => $account, + 'Su_Password' => $password, + ]); + } + + /** + * 系統登入者 (company_id = null) → identity: system + */ + public function test_system_admin_login_returns_identity_system(): void + { + User::factory()->create([ + 'company_id' => null, + 'username' => 'sysadmin', + 'password' => Hash::make('password'), + ]); + + $this->loginAs('sysadmin') + ->assertStatus(200) + ->assertJson([ + 'success' => true, + 'code' => 200, + 'identity' => 'system', + ]); + } + + /** + * 公司帳號 (租戶管理員,is_admin = true,同 company_id 機台) → identity: company + */ + public function test_company_admin_login_returns_identity_company(): void + { + User::factory()->create([ + 'company_id' => $this->company->id, + 'username' => 'companyadmin', + 'is_admin' => true, + 'password' => Hash::make('password'), + ]); + + $this->loginAs('companyadmin') + ->assertStatus(200) + ->assertJson([ + 'success' => true, + 'identity' => 'company', + ]); + } + + /** + * 一般人員 (is_admin = false,已加入 machine_user 授權) → identity: staff + */ + public function test_staff_login_returns_identity_staff(): void + { + $staff = User::factory()->create([ + 'company_id' => $this->company->id, + 'username' => 'staff', + 'is_admin' => false, + 'password' => Hash::make('password'), + ]); + $staff->machines()->attach($this->machine->id); + + $this->loginAs('staff') + ->assertStatus(200) + ->assertJson([ + 'success' => true, + 'identity' => 'staff', + ]); + } + + /** + * 帳密錯誤 → 401,回應不含 identity + */ + public function test_invalid_credentials_returns_401_without_identity(): void + { + User::factory()->create([ + 'company_id' => null, + 'username' => 'sysadmin', + 'password' => Hash::make('password'), + ]); + + $response = $this->loginAs('sysadmin', 'wrong-password'); + + $response->assertStatus(401) + ->assertJson(['success' => false, 'code' => 401]); + $this->assertArrayNotHasKey('identity', $response->json()); + } + + /** + * 未授權 (一般人員未加入 machine_user) → 403,回應不含 identity + */ + public function test_unauthorized_staff_returns_403_without_identity(): void + { + User::factory()->create([ + 'company_id' => $this->company->id, + 'username' => 'staff', + 'is_admin' => false, + 'password' => Hash::make('password'), + ]); + + $response = $this->loginAs('staff'); + + $response->assertStatus(403) + ->assertJson(['success' => false, 'code' => 403]); + $this->assertArrayNotHasKey('identity', $response->json()); + } +}