refactor: reorganize project structure and fix broken references
- Move scripts to scripts/ directory (roda.sh, prepara_db.py, etc.) - Move shell config to shell/ directory (Caddyfile, auth.py, haloy.yml) - Move basedosdados.duckdb to data/ directory - Update Dockerfile and start.sh with new file paths - Update README.md with correct script paths - Remove Python ask.py (replaced by Rust binary in ask/ask) - Add Rust source files (schema_filter.rs, sql_generator.rs, table_selector.rs) - Remove sentence-transformer dependencies from ask - Move docs and context artifacts to their directories
This commit is contained in:
50
shell/Caddyfile
Normal file
50
shell/Caddyfile
Normal file
@@ -0,0 +1,50 @@
|
||||
:8080 {
|
||||
# Route based on Host header (set by haloy)
|
||||
@ask {
|
||||
host ask.xn--2dk.xyz
|
||||
}
|
||||
@db {
|
||||
host db.xn--2dk.xyz
|
||||
}
|
||||
|
||||
handle @ask {
|
||||
reverse_proxy localhost:7682 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
|
||||
handle @db {
|
||||
handle /health {
|
||||
respond 200
|
||||
}
|
||||
|
||||
handle /login {
|
||||
reverse_proxy 127.0.0.1:8081
|
||||
}
|
||||
|
||||
handle /query {
|
||||
reverse_proxy 127.0.0.1:8081
|
||||
}
|
||||
|
||||
@websocket {
|
||||
header Connection *Upgrade*
|
||||
header Upgrade websocket
|
||||
}
|
||||
|
||||
handle @websocket {
|
||||
reverse_proxy localhost:7681 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
|
||||
handle {
|
||||
forward_auth 127.0.0.1:8081 {
|
||||
uri /auth
|
||||
copy_headers Cookie
|
||||
}
|
||||
reverse_proxy localhost:7681 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
shell/auth.py
Normal file
96
shell/auth.py
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal cookie-session auth gate for DuckDB shell."""
|
||||
import hmac, hashlib, os, secrets, subprocess, time
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
PASSWORD = os.environ.get('BASIC_AUTH_PASSWORD', '').encode()
|
||||
_SECRET = secrets.token_bytes(32)
|
||||
|
||||
def _make_token():
|
||||
day = str(int(time.time()) // 86400)
|
||||
return hmac.new(_SECRET, day.encode(), hashlib.sha256).hexdigest()
|
||||
|
||||
def _valid(token):
|
||||
if not token:
|
||||
return False
|
||||
for delta in (0, 1):
|
||||
day = str(int(time.time()) // 86400 - delta)
|
||||
expected = hmac.new(_SECRET, day.encode(), hashlib.sha256).hexdigest()
|
||||
if hmac.compare_digest(token, expected):
|
||||
return True
|
||||
return False
|
||||
|
||||
LOGIN_HTML = """<!DOCTYPE html>
|
||||
<html><head><title>DB Shell</title><style>
|
||||
body{display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#0f1117;font-family:sans-serif}
|
||||
form{background:#1a1d27;padding:2rem;border-radius:8px;display:flex;flex-direction:column;gap:1rem;min-width:280px}
|
||||
h2{color:#fff;margin:0}
|
||||
input{padding:.6rem;border-radius:4px;border:1px solid #333;background:#0f1117;color:#fff;font-size:1rem}
|
||||
button{padding:.6rem;border-radius:4px;border:none;background:#f4c543;color:#000;font-size:1rem;cursor:pointer;font-weight:600}
|
||||
</style></head>
|
||||
<body><form method="POST" action="/login">
|
||||
<h2>DB Shell</h2>
|
||||
<input type="password" name="password" placeholder="Password" autofocus>
|
||||
<button type="submit">Enter</button>
|
||||
</form></body></html>""".encode()
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def _cookie(self):
|
||||
for part in self.headers.get('Cookie', '').split(';'):
|
||||
part = part.strip()
|
||||
if part.startswith('ddb_auth='):
|
||||
return part[9:]
|
||||
return ''
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == '/auth':
|
||||
if _valid(self._cookie()):
|
||||
self._resp(200)
|
||||
else:
|
||||
self.send_response(302)
|
||||
self.send_header('Location', '/login')
|
||||
self.end_headers()
|
||||
else:
|
||||
self._resp(200, LOGIN_HTML, 'text/html; charset=utf-8')
|
||||
|
||||
def do_POST(self):
|
||||
if self.path == '/query':
|
||||
pwd = self.headers.get('X-Password', '').encode()
|
||||
if not hmac.compare_digest(pwd, PASSWORD):
|
||||
self._resp(401, b'Unauthorized\n')
|
||||
return
|
||||
sql = self.rfile.read(int(self.headers.get('Content-Length', 0))).decode(errors='replace')
|
||||
try:
|
||||
r = subprocess.run(
|
||||
['duckdb', '-readonly', '--init', '/app/ssh_init.sql', '/app/basedosdados.duckdb'],
|
||||
input=sql, capture_output=True, text=True, timeout=120
|
||||
)
|
||||
out = (r.stdout + r.stderr).encode()
|
||||
except subprocess.TimeoutExpired:
|
||||
out = b'timeout\n'
|
||||
self._resp(200, out, 'text/plain; charset=utf-8')
|
||||
return
|
||||
body = self.rfile.read(int(self.headers.get('Content-Length', 0))).decode(errors='replace')
|
||||
pwd = parse_qs(body).get('password', [''])[0].encode()
|
||||
if hmac.compare_digest(pwd, PASSWORD):
|
||||
self.send_response(302)
|
||||
self.send_header('Set-Cookie', f'ddb_auth={_make_token()}; Path=/; HttpOnly; SameSite=Strict')
|
||||
self.send_header('Location', '/')
|
||||
self.end_headers()
|
||||
else:
|
||||
self._resp(200, LOGIN_HTML, 'text/html; charset=utf-8')
|
||||
|
||||
def _resp(self, code, body=b'', ct='text/plain'):
|
||||
self.send_response(code)
|
||||
if body:
|
||||
self.send_header('Content-Type', ct)
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.end_headers()
|
||||
if body:
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, *_):
|
||||
pass
|
||||
|
||||
HTTPServer(('127.0.0.1', 8081), H).serve_forever()
|
||||
31
shell/haloy.yml
Normal file
31
shell/haloy.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
name: basedosdados
|
||||
server: haloy.xn--2dk.xyz
|
||||
api_token:
|
||||
from:
|
||||
env: HALOY_TOKEN
|
||||
domains:
|
||||
- domain: db.xn--2dk.xyz
|
||||
port: 8080
|
||||
health_check_path: /health
|
||||
- domain: ask.xn--2dk.xyz
|
||||
port: 8080
|
||||
health_check_path: /health
|
||||
env:
|
||||
- name: GEMINI_API_KEY
|
||||
from:
|
||||
env: GEMINI_API_KEY
|
||||
- name: HETZNER_S3_ENDPOINT
|
||||
from:
|
||||
env: HETZNER_S3_ENDPOINT
|
||||
- name: AWS_ACCESS_KEY_ID
|
||||
from:
|
||||
env: AWS_ACCESS_KEY_ID
|
||||
- name: AWS_SECRET_ACCESS_KEY
|
||||
from:
|
||||
env: AWS_SECRET_ACCESS_KEY
|
||||
- name: BASIC_AUTH_PASSWORD
|
||||
from:
|
||||
env: BASIC_AUTH_PASSWORD
|
||||
- name: BUCKET_REGION
|
||||
from:
|
||||
env: BUCKET_REGION
|
||||
Reference in New Issue
Block a user