491 lines
18 KiB
PHP
491 lines
18 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Agent;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Support\Facades\Hash;
|
|
|
|
class AgentController extends Controller
|
|
{
|
|
/**
|
|
* Display a listing of the resource.
|
|
*/
|
|
public function index(Request $request)
|
|
{
|
|
// Enforce Master Role
|
|
// The middleware CheckAgentSecret attaches the agent to the request,
|
|
// but let's double check if it exists before accessing property
|
|
if (!isset($request->_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);
|
|
}
|
|
}
|
|
}
|