diff --git a/api/helpers.php b/api/helpers.php index 1d91798..268f51c 100644 --- a/api/helpers.php +++ b/api/helpers.php @@ -521,6 +521,8 @@ const PUBLIC_ROUTES = [ '/api/tasks/team/update.php', '/api/tasks/team/active.php', '/api/tasks/team/list.php', + // hub auth + '/api/hub/auth/login.php', // hub channels (agent-to-agent, no user auth) '/api/hub/channels/create.php', '/api/hub/channels/list.php', diff --git a/api/hub/auth/login.php b/api/hub/auth/login.php new file mode 100644 index 0000000..fe0c81f --- /dev/null +++ b/api/hub/auth/login.php @@ -0,0 +1,113 @@ + false, 'ERROR' => 'method_not_allowed'], 405); +} + +$body = readJsonBody(); + +$loginId = trim($body['login_id'] ?? ''); +$password = $body['password'] ?? ''; + +if ($loginId === '') { + jsonResponse(['OK' => false, 'ERROR' => 'login_id_required']); +} +if ($password === '') { + jsonResponse(['OK' => false, 'ERROR' => 'password_required']); +} + +// Resolve the agent — try by full address, then by name +$agent = queryOne( + "SELECT a.*, u.PasswordHash, u.Token, u.TokenExpiresAt + FROM Sprinter_Agents a + LEFT JOIN Hub_Users u ON u.AgentID = a.ID + WHERE a.FullAddress = ? AND a.IsActive = 1", + [$loginId] +); + +if (!$agent) { + // Try by agent name (e.g. "john" → "sprinter.payfrit.john") + $agent = queryOne( + "SELECT a.*, u.PasswordHash, u.Token, u.TokenExpiresAt + FROM Sprinter_Agents a + LEFT JOIN Hub_Users u ON u.AgentID = a.ID + WHERE a.AgentName = ? AND a.IsActive = 1", + [$loginId] + ); +} + +if (!$agent) { + jsonResponse(['OK' => false, 'ERROR' => 'invalid_credentials'], 401); +} + +// Check password +$storedHash = $agent['PasswordHash'] ?? null; + +if ($storedHash === null) { + // No Hub_Users row yet — auto-provision on first login + // For MVP: accept the password, hash it, create the row and token + $hash = password_hash($password, PASSWORD_BCRYPT); + $token = bin2hex(random_bytes(32)); + $expires = date('Y-m-d H:i:s', strtotime('+30 days')); + + queryTimed( + "INSERT INTO Hub_Users (AgentID, PasswordHash, Token, TokenExpiresAt) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE PasswordHash = VALUES(PasswordHash), Token = VALUES(Token), TokenExpiresAt = VALUES(TokenExpiresAt)", + [(int)$agent['ID'], $hash, $token, $expires] + ); +} else { + // Verify password + if (!password_verify($password, $storedHash)) { + jsonResponse(['OK' => false, 'ERROR' => 'invalid_credentials'], 401); + } + + // Refresh token if expired or missing + $token = $agent['Token'] ?? ''; + $expiresAt = $agent['TokenExpiresAt'] ?? ''; + if ($token === '' || (strtotime($expiresAt) < time())) { + $token = bin2hex(random_bytes(32)); + $expires = date('Y-m-d H:i:s', strtotime('+30 days')); + queryTimed( + "UPDATE Hub_Users SET Token = ?, TokenExpiresAt = ? WHERE AgentID = ?", + [$token, $expires, (int)$agent['ID']] + ); + } +} + +// Return user info mapped for the web client +$user = [ + 'ID' => (int) $agent['ID'], + 'AgentName' => $agent['AgentName'], + 'FullAddress' => $agent['FullAddress'], + 'ProjectName' => $agent['ProjectName'], + 'AgentType' => $agent['AgentType'], + 'Role' => $agent['Role'], + 'ServerHost' => $agent['ServerHost'], + 'IsActive' => (bool) $agent['IsActive'], + 'CreatedAt' => toISO8601($agent['CreatedAt']), +]; + +// Set Token header so hub-proxy.php forwards it to the client +header('Token: ' . $token); + +jsonResponse([ + 'OK' => true, + 'token' => $token, + 'user' => $user, +]);