diff --git a/mongoose.c b/mongoose.c index 0d4f7fc5..d36f3aee 100644 --- a/mongoose.c +++ b/mongoose.c @@ -124,6 +124,14 @@ fail: #define MG_NO_CACHE_HEADERS "Cache-Control: no-cache\r\n" #define MG_JSON_HEADERS "Content-Type: application/json\r\n" MG_NO_CACHE_HEADERS +struct mg_dash_user { + struct mg_dash_user *next; + char name[32]; // User name + char token[21]; // Login token + int level; // Access level + uint64_t expire; // Expiration timestamp +}; + struct mg_upload_state { char marker; // Tells that we're a file upload connection size_t expected; // POST data length, bytes @@ -277,8 +285,10 @@ static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { ok = mg_json_get_bool(json, json_path, (bool *) f->value); } else if (f->type == MG_VAL_INT) { double d; - ok = mg_json_get_num(json, json_path, &d); - if (ok) *(int *) f->value = (int) d; + if (mg_json_get_num(json, json_path, &d) && d == (int) d) { + *(int *) f->value = (int) d; + ok = true; + } } else if (f->type == MG_VAL_DBL) { ok = mg_json_get_num(json, json_path, (double *) f->value); } else if (f->type == MG_VAL_STR) { @@ -294,24 +304,25 @@ static int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash, struct mg_str json) { struct mg_str key, val; size_t ofs = 0; - int count = 0; - // struct mg_field *f = NULL; + int total_count = 0; while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) { struct mg_field_set *set = mg_dash_find_field_set(dash, trimq(key)); + int count = 0; if (set != NULL) { size_t i; for (i = 0; set->fields[i].name != NULL; i++) { if (mg_dash_parse_field(val, &set->fields[i])) count++; } - if (count > 0) { + if (count) { if (set->writer) set->writer(); mg_dash_send_change(c->mgr, set); + total_count += count; } } else { MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf)); } } - return count; + return total_count; } static void mg_dash_process_msg(struct mg_connection *c, @@ -405,8 +416,8 @@ static void file_ev_handler(struct mg_connection *c, int ev, void *ev_data) { mg_http_reply(c, 500, NULL, "File upload error %d\n", errno); } else { mg_dash_file_add(hm->uri, hm->body.len); - MG_DEBUG(("Starting upload, [%.*s] %lu", hm->uri.len, hm->uri.buf, - hm->body.len)); + MG_DEBUG(("%lu Starting upload, [%.*s] %lu", c->id, hm->uri.len, + hm->uri.buf, hm->body.len)); us->marker = 'U'; us->expected = hm->body.len; us->received = 0; @@ -429,7 +440,7 @@ static void file_ev_handler(struct mg_connection *c, int ev, void *ev_data) { c->recv.len = 0; if (us->received >= us->expected) { fclose((FILE *) us->ctx); - MG_DEBUG(("Uploaded %lu", us->expected)); + MG_DEBUG(("%lu Uploaded %lu", c->id, us->expected)); mg_http_reply(c, 200, NULL, "%lu uploaded\n", us->expected); mg_dash_send_change(c->mgr, &set_files); memset(us, 0, sizeof(*us)); @@ -438,6 +449,91 @@ static void file_ev_handler(struct mg_connection *c, int ev, void *ev_data) { } } +static uint64_t mg_dash_make_expiration_time(struct mg_dash *dash) { + unsigned t = (unsigned) dash->session_auto_expiration_seconds; + if (t == 0) t = 3600; // Default expiration time in seconds + return mg_millis() + t * 1000; +} + +// Parse HTTP requests, return authenticated user or NULL +static struct mg_dash_user *mg_dash_authenticate(struct mg_http_message *hm, + struct mg_dash *dash) { + static struct mg_dash_user *s_users; // List of authenticated users + char user[100], pass[100]; + struct mg_dash_user *u, *tmp, *result = NULL; + + if (dash->authenticate == NULL) return NULL; + mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass)); + + // Remove expired users + for (u = s_users; u != NULL; u = tmp) { + tmp = u->next; + if (u->expire < mg_millis()) { + MG_DEBUG(("Deleting expired auth %s/%d %llu %u", u->name, u->level, + u->expire, mg_millis() - u->expire)); + LIST_DELETE(struct mg_dash_user, &s_users, u); + } + } + + if (pass[0] != '\0') { + struct mg_str *ah = mg_http_get_header(hm, "Authorization"); + if (ah != NULL) { + // Auth header and password are set, auth by user/password via glue API + int num_users = 0, level = dash->authenticate(user, pass); + MG_DEBUG(("user %s, level: %d", user, level)); + if (level > 0) { // Proceed only if the firmware authenticated us + for (u = s_users; u != NULL && result == NULL; + u = u->next, num_users++) { + if (strcmp(user, u->name) == 0) { + u->expire = mg_dash_make_expiration_time(dash); + result = u; + } + } + // Not yet authenticated, add to the list + if (result == NULL && num_users < 10) { + result = (struct mg_dash_user *) mg_calloc(1, sizeof(*result)); + mg_snprintf(result->name, sizeof(result->name), "%s", user); + mg_random_str(result->token, sizeof(result->token) - 1); + result->level = level, result->next = s_users, s_users = result; + result->expire = mg_dash_make_expiration_time(dash); + } + } + } else if (ah == NULL) { + for (u = s_users; u != NULL && result == NULL; u = u->next) { + if (strcmp(u->token, pass) == 0) { + u->expire = mg_dash_make_expiration_time(dash); + result = u; + } + } + } + } + + // MG_DEBUG(("[%s/%s] -> %s", user, pass, result ? "OK" : "FAIL")); + return result; +} + +static void mg_handle_login(struct mg_connection *c, struct mg_dash_user *u) { + char cookie[256]; + mg_snprintf(cookie, sizeof(cookie), + "Set-Cookie: access_token=%s; Path=/; " + "%sHttpOnly; SameSite=Lax; Max-Age=%d\r\n%s", + u->token, c->is_tls ? "Secure; " : "", 3600 * 24, + MG_JSON_HEADERS); + mg_http_reply(c, 200, cookie, "{%m:%m,%m:%d}\n", // + MG_ESC("user"), MG_ESC(u->name), // + MG_ESC("level"), u->level); +} + +static void mg_handle_logout(struct mg_connection *c) { + char cookie[256]; + mg_snprintf(cookie, sizeof(cookie), + "Set-Cookie: access_token=; Path=/; " + "Expires=Thu, 01 Jan 1970 00:00:00 UTC; " + "%sHttpOnly; Max-Age=0; \r\n", + c->is_tls ? "Secure; " : ""); + mg_http_reply(c, 401, cookie, "Unauthorized\n"); +} + void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { struct mg_dash *dash = (struct mg_dash *) c->fn_data; if (ev == MG_EV_OPEN) { @@ -446,9 +542,24 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { } } else if (ev == MG_EV_HTTP_MSG) { struct mg_http_message *hm = (struct mg_http_message *) ev_data; + struct mg_dash_user *u = mg_dash_authenticate(hm, dash); struct mg_str parts[5]; memset(parts, 0, sizeof(parts)); - if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) { + + MG_DEBUG(("%lu %p %s", c->id, u, u ? u->name : "")); + + if (mg_match(hm->uri, mg_str("/api/hi"), NULL)) { + mg_http_reply(c, 200, MG_JSON_HEADERS, "hi\n"); + } else if (mg_match(hm->uri, mg_str("/api/logout"), NULL)) { + mg_handle_logout(c); + mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m}", MG_ESC("method"), + MG_ESC("logout")); + } else if (mg_match(hm->uri, mg_str("/api/login"), NULL) && u != NULL) { + mg_handle_login(c, u); + } else if (dash->authenticate && u == NULL && + mg_match(hm->uri, mg_str("/api/#"), NULL)) { + mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); + } else if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) { mg_ws_upgrade(c, hm, NULL); } else if (mg_match(hm->uri, mg_str("/files/#"), NULL)) { file_ev_handler(c, ev, ev_data); @@ -481,8 +592,7 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { for (fs = dash->sets; fs != NULL; fs = fs->next) { mg_dash_send_change(c->mgr, fs); } - // Send change notification with no params to indicate we're done for now - mg_dash_broadcast(c->mgr, "{%m:%m}", MG_ESC("method"), MG_ESC("change")); + mg_dash_broadcast(c->mgr, "{%m:%m}", MG_ESC("method"), MG_ESC("ready")); } else if (ev == MG_EV_WS_MSG) { // Add this to automatically handle "get" and "set" JSON-RPC calls struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; diff --git a/mongoose.h b/mongoose.h index febf140b..aedd407b 100644 --- a/mongoose.h +++ b/mongoose.h @@ -3193,6 +3193,8 @@ struct mg_dash_file { struct mg_dash { struct mg_field_set *sets; struct mg_dash_custom_handler *custom_handlers; + int (*authenticate)(const char *user, const char *pass); + int session_auto_expiration_seconds; //struct mg_dash_file *files; }; diff --git a/resources/dashboard.js b/resources/dashboard.js index 1feb05c3..2775e2c4 100644 --- a/resources/dashboard.js +++ b/resources/dashboard.js @@ -5,7 +5,7 @@ const Rpc = (function() { function Rpc(url, onev) { - let ws, id = 1, oncall, q = []; + let ws, id = 1, oncall, q = [], reconnect = true; const pending = new Map(); const realsend = s => { ws.send(s); onev && onev('send', s); }; const fail = e => { for (const p of pending.values()) p[1](e || 'disconnected'); pending.clear(); }; @@ -15,7 +15,7 @@ const connect = () => { ws = new WebSocket(url); ws.onopen = () => { onev && onev('open'); while (q.length) realsend(JSON.stringify(q.shift())); }; - ws.onclose = () => { onev && onev('close'); fail(); setTimeout(connect, 1000); }; + ws.onclose = () => { onev && onev('close'); fail(); reconnect && setTimeout(connect, 1000); }; ws.onmessage = e => { onev && onev('message', e.data); let m; @@ -43,6 +43,7 @@ connect(); return { + close: () => { reconnect = false; ws?.close(); }, call: (method, params) => new Promise((ok, bad) => { const i = id++; pending.set(i, [ok, bad]); @@ -57,16 +58,75 @@ return Rpc; })(); + function rpc_connect() { + if (isMock) { + setTimeout(() => userhandlers['ready']?.(), 500); + return; + } + if (rpc) return rpc; + //if (settings.auth && !status.authed) return null; + rpc = Rpc('api/websocket', function (evname, args) { + if (evname == 'open') status.online = true, rescan(); + if (evname == 'close') status.online = false, rescan(); + if (settings.debug) console.log('WS', evname, args); + }); + rpc.handle((method, args) => { + if (method == 'change' && args) apply_and_rescan(settings.data, args); + if (method == 'logout') rpc_disconnect(); + if (userhandlers[method]) userhandlers[method](args); + }); + return rpc; + } + + function rpc_disconnect() { + if (!rpc) return; + rpc.close(); + rpc = null; + status.online = false; + } + + + function login(params) { + if (isMock) { rpc_connect(); return Promise.resolve({}); } + const opts = {}; + if (params) { + const username = (params?.username ?? '').trim(); + const password = params?.password ?? ''; + const join = username + ':' + password; + opts.headers = { Authorization: `Basic ${btoa(join)}` }; + } + return fetch('api/login', opts) + .then(r => r.ok ? r.json() : r.text().then(t => Promise.reject(t || r.statusText))) + .then(resp => { + status.username = resp?.user || ''; + status.userlevel = resp?.level || 0; + //status.authed = !!status.username; + //if (settings.auth && status.authed) { + rpc_connect(); + //rescan(); + return resp; + }); + }; + + function logout() { + if (isMock) { rpc_disconnect(); return Promise.resolve({}); } + return fetch('api/logout') + .catch(() => true) + .then(() => { + status.username = ''; + status.userlevel = 0; + rpc_disconnect(); + rescan(); + return true; + }); + } + if (global.Dashboard) return; - const isMock = !!window.frameElement || (window !== window.top); + const isMock = location.protocol === 'file:' || !!window.frameElement || (window !== window.top); const settings = {data: {}, edits: {}, debug: true}; const userhandlers = {}; // User event handlers - const status = { online: true, username: '', userlevel: 0, lastseen: '', initialized: false }; - const rpc = isMock ? null : Rpc('api/websocket', function (evname, args) { - if (evname == 'open') status.online = true, rescan(); - if (evname == 'close') status.online = false, rescan(); - if (settings.debug) console.log('WS', evname, args); - }); + const status = { online: true, username: '', userlevel: 0, lastseen: '', ready: false }; + let rpc = null; const fromPath = (path, value) => path.split('.').reverse().reduce((acc, k) => ({ [k]: acc }), value); const get = (obj, path) => path.split('.').reduce((o, k) => o?.[k], obj); @@ -327,40 +387,10 @@ userhandlers[name] = fn; }; - const setbody = visibility => document.body.style.visibility = visibility; - function init(conf) { apply_and_rescan(settings, conf); - if (rpc) { - rpc.handle((method, args) => { - if (method == 'change') { - // "initialized" means that the device has sent us all initial - // data change notifications, and our settings.data contain - // actual refreshed device data - not the UI mock - if (!args) { - setbody('visible'); - status.initialized = true; - } - if (args) apply_and_rescan(settings.data, args); - } - if (method != 'change') console.log('UNKNOWN REQUEST', method, args); - if (userhandlers[method]) userhandlers[method](args); - }); - // After init, fetch all device data - //rpc.call('get').then(r => apply_and_rescan(settings.data, r)); - } else { - setTimeout(function() { - status.initialized = true; - setbody('visible'); - }, 500); - } + if (!settings.auth) rpc_connect(); }; - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", () => setbody('hidden'), { once: true }); - } else { - setbody('hidden'); - } - - global.Dashboard = { init, call, on, status }; + global.Dashboard = { init, call, on, status, login, logout }; })(window); diff --git a/src/dash.c b/src/dash.c index 23008c02..62d4947e 100644 --- a/src/dash.c +++ b/src/dash.c @@ -3,6 +3,14 @@ #define MG_NO_CACHE_HEADERS "Cache-Control: no-cache\r\n" #define MG_JSON_HEADERS "Content-Type: application/json\r\n" MG_NO_CACHE_HEADERS +struct mg_dash_user { + struct mg_dash_user *next; + char name[32]; // User name + char token[21]; // Login token + int level; // Access level + uint64_t expire; // Expiration timestamp +}; + struct mg_upload_state { char marker; // Tells that we're a file upload connection size_t expected; // POST data length, bytes @@ -156,8 +164,10 @@ static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { ok = mg_json_get_bool(json, json_path, (bool *) f->value); } else if (f->type == MG_VAL_INT) { double d; - ok = mg_json_get_num(json, json_path, &d); - if (ok) *(int *) f->value = (int) d; + if (mg_json_get_num(json, json_path, &d) && d == (int) d) { + *(int *) f->value = (int) d; + ok = true; + } } else if (f->type == MG_VAL_DBL) { ok = mg_json_get_num(json, json_path, (double *) f->value); } else if (f->type == MG_VAL_STR) { @@ -173,24 +183,25 @@ static int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash, struct mg_str json) { struct mg_str key, val; size_t ofs = 0; - int count = 0; - // struct mg_field *f = NULL; + int total_count = 0; while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) { struct mg_field_set *set = mg_dash_find_field_set(dash, trimq(key)); + int count = 0; if (set != NULL) { size_t i; for (i = 0; set->fields[i].name != NULL; i++) { if (mg_dash_parse_field(val, &set->fields[i])) count++; } - if (count > 0) { + if (count) { if (set->writer) set->writer(); mg_dash_send_change(c->mgr, set); + total_count += count; } } else { MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf)); } } - return count; + return total_count; } static void mg_dash_process_msg(struct mg_connection *c, @@ -284,8 +295,8 @@ static void file_ev_handler(struct mg_connection *c, int ev, void *ev_data) { mg_http_reply(c, 500, NULL, "File upload error %d\n", errno); } else { mg_dash_file_add(hm->uri, hm->body.len); - MG_DEBUG(("Starting upload, [%.*s] %lu", hm->uri.len, hm->uri.buf, - hm->body.len)); + MG_DEBUG(("%lu Starting upload, [%.*s] %lu", c->id, hm->uri.len, + hm->uri.buf, hm->body.len)); us->marker = 'U'; us->expected = hm->body.len; us->received = 0; @@ -308,7 +319,7 @@ static void file_ev_handler(struct mg_connection *c, int ev, void *ev_data) { c->recv.len = 0; if (us->received >= us->expected) { fclose((FILE *) us->ctx); - MG_DEBUG(("Uploaded %lu", us->expected)); + MG_DEBUG(("%lu Uploaded %lu", c->id, us->expected)); mg_http_reply(c, 200, NULL, "%lu uploaded\n", us->expected); mg_dash_send_change(c->mgr, &set_files); memset(us, 0, sizeof(*us)); @@ -317,6 +328,91 @@ static void file_ev_handler(struct mg_connection *c, int ev, void *ev_data) { } } +static uint64_t mg_dash_make_expiration_time(struct mg_dash *dash) { + unsigned t = (unsigned) dash->session_auto_expiration_seconds; + if (t == 0) t = 3600; // Default expiration time in seconds + return mg_millis() + t * 1000; +} + +// Parse HTTP requests, return authenticated user or NULL +static struct mg_dash_user *mg_dash_authenticate(struct mg_http_message *hm, + struct mg_dash *dash) { + static struct mg_dash_user *s_users; // List of authenticated users + char user[100], pass[100]; + struct mg_dash_user *u, *tmp, *result = NULL; + + if (dash->authenticate == NULL) return NULL; + mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass)); + + // Remove expired users + for (u = s_users; u != NULL; u = tmp) { + tmp = u->next; + if (u->expire < mg_millis()) { + MG_DEBUG(("Deleting expired auth %s/%d %llu %u", u->name, u->level, + u->expire, mg_millis() - u->expire)); + LIST_DELETE(struct mg_dash_user, &s_users, u); + } + } + + if (pass[0] != '\0') { + struct mg_str *ah = mg_http_get_header(hm, "Authorization"); + if (ah != NULL) { + // Auth header and password are set, auth by user/password via glue API + int num_users = 0, level = dash->authenticate(user, pass); + MG_DEBUG(("user %s, level: %d", user, level)); + if (level > 0) { // Proceed only if the firmware authenticated us + for (u = s_users; u != NULL && result == NULL; + u = u->next, num_users++) { + if (strcmp(user, u->name) == 0) { + u->expire = mg_dash_make_expiration_time(dash); + result = u; + } + } + // Not yet authenticated, add to the list + if (result == NULL && num_users < 10) { + result = (struct mg_dash_user *) mg_calloc(1, sizeof(*result)); + mg_snprintf(result->name, sizeof(result->name), "%s", user); + mg_random_str(result->token, sizeof(result->token) - 1); + result->level = level, result->next = s_users, s_users = result; + result->expire = mg_dash_make_expiration_time(dash); + } + } + } else if (ah == NULL) { + for (u = s_users; u != NULL && result == NULL; u = u->next) { + if (strcmp(u->token, pass) == 0) { + u->expire = mg_dash_make_expiration_time(dash); + result = u; + } + } + } + } + + // MG_DEBUG(("[%s/%s] -> %s", user, pass, result ? "OK" : "FAIL")); + return result; +} + +static void mg_handle_login(struct mg_connection *c, struct mg_dash_user *u) { + char cookie[256]; + mg_snprintf(cookie, sizeof(cookie), + "Set-Cookie: access_token=%s; Path=/; " + "%sHttpOnly; SameSite=Lax; Max-Age=%d\r\n%s", + u->token, c->is_tls ? "Secure; " : "", 3600 * 24, + MG_JSON_HEADERS); + mg_http_reply(c, 200, cookie, "{%m:%m,%m:%d}\n", // + MG_ESC("user"), MG_ESC(u->name), // + MG_ESC("level"), u->level); +} + +static void mg_handle_logout(struct mg_connection *c) { + char cookie[256]; + mg_snprintf(cookie, sizeof(cookie), + "Set-Cookie: access_token=; Path=/; " + "Expires=Thu, 01 Jan 1970 00:00:00 UTC; " + "%sHttpOnly; Max-Age=0; \r\n", + c->is_tls ? "Secure; " : ""); + mg_http_reply(c, 401, cookie, "Unauthorized\n"); +} + void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { struct mg_dash *dash = (struct mg_dash *) c->fn_data; if (ev == MG_EV_OPEN) { @@ -325,9 +421,24 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { } } else if (ev == MG_EV_HTTP_MSG) { struct mg_http_message *hm = (struct mg_http_message *) ev_data; + struct mg_dash_user *u = mg_dash_authenticate(hm, dash); struct mg_str parts[5]; memset(parts, 0, sizeof(parts)); - if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) { + + MG_DEBUG(("%lu %p %s", c->id, u, u ? u->name : "")); + + if (mg_match(hm->uri, mg_str("/api/hi"), NULL)) { + mg_http_reply(c, 200, MG_JSON_HEADERS, "hi\n"); + } else if (mg_match(hm->uri, mg_str("/api/logout"), NULL)) { + mg_handle_logout(c); + mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m}", MG_ESC("method"), + MG_ESC("logout")); + } else if (mg_match(hm->uri, mg_str("/api/login"), NULL) && u != NULL) { + mg_handle_login(c, u); + } else if (dash->authenticate && u == NULL && + mg_match(hm->uri, mg_str("/api/#"), NULL)) { + mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); + } else if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) { mg_ws_upgrade(c, hm, NULL); } else if (mg_match(hm->uri, mg_str("/files/#"), NULL)) { file_ev_handler(c, ev, ev_data); @@ -360,8 +471,7 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { for (fs = dash->sets; fs != NULL; fs = fs->next) { mg_dash_send_change(c->mgr, fs); } - // Send change notification with no params to indicate we're done for now - mg_dash_broadcast(c->mgr, "{%m:%m}", MG_ESC("method"), MG_ESC("change")); + mg_dash_broadcast(c->mgr, "{%m:%m}", MG_ESC("method"), MG_ESC("ready")); } else if (ev == MG_EV_WS_MSG) { // Add this to automatically handle "get" and "set" JSON-RPC calls struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; diff --git a/src/dash.h b/src/dash.h index ef2b4444..e92a1290 100644 --- a/src/dash.h +++ b/src/dash.h @@ -48,6 +48,8 @@ struct mg_dash_file { struct mg_dash { struct mg_field_set *sets; struct mg_dash_custom_handler *custom_handlers; + int (*authenticate)(const char *user, const char *pass); + int session_auto_expiration_seconds; //struct mg_dash_file *files; }; diff --git a/tutorials/device-dashboard/full/dashboard.c b/tutorials/device-dashboard/full/dashboard.c index e02956e9..545271ff 100644 --- a/tutorials/device-dashboard/full/dashboard.c +++ b/tutorials/device-dashboard/full/dashboard.c @@ -162,6 +162,17 @@ static struct mg_field_set set_graph2 = { "graph2", fields_graph2, read_graph2, NULL, 0, 0, NULL, }; +static int authenticate(const char *user, const char *pass) { + int level = 0; // Authentication failure + if (strcmp(pass, "admin") == 0) { + level = 7; // Administrator + } else if (strcmp(pass, "user") == 0) { + level = 3; // Ordinary dude + } + (void) user; + return level; +} + void mg_dash_init(struct mg_mgr *mgr) { static struct mg_dash dash; // Important: keep it static! @@ -176,6 +187,9 @@ void mg_dash_init(struct mg_mgr *mgr) { mg_dash_file_add(mg_str("device-config.json"), 1234); mg_dash_file_add(mg_str("device-log-2026-04-25.txt"), 1327854); + // Require authentication + dash.authenticate = authenticate; + mg_http_listen(mgr, MG_HTTP_ADDR, mg_dash_ev_handler, &dash); } diff --git a/tutorials/device-dashboard/full/dashboard.html b/tutorials/device-dashboard/full/dashboard.html index 79f6076f..bfabe58d 100644 --- a/tutorials/device-dashboard/full/dashboard.html +++ b/tutorials/device-dashboard/full/dashboard.html @@ -4,10 +4,14 @@ My Dashboard + + - -
+ +

LED control version ${status.version}