mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-19 16:27:37 +00:00
refactor: remove legacy system.json API token management
- Remove old /api/system/api-token endpoints - Remove APIToken field from SystemSettings struct - Remove token handling from system_settings.go - Clean up config.Load() to not read token from system.json - Remove unused frontend API token functions - Remove unused APITokenManager and CurrentAPIToken components API tokens are now managed exclusively via .env file with the new /api/security/regenerate-token endpoint. This eliminates confusion between the two systems and ensures consistency.
This commit is contained in:
parent
4323339c5e
commit
f524166f9d
6 changed files with 37 additions and 422 deletions
|
|
@ -1,53 +1,16 @@
|
|||
// System API for managing system settings including API tokens
|
||||
// System API for managing system settings
|
||||
import { apiFetchJSON } from '@/utils/apiClient';
|
||||
|
||||
export interface APITokenStatus {
|
||||
hasToken: boolean;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export interface SystemSettings {
|
||||
pollingInterval: number;
|
||||
updateChannel?: string;
|
||||
autoUpdateEnabled: boolean;
|
||||
autoUpdateCheckInterval?: number;
|
||||
autoUpdateTime?: string;
|
||||
apiToken?: string;
|
||||
// apiToken removed - now handled via security API
|
||||
}
|
||||
|
||||
export class SystemAPI {
|
||||
// API Token Management
|
||||
static async getAPITokenStatus(): Promise<APITokenStatus> {
|
||||
return apiFetchJSON('/api/system/api-token');
|
||||
}
|
||||
|
||||
static async getAPIToken(reveal: boolean = false): Promise<APITokenStatus> {
|
||||
const url = reveal ? '/api/system/api-token?reveal=true' : '/api/system/api-token';
|
||||
return apiFetchJSON(url);
|
||||
}
|
||||
|
||||
static async generateAPIToken(): Promise<APITokenStatus> {
|
||||
const result = await apiFetchJSON('/api/system/api-token/generate', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
// Store the new token locally
|
||||
if (result.token) {
|
||||
localStorage.setItem('apiToken', result.token);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static async deleteAPIToken(): Promise<void> {
|
||||
await apiFetchJSON('/api/system/api-token/delete', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
// Clear local storage
|
||||
localStorage.removeItem('apiToken');
|
||||
}
|
||||
|
||||
// System Settings
|
||||
static async getSystemSettings(): Promise<SystemSettings> {
|
||||
return apiFetchJSON('/api/system/settings');
|
||||
|
|
|
|||
|
|
@ -1,201 +0,0 @@
|
|||
import { createSignal, Show, onMount } from 'solid-js';
|
||||
import { SystemAPI, APITokenStatus } from '@/api/system';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
|
||||
export function APITokenManager() {
|
||||
const [tokenStatus, setTokenStatus] = createSignal<APITokenStatus | null>(null);
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
const [showToken, setShowToken] = createSignal(false);
|
||||
const [currentToken, setCurrentToken] = createSignal<string | null>(null);
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
const [copied, setCopied] = createSignal(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = createSignal(false);
|
||||
|
||||
// Load initial status and fetch the actual token if it exists
|
||||
onMount(async () => {
|
||||
try {
|
||||
const status = await SystemAPI.getAPITokenStatus();
|
||||
setTokenStatus(status);
|
||||
|
||||
// If there's a token, fetch it immediately
|
||||
if (status.hasToken) {
|
||||
try {
|
||||
const tokenData = await SystemAPI.getAPIToken(true);
|
||||
if (tokenData.token) {
|
||||
setCurrentToken(tokenData.token);
|
||||
setShowToken(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch existing API token:', err);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load API token status:', err);
|
||||
}
|
||||
});
|
||||
|
||||
const generateToken = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await SystemAPI.generateAPIToken();
|
||||
setTokenStatus(result);
|
||||
setCurrentToken(result.token || null);
|
||||
setShowToken(true);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to generate token');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteToken = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await SystemAPI.deleteAPIToken();
|
||||
setTokenStatus({ hasToken: false });
|
||||
setCurrentToken(null);
|
||||
setShowToken(false);
|
||||
setShowDeleteConfirm(false);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to delete token');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!currentToken()) return;
|
||||
|
||||
const success = await copyToClipboard(currentToken()!);
|
||||
if (success) {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} else {
|
||||
setError('Failed to copy - please select and copy manually');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
<Show when={error()}>
|
||||
<div class="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<p class="text-sm text-red-700 dark:text-red-300">{error()}</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={tokenStatus()?.hasToken}
|
||||
fallback={
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
No token configured
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={generateToken}
|
||||
disabled={loading()}
|
||||
class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading() ? 'Generating...' : 'Generate Token'}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Token active
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
class="text-sm text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={currentToken() && showToken()}>
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
API Token
|
||||
</p>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={currentToken()!}
|
||||
readonly
|
||||
class="w-full px-3 py-2 pr-20 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md"
|
||||
onClick={(e) => e.currentTarget.select()}
|
||||
/>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1 text-xs bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
{copied() ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 mt-2 space-y-2">
|
||||
<p>Use this token for API authentication:</p>
|
||||
<code class="block bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
|
||||
curl -H "X-API-Token: {currentToken()}" {window.location.origin}/api/health
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
onClick={generateToken}
|
||||
disabled={loading()}
|
||||
class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading() ? 'Generating...' : 'Regenerate Token'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={loading()}
|
||||
class="px-4 py-2 bg-red-600 text-white text-sm rounded-md hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Remove Token
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<Show when={showDeleteConfirm()}>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">
|
||||
Remove API Token?
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
This will remove API authentication from your Pulse instance. All configuration endpoints
|
||||
will be accessible without credentials.
|
||||
</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
class="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={deleteToken}
|
||||
disabled={loading()}
|
||||
class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading() ? 'Removing...' : 'Remove Token'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -601,9 +601,7 @@ func (r *Router) setupRoutes() {
|
|||
systemSettingsHandler := NewSystemSettingsHandler(r.config, r.persistence)
|
||||
r.mux.HandleFunc("/api/system/settings", systemSettingsHandler.HandleGetSystemSettings)
|
||||
r.mux.HandleFunc("/api/system/settings/update", systemSettingsHandler.HandleUpdateSystemSettings)
|
||||
r.mux.HandleFunc("/api/system/api-token", systemSettingsHandler.HandleGetAPIToken)
|
||||
r.mux.HandleFunc("/api/system/api-token/generate", systemSettingsHandler.HandleGenerateAPIToken)
|
||||
r.mux.HandleFunc("/api/system/api-token/delete", systemSettingsHandler.HandleDeleteAPIToken)
|
||||
// Old API token endpoints removed - now using /api/security/regenerate-token
|
||||
|
||||
// WebSocket endpoint
|
||||
r.mux.HandleFunc("/ws", r.handleWebSocket)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// SystemSettingsHandler handles system settings including API token management
|
||||
// SystemSettingsHandler handles system settings
|
||||
type SystemSettingsHandler struct {
|
||||
config *config.Config
|
||||
persistence *config.ConfigPersistence
|
||||
|
|
@ -26,147 +23,7 @@ func NewSystemSettingsHandler(cfg *config.Config, persistence *config.ConfigPers
|
|||
}
|
||||
}
|
||||
|
||||
// APITokenResponse represents the API token response
|
||||
type APITokenResponse struct {
|
||||
HasToken bool `json:"hasToken"`
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
// HandleGetAPIToken returns the current API token status
|
||||
func (h *SystemSettingsHandler) HandleGetAPIToken(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if API token is set
|
||||
hasToken := h.config.APIToken != ""
|
||||
|
||||
response := APITokenResponse{
|
||||
HasToken: hasToken,
|
||||
}
|
||||
|
||||
// Only return the token if it's requested with proper auth
|
||||
if hasToken && r.URL.Query().Get("reveal") == "true" {
|
||||
// Verify the request is authenticated (session, password, or API token)
|
||||
if CheckAuth(h.config, w, r) {
|
||||
response.Token = h.config.APIToken
|
||||
} else {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// HandleGenerateAPIToken generates a new API token
|
||||
func (h *SystemSettingsHandler) HandleGenerateAPIToken(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Use standard auth check if any authentication is configured
|
||||
// This allows generation via web UI when logged in with password
|
||||
if !CheckAuth(h.config, w, r) {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a new secure token
|
||||
tokenBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(tokenBytes); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to generate random token")
|
||||
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
newToken := hex.EncodeToString(tokenBytes)
|
||||
|
||||
// Save to system settings
|
||||
settings, err := h.persistence.LoadSystemSettings()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to load system settings")
|
||||
settings = &config.SystemSettings{}
|
||||
}
|
||||
|
||||
settings.APIToken = newToken
|
||||
|
||||
if err := h.persistence.SaveSystemSettings(*settings); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save system settings")
|
||||
http.Error(w, "Failed to save token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the running config
|
||||
h.config.APIToken = newToken
|
||||
|
||||
// Don't override if env var is set
|
||||
if os.Getenv("API_TOKEN") != "" {
|
||||
log.Warn().Msg("API_TOKEN environment variable is set and will override UI-configured token on restart")
|
||||
}
|
||||
|
||||
log.Info().Msg("API token generated via UI")
|
||||
|
||||
response := APITokenResponse{
|
||||
HasToken: true,
|
||||
Token: newToken,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// HandleDeleteAPIToken removes the API token
|
||||
func (h *SystemSettingsHandler) HandleDeleteAPIToken(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Use standard auth check (allows session, password, or API token)
|
||||
// This allows deletion via web UI when logged in with password
|
||||
if !CheckAuth(h.config, w, r) {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Save to system settings
|
||||
settings, err := h.persistence.LoadSystemSettings()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to load system settings")
|
||||
settings = &config.SystemSettings{}
|
||||
}
|
||||
|
||||
settings.APIToken = ""
|
||||
|
||||
if err := h.persistence.SaveSystemSettings(*settings); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save system settings")
|
||||
http.Error(w, "Failed to remove token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the running config
|
||||
h.config.APIToken = ""
|
||||
|
||||
// Warn if env var is set
|
||||
if os.Getenv("API_TOKEN") != "" {
|
||||
log.Warn().Msg("API_TOKEN environment variable is set and will override this change on restart")
|
||||
}
|
||||
|
||||
log.Info().Msg("API token removed via UI")
|
||||
|
||||
response := APITokenResponse{
|
||||
HasToken: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// HandleGetSystemSettings returns all system settings
|
||||
// HandleGetSystemSettings returns the current system settings
|
||||
func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
|
|
@ -176,29 +33,24 @@ func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r
|
|||
settings, err := h.persistence.LoadSystemSettings()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to load system settings")
|
||||
http.Error(w, "Failed to load settings", http.StatusInternalServerError)
|
||||
return
|
||||
settings = &config.SystemSettings{
|
||||
PollingInterval: 5,
|
||||
}
|
||||
}
|
||||
|
||||
// Don't expose the actual token in this endpoint
|
||||
if settings.APIToken != "" {
|
||||
settings.APIToken = "***HIDDEN***"
|
||||
}
|
||||
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(settings)
|
||||
}
|
||||
|
||||
// HandleUpdateSystemSettings updates system settings
|
||||
// HandleUpdateSystemSettings updates the system settings
|
||||
func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Use standard auth check (allows session, password, or API token)
|
||||
// Require authentication
|
||||
if !CheckAuth(h.config, w, r) {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -207,26 +59,25 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Don't allow updating API token through this endpoint
|
||||
existingSettings, _ := h.persistence.LoadSystemSettings()
|
||||
if existingSettings != nil {
|
||||
settings.APIToken = existingSettings.APIToken
|
||||
}
|
||||
|
||||
if err := h.persistence.SaveSystemSettings(settings); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save system settings")
|
||||
http.Error(w, "Failed to save settings", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Update relevant config fields
|
||||
|
||||
// Load existing settings to preserve fields not in the request
|
||||
// (removed - not needed without API token preservation)
|
||||
|
||||
// Update the config
|
||||
if settings.PollingInterval > 0 {
|
||||
h.config.PollingInterval = time.Duration(settings.PollingInterval) * time.Second
|
||||
}
|
||||
if settings.AllowedOrigins != "" {
|
||||
h.config.AllowedOrigins = settings.AllowedOrigins
|
||||
}
|
||||
if settings.ConnectionTimeout > 0 {
|
||||
h.config.ConnectionTimeout = time.Duration(settings.ConnectionTimeout) * time.Second
|
||||
}
|
||||
if settings.UpdateChannel != "" {
|
||||
h.config.UpdateChannel = settings.UpdateChannel
|
||||
}
|
||||
|
||||
// Update auto-update settings
|
||||
h.config.AutoUpdateEnabled = settings.AutoUpdateEnabled
|
||||
if settings.AutoUpdateCheckInterval > 0 {
|
||||
h.config.AutoUpdateCheckInterval = time.Duration(settings.AutoUpdateCheckInterval) * time.Hour
|
||||
|
|
@ -234,9 +85,16 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
if settings.AutoUpdateTime != "" {
|
||||
h.config.AutoUpdateTime = settings.AutoUpdateTime
|
||||
}
|
||||
|
||||
log.Info().Msg("System settings updated via UI")
|
||||
|
||||
|
||||
// Save to persistence
|
||||
if err := h.persistence.SaveSystemSettings(settings); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save system settings")
|
||||
http.Error(w, "Failed to save settings", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msg("System settings updated")
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
|
@ -229,13 +229,10 @@ func Load() (*Config, error) {
|
|||
if systemSettings.ConnectionTimeout > 0 {
|
||||
cfg.ConnectionTimeout = time.Duration(systemSettings.ConnectionTimeout) * time.Second
|
||||
}
|
||||
if systemSettings.APIToken != "" {
|
||||
cfg.APIToken = systemSettings.APIToken
|
||||
}
|
||||
// APIToken no longer loaded from system.json - only from .env
|
||||
log.Info().
|
||||
Dur("interval", cfg.PollingInterval).
|
||||
Str("updateChannel", cfg.UpdateChannel).
|
||||
Bool("hasAPIToken", cfg.APIToken != "").
|
||||
Msg("Loaded system configuration")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ type SystemSettings struct {
|
|||
AutoUpdateEnabled bool `json:"autoUpdateEnabled"` // Removed omitempty so false is saved
|
||||
AutoUpdateCheckInterval int `json:"autoUpdateCheckInterval,omitempty"`
|
||||
AutoUpdateTime string `json:"autoUpdateTime,omitempty"`
|
||||
APIToken string `json:"apiToken,omitempty"`
|
||||
// APIToken removed - now handled via .env file only
|
||||
}
|
||||
|
||||
// SaveNodesConfig saves nodes configuration to file (encrypted)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue