Add Hub auth/login.php endpoint — fixes Web client login

The web client calls auth/login to authenticate users, but this endpoint
was missing from the Hub API. Creates:
- api/hub/auth/login.php: password-based auth with token generation
- Hub_Users table: stores bcrypt password hashes and session tokens
- Auto-provisions on first login (creates credentials for existing agents)
- Adds route to PUBLIC_ROUTES in helpers.php

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Netasha 2026-03-27 21:41:24 +00:00
parent 1dacefcf70
commit c8ac6ae3fa
2 changed files with 115 additions and 0 deletions

View file

@ -521,6 +521,8 @@ const PUBLIC_ROUTES = [
'/api/tasks/team/update.php', '/api/tasks/team/update.php',
'/api/tasks/team/active.php', '/api/tasks/team/active.php',
'/api/tasks/team/list.php', '/api/tasks/team/list.php',
// hub auth
'/api/hub/auth/login.php',
// hub channels (agent-to-agent, no user auth) // hub channels (agent-to-agent, no user auth)
'/api/hub/channels/create.php', '/api/hub/channels/create.php',
'/api/hub/channels/list.php', '/api/hub/channels/list.php',

113
api/hub/auth/login.php Normal file
View file

@ -0,0 +1,113 @@
<?php
/**
* POST /api/hub/auth/login.php
*
* Authenticate a user (human or bot) against the Hub.
*
* Body:
* login_id string REQUIRED agent name, full address, or email
* password string REQUIRED password (checked against Hub_Users or Sprinter_Agents)
*
* Response:
* { OK: true, token: "...", user: { ... } }
* Also sets the Token response header for the proxy layer.
*/
require_once __DIR__ . '/../../helpers.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['OK' => 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,
]);