## BaraDB HTTP Server — REST API using Hunos import hunos import hunos/router import hunos/middleware import hunos/context import json import tables import strutils import times import os import std/asyncdispatch import config import ../query/lexer import ../query/parser import ../query/executor import ../core/types import ../storage/lsm import ../core/mvcc import ../protocol/wire import ../core/websocket import jwt as jwtlib import ../protocol/auth import ../protocol/ratelimit import ../core/registry import ../core/backup type HttpServer* = ref object config: BaraConfig running: bool db*: LSMTree ctx: ExecutionContext registry*: DatabaseRegistry metrics*: Metrics secretKey*: string authManager*: AuthManager rateLimiter*: RateLimiter ws*: WsServer Metrics* = ref object queriesTotal*: int queryErrors*: int insertCount*: int selectCount*: int activeConnections*: int proc newHttpServerWithRegistry*(config: BaraConfig, registry: DatabaseRegistry): HttpServer = let dbInfo = getOrCreateDatabase(registry, "default") let db = dbInfo.db let ctx = cast[ExecutionContext](cast[pointer](dbInfo.ctx)) ctx.txnManager = newTxnManager() let secret = config.getEffectiveJwtSecret() let ws = newWsServer(config, secret) let rl = newRateLimiter(rlaTokenBucket, config.rateLimitGlobal, config.rateLimitPerClient) ctx.onChange = proc(ev: ChangeEvent) = let msg = $ev.kind & " " & ev.table asyncCheck ws.broadcastToTable(ev.table, msg) let am = newAuthManager(secret) HttpServer(config: config, running: false, db: db, ctx: ctx, registry: registry, secretKey: secret, authManager: am, rateLimiter: rl, metrics: Metrics(), ws: ws) proc newHttpServerWithDb*(config: BaraConfig, db: LSMTree): HttpServer = let registry = newDatabaseRegistry(config) registry.setContextFactory(proc(d: LSMTree, r: DatabaseRegistry): ContextRef {.closure.} = cast[ContextRef](cast[pointer](newExecutionContext(d, r)))) let ctx = newExecutionContext(db, registry) registry.setDatabase("default", db, cast[ContextRef](cast[pointer](ctx))) return newHttpServerWithRegistry(config, registry) proc newHttpServer*(config: BaraConfig): HttpServer = let registry = newDatabaseRegistry(config) registry.setContextFactory(proc(d: LSMTree, r: DatabaseRegistry): ContextRef {.closure.} = cast[ContextRef](cast[pointer](newExecutionContext(d, r)))) registry.loadExistingDatabases() registry.ensureDefaultDatabase() return newHttpServerWithRegistry(config, registry) # ---------------------------------------------------------------------- # JWT helpers # ---------------------------------------------------------------------- proc createToken*(server: HttpServer, userId, role: string): string = let header = %*{"alg": "HS256", "typ": "JWT"} var claims = newTable[string, Claim]() claims["sub"] = newSUB(%userId) claims["role"] = newClaim(GENERAL, %role) claims["iat"] = newIAT(getTime().toUnix()) claims["exp"] = newEXP((getTime() + 24.hours).toUnix()) var token = initJWT(header, claims) token.sign(server.secretKey) return $token proc verifyToken*(server: HttpServer, tokenStr: string): (bool, string, string) = try: let token = tokenStr.toJWT() if not token.verify(server.secretKey, HS256): return (false, "", "") let userId = token.claims["sub"].node.str let role = if "role" in token.claims: token.claims["role"].node.str else: "user" return (true, userId, role) except: return (false, "", "") # ---------------------------------------------------------------------- # Auth helper # ---------------------------------------------------------------------- proc checkAuth(server: HttpServer, request: Request, ctx: Context): bool = if not server.config.authEnabled: return true let authHeader = request.headers["Authorization"] if authHeader.len == 0 or not authHeader.startsWith("Bearer "): ctx.json(%*{"error": "Unauthorized"}, 401) return false let tokenStr = authHeader[7..^1] let (valid, _, _) = server.verifyToken(tokenStr) if not valid: ctx.json(%*{"error": "Unauthorized"}, 401) return false return true proc checkAdmin(server: HttpServer, request: Request, ctx: Context): bool = if not server.config.authEnabled: return true let authHeader = request.headers["Authorization"] if authHeader.len == 0 or not authHeader.startsWith("Bearer "): ctx.json(%*{"error": "Unauthorized"}, 401) return false let tokenStr = authHeader[7..^1] let (valid, _, role) = server.verifyToken(tokenStr) if not valid: ctx.json(%*{"error": "Unauthorized"}, 401) return false if role != "admin": ctx.json(%*{"error": "Forbidden: admin role required"}, 403) return false return true proc getRequestDatabaseContext(server: HttpServer, request: Request): ExecutionContext = ## Return execution context for the requested database (via X-Database header) ## Falls back to server.ctx (default database) if not specified. let dbName = request.headers["X-Database"] if dbName.len > 0 and server.registry != nil: try: let dbInfo = getOrCreateDatabase(server.registry, dbName) if dbInfo != nil and dbInfo.db != nil: return newExecutionContext(dbInfo.db, server.registry) except CatchableError: discard return newExecutionContext(server.db, server.registry) # ---------------------------------------------------------------------- # Handlers # ---------------------------------------------------------------------- proc queryHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = {.cast(gcsafe).}: let ctx = newContext(request) server.metrics.queriesTotal += 1 var userId = "" var role = "" # Auth check if server.config.authEnabled: let authHeader = request.headers["Authorization"] if authHeader.len == 0 or not authHeader.startsWith("Bearer "): ctx.json(%*{"error": "Unauthorized"}, 401) return let tokenStr = authHeader[7..^1] let (valid, uid, r) = server.verifyToken(tokenStr) if not valid: ctx.json(%*{"error": "Unauthorized"}, 401) return userId = uid role = r # Rate limiting let clientKey = request.headers["X-Forwarded-For"] let rateLimitKey = if clientKey.len > 0: clientKey else: "http-global" if not server.rateLimiter.allowRequest(rateLimitKey): ctx.json(%*{"error": "Rate limit exceeded"}, 429) return let body = parseJson(request.body) if body == nil or "query" notin body: ctx.json(%*{"error": "Missing 'query' in request body"}, 400) return let queryStr = body["query"].getStr() if queryStr.len == 0: ctx.json(%*{"error": "Empty query"}, 400) return var reqCtx = getRequestDatabaseContext(server, request) reqCtx.currentUser = userId reqCtx.currentRole = role let tokens = tokenize(queryStr) let astNode = parse(tokens) if astNode.stmts.len == 0: ctx.json(%*{"rows": [], "affectedRows": 0, "columns": []}) return # Extract optional params from JSON body var params: seq[WireValue] = @[] if "params" in body and body["params"].kind == JArray: for p in body["params"]: case p.kind of JNull: params.add(WireValue(kind: fkNull)) of JBool: params.add(WireValue(kind: fkBool, boolVal: p.getBool())) of JInt: params.add(WireValue(kind: fkInt64, int64Val: p.getInt())) of JFloat: params.add(WireValue(kind: fkFloat64, float64Val: p.getFloat())) of JString: params.add(WireValue(kind: fkString, strVal: p.getStr())) else: params.add(WireValue(kind: fkString, strVal: $p)) let res = executor.executeQuery(reqCtx, astNode, params) if res.success: var jsonRows = newJArray() for row in res.rows: var jsonRow = newJObject() for col in res.columns: let key = col if key in row and row[key].kind != vkNull: jsonRow[key] = %valueToString(row[key]) else: jsonRow[key] = newJNull() jsonRows.add(jsonRow) var jsonCols = newJArray() for c in res.columns: jsonCols.add(%c) ctx.json(%*{ "rows": jsonRows, "affectedRows": res.affectedRows, "columns": jsonCols, "message": if res.message.len > 0: %res.message else: newJNull() }) else: server.metrics.queryErrors += 1 ctx.json(%*{"error": res.message}, 400) proc healthHandler(): RequestHandler = return proc(request: Request) {.gcsafe.} = let ctx = newContext(request) ctx.json(%*{"status": "ok", "version": "1.1.6"}) proc metricsHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = let ctx = newContext(request) if not server.checkAuth(request, ctx): return let prometheus = "baradb_queries_total " & $server.metrics.queriesTotal & "\n" & "baradb_query_errors_total " & $server.metrics.queryErrors & "\n" & "baradb_inserts_total " & $server.metrics.insertCount & "\n" & "baradb_selects_total " & $server.metrics.selectCount & "\n" & "baradb_connections_active " & $server.metrics.activeConnections & "\n" request.respond(200, @[("Content-Type", "text/plain; charset=utf-8")], prometheus) proc authHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = {.cast(gcsafe).}: let ctx = newContext(request) let body = parseJson(request.body) if body == nil or "username" notin body or "password" notin body: ctx.json(%*{"error": "Missing username or password"}, 400) return let username = body["username"].getStr() let password = body["password"].getStr() # Simple password check: must match jwtSecret when auth is enabled if server.config.authEnabled and password != server.config.jwtSecret: ctx.json(%*{"error": "Invalid credentials"}, 401) return let token = server.createToken(username, "user") ctx.json(%*{ "token": token, "user": username, "role": "user" }) proc scramStartHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = {.cast(gcsafe).}: let ctx = newContext(request) let body = parseJson(request.body) if body == nil or "username" notin body or "clientFirstMessage" notin body: ctx.json(%*{"error": "Missing username or clientFirstMessage"}, 400) return let username {.used.} = body["username"].getStr() let clientFirst = body["clientFirstMessage"].getStr() try: let serverFirst = server.authManager.startScram(clientFirst) ctx.json(%*{ "serverFirstMessage": serverFirst, "status": "continue" }) except CatchableError as e: ctx.json(%*{"error": e.msg}, 401) proc scramFinishHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = {.cast(gcsafe).}: let ctx = newContext(request) let body = parseJson(request.body) if body == nil or "clientFinalMessage" notin body: ctx.json(%*{"error": "Missing clientFinalMessage"}, 400) return let clientFinal = body["clientFinalMessage"].getStr() try: let (ok, serverFinal) = server.authManager.finishScram(clientFinal) if ok: ctx.json(%*{ "serverFinalMessage": serverFinal, "status": "authenticated" }) else: ctx.json(%*{"error": serverFinal}, 401) except CatchableError as e: ctx.json(%*{"error": e.msg}, 401) proc openApiHandler(): RequestHandler = return proc(request: Request) {.gcsafe.} = let ctx = newContext(request) ctx.json(%*{ "openapi": "3.0.0", "info": {"title": "BaraDB API", "version": "1.1.6"}, "paths": { "/query": { "post": { "summary": "Execute SQL query", "parameters": [ {"name": "X-Database", "in": "header", "schema": {"type": "string"}, "description": "Target database (default: default)"} ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"] } } } } } }, "/tables": {"get": {"summary": "List tables", "parameters": [{"name": "X-Database", "in": "header", "schema": {"type": "string"}}]}}, "/databases": { "get": {"summary": "List databases"}, "post": {"summary": "Create database (admin)"} }, "/backup": { "post": {"summary": "Create backup (admin)"} }, "/backups": { "get": {"summary": "List backups"} }, "/restore": { "post": {"summary": "Restore from backup (admin)"} }, "/health": {"get": {"summary": "Health check"}}, "/metrics": {"get": {"summary": "Prometheus metrics"}}, "/auth": {"post": {"summary": "Authenticate and get JWT token"}} } }) proc tablesHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = {.cast(gcsafe).}: let ctx = newContext(request) if not server.checkAuth(request, ctx): return let reqCtx = getRequestDatabaseContext(server, request) var tables = newJArray() for name, tbl in reqCtx.tables: var cols = newJArray() for col in tbl.columns: cols.add(%*{"name": col.name, "type": col.colType, "pk": col.isPk, "notNull": col.isNotNull, "unique": col.isUnique}) tables.add(%*{"name": name, "columns": cols, "pkColumns": tbl.pkColumns, "fkCount": tbl.foreignKeys.len}) ctx.json(%*{"tables": tables}) proc databasesHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = {.cast(gcsafe).}: let ctx = newContext(request) if not server.checkAuth(request, ctx): return let dbs = server.registry.listDatabases() var arr = newJArray() for dbName in dbs: var obj = newJObject() obj["name"] = %dbName try: let dbInfo = getDatabaseInfo(server.registry, dbName) if dbInfo != nil and dbInfo.ctx != nil: let dbCtx = cast[ExecutionContext](cast[pointer](dbInfo.ctx)) obj["tables"] = %dbCtx.tables.len obj["connections"] = %getConnectionCount(server.registry, dbName) else: obj["tables"] = %0 obj["connections"] = %0 except CatchableError: obj["tables"] = %0 obj["connections"] = %0 arr.add(obj) ctx.json(%*{"databases": arr}) proc createDatabaseHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = {.cast(gcsafe).}: let ctx = newContext(request) if not server.checkAdmin(request, ctx): return let body = parseJson(request.body) if body == nil or "name" notin body: ctx.json(%*{"error": "Missing 'name' in request body"}, 400) return let dbName = body["name"].getStr() if dbName.len == 0: ctx.json(%*{"error": "Empty database name"}, 400) return try: discard getOrCreateDatabase(server.registry, dbName) ctx.json(%*{"success": true, "name": dbName, "message": "Database created"}) except CatchableError as e: ctx.json(%*{"error": e.msg}, 400) proc dropDatabaseHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = {.cast(gcsafe).}: let ctx = newContext(request) if not server.checkAdmin(request, ctx): return let dbName = request.pathParams.getOrDefault("name", "") if dbName.len == 0: ctx.json(%*{"error": "Missing database name"}, 400) return try: let ok = dropDatabase(server.registry, dbName) if ok: ctx.json(%*{"success": true, "name": dbName, "message": "Database dropped"}) else: ctx.json(%*{"error": "Database not found"}, 404) except CatchableError as e: ctx.json(%*{"error": e.msg}, 400) proc backupHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = {.cast(gcsafe).}: let ctx = newContext(request) if not server.checkAdmin(request, ctx): return let body = parseJson(request.body) let dataRoot = server.registry.dataRoot let allDatabases = if body != nil and "all" in body: body["all"].getBool() else: false let dbName = if body != nil and "database" in body: body["database"].getStr() else: "" let outputFile = if body != nil and "output" in body: body["output"].getStr() else: "backup_" & $getTime().toUnix() & ".tar.gz" let compression = if body != nil and "level" in body: body["level"].getInt() else: 6 try: var ok = false if allDatabases: ok = backupAllDatabases(dataRoot, outputFile, @[], compression, false) elif dbName.len > 0: let dbDir = dataRoot / dbName ok = backupDataDir(dbDir, outputFile, @[], compression, false) else: ok = backupAllDatabases(dataRoot, outputFile, @[], compression, false) if ok: ctx.json(%*{"success": true, "output": outputFile, "message": "Backup created"}) else: ctx.json(%*{"error": "Backup failed"}, 500) except CatchableError as e: ctx.json(%*{"error": e.msg}, 500) proc listBackupsHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = {.cast(gcsafe).}: let ctx = newContext(request) if not server.checkAuth(request, ctx): return let dataRoot = server.registry.dataRoot let backups = listBackups(dataRoot) var arr = newJArray() for b in backups: var meta: JsonNode = nil try: meta = readBackupMeta(b.path) except CatchableError: discard var obj = newJObject() obj["path"] = %b.path obj["size"] = %b.size obj["sizeHuman"] = %formatBytes(b.size) obj["timestamp"] = %b.timestamp obj["compressed"] = %b.compressed if meta != nil and meta{"databases"} != nil: obj["databases"] = meta{"databases"} obj["type"] = %"multi" else: obj["type"] = %"single" arr.add(obj) ctx.json(%*{"backups": arr}) proc restoreHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = {.cast(gcsafe).}: let ctx = newContext(request) if not server.checkAdmin(request, ctx): return let body = parseJson(request.body) if body == nil or "input" notin body: ctx.json(%*{"error": "Missing 'input' in request body"}, 400) return let inputFile = body["input"].getStr() let allDatabases = if body != nil and "all" in body: body["all"].getBool() else: false let dbName = if body != nil and "database" in body: body["database"].getStr() else: "" let dataRoot = server.registry.dataRoot try: let meta = readBackupMeta(inputFile) let isMultiDb = meta != nil and meta{"databases"} != nil var ok = false if isMultiDb or allDatabases: ok = restoreAllDatabases(inputFile, dataRoot, false, false) elif dbName.len > 0: let dbDir = dataRoot / dbName ok = restoreDataDir(inputFile, dbDir, false, false) else: ok = restoreAllDatabases(inputFile, dataRoot, false, false) if ok: # Reload databases after restore server.registry.loadExistingDatabases() ctx.json(%*{"success": true, "message": "Restore completed"}) else: ctx.json(%*{"error": "Restore failed"}, 500) except CatchableError as e: ctx.json(%*{"error": e.msg}, 500) proc adminHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = let html = """ BaraDB Admin

