feat: dashboard token auth + README v2/v3 payload sections split
Dashboard: - Optional DASHBOARD_TOKEN env var (K8s secret 'snek-dashboard-secret') - Flask before_request check via Authorization header or ?token= param - /webhook and /healthz remain public (SNEK POST needs to reach webhook) - Frontend prompts for token on first load, stores in localStorage - Token forwarded on fetch() calls and EventSource URL README decoding section: - Split into two full sub-sections for Sens'it Discovery (v3) and Sens'it v2 - Explicit bit ordering note (MSB-LSB v3 vs LSB-MSB v2) - Byte-by-byte tables, formulas per mode, worked examples - Note that auto-detection uses byte 0 bits 2-0 == 0b110 marker for v3 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -15,8 +15,24 @@ from sensit_decoder import decode as sensit_decode
|
||||
DB_PATH = os.environ.get("DB_PATH", "/data/messages.db")
|
||||
Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
TOKEN = os.environ.get("DASHBOARD_TOKEN", "").strip()
|
||||
PUBLIC_ROUTES = {"/webhook", "/healthz"}
|
||||
|
||||
app = Flask(__name__, static_folder="static", static_url_path="/static")
|
||||
|
||||
|
||||
@app.before_request
|
||||
def _check_token():
|
||||
if not TOKEN or request.path in PUBLIC_ROUTES:
|
||||
return None
|
||||
supplied = (
|
||||
request.headers.get("Authorization", "").removeprefix("Bearer ").strip()
|
||||
or request.args.get("token", "").strip()
|
||||
)
|
||||
if supplied != TOKEN:
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
return None
|
||||
|
||||
# --- SSE broadcast ---
|
||||
_subscribers: list[queue.Queue] = []
|
||||
_subscribers_lock = threading.Lock()
|
||||
|
||||
@@ -73,6 +73,35 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- Token gate --------------------------------------------------------------
|
||||
const TOKEN_KEY = 'snek_dashboard_token';
|
||||
let token = localStorage.getItem(TOKEN_KEY) || '';
|
||||
|
||||
async function ensureToken() {
|
||||
// Probe if the server requires a token
|
||||
const probe = await fetch('/api/devices', { headers: authHeaders() });
|
||||
if (probe.ok) return;
|
||||
if (probe.status !== 401) return;
|
||||
while (true) {
|
||||
const t = window.prompt('Access token');
|
||||
if (t === null) return;
|
||||
const r = await fetch('/api/devices', { headers: { Authorization: 'Bearer ' + t } });
|
||||
if (r.ok) {
|
||||
token = t.trim();
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function authHeaders() {
|
||||
return token ? { Authorization: 'Bearer ' + token } : {};
|
||||
}
|
||||
|
||||
function authQuery() {
|
||||
return token ? '?token=' + encodeURIComponent(token) : '';
|
||||
}
|
||||
|
||||
const state = { messagesByDevice: {}, chart: null, selectedDevice: null };
|
||||
|
||||
const fmtTime = ts => new Date(ts * 1000).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
@@ -286,7 +315,7 @@ function renderChart() {
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const r = await fetch('/api/messages?limit=200');
|
||||
const r = await fetch('/api/messages?limit=200', { headers: authHeaders() });
|
||||
const msgs = await r.json();
|
||||
msgs.forEach(m => {
|
||||
if (!state.messagesByDevice[m.device]) state.messagesByDevice[m.device] = [];
|
||||
@@ -298,7 +327,7 @@ async function bootstrap() {
|
||||
}
|
||||
|
||||
function connectSSE() {
|
||||
const es = new EventSource('/events');
|
||||
const es = new EventSource('/events' + authQuery());
|
||||
es.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data);
|
||||
@@ -317,7 +346,7 @@ document.getElementById('chart-device').addEventListener('change', (e) => {
|
||||
renderChart();
|
||||
});
|
||||
|
||||
bootstrap().then(connectSSE);
|
||||
ensureToken().then(bootstrap).then(connectSSE);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user