Commit a4b2fa509109a3b3bf0b7c268e45eae915ed4b41
Commits[COMMIT BEGIN]commit a4b2fa509109a3b3bf0b7c268e45eae915ed4b41 Author: 0x4248 <[email protected]> Date: Thu Jan 15 12:27:31 2026 +0000 orion: add sqlite functions and parse modes Signed-off-by: 0x4248 <[email protected]> diff --git a/lab/orion/commands/system/sqldb.py b/lab/orion/commands/system/sqldb.py new file mode 100644 index 0000000..c0a7795 --- /dev/null +++ b/lab/orion/commands/system/sqldb.py @@ -0,0 +1,124 @@ +# SPDX-License-Identifier: GPL-3.0-only +# Orion System +# +# Copyright (C) 2026 0x4248 +# Copyright (C) 2026 4248 Systems +# +# Orion is free software; you may redistribute it and/or modify it +# under the terms of the GNU General Public License version 3 only, +# as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +import os +from fastapi import Request +from core.registry import registry +from core.commands import Command +from core import page +import sqlite3 +from core.auth import users as auth_users +from core.console import logger +DB_PATH = "data/orion.db" + +db = sqlite3.connect(DB_PATH) +cursor = db.cursor() + +# if tables dont exist make DEMO and ACCOUNTS tables for testing +cursor.execute(""" +CREATE TABLE IF NOT EXISTS DEMO ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + value TEXT NOT NULL +); +""") +cursor.execute(""" +CREATE TABLE IF NOT EXISTS ACCOUNTS ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + password TEXT NOT NULL +); +""") +db.commit() + +cursor.execute("INSERT INTO DEMO (name, value) VALUES ('example1', 'value1')") +cursor.execute("INSERT INTO DEMO (name, value) VALUES ('example2', 'value2')") +cursor.execute("INSERT INTO ACCOUNTS (username, password) VALUES ('admin', 'admin')") +cursor.execute("INSERT INTO ACCOUNTS (username, password) VALUES ('user', 'password')") +db.commit() + +def auth_check(request: Request): + logger.info(m="Checking auth for SQLDB command", caller="SQLDB_Command") + user = request.cookies.get('user', 'None') + if not user: + return False + elif 'admin' not in auth_users[user]["roles"] and 'db' not in auth_users[user]["roles"]: + return False + return True + +def sql_print_table(request: Request, *args): + if not auth_check(request): + return page.message(request, "SQL ERROR", error="Unauthorized: Admin/DB role required.") + if len(args) == 0 or args[0] == "*": + logger.info(m="Selecting all tables", caller="SQLDB_Command") + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") + args = [row[0] for row in cursor.fetchall()] + logger.info(m=f"Found tables: {args}", caller="SQLDB_Command") + output = "<span class='grey-text'>F4 to exit table view.</span><br><br>" + for arg in args: + logger.info(m=f"Printing table: {arg}", caller="SQLDB_Command") + table_name = arg + + try: + cursor.execute(f"SELECT * FROM {table_name}") + rows = cursor.fetchall() + columns = [description[0] for description in cursor.description] + + output += f"<h2>{table_name}</h2><table style='width:100%; border-collapse: collapse;' border='1'><tr>" + for col in columns: + output += f"<th style='text-align:left'>{col}</th>" + output += "</tr>" + for row in rows: + output += "<tr>" + for cell in row: + output += f"<td>{cell}</td>" + output += "</tr>" + output += "</table><br>" + except sqlite3.Error as e: + output = f"Error accessing table '{table_name}': {e}" + return page.message(request, "SQL ERROR", output) + if output: + return page.static(request, "SQL TABLES", output) + else: + return page.message(request, "SQL TABLES", error="No tables found in search.") + +registry.register(Command( + name="db.table.print", + handler=sql_print_table, + summary="Print a database table", + mode="cli", +)) + +def sql_exec(request: Request, *args): + if not auth_check(request): + return page.message(request, "SQL ERROR", error="Unauthorized: Admin/DB role required.") + query = " ".join(args) + response = "" + try: + cursor.execute(query) + response = cursor.fetchall() + db.commit() + return page.message(request, "SQL EXECUTED", f"Successfully executed SQL command: {query}<br>Response: {response}") + except sqlite3.Error as e: + return page.message(request, "SQL ERROR", error=f"Error executing SQL command: {e}") + +registry.register(Command( + name="db.sql", + handler=sql_exec, + summary="Execute an SQL command", + mode="cli", + parse_mode="raw", +)) + +# E.G db.sql SELECT * FROM DEMO diff --git a/lab/orion/commands/templates/hello_both.py b/lab/orion/commands/templates/hello_both.py new file mode 100644 index 0000000..1115b5a --- /dev/null +++ b/lab/orion/commands/templates/hello_both.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: GPL-3.0-only +# Orion System +# +# Copyright (C) 2026 0x4248 +# Copyright (C) 2026 4248 Systems +# +# Orion is free software; you may redistribute it and/or modify it +# under the terms of the GNU General Public License version 3 only, +# as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +from fastapi import Request +from core.registry import registry +from core.commands import Command +from core import page + +def both(request: Request, value: str = "", extra: str = ""): + return page.message( + request, + "BOTH", + f"value = {value or '(none)'} | extra = {extra or '(none)'}" + ) + +registry.register(Command( + name="both", + handler=both, + summary="CLI + UI command", + mode="both", + form_fields=[ + {"name": "value", "type": "text"}, + {"name": "extra", "type": "date"} + ], +)) \ No newline at end of file diff --git a/lab/orion/commands/templates/hello_cli.py b/lab/orion/commands/templates/hello_cli.py new file mode 100644 index 0000000..ec3a957 --- /dev/null +++ b/lab/orion/commands/templates/hello_cli.py @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-3.0-only +# Orion System +# +# Copyright (C) 2026 0x4248 +# Copyright (C) 2026 4248 Systems +# +# Orion is free software; you may redistribute it and/or modify it +# under the terms of the GNU General Public License version 3 only, +# as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +from fastapi import Request +from core.registry import registry +from core.commands import Command +from core import page + +def hello_cli(request: Request, *args): + name = args[0] if args else "world" + return page.message(request, "HELLO (CLI)", f"hello {name}") + +registry.register(Command( + name="hello.world", + handler=hello_cli, + summary="CLI-only hello", + mode="cli", +)) \ No newline at end of file diff --git a/lab/orion/commands/testing/demo.py b/lab/orion/commands/testing/demo.py index 236403f..42d15f2 100644 --- a/lab/orion/commands/testing/demo.py +++ b/lab/orion/commands/testing/demo.py @@ -60,7 +60,7 @@ registry.register(Command( mode="both", form_fields=[ {"name": "value", "type": "text"}, - {"name": "extra", "type": "text"} + {"name": "extra", "type": "time"} ], )) diff --git a/lab/orion/config/modules.py b/lab/orion/config/modules.py index a871dcf..15f4620 100644 --- a/lab/orion/config/modules.py +++ b/lab/orion/config/modules.py @@ -20,5 +20,6 @@ MODULES = [ "commands.system.open", "commands.system.heartbeats", "commands.testing.demo", - "commands.echo" + "commands.echo", + "commands.system.sqldb" ] \ No newline at end of file diff --git a/lab/orion/core/auth.py b/lab/orion/core/auth.py index e93998e..90fde91 100644 --- a/lab/orion/core/auth.py +++ b/lab/orion/core/auth.py @@ -15,7 +15,7 @@ from fastapi import APIRouter, Request, Form from fastapi.responses import RedirectResponse from core import page as p - +from core import console router = APIRouter() # ---- users DB (placeholder) ---- @@ -23,15 +23,22 @@ router = APIRouter() users = { "admin": { "password": "admin", - "roles": ["admin", "user"], + "roles": ["admin", "user", "db"], "email": "admin@orion", "telephone": "101", + }, + "user": { + "password": "user", + "roles": ["user"], + "email": "user@orion", + "telephone": "111", } } # ---- middleware ---- async def auth_middleware(request: Request, call_next): + console.logger.info(m=f"{request.method} {request.url.path} from {request.client.host}. USER: {request.cookies.get('user', 'Anonymous')}", caller="HTTP_Middleware") if request.url.path.startswith("/login"): return await call_next(request) diff --git a/lab/orion/core/commands.py b/lab/orion/core/commands.py index d026d25..60cf404 100644 --- a/lab/orion/core/commands.py +++ b/lab/orion/core/commands.py @@ -16,7 +16,7 @@ from dataclasses import dataclass, field from typing import Callable, Dict, List, Optional, Any, Literal Mode = Literal["cli", "ui", "both"] - +ParseModes = Literal["shlex", "raw"] @dataclass class Command: name: str @@ -24,6 +24,8 @@ class Command: summary: str = "" mode: Mode = "cli" form_fields: List[Dict[str, Any]] = field(default_factory=list) + parse_mode: ParseModes = "shlex" + custom_attributes: Dict[str, Any] = field(default_factory=dict) def supports_cli(self) -> bool: return self.mode in ("cli", "both") diff --git a/lab/orion/core/layout.py b/lab/orion/core/layout.py index ffbe2a3..c3085b2 100644 --- a/lab/orion/core/layout.py +++ b/lab/orion/core/layout.py @@ -81,7 +81,7 @@ label { } -.gray-span { +.grey-text { color: gray; font-style: italic; } diff --git a/lab/orion/main.py b/lab/orion/main.py index 6f28581..2c126df 100644 --- a/lab/orion/main.py +++ b/lab/orion/main.py @@ -24,7 +24,7 @@ from config.modules import MODULES def load_commands(): for module_path in MODULES: - console.logger.info(m=f"Loading module: {module_path}") + console.logger.info(m=f"Loading module: {module_path}", caller="ModuleLoader") __import__(module_path) load_commands() @@ -37,7 +37,7 @@ handler.setFormatter(logging.Formatter("%(message)s")) root = logging.getLogger() root.handlers.clear() root.addHandler(handler) -root.setLevel(logging.DEBUG) +root.setLevel(logging.WARNING) for name in ( "uvicorn", @@ -73,6 +73,15 @@ async def not_found(request: Request, exc): "<a href='javascript:history.back()'>[ BACK ]</a>", ) [email protected]_handler(500) +async def server_error(request: Request, exc): + return p.static( + request, + "500 SERVER ERROR", + "<pre class='error'>500 SERVER ERROR</pre><pre class='grey-text'>{}</pre>".format(str(exc)) + + "<a href='javascript:history.back()'>[ BACK ]</a>", + ) + @app.get("/favicon.ico") async def favicon(): return FileResponse("./static/mascot.ico") diff --git a/lab/orion/pages/command.py b/lab/orion/pages/command.py index cc93908..973b436 100644 --- a/lab/orion/pages/command.py +++ b/lab/orion/pages/command.py @@ -12,6 +12,7 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +import difflib import shlex from fastapi import APIRouter, Request, Form from fastapi.responses import RedirectResponse @@ -41,6 +42,11 @@ async def command_page(request: Request): """ ) +def find_similar_commands(name: str, cutoff: float = 0.6): + commands = [c.name for c in registry.all() if c.supports_cli()] + return difflib.get_close_matches(name, commands, n=3, cutoff=cutoff) + + @router.post("/command") async def command_submit(request: Request, command: str = Form(...)): try: @@ -55,11 +61,20 @@ async def command_submit(request: Request, command: str = Form(...)): cmd = registry.get(name) if not cmd or not cmd.supports_cli(): - return p.message(request, "UNKNOWN", error=f"'{name}' is not a valid CLI command") + did_you_mean = find_similar_commands(name) + if did_you_mean: + hint = ", ".join(f"<a href='/command'>{cmd}</a>" for cmd in did_you_mean) + else: + hint = "no similar commands found" + return p.message(request, "UNKNOWN", error=f"'{name}' is not a valid CLI command <br>Did you mean? {hint}") if cmd.supports_ui() and not args and cmd.form_fields: return RedirectResponse(f"/command/{name}", 303) + if cmd.parse_mode == "raw": + raw_args = command[len(name):].lstrip() + args = [raw_args] + return await dispatcher.dispatch(cmd.handler, request, *args) diff --git a/lab/orion/pages/console.py b/lab/orion/pages/console.py index 00a7d5e..939666d 100644 --- a/lab/orion/pages/console.py +++ b/lab/orion/pages/console.py @@ -25,7 +25,7 @@ router = APIRouter() @router.get("/console") async def console_page(request: Request): body = "<h2>System Logs</h2>\n" - body += "<span class='gray-span'>Press F4 to exit</span>\n" + body += "<span class='grey-text'>Press F4 to exit</span>\n" body += "<pre>\n" for log_id in sorted(console.log_db.logs.keys(), reverse=True): entry = console.log_db.logs[log_id][COMMIT END](C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.