_agent) || $request->_agent->role !== 'master') { // If accessed via public route without middleware, or non-master // Check if user is authenticated via other means or just return 403 // For now assuming middleware is active for this route if (isset($request->_agent) && $request->_agent->role !== 'master') { return response()->json(['success' => false, 'message' => 'Unauthorized. Master access required.'], 403); } } $query = Agent::query(); // Search functionality if ($request->has('q') && !empty($request->q)) { $term = $request->q; // Use regex for case-insensitive partial match $query->where(function ($q) use ($term) { $pattern = '/' . preg_quote($term, '/') . '/i'; $q->where('agent_id', 'regexp', $pattern) ->orWhere('company_name', 'regexp', $pattern) ->orWhere('email', 'regexp', $pattern); }); } $agents = $query->orderBy('created_at', 'desc')->get(); return response()->json([ 'success' => true, 'agents' => $agents ]); } /** * Store a newly created resource in storage. */ public function store(Request $request) { $validated = $request->validate([ 'agent_id' => 'required|unique:agents,agent_id', 'company_name' => 'required|string', 'email' => 'required|email', 'phone' => 'nullable|string', 'address' => 'nullable|string', 'subscription_duration' => 'required|integer', 'is_active' => 'required|boolean', 'ip_whitelist' => 'nullable|array', 'password' => 'required|string|min:6', // [NEW] ]); // Auto generate secret key if not provided (safety) $validated['api_secret_key'] = 'sk_live_' . Str::random(32); // Hash password $validated['password'] = Hash::make($validated['password']); // Force role to agent for public sign up $validated['role'] = 'agent'; $agent = Agent::create($validated); return response()->json([ 'success' => true, 'message' => 'Agent created successfully', 'agent' => $agent ]); } /** * Display the specified resource. */ public function show(string $id) { $agent = Agent::find($id); if (!$agent) { return response()->json(['success' => false, 'message' => 'Agent not found'], 404); } return response()->json([ 'success' => true, 'agent' => $agent ]); } /** * Update the specified resource in storage. */ public function update(Request $request, string $id) { // Enforce: Master OR Self if ($request->_agent->role !== 'master' && $request->_agent->id !== $id) { return response()->json(['success' => false, 'message' => 'Unauthorized.'], 403); } $agent = Agent::find($id); if (!$agent) { return response()->json(['success' => false, 'message' => 'Agent not found'], 404); } $validated = $request->validate([ 'agent_id' => 'sometimes|required|unique:agents,agent_id,' . $id, 'company_name' => 'sometimes|required|string', 'email' => 'sometimes|required|email', 'phone' => 'nullable|string', 'address' => 'nullable|string', 'subscription_duration' => 'sometimes|required|integer', 'is_active' => 'sometimes|required|boolean', 'ip_whitelist' => 'nullable|array', 'password' => 'sometimes|string|min:6', // [NEW] ]); // Hash password if provided if (isset($validated['password'])) { $validated['password'] = Hash::make($validated['password']); } // Prevent Non-Master from changing role if ($request->_agent->role !== 'master' && isset($validated['role'])) { unset($validated['role']); } $agent->update($validated); return response()->json([ 'success' => true, 'message' => 'Agent updated successfully', 'agent' => $agent ]); } /** * Remove the specified resource from storage. */ public function destroy(Request $request, string $id) { // Enforce Master Role if ($request->_agent->role !== 'master') { return response()->json(['success' => false, 'message' => 'Unauthorized. Master access required.'], 403); } $agent = Agent::find($id); if (!$agent) { return response()->json(['success' => false, 'message' => 'Agent not found'], 404); } // Check Dependencies: Staff $staffCount = \App\Models\User::where('agent_id', $id)->count(); if ($staffCount > 0) { return response()->json([ 'success' => false, 'message' => "Cannot delete Agent. Found {$staffCount} staff members associated with this agent." ], 400); } // Check Dependencies: Customers $customerCount = \App\Models\Customer::where('agent_id', $id)->count(); if ($customerCount > 0) { return response()->json([ 'success' => false, 'message' => "Cannot delete Agent. Found {$customerCount} customers associated with this agent." ], 400); } $agent->delete(); return response()->json([ 'success' => true, 'message' => 'Agent deleted successfully' ]); } /** * Regenerate API Secret Key */ public function regenerateKey(string $id) { $agent = Agent::find($id); if (!$agent) { return response()->json(['success' => false, 'message' => 'Agent not found'], 404); } $newKey = 'sk_live_' . Str::random(32); $agent->api_secret_key = $newKey; $agent->save(); return response()->json([ 'success' => true, 'message' => 'API Key regenerated successfully', 'api_secret_key' => $newKey ]); } /** * Agent Sign In */ /** * Agent Sign In */ public function login(Request $request) { $validated = $request->validate([ 'login_id' => 'required|string', // Can be email or company_name/username 'password' => 'required|string', 'agent_code' => 'nullable|string', // Agent Code (Target Context) 'secret_key' => 'nullable|string', // Optional ]); // --------------------------------------------------------- // 1. Try Authenticating as AGENT (Master/Admin) // --------------------------------------------------------- // Prepare Regex for insensitive search $term = preg_quote($validated['login_id'], '/'); $pattern = '/^' . $term . '$/i'; // If agent_code is provided, we can scope the search, but typical Agent login // uses agent_code as the login_id/username sometimes. // Let's stick to existing logic for finding the Agent self-login first. $agent = Agent::where('email', 'regexp', $pattern) ->orWhere('company_name', 'regexp', $pattern) ->orWhere('username', 'regexp', $pattern) ->orWhere('employee_id', 'regexp', $pattern) // Also check agent_id directly if they put "AGT-XXX" in username field ->orWhere('agent_id', 'regexp', $pattern) ->first(); if ($agent && $agent->is_active) { // [SECURITY FIX] Context Validation // If request includes agent_code (Staff context), make sure found Agent MATCHES that code. // If not match, it means we found a different Agent by name accident (e.g. User 'kartsandy' found Company 'Kartsandy') // In that case, we MUST skip this block and fall through to Staff check. $skipAgentLogin = false; if (!empty($validated['agent_code'])) { // If provided Code does NOT match found Agent's ID, then this is NOT the intended Agent login. // It is likely a Staff login attempt where the username accidentally matches an Agent's company name. if (strcasecmp($agent->agent_id, $validated['agent_code']) !== 0) { $skipAgentLogin = true; } } if (!$skipAgentLogin) { // Verify Password if (Hash::check($validated['password'], $agent->password) || $validated['password'] === $agent->password) { // Update hash if legacy plain text if ($validated['password'] === $agent->password) { $agent->password = Hash::make($validated['password']); $agent->save(); } // Return Agent (Master) Response // Master has access to ALL menus by default return response()->json([ 'success' => true, 'message' => 'Login successful (Master)', 'agent' => [ 'id' => $agent->id, 'agent_id' => $agent->agent_id, 'company_name' => $agent->company_name, 'email' => $agent->email, 'role' => 'master', 'api_secret_key' => $agent->api_secret_key, // Master gets wildcard access 'allowed_menu_ids' => ['*'] ], ]); } } } // --------------------------------------------------------- // 2. Try Authenticating as STAFF (User) // --------------------------------------------------------- // We need the Parent Agent context first. // Use 'agent_code' from request, or fallback to trying to find agent from login_id (unlikely) $parentAgent = null; if (!empty($validated['agent_code'])) { $parentAgent = Agent::where('agent_id', 'regexp', '/^' . preg_quote($validated['agent_code'], '/') . '$/i')->first(); } if ($parentAgent) { // Find User belonging to this Agent $user = \App\Models\User::where('agent_id', $parentAgent->id) ->where(function ($q) use ($pattern) { $q->where('email', 'regexp', $pattern) ->orWhere('employee_id', 'regexp', $pattern); }) ->where('is_active', true) ->first(); if ($user && (Hash::check($validated['password'], $user->password) || $validated['password'] === $user->password)) { // Update legacy password if needed (though Users usually created with hash) // Get Group/Role Permissions $group = $user->group; $menuIds = $group ? $group->allowed_menu_ids : []; return response()->json([ 'success' => true, 'message' => 'Login successful (Staff)', 'agent' => [ // Map User fields to the structure frontend expects (simulating Agent object) 'id' => $user->id, 'agent_id' => $parentAgent->agent_id, // Keep parent agent ID for context 'company_name' => $parentAgent->company_name, 'name' => $user->name, 'email' => $user->email, 'role' => 'staff', // Fixed role 'user_group_name' => $group ? $group->name : null, 'allowed_menu_ids' => $menuIds, 'is_staff' => true, // Staff don't usually use secret key, but we can pass parent's if needed for API calls 'api_secret_key' => $parentAgent->api_secret_key ], ]); } } return response()->json(['success' => false, 'message' => 'Invalid credentials or inactive account.'], 401); } /** * External Agent Sign In (For External Backoffice Integration) * * This endpoint allows external systems to authenticate and get agent access * using only the API Secret Key. No password required. * * Use Case: Open agent backoffice from another system without manual login */ public function externalSignin(Request $request) { $validated = $request->validate([ 'secret_key' => 'required|string', ]); // Find agent by secret key $agent = Agent::where('api_secret_key', $validated['secret_key'])->first(); // 1. Check if agent exists if (!$agent) { return response()->json([ 'success' => false, 'message' => 'Invalid API Secret Key.', 'error_code' => 'INVALID_KEY' ], 401); } // 2. Check if active if (!$agent->is_active) { return response()->json([ 'success' => false, 'message' => 'Agent account is inactive.', 'error_code' => 'INACTIVE_AGENT' ], 403); } // 3. Generate a temporary session token (valid for 24 hours) $sessionToken = base64_encode(json_encode([ 'agent_id' => (string) $agent->_id, 'secret_key' => $agent->api_secret_key, 'issued_at' => now()->toISOString(), 'expires_at' => now()->addHours(24)->toISOString(), ])); // 4. Build the redirect URL for the backoffice $backofficeUrl = config('app.frontend_url', env('FRONTEND_URL', 'http://localhost:3000')); $autoLoginUrl = $backofficeUrl . '/auto-login?token=' . urlencode($sessionToken); // 5. Success - Return agent info + session data return response()->json([ 'success' => true, 'message' => 'External sign-in successful', 'data' => [ 'agent' => [ 'id' => (string) $agent->_id, 'agent_id' => $agent->agent_id, 'company_name' => $agent->company_name, 'email' => $agent->email, 'role' => $agent->role ?? 'agent', ], 'session' => [ 'token' => $sessionToken, 'secret_key' => $agent->api_secret_key, 'expires_at' => now()->addHours(24)->toISOString(), ], 'urls' => [ 'backoffice' => $backofficeUrl, 'auto_login' => $autoLoginUrl, ] ] ]); } /** * Validate Session Token (For Auto-Login) */ public function validateSession(Request $request) { $validated = $request->validate([ 'token' => 'required|string', ]); try { // Decode token $decoded = json_decode(base64_decode($validated['token']), true); if (!$decoded || !isset($decoded['agent_id']) || !isset($decoded['expires_at'])) { return response()->json([ 'success' => false, 'message' => 'Invalid session token format.', 'error_code' => 'INVALID_TOKEN' ], 400); } // Check expiration $expiresAt = \Carbon\Carbon::parse($decoded['expires_at']); if ($expiresAt->isPast()) { return response()->json([ 'success' => false, 'message' => 'Session token has expired.', 'error_code' => 'TOKEN_EXPIRED' ], 401); } // Find agent $agent = Agent::find($decoded['agent_id']); if (!$agent) { return response()->json([ 'success' => false, 'message' => 'Agent not found.', 'error_code' => 'AGENT_NOT_FOUND' ], 404); } // Verify secret key matches if ($agent->api_secret_key !== $decoded['secret_key']) { return response()->json([ 'success' => false, 'message' => 'Session token is no longer valid.', 'error_code' => 'KEY_MISMATCH' ], 401); } // Check if active if (!$agent->is_active) { return response()->json([ 'success' => false, 'message' => 'Agent account is inactive.', 'error_code' => 'INACTIVE_AGENT' ], 403); } // Success - return full agent info for auto-login return response()->json([ 'success' => true, 'message' => 'Session is valid', 'agent' => $agent, ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Failed to validate token.', 'error_code' => 'VALIDATION_ERROR' ], 500); } } }