Author: Andy Green Date: Fri Jul 10 16:48:47 2026 +0100 oauth: provide central enum at front and backend diff --git a/assets/sai.css b/assets/sai.css index bdbf454..564f857 100644 --- a/assets/sai.css +++ b/assets/sai.css @@ -1700,3 +1700,39 @@ div.ibuil.vm-builder { background-color: var(--term-cursor-bg); color: var(--term-cursor-fg); } + +/* Color the avatar silhouette based on grant level */ +.login-status-container.grant-admin img, +.login-status-container.grant-admin svg, +.login-status-container.grant-admin .avatar { + filter: drop-shadow(0 0 2px rgba(46, 204, 64, 0.4)); + border-color: #2ecc40 !important; +} + +.login-status-container.grant-admin svg path { + fill: #2ecc40 !important; +} + +.login-status-container.grant-user img, +.login-status-container.grant-user svg, +.login-status-container.grant-user .avatar { + filter: drop-shadow(0 0 2px rgba(243, 156, 18, 0.4)); + border-color: #f39c12 !important; +} + +.login-status-container.grant-user svg path { + fill: #f39c12 !important; +} + +.login-status-container.grant-none img, +.login-status-container.grant-none svg, +.login-status-container.grant-none .avatar { + filter: drop-shadow(0 0 2px rgba(231, 76, 60, 0.4)); + border-color: #e74c3c !important; + opacity: 0.6; +} + +.login-status-container.grant-none svg path { + fill: #e74c3c !important; +} + diff --git a/assets/sai.js b/assets/sai.js index 1907408..4e3071c 100644 --- a/assets/sai.js +++ b/assets/sai.js @@ -394,7 +394,14 @@ var lang_zhs = "{" + "\"%{pf}前创建, 创作时间: %{ct}ms \"" + "}}"; -var logs = "", redpend = 0, gitohashi_integ = 0, authd = 0, auth_is_admin = 0, auth_grant_level = -1, exptimer, auth_user = "", +const SaiAuthState = { + NOT_LOGGED_IN: 0, + LOGGED_IN_NO_GRANT: 1, + LOGGED_IN_GRANT_USER: 2, // < :2 + LOGGED_IN_GRANT_ADMIN: 3 // >= :2 +}; + +var logs = "", redpend = 0, gitohashi_integ = 0, authd = 0, auth_is_admin = 0, auth_grant_level = -1, auth_state = SaiAuthState.NOT_LOGGED_IN, exptimer, auth_user = "", active_terminals = {}; logAnsiState = {}, logs_pending = "", lines_pending = "", times_pending = "", ongoing_task_activities = {}, last_log_timestamp = 0, spreadsheet_data_cache = {}, loadreport_data_cache = {}, @@ -1079,10 +1086,10 @@ function sai_taskinfo_render(t, now_ut) sai_event_render(t, now_ut, 0) + "" + "" + sai_plat_icon(t.t.platform, 2) + san(t.t.platform) + " "; - if (auth_is_admin && t.t.state != 0 && t.t.state != 3 && t.t.state != 4 && t.t.state != 5) + if (auth_state === SaiAuthState.LOGGED_IN_GRANT_ADMIN && t.t.state != 0 && t.t.state != 3 && t.t.state != 4 && t.t.state != 5) s += "\"stop "; - if (auth_is_admin) + if (auth_state === SaiAuthState.LOGGED_IN_GRANT_ADMIN) s += "\"rebuild\" "; s += sai_stateful_taskname(t.t.state, t.t.taskname, 1); @@ -1308,7 +1315,7 @@ function sai_event_summary_render(o, now_ut, reset_all_icon) s += "
"; s += ""; - if (reset_all_icon && !gitohashi_integ && auth_is_admin) { + if (reset_all_icon && !gitohashi_integ && auth_state === SaiAuthState.LOGGED_IN_GRANT_ADMIN) { s += "
\"rebuild "; s += "\"delete
` + `
` + ``; - if (authd && auth_is_admin && plat.peer_ip) + if (auth_state === SaiAuthState.LOGGED_IN_GRANT_ADMIN && plat.peer_ip) innerHTML += `
${hsanitize(plat.peer_ip)}
`; innerHTML += ``; @@ -1666,7 +1673,7 @@ function createBuilderDiv(plat) { { label: `LWS: ${plat.lws_hash}` }, ]; - if (authd && auth_is_admin && !plat.online) { + if (auth_state === SaiAuthState.LOGGED_IN_GRANT_ADMIN && !plat.online) { menuItems.push({ label: "Delete Builder", callback: () => { @@ -1681,7 +1688,7 @@ function createBuilderDiv(plat) { }); } - if (authd && auth_is_admin) { + if (auth_state === SaiAuthState.LOGGED_IN_GRANT_ADMIN) { menuItems.push({ label: "Open Shell", callback: () => { @@ -1895,7 +1902,7 @@ function createPconDiv(pcon) { { label: `PCON: ${pcon.name}` } ]; - if (authd) { + if (auth_state === SaiAuthState.LOGGED_IN_GRANT_ADMIN) { if (isActuallyOn) { menuItems.push({ label: "Turn Off", @@ -3268,11 +3275,33 @@ window.addEventListener("load", function() { .then(function(res) { return res.json(); }) .then(function(data) { console.log("LOGIN STATUS DEBUG:", data); - if (data.logged_in && data.has_grant) { - authd = 1; - auth_grant_level = data.grant_level !== undefined ? data.grant_level : -1; - if (data.is_admin || auth_grant_level >= 2) - auth_is_admin = 1; + auth_state = SaiAuthState.NOT_LOGGED_IN; + if (data.logged_in) { + if (data.has_grant) { + authd = 1; + auth_grant_level = data.grant_level !== undefined ? data.grant_level : -1; + const isAdmin = data.is_admin === true || data.is_admin === 1 || data.is_admin === "true" || data.is_admin === "1"; + if (auth_grant_level >= 2 || (auth_grant_level === -1 && isAdmin)) { + auth_state = SaiAuthState.LOGGED_IN_GRANT_ADMIN; + auth_is_admin = 1; + } else { + auth_state = SaiAuthState.LOGGED_IN_GRANT_USER; + } + } else { + auth_state = SaiAuthState.LOGGED_IN_NO_GRANT; + } + + const container = document.getElementById('lws-login-status-container'); + if (container) { + container.classList.remove('grant-admin', 'grant-user', 'grant-none'); + if (auth_state === SaiAuthState.LOGGED_IN_GRANT_ADMIN) { + container.classList.add('grant-admin'); + } else if (auth_state === SaiAuthState.LOGGED_IN_GRANT_USER) { + container.classList.add('grant-user'); + } else if (auth_state === SaiAuthState.LOGGED_IN_NO_GRANT) { + container.classList.add('grant-none'); + } + } } }) .catch(function(err) { @@ -3310,7 +3339,7 @@ window.addEventListener("load", function() { target = target.parentElement; } - if (taskDiv && auth_is_admin) { + if (taskDiv && auth_state === SaiAuthState.LOGGED_IN_GRANT_ADMIN) { event.preventDefault(); const taskUuid = taskDiv.id.substring(10); diff --git a/src/web/w-comms.c b/src/web/w-comms.c index 7eb11d2..82fab82 100644 --- a/src/web/w-comms.c +++ b/src/web/w-comms.c @@ -505,16 +505,24 @@ http_resp: } #endif - pss->authorized = 0; + pss->auth_state = SAI_AUTH_STATE_NOT_LOGGED_IN; if (vhd->has_jwk) { #if defined(LWS_WITH_JOSE) const char *reason = "unknown"; struct lws_jwt_auth *ja = lws_jwt_auth_create(wsi, &vhd->jwk, vhd->cookie_name, NULL, NULL, &reason); if (ja) { - if (lws_jwt_auth_query_grant(ja, "*") >= 1 || lws_jwt_auth_query_grant(ja, "com.warmcat.sai") >= 1) { - pss->authorized = 1; + int grant = (int)lws_jwt_auth_query_grant(ja, "com.warmcat.sai"); + int grant_all = (int)lws_jwt_auth_query_grant(ja, "*"); + int max_grant = grant > grant_all ? grant : grant_all; + + if (max_grant >= 2) { + pss->auth_state = SAI_AUTH_STATE_LOGGED_IN_GRANT_ADMIN; lwsl_wsi_notice(wsi, "Authorized WebSocket connection (admin/grant)"); + } else if (max_grant >= 1) { + pss->auth_state = SAI_AUTH_STATE_LOGGED_IN_GRANT_USER; + lwsl_wsi_notice(wsi, "Authorized WebSocket connection (user/grant)"); } else { + pss->auth_state = SAI_AUTH_STATE_LOGGED_IN_NO_GRANT; lwsl_wsi_err(wsi, "JWT validation passed, but no grant found"); } lws_jwt_auth_destroy(&ja); diff --git a/src/web/w-private.h b/src/web/w-private.h index 2e9e057..b9e9ecf 100644 --- a/src/web/w-private.h +++ b/src/web/w-private.h @@ -59,6 +59,14 @@ enum { SAIM_SPECIFIC_TASK, }; +typedef enum { + SAI_AUTH_STATE_NOT_LOGGED_IN, + SAI_AUTH_STATE_LOGGED_IN_NO_GRANT, + SAI_AUTH_STATE_LOGGED_IN_GRANT_USER, /* < :2 */ + SAI_AUTH_STATE_LOGGED_IN_GRANT_ADMIN /* >= :2 */ +} sai_auth_state_t; + + struct pss { struct vhd *vhd; struct lws *wsi; @@ -124,7 +132,7 @@ struct pss { unsigned int announced:1; unsigned int bulk_binary_data:1; unsigned int toggle_favour_sch:1; - unsigned int authorized:1; + sai_auth_state_t auth_state; }; struct vhd { diff --git a/src/web/w-ws-browser.c b/src/web/w-ws-browser.c index 295ed0b..fdc352b 100644 --- a/src/web/w-ws-browser.c +++ b/src/web/w-ws-browser.c @@ -645,7 +645,7 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, * matched on */ - if (!pss->authorized && ( + if (pss->auth_state != SAI_AUTH_STATE_LOGGED_IN_GRANT_ADMIN && ( a.top_schema_index == SAIM_WS_BROWSER_RX_TASKRESET || a.top_schema_index == SAIM_WS_BROWSER_RX_TASKREMOVEALLTRIES || a.top_schema_index == SAIM_WS_BROWSER_RX_TASKREBUILDLASTSTEP || @@ -654,6 +654,8 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, a.top_schema_index == SAIM_WS_BROWSER_RX_TASKCANCEL || a.top_schema_index == SAIM_WS_BROWSER_RX_REBUILD || a.top_schema_index == SAIM_WS_BROWSER_RX_PLATRESET || + a.top_schema_index == SAIM_WS_BROWSER_RX_STAY || + a.top_schema_index == SAIM_WS_BROWSER_RX_PCON_CONTROL || a.top_schema_index == SAIM_WS_BROWSER_RX_BUILDERDELETE || a.top_schema_index == SAIM_WS_BROWSER_RX_OPENSHELL || a.top_schema_index == SAIM_WS_BROWSER_RX_CLOSESHELL ||