diff --git a/main/service/auth.c b/main/service/auth.c new file mode 100644 index 0000000..69149e8 --- /dev/null +++ b/main/service/auth.c @@ -0,0 +1,222 @@ +#include "auth.h" + +#include +#include +#include +#include +#include +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +static const char* TAG = "AUTH"; + +typedef struct +{ + char token[TOKEN_LENGTH]; + bool active; + time_t creation_time; +} auth_token_t; + +static auth_token_t s_tokens[MAX_TOKENS]; +static SemaphoreHandle_t s_token_mutex; + +void auth_init(void) +{ + s_token_mutex = xSemaphoreCreateMutex(); + if (s_token_mutex == NULL) + { + ESP_LOGE(TAG, "Failed to create token mutex"); + return; + } + for (int i = 0; i < MAX_TOKENS; i++) + { + s_tokens[i].active = false; + s_tokens[i].token[0] = '\0'; + } + ESP_LOGI(TAG, "Auth module initialized."); +} + +char* auth_generate_token(void) +{ + if (xSemaphoreTake(s_token_mutex, portMAX_DELAY) != pdTRUE) + { + ESP_LOGE(TAG, "Failed to take token mutex"); + return NULL; + } + + int free_slot = -1; + for (int i = 0; i < MAX_TOKENS; i++) + { + if (!s_tokens[i].active) + { + free_slot = i; + break; + } + } + + if (free_slot == -1) + { + ESP_LOGW(TAG, "No free token slots available. Invalidating oldest token."); + time_t oldest_time = time(NULL); + int oldest_idx = -1; + for (int i = 0; i < MAX_TOKENS; i++) + { + if (s_tokens[i].active && s_tokens[i].creation_time < oldest_time) + { + oldest_time = s_tokens[i].creation_time; + oldest_idx = i; + } + } + if (oldest_idx != -1) + { + s_tokens[oldest_idx].active = false; + free_slot = oldest_idx; + ESP_LOGI(TAG, "Oldest token at index %d invalidated.", oldest_idx); + } + else + { + ESP_LOGE(TAG, "Could not find an oldest token to invalidate. This should not happen if all are active."); + xSemaphoreGive(s_token_mutex); + return NULL; + } + } + + const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + char* new_token = (char*)malloc(TOKEN_LENGTH); + if (new_token == NULL) + { + ESP_LOGE(TAG, "Failed to allocate memory for new token"); + xSemaphoreGive(s_token_mutex); + return NULL; + } + + for (int i = 0; i < TOKEN_LENGTH - 1; i++) + { + new_token[i] = charset[esp_random() % (sizeof(charset) - 1)]; + } + new_token[TOKEN_LENGTH - 1] = '\0'; + + strncpy(s_tokens[free_slot].token, new_token, TOKEN_LENGTH); + s_tokens[free_slot].active = true; + s_tokens[free_slot].creation_time = time(NULL); + + ESP_LOGI(TAG, "Generated new token at slot %d: %s", free_slot, new_token); + + xSemaphoreGive(s_token_mutex); + return new_token; +} + +bool auth_validate_token(const char* token) +{ + if (token == NULL) + { + return false; + } + + if (xSemaphoreTake(s_token_mutex, portMAX_DELAY) != pdTRUE) + { + ESP_LOGE(TAG, "Failed to take token mutex"); + return false; + } + + bool valid = false; + for (int i = 0; i < MAX_TOKENS; i++) + { + if (s_tokens[i].active && strcmp(s_tokens[i].token, token) == 0) + { + valid = true; + break; + } + } + + xSemaphoreGive(s_token_mutex); + return valid; +} + +void auth_invalidate_token(const char* token) +{ + if (token == NULL) + { + return; + } + + if (xSemaphoreTake(s_token_mutex, portMAX_DELAY) != pdTRUE) + { + ESP_LOGE(TAG, "Failed to take token mutex"); + return; + } + + for (int i = 0; i < MAX_TOKENS; i++) + { + if (s_tokens[i].active && strcmp(s_tokens[i].token, token) == 0) + { + s_tokens[i].active = false; + s_tokens[i].token[0] = '\0'; // Clear token string + ESP_LOGI(TAG, "Token at slot %d invalidated.", i); + break; + } + } + + xSemaphoreGive(s_token_mutex); +} + +void auth_cleanup_expired_tokens(void) { ESP_LOGD(TAG, "auth_cleanup_expired_tokens called (no-op for now)."); } + +static const char* get_token_from_header(httpd_req_t* req) +{ + char* auth_header = NULL; + size_t buf_len; + + if (httpd_req_get_hdr_value_len(req, "Authorization") == 0) + { + return NULL; + } + + buf_len = httpd_req_get_hdr_value_len(req, "Authorization") + 1; + auth_header = (char*)malloc(buf_len); + if (auth_header == NULL) + { + ESP_LOGE(TAG, "Failed to allocate memory for auth header"); + return NULL; + } + + if (httpd_req_get_hdr_value_str(req, "Authorization", auth_header, buf_len) != ESP_OK) + { + free(auth_header); + return NULL; + } + + if (strncmp(auth_header, "Bearer ", 7) == 0) + { + const char* token = auth_header + 7; + return token; + } + + free(auth_header); + return NULL; +} + +esp_err_t api_auth_check(httpd_req_t* req) +{ + const char* token = get_token_from_header(req); + + if (token == NULL) + { + ESP_LOGW(TAG, "API access attempt without token for URI: %s", req->uri); + httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Authorization token required"); + return ESP_FAIL; + } + + if (!auth_validate_token(token)) + { + ESP_LOGW(TAG, "API access attempt with invalid token for URI: %s", req->uri); + httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Invalid or expired token"); + free((void*)token - 7); + return ESP_FAIL; + } + + ESP_LOGD(TAG, "Token validated for URI: %s", req->uri); + free((void*)token - 7); + return ESP_OK; +} diff --git a/main/service/auth.h b/main/service/auth.h new file mode 100644 index 0000000..48ea4f6 --- /dev/null +++ b/main/service/auth.h @@ -0,0 +1,28 @@ +#ifndef AUTH_H +#define AUTH_H + +#include +#include "esp_err.h" +#include "esp_http_server.h" + +#define MAX_TOKENS 4 +#define TOKEN_LENGTH 33 // 32 characters + null terminator + +// Function to initialize the authentication module +void auth_init(void); + +// Function to generate a new token +char* auth_generate_token(void); + +// Function to validate a token +bool auth_validate_token(const char* token); + +// Function to invalidate a token (e.g., on logout) +void auth_invalidate_token(const char* token); + +// Function to clean up expired tokens (if any) +void auth_cleanup_expired_tokens(void); + +esp_err_t api_auth_check(httpd_req_t* req); + +#endif // AUTH_H diff --git a/main/service/control.c b/main/service/control.c index 429a5f9..f0fbf8d 100644 --- a/main/service/control.c +++ b/main/service/control.c @@ -5,9 +5,15 @@ #include "freertos/FreeRTOS.h" #include "sw.h" #include "webserver.h" +#include "auth.h" static esp_err_t control_get_handler(httpd_req_t* req) { + esp_err_t err = api_auth_check(req); + if (err != ESP_OK) { + return err; + } + cJSON* root = cJSON_CreateObject(); cJSON_AddBoolToObject(root, "load_12v_on", get_main_load_switch()); @@ -25,6 +31,11 @@ static esp_err_t control_get_handler(httpd_req_t* req) static esp_err_t control_post_handler(httpd_req_t* req) { + esp_err_t err = api_auth_check(req); + if (err != ESP_OK) { + return err; + } + char buf[128]; int ret, remaining = req->content_len; diff --git a/main/service/setting.c b/main/service/setting.c index c75158c..c47a88f 100644 --- a/main/service/setting.c +++ b/main/service/setting.c @@ -8,11 +8,17 @@ #include "nconfig.h" #include "webserver.h" #include "wifi.h" +#include "auth.h" static const char* TAG = "webserver"; static esp_err_t setting_get_handler(httpd_req_t* req) { + esp_err_t err = api_auth_check(req); + if (err != ESP_OK) { + return err; + } + wifi_ap_record_t ap_info; cJSON* root = cJSON_CreateObject(); @@ -103,6 +109,11 @@ static esp_err_t setting_get_handler(httpd_req_t* req) static esp_err_t wifi_scan(httpd_req_t* req) { + esp_err_t err = api_auth_check(req); + if (err != ESP_OK) { + return err; + } + wifi_ap_record_t* ap_records; uint16_t count; @@ -133,6 +144,11 @@ static esp_err_t wifi_scan(httpd_req_t* req) static esp_err_t setting_post_handler(httpd_req_t* req) { + esp_err_t err = api_auth_check(req); + if (err != ESP_OK) { + return err; + } + char buf[512]; int received = httpd_req_recv(req, buf, sizeof(buf) - 1); diff --git a/main/service/system.c b/main/service/system.c index 3992eaa..b55f6a2 100644 --- a/main/service/system.c +++ b/main/service/system.c @@ -9,6 +9,7 @@ #include #include "esp_http_server.h" #include "esp_system.h" +#include "auth.h" static const char* TAG = "odroid"; @@ -50,6 +51,11 @@ void start_reboot_timer(int sec) static esp_err_t reboot_post_handler(httpd_req_t* req) { + esp_err_t err = api_auth_check(req); + if (err != ESP_OK) { + return err; + } + httpd_resp_set_type(req, "application/json"); const char* resp_str = "{\"status\": \"reboot timer started\"}"; httpd_resp_send(req, resp_str, strlen(resp_str)); @@ -80,6 +86,11 @@ void register_reboot_endpoint(httpd_handle_t server) static esp_err_t version_get_handler(httpd_req_t* req) { + esp_err_t err = api_auth_check(req); + if (err != ESP_OK) { + return err; + } + httpd_resp_set_type(req, "application/json"); char buf[100]; sprintf(buf, "{\"version\": \"%s-%s\"}", VERSION_TAG, VERSION_HASH); diff --git a/main/service/webserver.c b/main/service/webserver.c index bb2f3db..e5e5edf 100644 --- a/main/service/webserver.c +++ b/main/service/webserver.c @@ -11,6 +11,8 @@ #include "monitor.h" #include "nconfig.h" #include "system.h" +#include "cJSON.h" +#include "auth.h" static const char* TAG = "WEBSERVER"; @@ -42,9 +44,70 @@ static esp_err_t index_handler(httpd_req_t* req) return ESP_OK; } +static esp_err_t login_handler(httpd_req_t* req) +{ + char content[100]; // Adjust size as needed for username/password + int ret = httpd_req_recv(req, content, sizeof(content) - 1); // -1 for null terminator + if (ret <= 0) { // 0 means connection closed, < 0 means error + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + httpd_resp_send_408(req); + } + return ESP_FAIL; + } + content[ret] = '\0'; // Null-terminate the received data + + ESP_LOGI(TAG, "Received login request: %s", content); + + cJSON *root = cJSON_Parse(content); + if (root == NULL) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); + return ESP_FAIL; + } + + cJSON *username_json = cJSON_GetObjectItemCaseSensitive(root, "username"); + cJSON *password_json = cJSON_GetObjectItemCaseSensitive(root, "password"); + + if (!cJSON_IsString(username_json) || (username_json->valuestring == NULL) || + !cJSON_IsString(password_json) || (password_json->valuestring == NULL)) { + cJSON_Delete(root); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing username or password"); + return ESP_FAIL; + } + + const char *username = username_json->valuestring; + const char *password = password_json->valuestring; + + // TODO: Implement actual credential validation + // For now, a simple hardcoded check + if (strcmp(username, "admin") == 0 && strcmp(password, "password") == 0) { + char *token = auth_generate_token(); + if (token) { + cJSON *response_root = cJSON_CreateObject(); + cJSON_AddStringToObject(response_root, "token", token); + char *json_response = cJSON_Print(response_root); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, json_response); + + free(token); // Free the token generated by auth_generate_token + free(json_response); + cJSON_Delete(response_root); + } else { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to generate token"); + } + } else { + httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Invalid credentials"); + } + + cJSON_Delete(root); + return ESP_OK; +} + void start_webserver(void) { + auth_init(); + httpd_handle_t server = NULL; httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.stack_size = 1024 * 8; @@ -61,6 +124,10 @@ void start_webserver(void) httpd_uri_t index = {.uri = "/", .method = HTTP_GET, .handler = index_handler, .user_ctx = NULL}; httpd_register_uri_handler(server, &index); + // Login endpoint + httpd_uri_t login = {.uri = "/login", .method = HTTP_POST, .handler = login_handler, .user_ctx = NULL}; + httpd_register_uri_handler(server, &login); + register_wifi_endpoint(server); register_ws_endpoint(server); register_control_endpoint(server); diff --git a/main/service/ws.c b/main/service/ws.c index 81b4022..3f437ae 100644 --- a/main/service/ws.c +++ b/main/service/ws.c @@ -12,6 +12,7 @@ #include "pb_encode.h" #include "status.pb.h" #include "webserver.h" +#include "auth.h" #define UART_NUM UART_NUM_1 #define BUF_SIZE (2048) @@ -202,6 +203,11 @@ static void uart_event_task(void* arg) static esp_err_t ws_handler(httpd_req_t* req) { + // esp_err_t err = api_auth_check(req); + // if (err != ESP_OK) { + // return err; + // } + if (req->method == HTTP_GET) { ESP_LOGI(TAG, "Handshake done, the new connection was opened"); diff --git a/page/index.html b/page/index.html index 9f7efa9..8f3d57e 100644 --- a/page/index.html +++ b/page/index.html @@ -8,7 +8,33 @@ -
+ + +
@@ -37,6 +63,9 @@ data-bs-target="#settingsModal"> +
diff --git a/page/src/api.js b/page/src/api.js index 37cb096..44eadfe 100644 --- a/page/src/api.js +++ b/page/src/api.js @@ -4,15 +4,63 @@ * It abstracts the fetch logic, error handling, and JSON parsing for network and control operations. */ +// Function to get authentication headers +export function getAuthHeaders() { + const token = localStorage.getItem('authToken'); + if (token) { + return { 'Authorization': `Bearer ${token}` }; + } + return {}; +} + +// Global error handler for unauthorized responses +export async function handleResponse(response) { + if (response.status === 401) { + // Unauthorized, log out the user + localStorage.removeItem('authToken'); + // Redirect to login or trigger a logout event + // For now, we'll just reload the page, which will trigger the login screen + window.location.reload(); + throw new Error('Unauthorized: Session expired or invalid token.'); + } + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || `HTTP error! status: ${response.status}`); + } + return response; +} + +/** + * Authenticates a user with the provided username and password. + * @param {string} username The user's username. + * @param {string} password The user's password. + * @returns {Promise} A promise that resolves to the server's JSON response containing a token. + * @throws {Error} Throws an error if the authentication fails. + */ +export async function login(username, password) { + const response = await fetch('/login', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ username, password }), + }); + // Login function does not use handleResponse as it's for obtaining the token + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || `Login failed with status: ${response.status}`); + } + return await response.json(); +} + /** * Fetches the list of available Wi-Fi networks from the server. * @returns {Promise>} A promise that resolves to an array of Wi-Fi access point objects. * @throws {Error} Throws an error if the network request fails. */ export async function fetchWifiScan() { - const response = await fetch('/api/wifi/scan'); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - return await response.json(); + const response = await fetch('/api/wifi/scan', { + headers: getAuthHeaders(), + }); + return await handleResponse(response).then(res => res.json()); } /** @@ -23,16 +71,15 @@ export async function fetchWifiScan() { * @throws {Error} Throws an error if the connection request fails. */ export async function postWifiConnect(ssid, password) { - const response = await fetch('/api/setting', { // Updated URL + const response = await fetch('/api/setting', { method: 'POST', - headers: {'Content-Type': 'application/json'}, + headers: { + 'Content-Type': 'application/json', + ...getAuthHeaders(), + }, body: JSON.stringify({ssid, password}), }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || `Connection failed with status: ${response.status}`); - } - return await response.json(); + return await handleResponse(response).then(res => res.json()); } /** @@ -42,16 +89,15 @@ export async function postWifiConnect(ssid, password) { * @throws {Error} Throws an error if the request fails. */ export async function postNetworkSettings(payload) { - const response = await fetch('/api/setting', { // Updated URL + const response = await fetch('/api/setting', { method: 'POST', - headers: {'Content-Type': 'application/json'}, + headers: { + 'Content-Type': 'application/json', + ...getAuthHeaders(), + }, body: JSON.stringify(payload), }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || `Failed to apply settings with status: ${response.status}`); - } - return response; + return await handleResponse(response); } /** @@ -63,14 +109,13 @@ export async function postNetworkSettings(payload) { export async function postBaudRateSetting(baudrate) { const response = await fetch('/api/setting', { method: 'POST', - headers: {'Content-Type': 'application/json'}, + headers: { + 'Content-Type': 'application/json', + ...getAuthHeaders(), + }, body: JSON.stringify({baudrate}), }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || `Failed to apply baudrate with status: ${response.status}`); - } - return response; + return await handleResponse(response); } /** @@ -79,9 +124,10 @@ export async function postBaudRateSetting(baudrate) { * @throws {Error} Throws an error if the network request fails. */ export async function fetchSettings() { - const response = await fetch('/api/setting'); // Updated URL - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - return await response.json(); + const response = await fetch('/api/setting', { + headers: getAuthHeaders(), + }); + return await handleResponse(response).then(res => res.json()); } /** @@ -90,9 +136,10 @@ export async function fetchSettings() { * @throws {Error} Throws an error if the network request fails. */ export async function fetchControlStatus() { - const response = await fetch('/api/control'); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - return await response.json(); + const response = await fetch('/api/control', { + headers: getAuthHeaders(), + }); + return await handleResponse(response).then(res => res.json()); } /** @@ -104,11 +151,13 @@ export async function fetchControlStatus() { export async function postControlCommand(command) { const response = await fetch('/api/control', { method: 'POST', - headers: {'Content-Type': 'application/json'}, + headers: { + 'Content-Type': 'application/json', + ...getAuthHeaders(), + }, body: JSON.stringify(command) }); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - return response; + return await handleResponse(response); } /** @@ -117,7 +166,8 @@ export async function postControlCommand(command) { * @throws {Error} Throws an error if the network request fails. */ export async function fetchVersion() { - const response = await fetch('/api/version'); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - return await response.json(); + const response = await fetch('/api/version', { + headers: getAuthHeaders(), + }); + return await handleResponse(response).then(res => res.json()); } diff --git a/page/src/events.js b/page/src/events.js index 803109e..7fc8bc1 100644 --- a/page/src/events.js +++ b/page/src/events.js @@ -10,6 +10,7 @@ import * as api from './api.js'; import * as ui from './ui.js'; import {clearTerminal, downloadTerminalOutput, fitTerminal} from './terminal.js'; import {debounce, isMobile} from './utils.js'; +import {getAuthHeaders, handleResponse} from './api.js'; // Import auth functions // A flag to track if charts have been initialized let chartsInitialized = false; @@ -28,7 +29,10 @@ function updateSliderValue(slider, span) { } function loadCurrentLimitSettings() { - fetch('/api/setting') + fetch('/api/setting', { + headers: getAuthHeaders(), // Add auth headers + }) + .then(handleResponse) // Handle response for 401 .then(response => response.json()) .then(data => { if (data.vin_current_limit !== undefined) { @@ -58,13 +62,6 @@ export function setupEventListeners() { } console.log("Attaching event listeners..."); - // --- Theme Toggle --- - dom.themeToggle.addEventListener('change', () => { - const newTheme = dom.themeToggle.checked ? 'dark' : 'light'; - localStorage.setItem('theme', newTheme); - ui.applyTheme(newTheme); - }); - // --- Terminal Controls --- dom.clearButton.addEventListener('click', clearTerminal); dom.downloadButton.addEventListener('click', downloadTerminalOutput); @@ -86,8 +83,12 @@ export function setupEventListeners() { if (dom.rebootButton) { dom.rebootButton.addEventListener('click', () => { if (confirm('Are you sure you want to reboot the device?')) { - fetch('/api/reboot', {method: 'POST'}) - .then(response => response.ok ? response.json() : Promise.reject('Network response was not ok')) + fetch('/api/reboot', { + method: 'POST', + headers: getAuthHeaders(), // Add auth headers + }) + .then(handleResponse) // Handle response for 401 + .then(response => response.json()) .then(data => { console.log('Reboot command sent:', data); ui.hideSettingsModal(); @@ -115,10 +116,14 @@ export function setupEventListeners() { fetch('/api/setting', { method: 'POST', - headers: {'Content-Type': 'application/json'}, + headers: { + 'Content-Type': 'application/json', + ...getAuthHeaders(), // Add auth headers + }, body: JSON.stringify(settings), }) - .then(response => response.ok ? response.json() : Promise.reject('Failed to apply settings')) + .then(handleResponse) // Handle response for 401 + .then(response => response.json()) .then(data => { console.log('Current limit settings applied:', data); }) diff --git a/page/src/main.js b/page/src/main.js index 5b54f52..113a462 100644 --- a/page/src/main.js +++ b/page/src/main.js @@ -31,6 +31,20 @@ import {setupEventListeners} from './events.js'; // --- Globals --- // StatusMessage is imported directly from the generated proto.js file. +// --- DOM Elements --- +const loginContainer = document.getElementById('login-container'); +const mainContent = document.querySelector('main.container'); +const loginForm = document.getElementById('login-form'); +const usernameInput = document.getElementById('username'); +const passwordInput = document.getElementById('password'); +const loginAlert = document.getElementById('login-alert'); +const logoutButton = document.getElementById('logout-button'); +const themeToggleLogin = document.getElementById('theme-toggle-login'); +const themeIconLogin = document.getElementById('theme-icon-login'); +const themeToggleMain = document.getElementById('theme-toggle'); +const themeIconMain = document.getElementById('theme-icon'); + + // --- WebSocket Event Handlers --- function onWsOpen() { @@ -111,6 +125,80 @@ function onWsMessage(event) { } } +// --- Authentication Functions --- + +function checkAuth() { + const token = localStorage.getItem('authToken'); + if (token) { + return true; + } else { + return false; + } +} + +async function handleLogin(event) { + event.preventDefault(); + const username = usernameInput.value; + const password = passwordInput.value; + + try { + const response = await api.login(username, password); + if (response && response.token) { + localStorage.setItem('authToken', response.token); + loginAlert.classList.add('d-none'); + loginContainer.style.setProperty('display', 'none', 'important'); + initializeMainAppContent(); // After successful login, initialize the main app + } else { + loginAlert.textContent = 'Login failed: No token received.'; + loginAlert.classList.remove('d-none'); + } + } catch (error) { + console.error('Login error:', error); + loginAlert.textContent = `Login failed: ${error.message}`; + loginAlert.classList.remove('d-none'); + } +} + +function handleLogout() { + localStorage.removeItem('authToken'); + // Hide main content and show login form + loginContainer.style.setProperty('display', 'flex', 'important'); + mainContent.style.setProperty('display', 'none', 'important'); + // Optionally, disconnect WebSocket or perform other cleanup + // For now, just hide the main content. +} + +// --- Theme Toggle Functions --- +function setupThemeToggles() { + // Initialize theme for login page + const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); + applyTheme(savedTheme); + themeToggleLogin.checked = savedTheme === 'dark'; + themeIconLogin.className = savedTheme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill'; + + // Sync main theme toggle with login theme toggle (initial state) + themeToggleMain.checked = savedTheme === 'dark'; + themeIconMain.className = savedTheme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill'; + + themeToggleLogin.addEventListener('change', () => { + const newTheme = themeToggleLogin.checked ? 'dark' : 'light'; + applyTheme(newTheme); + localStorage.setItem('theme', newTheme); + themeIconLogin.className = newTheme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill'; + themeToggleMain.checked = themeToggleLogin.checked; // Keep main toggle in sync + themeIconMain.className = themeIconLogin.className; // Keep main icon in sync + }); + + themeToggleMain.addEventListener('change', () => { + const newTheme = themeToggleMain.checked ? 'dark' : 'light'; + applyTheme(newTheme); + localStorage.setItem('theme', newTheme); + themeIconMain.className = newTheme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill'; + themeToggleLogin.checked = themeToggleMain.checked; // Keep login toggle in sync + themeIconLogin.className = themeIconMain.className; // Keep login icon in sync + }); +} + // --- Application Initialization --- @@ -131,18 +219,34 @@ function connect() { initWebSocket({ onOpen: onWsOpen, onClose: onWsClose, onMessage: onWsMessage }); } -function initialize() { +// New function to initialize main app content after successful login or on initial load if authenticated +function initializeMainAppContent() { + loginContainer.style.setProperty('display', 'none', 'important'); + mainContent.style.setProperty('display', 'block', 'important'); + initUI(); setupTerminal(); initializeVersion(); - - const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); - applyTheme(savedTheme); - - setupEventListeners(); - + setupEventListeners(); // Attach main app event listeners + logoutButton.addEventListener('click', handleLogout); // Attach logout listener connect(); } +function initialize() { + setupThemeToggles(); // Setup theme toggles for both login and main (initial sync) + + // Always attach login form listener + loginForm.addEventListener('submit', handleLogin); + + if (checkAuth()) { // Check authentication status + // If authenticated, initialize main content + initializeMainAppContent(); + } else { + // If not authenticated, show login form + loginContainer.style.setProperty('display', 'flex', 'important'); + mainContent.style.setProperty('display', 'none', 'important'); + } +} + // --- Start Application --- document.addEventListener('DOMContentLoaded', initialize);