adding auth
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Jakach\Logging\Api;
|
||||
|
||||
use Jakach\Logging\Storage\Repository;
|
||||
|
||||
class AuthMiddleware
|
||||
{
|
||||
private Repository $repo;
|
||||
|
||||
public function __construct(Repository $repo)
|
||||
{
|
||||
$this->repo = $repo;
|
||||
}
|
||||
|
||||
public function requireAuth(): ?array
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 86400 * 7,
|
||||
'path' => '/',
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (empty($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$allowedTokens = $this->repo->getAllowedUserTokens();
|
||||
if (!empty($allowedTokens)) {
|
||||
$userToken = $_SESSION['user_token'] ?? '';
|
||||
if (!in_array($userToken, $allowedTokens, true)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'username' => $_SESSION['username'] ?? 'unknown',
|
||||
'user_token' => $_SESSION['user_token'] ?? '',
|
||||
'email' => $_SESSION['email'] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
+125
-33
@@ -8,11 +8,13 @@ use Jakach\Logging\Storage\{Database, Repository};
|
||||
class Router
|
||||
{
|
||||
private Repository $repo;
|
||||
private AuthMiddleware $auth;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$db = new Database();
|
||||
$this->repo = new Repository($db);
|
||||
$this->auth = new AuthMiddleware($this->repo);
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
@@ -20,51 +22,123 @@ class Router
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
$path = rtrim($path, '/');
|
||||
$path = $path ?: '/';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$result = match (true) {
|
||||
$path === '/sources' && $method === 'GET' => $this->repo->getSources(),
|
||||
$path === '/sources' && $method === 'POST' => $this->createSource(),
|
||||
$path === '/rules' && $method === 'GET' => $this->repo->getRules(),
|
||||
$path === '/rules' && $method === 'POST' => $this->createRule(),
|
||||
$path === '/alerts' && $method === 'GET' => $this->getAlerts(),
|
||||
preg_match('#^/alerts/(\d+)/ack$#', $path, $m) && $method === 'POST' => $this->ackAlert((int) $m[1]),
|
||||
preg_match('#^/alerts/counts$#', $path) && $method === 'GET' => $this->repo->getAlertCounts(),
|
||||
$path === '/health' && $method === 'GET' => ['status' => 'ok'],
|
||||
default => throw new \RuntimeException('Not found', 404),
|
||||
};
|
||||
|
||||
if ($result instanceof \UnitEnum || is_scalar($result)) {
|
||||
$result = ['data' => $result];
|
||||
} elseif (is_array($result) && !empty($result) && $result[array_key_first($result)] instanceof \UnitEnum) {
|
||||
$result = ['data' => $result];
|
||||
} elseif (is_array($result)) {
|
||||
$needsWrap = false;
|
||||
foreach ($result as $key => $val) {
|
||||
if (is_object($val) && method_exists($val, 'toArray')) {
|
||||
$result[$key] = $val->toArray();
|
||||
} else {
|
||||
$needsWrap = true;
|
||||
}
|
||||
$publicPaths = ['/health', '/oauth', '/auth/me', '/auth/logout'];
|
||||
$isPublic = false;
|
||||
foreach ($publicPaths as $pp) {
|
||||
if ($path === $pp || str_starts_with($path, $pp . '/')) {
|
||||
$isPublic = true;
|
||||
break;
|
||||
}
|
||||
if ($needsWrap || empty($result)) {
|
||||
$result = ['data' => $result];
|
||||
}
|
||||
} elseif (is_object($result) && method_exists($result, 'toArray')) {
|
||||
$result = ['data' => $result->toArray()];
|
||||
}
|
||||
|
||||
http_response_code(200);
|
||||
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
if (!$isPublic) {
|
||||
$user = $this->auth->requireAuth();
|
||||
if (!$user) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Unauthorized', 'login_url' => $this->loginUrl()]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$result = match (true) {
|
||||
$path === '/health' && $method === 'GET' => ['status' => 'ok'],
|
||||
|
||||
$path === '/auth/me' && $method === 'GET' => $this->getMe(),
|
||||
$path === '/auth/logout' && $method === 'POST' => $this->logout(),
|
||||
|
||||
$path === '/sources' && $method === 'GET' => $this->repo->getSources(),
|
||||
$path === '/sources' && $method === 'POST' => $this->createSource(),
|
||||
preg_match('#^/sources/(\d+)$#', $path, $m) && $method === 'DELETE'
|
||||
=> $this->deleteEntity('source', (int) $m[1]),
|
||||
|
||||
$path === '/rules' && $method === 'GET' => $this->repo->getRules(),
|
||||
$path === '/rules' && $method === 'POST' => $this->createRule(),
|
||||
preg_match('#^/rules/(\d+)$#', $path, $m) && $method === 'DELETE'
|
||||
=> $this->deleteEntity('rule', (int) $m[1]),
|
||||
|
||||
$path === '/alerts' && $method === 'GET' => $this->getAlerts(),
|
||||
preg_match('#^/alerts/(\d+)/ack$#', $path, $m) && $method === 'POST'
|
||||
=> $this->ackAlert((int) $m[1]),
|
||||
preg_match('#^/alerts/counts$#', $path) && $method === 'GET'
|
||||
=> $this->repo->getAlertCounts(),
|
||||
|
||||
$path === '/config/allowed_tokens' && $method === 'GET'
|
||||
=> ['tokens' => $this->repo->getAllowedUserTokens()],
|
||||
$path === '/config/allowed_tokens' && $method === 'PUT'
|
||||
=> $this->updateAllowedTokens(),
|
||||
|
||||
default => throw new \RuntimeException('Not found', 404),
|
||||
};
|
||||
|
||||
$this->respond(200, $result);
|
||||
|
||||
} catch (\RuntimeException $e) {
|
||||
http_response_code($e->getCode() ?: 500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
$this->respond($e->getCode() ?: 500, ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function respond(int $code, mixed $result): void
|
||||
{
|
||||
http_response_code($code);
|
||||
if (is_array($result)) {
|
||||
$hasObjects = false;
|
||||
foreach ($result as $key => $val) {
|
||||
if (is_object($val) && method_exists($val, 'toArray')) {
|
||||
$result[$key] = $val->toArray();
|
||||
$hasObjects = true;
|
||||
}
|
||||
}
|
||||
if ($hasObjects) {
|
||||
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
return;
|
||||
}
|
||||
if (array_is_list($result) && (empty($result) || !isset($result['data']))) {
|
||||
$result = ['data' => $result];
|
||||
}
|
||||
} elseif (is_object($result) && method_exists($result, 'toArray')) {
|
||||
$result = ['data' => $result->toArray()];
|
||||
}
|
||||
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
private function loginUrl(): string
|
||||
{
|
||||
$redirect = 'https://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/oauth.php';
|
||||
return 'https://auth.jakach.ch/?send_to=' . urlencode($redirect);
|
||||
}
|
||||
|
||||
private function getMe(): array
|
||||
{
|
||||
$user = $this->auth->requireAuth();
|
||||
if (!$user) {
|
||||
http_response_code(401);
|
||||
return ['error' => 'Not logged in', 'login_url' => $this->loginUrl()];
|
||||
}
|
||||
return ['user' => $user];
|
||||
}
|
||||
|
||||
private function logout(): array
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
$_SESSION = [];
|
||||
if (ini_get("session.use_cookies")) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000,
|
||||
$params["path"], $params["domain"],
|
||||
$params["secure"], $params["httponly"]
|
||||
);
|
||||
}
|
||||
session_destroy();
|
||||
return ['status' => 'logged_out'];
|
||||
}
|
||||
|
||||
private function createSource(): array
|
||||
{
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
@@ -88,6 +162,16 @@ class Router
|
||||
);
|
||||
}
|
||||
|
||||
private function deleteEntity(string $type, int $id): array
|
||||
{
|
||||
if ($type === 'source') {
|
||||
$this->repo->deleteSource($id);
|
||||
} else {
|
||||
$this->repo->deleteRule($id);
|
||||
}
|
||||
return ['status' => 'deleted', 'id' => $id];
|
||||
}
|
||||
|
||||
private function getAlerts(): array
|
||||
{
|
||||
$limit = (int) ($_GET['limit'] ?? 100);
|
||||
@@ -102,4 +186,12 @@ class Router
|
||||
$this->repo->updateAlertStatus($id, AlertStatus::Acknowledged);
|
||||
return ['status' => 'acknowledged', 'id' => $id];
|
||||
}
|
||||
|
||||
private function updateAllowedTokens(): array
|
||||
{
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$tokens = $body['tokens'] ?? [];
|
||||
$this->repo->setAllowedUserTokens($tokens);
|
||||
return ['status' => 'saved', 'tokens' => $this->repo->getAllowedUserTokens()];
|
||||
}
|
||||
}
|
||||
@@ -83,5 +83,12 @@ class Database
|
||||
PRIMARY KEY (rule_id, window_start)
|
||||
)
|
||||
");
|
||||
|
||||
$this->pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)
|
||||
");
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,28 @@ class Repository
|
||||
)->fetchAll();
|
||||
}
|
||||
|
||||
// --- Config ---
|
||||
|
||||
public function getAllowedUserTokens(): array
|
||||
{
|
||||
$stmt = $this->db->pdo()->prepare("SELECT value FROM config WHERE key = ?");
|
||||
$stmt->execute(['allowed_user_tokens']);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row || empty($row['value'])) {
|
||||
return [];
|
||||
}
|
||||
return json_decode($row['value'], true) ?? [];
|
||||
}
|
||||
|
||||
public function setAllowedUserTokens(array $tokens): void
|
||||
{
|
||||
$stmt = $this->db->pdo()->prepare(
|
||||
"INSERT INTO config (key, value) VALUES (?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value"
|
||||
);
|
||||
$stmt->execute(['allowed_user_tokens', json_encode(array_values($tokens))]);
|
||||
}
|
||||
|
||||
// --- Rate Limiting ---
|
||||
|
||||
public function checkRateLimit(int $ruleId, int $windowSeconds): bool
|
||||
|
||||
Reference in New Issue
Block a user