Tweaks to database logic, create slot

This commit is contained in:
2025-03-15 14:39:45 +01:00
parent 0dafc0321e
commit 16a647db82
11 changed files with 230 additions and 17 deletions
+2 -1
View File
@@ -22,7 +22,8 @@
"symfony/cache": "^7.2",
"psr/log": "^3.0",
"monolog/monolog": "^3.8",
"scienta/doctrine-json-functions": "^6.3"
"scienta/doctrine-json-functions": "^6.3",
"symfony/yaml": "^7.2"
},
"bin": [
"bin/slotdbd"
Generated
+73 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f841aaa059a224972f8fd4701e4e2387",
"content-hash": "8c809e4306164e200a0b53086633b24b",
"packages": [
{
"name": "doctrine/collections",
@@ -3014,6 +3014,78 @@
}
],
"time": "2025-02-13T10:27:23+00:00"
},
{
"name": "symfony/yaml",
"version": "v7.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "ac238f173df0c9c1120f862d0f599e17535a87ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec",
"reference": "ac238f173df0c9c1120f862d0f599e17535a87ec",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
"symfony/console": "<6.4"
},
"require-dev": {
"symfony/console": "^6.4|^7.0"
},
"bin": [
"Resources/bin/yaml-lint"
],
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v7.2.3"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-01-07T12:55:42+00:00"
}
],
"packages-dev": [],
+42
View File
@@ -16,6 +16,48 @@ slotdbd - SlotDB database daemon
**\-h**, **\-\-help**
: Display help
**\-v**,**\-\-verbose**
: Include debug level logging to STDOUT
**\-V**,**\-\-version**
: Print version and exit
**\-l**,**\-\-listen** *ADDR*
: Set listen address for HTTP requests (env: SLOTDB_LISTEN)
**\-c**,**\-\-config** *FILE*
: Read configuration from file (env: SLOTDB_CONFIG)
**\-\-db** *URI*
:Use custom database connection URI (env: SLOTDB_DATABASE)
**\-\-init-db**
:Initialize database schema
**\-\-upgrade-db**
:Upgrade database schema
**\-\-root** *FILE*
:Set root CA certificate for SSL (env: SLOTDB_SSL_ROOT)
**\-\-cert** *FILE*
:Set certificate for SSL (env: SLOTDB_SSL_CERT)
**\-\-key** *FILE*
:Set private key file for SSL (env: SLOTDB_SSL_KEY)
**\-\-jwt-secret** *SECRET*
:Use JWT auth with secret (env: SLOTDB_JWT_SECRET)
**\-\-jwt-claim** *CLAIM*
:Key in JWT claims that contains ACLs (env: SLOTDB_JWT_CLAIM)
**\-\-tokens** *FILE*
:Read access tokens from file (env: SLOTDB_TOKENS_FILE)
**\-\-http-log** *FILE*
:Log HTTP requests to file (env: SLOTDB_HTTP_LOG)
# EXAMPLES
**slotdbd**
+3 -1
View File
@@ -41,6 +41,7 @@ class Daemon
?string $dataDir = null,
?LoggerInterface $logger = null,
?LoggerInterface $httpLogger = null,
array $securityOpts = [],
)
{
$this->logger = $logger ?? new NullLogger();
@@ -52,6 +53,7 @@ class Daemon
'driver' => 'pdo_sqlite',
'path' => $this->dataDir."/slots.db",
]);
$this->database = new Database($conn, $logger);
$this->database->updateSchema($upgradeDb);
@@ -68,7 +70,7 @@ class Daemon
$this->server = new HttpServer(
new Http\Middleware\LoggingMiddleware($httpLogger ?? $logger),
new SecurityMiddleware(),
new SecurityMiddleware(...$securityOpts),
new RoutingMiddleware($routes),
new RequestHandler(),
function (ServerRequestInterface $request) {
+2 -1
View File
@@ -3,9 +3,10 @@
namespace SlotDb\SlotDb\Data\Hook;
use Doctrine\ORM\Mapping as ORM;
use SlotDb\SlotDb\Data\Group\HookRepository;
#[ORM\Table(name:"hooks")]
#[ORM\Entity()]
#[ORM\Entity(repositoryClass: HookRepository::class)]
class Hook
{
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace SlotDb\SlotDb\Data\Group;
use Doctrine\ORM\EntityRepository;
class HookRepository extends EntityRepository
{
public function flush(...$objs): void
{
foreach ($objs as $obj) $this->getEntityManager()->persist($obj);
$this->getEntityManager()->flush();
$this->getEntityManager()->clear();
}
}
+20 -4
View File
@@ -2,6 +2,7 @@
namespace SlotDb\SlotDb\Data\Slot;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityRepository;
class SlotRepository extends EntityRepository
@@ -13,8 +14,6 @@ class SlotRepository extends EntityRepository
slotId: $id,
name: $name??$id,
);
$this->getEntityManager()->persist($slot);
$this->getEntityManager()->flush();
return $slot;
}
@@ -27,6 +26,15 @@ class SlotRepository extends EntityRepository
return $slot;
}
public function hasSlot(string $id): bool
{
$slot = $this->findOneBy([ 'slotId' => $id ]);
if (!$slot) {
return false;
}
return true;
}
public function findSlots(int $limit = 0, int $page = 0): array
{
$builder = $this->createQueryBuilder('s');
@@ -87,7 +95,15 @@ class SlotRepository extends EntityRepository
public function flush(...$slots): void
{
foreach ($slots as $slot) $this->getEntityManager()->persist($slot);
$this->getEntityManager()->flush();
$this->getEntityManager()->clear();
try {
$this->getEntityManager()->flush();
$this->getEntityManager()->clear();
}
catch (UniqueConstraintViolationException $e) {
throw new \Exception("Duplicate index", previous: $e);
}
catch (\Throwable $t) {
fwrite(STDERR, $t."\n");
}
}
}
+14 -6
View File
@@ -2,6 +2,7 @@
namespace SlotDb\SlotDb\Http\Controller;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use NoccyLabs\React\Http\Attributes\Route;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
@@ -67,14 +68,21 @@ class SlotsController extends Controller
foreach ($data as $item) {
$slotId = $item->slot;
$slotName = $item->name??null;
try {
$slot = $this->slots->createSlot($slotId, $slotName);
$props = $item->props ?? [];
if (!$this->slots->hasSlot($slotId)) {
$slot = new Slot($slotId, $slotName);
$slot->setProperties($props);
$slots[] = $slot;
}
catch (\Exception $e)
{
return Response::plaintext((string)$e)->withStatus(500);
}
}
try {
$this->slots->flush(...$slots);
}
catch (UniqueConstraintViolationException $e) {
return Response::json([ 'error'=>"Duplicate slot" ])->withStatus(Response::STATUS_CONFLICT);
}
catch (\Throwable $t) {
return Response::json([ 'error'=>$t->getMessage() ])->withStatus(Response::STATUS_INTERNAL_SERVER_ERROR);
}
return Response::json($slots);
+41 -1
View File
@@ -4,8 +4,10 @@ namespace SlotDb\SlotDb\Http\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;
use Symfony\Component\Yaml\Yaml;
class SecurityMiddleware
{
@@ -14,13 +16,18 @@ class SecurityMiddleware
private bool $useToken = false;
private array $tokens = [];
public function __construct(
private readonly LoggerInterface $logger,
private readonly ?string $jwtSecret = null,
private readonly ?string $jwtClaim = "slotdb",
private readonly ?string $tokenFile = null,
)
{
if ($tokenFile) {
$this->readTokenFile($tokenFile);
}
}
public function __invoke(ServerRequestInterface $request, ?callable $next = null)
@@ -31,6 +38,7 @@ class SecurityMiddleware
$request = $request->withAttribute("acl", [ "*/rw" ]);
} else {
if ($this->useJwt && $request->hasHeader('authorization')) {
// JWT authenticaton
$auth = $request->getHeader('authorization')[0];
[$type,$token] = explode(" ", $auth, 2);
$acl = $this->checkJwt($token);
@@ -39,12 +47,15 @@ class SecurityMiddleware
}
$request = $request->withAttribute("acl", $acl);
} elseif ($this->useToken && $request->hasHeader('x-token')) {
// Token-based authentication
$token = $request->getHeader('x-token')[0];
$acl = $this->checkToken($token);
if ($acl === null) {
return Response::json(['error'=>'Unauthorized'])->withStatus(Response::STATUS_UNAUTHORIZED);
}
$request = $request->withAttribute("acl", $acl);
} else {
return Response::json(['error'=>'Unauthorized'])->withStatus(Response::STATUS_UNAUTHORIZED);
}
}
@@ -52,6 +63,35 @@ class SecurityMiddleware
return $response;
}
private function readTokenFile(string $filename): void
{
// Set this so that the server does not become open if the token file is
// missing.
$this->useToken = true;
if (!file_exists($filename)) {
$this->logger->warning("Token file not found: {$filename} - no tokens read!");
return;
}
$this->logger->info("Reading tokens from file: {$filename}");
$tokens = Yaml::parseFile($filename);
$this->tokens = [];
foreach ($tokens as $token) {
$tok = $token['token'] ?? null;
$acl = $token['acl'] ?? null;
if (!($acl && $tok)) {
$this->logger->warning("Invalid token in configuration file: {$filename}");
} else {
$this->tokens[$tok] = $acl;
}
}
$this->logger->debug("Read ".count($this->tokens)." tokens from file.");
}
private function checkJwt(string $token): ?array
{
return null;
+14
View File
@@ -6,6 +6,7 @@ use Monolog\Formatter\JsonFormatter;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;
use Psr\Log\LogLevel;
use React\EventLoop\Loop;
use SlotDb\SlotDb\Daemon;
$buildInfo = (file_exists(__DIR__."/build.php"))
@@ -19,6 +20,7 @@ $opts = getopt("hl:vV", [
"db:", "init-db", "upgrade-db",
"root:", "cert:", "key:",
"jwt-secret:", "jwt-claim:",
"tokens:",
"http-log:", "version"
]);
@@ -100,6 +102,11 @@ $httpLog = $config->httpLog
?: ($opts['http-log']
?? null ));
$tokenFile = $config->tokenFile
?? (getenv("SLOTDB_TOKENS_FILE")
?: ($opts['tokens']
?? null ));
$consoleHandler = new Monolog\Handler\StreamHandler(STDOUT);
$logFormat = "[%datetime%] %channel%.%level_name%: %message%\n";
@@ -124,11 +131,18 @@ if ($verbose) {
$logger = new Monolog\Logger("main", $logHandlers);
$httpLogger = new Monolog\Logger("http", $httpLogHandlers);
$securityLogger = new Monolog\Logger("security", $logHandlers);
$daemon = new Daemon(
listen: $listen ?? '0.0.0.0:8080',
logger: $logger,
httpLogger: $httpLogger,
securityOpts: [
'tokenFile' => $tokenFile,
'logger' => $securityLogger,
]
);
$daemon->start();
Loop::run();
+2 -2
View File
@@ -1,2 +1,2 @@
/*.db
/*/*.json
*.db
*.json