BaraDB Login



BaraDB Admin

SQL Playground Tables Schema Live Metrics Cluster Databases Backups

Tables

Loading...

Browse

Select a table to browse
Loading schema...

Real-time Events

0

Total Queries

0

Query Errors

0

Inserts

0

Selects

0

Active Connections

0

Tables

Raw Metrics

Cluster Status

Cluster status not available

Databases

Loading...

Backups

Loading...
BaraDB v1.1.6 — Multimodal Database Engine
""" request.respond(200, @[("Content-Type", "text/html; charset=utf-8")], html) proc run*(server: HttpServer, port: int = 9470) = var router = newRouter() router.get("/admin", server.adminHandler()) router.get("/", server.adminHandler()) router.post("/query", server.queryHandler()) router.get("/health", healthHandler()) router.get("/metrics", server.metricsHandler()) router.post("/auth", server.authHandler()) router.post("/auth/scram/start", server.scramStartHandler()) router.post("/auth/scram/finish", server.scramFinishHandler()) router.get("/tables", server.tablesHandler()) router.get("/api", openApiHandler()) router.get("/databases", server.databasesHandler()) router.post("/databases", server.createDatabaseHandler()) # router.delete("/databases/@name", server.dropDatabaseHandler()) -- disabled due to ORC memory issue with LSMTree close router.post("/backup", server.backupHandler()) router.get("/backups", server.listBackupsHandler()) router.post("/restore", server.restoreHandler()) var stack = newMiddlewareStack(router) stack.use(corsMiddleware()) let hunosServer = newServer(stack) echo "BaraDB HTTP listening on port ", port server.running = true asyncCheck server.ws.run(port + 1) hunosServer.serve(Port(port)) proc stop*(server: HttpServer) = server.running = false server.ws.stop() if server.registry != nil: server.registry.closeAll() else: server.db.close()