## 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 os import times import std/asyncdispatch import config import ../query/lexer import ../query/parser import ../query/executor import ../storage/lsm import ../core/mvcc import ../protocol/wire import ../core/websocket import jwt as jwtlib import ../protocol/auth type HttpServer* = ref object config: BaraConfig running: bool db*: LSMTree ctx: ExecutionContext metrics*: Metrics secretKey*: string authManager*: AuthManager ws*: WsServer Metrics* = ref object queriesTotal*: int queryErrors*: int insertCount*: int selectCount*: int activeConnections*: int proc newHttpServer*(config: BaraConfig): HttpServer = let dataDir = config.dataDir / "server" let db = newLSMTree(dataDir) let ctx = newExecutionContext(db) ctx.txnManager = newTxnManager() let secret = config.getEffectiveJwtSecret() let ws = newWsServer(config, secret) 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, secretKey: secret, authManager: am, metrics: Metrics(), ws: ws) # ---------------------------------------------------------------------- # 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, userId, role) = server.verifyToken(tokenStr) if not valid: ctx.json(%*{"error": "Unauthorized"}, 401) return false return true # ---------------------------------------------------------------------- # Handlers # ---------------------------------------------------------------------- proc queryHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = {.cast(gcsafe).}: let ctx = newContext(request) server.metrics.queriesTotal += 1 # 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, userId, role) = server.verifyToken(tokenStr) if not valid: ctx.json(%*{"error": "Unauthorized"}, 401) 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 = cloneForConnection(server.ctx) 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 var val = "" if key in row: val = row[key] jsonRow[key] = %val 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.0"}) 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 = 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": "0.1.0"}, "paths": { "/query": { "post": { "summary": "Execute SQL query", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"] } } } } } }, "/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 var tables = newJArray() for name, tbl in server.ctx.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 adminHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = let html = """
Total Queries
Query Errors
Inserts
Selects
Active Connections
Tables