Harden API auth and token handling
This commit is contained in:
parent
77282bd3a6
commit
7075cef326
11 changed files with 268 additions and 87 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import { Component, createSignal, Show, onMount } from 'solid-js';
|
||||
import { showSuccess, showError } from '@/utils/toast';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { clearStoredAPIToken } from '@/utils/tokenStorage';
|
||||
import { clearApiToken as clearApiClientToken, setApiToken as setApiClientToken } from '@/utils/apiClient';
|
||||
import { STORAGE_KEYS } from '@/constants';
|
||||
import { SectionHeader } from '@/components/shared/SectionHeader';
|
||||
import { showTokenReveal } from '@/stores/tokenReveal';
|
||||
|
|
@ -11,7 +13,6 @@ export const FirstRunSetup: Component = () => {
|
|||
const [password, setPassword] = createSignal('');
|
||||
const [confirmPassword, setConfirmPassword] = createSignal('');
|
||||
const [useCustomPassword, setUseCustomPassword] = createSignal(false);
|
||||
const [, setApiToken] = createSignal('');
|
||||
const [isSettingUp, setIsSettingUp] = createSignal(false);
|
||||
const [showCredentials, setShowCredentials] = createSignal(false);
|
||||
const [savedUsername, setSavedUsername] = createSignal('');
|
||||
|
|
@ -98,7 +99,7 @@ export const FirstRunSetup: Component = () => {
|
|||
|
||||
// Generate API token
|
||||
const token = generateToken();
|
||||
setApiToken(token);
|
||||
setApiClientToken(token);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/security/quick-setup', {
|
||||
|
|
@ -139,7 +140,8 @@ export const FirstRunSetup: Component = () => {
|
|||
}
|
||||
|
||||
try {
|
||||
localStorage.removeItem('apiToken');
|
||||
clearStoredAPIToken();
|
||||
clearApiClientToken();
|
||||
} catch (storageError) {
|
||||
console.warn('Unable to clear cached API token', storageError);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { formatRelativeTime } from '@/utils/format';
|
|||
import { useWebSocket } from '@/App';
|
||||
import type { DockerHost, Host } from '@/types/api';
|
||||
import { showTokenReveal, useTokenRevealState } from '@/stores/tokenReveal';
|
||||
import { setStoredAPIToken } from '@/utils/tokenStorage';
|
||||
import { setApiToken as setApiClientToken } from '@/utils/apiClient';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { SectionHeader } from '@/components/shared/SectionHeader';
|
||||
import { ApiIcon } from '@/components/icons/ApiIcon';
|
||||
|
|
@ -261,14 +263,8 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
|||
showSuccess('New API token generated. Copy it below while it is still visible.');
|
||||
props.onTokensChanged?.();
|
||||
|
||||
try {
|
||||
window.localStorage.setItem('apiToken', token);
|
||||
window.dispatchEvent(
|
||||
new StorageEvent('storage', { key: 'apiToken', newValue: token }),
|
||||
);
|
||||
} catch (storageErr) {
|
||||
console.warn('Unable to persist API token in localStorage', storageErr);
|
||||
}
|
||||
setStoredAPIToken(token);
|
||||
setApiClientToken(token);
|
||||
} catch (err) {
|
||||
console.error('Failed to generate API token', err);
|
||||
showError('Failed to generate API token');
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { Component, createSignal, Show, createMemo, createEffect } from 'solid-j
|
|||
import { apiFetch } from '@/utils/apiClient';
|
||||
import { SecurityAPI, type APITokenRecord } from '@/api/security';
|
||||
import { showTokenReveal, useTokenRevealState } from '@/stores/tokenReveal';
|
||||
import { setStoredAPIToken } from '@/utils/tokenStorage';
|
||||
import { setApiToken as setApiClientToken } from '@/utils/apiClient';
|
||||
|
||||
interface CommandBuilderProps {
|
||||
command: string;
|
||||
|
|
@ -203,20 +205,14 @@ export const CommandBuilder: Component<CommandBuilderProps> = (props) => {
|
|||
|
||||
reopenLatestTokenDialog();
|
||||
// Auto-populate the command builder
|
||||
setTokenInput(newToken);
|
||||
if (props.onTokenGenerated) {
|
||||
props.onTokenGenerated(newToken, record);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
window.localStorage.setItem('apiToken', newToken);
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: 'apiToken', newValue: newToken }));
|
||||
} catch (storageErr) {
|
||||
console.warn('Unable to persist API token in localStorage', storageErr);
|
||||
}
|
||||
setTokenInput(newToken);
|
||||
if (props.onTokenGenerated) {
|
||||
props.onTokenGenerated(newToken, record);
|
||||
}
|
||||
|
||||
setStoredAPIToken(newToken);
|
||||
setApiClientToken(newToken);
|
||||
|
||||
window.showToast('success', 'New API token generated. Copy it from the dialog while it is visible.');
|
||||
} catch (error) {
|
||||
console.error('Token generation failed:', error);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { Component, createSignal, Show } from 'solid-js';
|
||||
import { showSuccess, showError } from '@/utils/toast';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { clearStoredAPIToken } from '@/utils/tokenStorage';
|
||||
import { clearApiToken as clearApiClientToken } from '@/utils/apiClient';
|
||||
import { SectionHeader } from '@/components/shared/SectionHeader';
|
||||
import { formField, labelClass, controlClass, formHelpText } from '@/components/shared/Form';
|
||||
|
||||
|
|
@ -125,7 +127,8 @@ export const QuickSecuritySetup: Component<QuickSecuritySetupProps> = (props) =>
|
|||
}
|
||||
|
||||
try {
|
||||
localStorage.removeItem('apiToken');
|
||||
clearStoredAPIToken();
|
||||
clearApiClientToken();
|
||||
} catch (storageError) {
|
||||
console.warn('Unable to clear cached API token', storageError);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ import { useNavigate, useLocation } from '@solidjs/router';
|
|||
import { useWebSocket } from '@/App';
|
||||
import { showSuccess, showError } from '@/utils/toast';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import {
|
||||
clearStoredAPIToken,
|
||||
getStoredAPIToken,
|
||||
setStoredAPIToken,
|
||||
} from '@/utils/tokenStorage';
|
||||
import { clearApiToken as clearApiClientToken, setApiToken as setApiClientToken } from '@/utils/apiClient';
|
||||
import { NodeModal } from './NodeModal';
|
||||
import { ChangePasswordModal } from './ChangePasswordModal';
|
||||
import { DockerAgents } from './DockerAgents';
|
||||
|
|
@ -1723,7 +1729,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
if (
|
||||
!hasPasswordAuth &&
|
||||
securityStatus()?.apiTokenConfigured &&
|
||||
!localStorage.getItem('apiToken')
|
||||
!getStoredAPIToken()
|
||||
) {
|
||||
setApiTokenModalSource('export');
|
||||
setShowApiTokenModal(true);
|
||||
|
|
@ -1747,7 +1753,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
}
|
||||
|
||||
// Add API token if configured
|
||||
const apiToken = localStorage.getItem('apiToken');
|
||||
const apiToken = getStoredAPIToken();
|
||||
if (apiToken) {
|
||||
headers['X-API-Token'] = apiToken;
|
||||
}
|
||||
|
|
@ -1767,9 +1773,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
const hasPasswordAuth = securityStatus()?.hasAuthentication;
|
||||
if (!hasPasswordAuth) {
|
||||
// Clear invalid token if we had one
|
||||
const hadToken = localStorage.getItem('apiToken');
|
||||
const hadToken = getStoredAPIToken();
|
||||
if (hadToken) {
|
||||
localStorage.removeItem('apiToken');
|
||||
clearStoredAPIToken();
|
||||
clearApiClientToken();
|
||||
showError('Invalid or expired API token. Please re-enter.');
|
||||
setApiTokenModalSource('export');
|
||||
setShowApiTokenModal(true);
|
||||
|
|
@ -1827,7 +1834,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
if (
|
||||
!hasPasswordAuth &&
|
||||
securityStatus()?.apiTokenConfigured &&
|
||||
!localStorage.getItem('apiToken')
|
||||
!getStoredAPIToken()
|
||||
) {
|
||||
setApiTokenModalSource('import');
|
||||
setShowApiTokenModal(true);
|
||||
|
|
@ -1861,7 +1868,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
}
|
||||
|
||||
// Add API token if configured
|
||||
const apiToken = localStorage.getItem('apiToken');
|
||||
const apiToken = getStoredAPIToken();
|
||||
if (apiToken) {
|
||||
headers['X-API-Token'] = apiToken;
|
||||
}
|
||||
|
|
@ -1884,9 +1891,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
const hasPasswordAuth = securityStatus()?.hasAuthentication;
|
||||
if (!hasPasswordAuth) {
|
||||
// Clear invalid token if we had one
|
||||
const hadToken = localStorage.getItem('apiToken');
|
||||
const hadToken = getStoredAPIToken();
|
||||
if (hadToken) {
|
||||
localStorage.removeItem('apiToken');
|
||||
clearStoredAPIToken();
|
||||
clearApiClientToken();
|
||||
showError('Invalid or expired API token. Please re-enter.');
|
||||
setApiTokenModalSource('import');
|
||||
setShowApiTokenModal(true);
|
||||
|
|
@ -6437,7 +6445,9 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
type="button"
|
||||
onClick={() => {
|
||||
if (apiTokenInput()) {
|
||||
localStorage.setItem('apiToken', apiTokenInput());
|
||||
const tokenValue = apiTokenInput()!;
|
||||
setStoredAPIToken(tokenValue);
|
||||
setApiClientToken(tokenValue);
|
||||
const source = apiTokenModalSource();
|
||||
setShowApiTokenModal(false);
|
||||
setApiTokenInput('');
|
||||
|
|
|
|||
|
|
@ -39,12 +39,21 @@ const readStoredToken = (primaryKey: string, legacyKeys: readonly string[] = [])
|
|||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
const primary = window.localStorage.getItem(primaryKey);
|
||||
let storage: Storage | undefined;
|
||||
try {
|
||||
storage = window.sessionStorage;
|
||||
} catch {
|
||||
storage = undefined;
|
||||
}
|
||||
if (!storage) {
|
||||
return null;
|
||||
}
|
||||
const primary = storage.getItem(primaryKey);
|
||||
if (primary) {
|
||||
return primary;
|
||||
}
|
||||
for (const key of legacyKeys) {
|
||||
const value = window.localStorage.getItem(key);
|
||||
const value = storage.getItem(key);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
|
|
@ -69,16 +78,22 @@ export const useScopedTokenManager = (options: UseScopedTokenManagerOptions): Sc
|
|||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
let storage: Storage | undefined;
|
||||
try {
|
||||
storage = window.sessionStorage;
|
||||
} catch {
|
||||
storage = undefined;
|
||||
}
|
||||
try {
|
||||
if (current) {
|
||||
window.localStorage.setItem(storageKey, current);
|
||||
storage?.setItem(storageKey, current);
|
||||
try {
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: storageKey, newValue: current }));
|
||||
} catch (eventErr) {
|
||||
console.debug('Unable to dispatch storage event for token update', eventErr);
|
||||
}
|
||||
} else {
|
||||
window.localStorage.removeItem(storageKey);
|
||||
storage?.removeItem(storageKey);
|
||||
try {
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: storageKey, newValue: null }));
|
||||
} catch (eventErr) {
|
||||
|
|
@ -86,7 +101,7 @@ export const useScopedTokenManager = (options: UseScopedTokenManagerOptions): Sc
|
|||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Unable to persist API token in localStorage', err);
|
||||
console.warn('Unable to persist API token in sessionStorage', err);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,21 @@ class ApiClient {
|
|||
sessionStorage.removeItem('pulse_auth_user');
|
||||
}
|
||||
|
||||
clearApiToken() {
|
||||
this.apiToken = null;
|
||||
try {
|
||||
const stored = sessionStorage.getItem('pulse_auth');
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
if (parsed?.type === 'token') {
|
||||
sessionStorage.removeItem('pulse_auth');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
sessionStorage.removeItem('pulse_auth');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have any auth configured
|
||||
hasAuth(): boolean {
|
||||
return !!(this.authHeader || this.apiToken);
|
||||
|
|
@ -263,5 +278,6 @@ export const setBasicAuth = (username: string, password: string) =>
|
|||
apiClient.setBasicAuth(username, password);
|
||||
export const setApiToken = (token: string) => apiClient.setApiToken(token);
|
||||
export const clearAuth = () => apiClient.clearAuth();
|
||||
export const clearApiToken = () => apiClient.clearApiToken();
|
||||
export const hasAuth = () => apiClient.hasAuth();
|
||||
export const checkAuthRequired = () => apiClient.checkAuthRequired();
|
||||
|
|
|
|||
98
frontend-modern/src/utils/tokenStorage.ts
Normal file
98
frontend-modern/src/utils/tokenStorage.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { onCleanup } from 'solid-js';
|
||||
|
||||
const STORAGE_KEY = 'pulse_api_token';
|
||||
|
||||
let memoryToken: string | null = null;
|
||||
const listeners = new Set<(value: string | null) => void>();
|
||||
|
||||
const getSessionStorage = (): Storage | undefined => {
|
||||
if (typeof window === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return window.sessionStorage;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const notify = (value: string | null) => {
|
||||
listeners.forEach((listener) => {
|
||||
try {
|
||||
listener(value);
|
||||
} catch {
|
||||
// Ignore listener failures to avoid breaking notification flow.
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: STORAGE_KEY, newValue: value ?? null }));
|
||||
} catch {
|
||||
// Some browsers disallow programmatic StorageEvent dispatch; ignore failures.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const writeValue = (value: string | null) => {
|
||||
memoryToken = value;
|
||||
const storage = getSessionStorage();
|
||||
if (!storage) {
|
||||
notify(value);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (value === null) {
|
||||
storage.removeItem(STORAGE_KEY);
|
||||
} else {
|
||||
storage.setItem(STORAGE_KEY, value);
|
||||
}
|
||||
} catch {
|
||||
// If sessionStorage is unavailable (e.g., Safari private mode), fall back to memory token only.
|
||||
}
|
||||
|
||||
notify(value);
|
||||
};
|
||||
|
||||
export const getStoredAPIToken = (): string | null => {
|
||||
const storage = getSessionStorage();
|
||||
if (!storage) {
|
||||
return memoryToken;
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = storage.getItem(STORAGE_KEY);
|
||||
if (stored !== null) {
|
||||
memoryToken = stored;
|
||||
}
|
||||
return stored ?? memoryToken;
|
||||
} catch {
|
||||
return memoryToken;
|
||||
}
|
||||
};
|
||||
|
||||
export const setStoredAPIToken = (token: string): void => {
|
||||
writeValue(token);
|
||||
};
|
||||
|
||||
export const clearStoredAPIToken = (): void => {
|
||||
writeValue(null);
|
||||
};
|
||||
|
||||
export const subscribeAPIToken = (listener: (token: string | null) => void): (() => void) => {
|
||||
listeners.add(listener);
|
||||
listener(getStoredAPIToken());
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
export const useAPITokenSubscription = (listener: (token: string | null) => void): void => {
|
||||
if (typeof window === 'undefined') {
|
||||
listener(getStoredAPIToken());
|
||||
return;
|
||||
}
|
||||
const unsubscribe = subscribeAPIToken(listener);
|
||||
onCleanup(unsubscribe);
|
||||
};
|
||||
|
|
@ -230,7 +230,6 @@ func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool
|
|||
}
|
||||
|
||||
// API-only mode: when only API token is configured (no password auth)
|
||||
// Allow read-only endpoints for the UI to work
|
||||
if cfg.AuthUser == "" && cfg.AuthPass == "" && cfg.HasAPITokens() {
|
||||
// Check if an API token was provided
|
||||
providedToken := r.Header.Get("X-API-Token")
|
||||
|
|
@ -251,32 +250,7 @@ func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool
|
|||
return false
|
||||
}
|
||||
|
||||
// No token provided - allow read-only endpoints for UI
|
||||
if r.Method == "GET" || r.URL.Path == "/ws" {
|
||||
// Allow these endpoints without auth for UI to function
|
||||
allowedPaths := []string{
|
||||
"/api/state",
|
||||
"/api/config/nodes",
|
||||
"/api/config/system",
|
||||
"/api/settings",
|
||||
"/api/discover",
|
||||
"/api/security/status",
|
||||
"/api/version",
|
||||
"/api/health",
|
||||
"/api/updates/check",
|
||||
"/api/system/diagnostics",
|
||||
"/api/guests/metadata",
|
||||
"/ws", // WebSocket for real-time updates
|
||||
}
|
||||
for _, path := range allowedPaths {
|
||||
if r.URL.Path == path || strings.HasPrefix(r.URL.Path, path+"/") {
|
||||
log.Debug().Str("path", r.URL.Path).Msg("Allowing read-only access in API-only mode")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Require token for everything else
|
||||
// Require a valid token for all requests in API-only mode
|
||||
if w != nil {
|
||||
w.Header().Set("WWW-Authenticate", `Bearer realm="API Token Required"`)
|
||||
http.Error(w, "API token required", http.StatusUnauthorized)
|
||||
|
|
@ -342,7 +316,15 @@ func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool
|
|||
// Check basic auth
|
||||
if cfg.AuthUser != "" && cfg.AuthPass != "" {
|
||||
auth := r.Header.Get("Authorization")
|
||||
log.Debug().Str("auth_header", auth).Str("url", r.URL.Path).Msg("Checking auth")
|
||||
authScheme := "none"
|
||||
if auth != "" {
|
||||
if idx := strings.IndexByte(auth, ' '); idx != -1 {
|
||||
authScheme = strings.ToLower(auth[:idx])
|
||||
} else {
|
||||
authScheme = strings.ToLower(auth)
|
||||
}
|
||||
}
|
||||
log.Debug().Str("auth_scheme", authScheme).Str("url", r.URL.Path).Msg("Checking Authorization header")
|
||||
if auth != "" {
|
||||
const prefix = "Basic "
|
||||
if strings.HasPrefix(auth, prefix) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
||||
|
|
@ -525,32 +526,85 @@ func (h *NotificationHandlers) TestWebhook(w http.ResponseWriter, r *http.Reques
|
|||
func (h *NotificationHandlers) HandleNotifications(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/notifications")
|
||||
|
||||
requireAnyScope := func(required string, scopes ...string) bool {
|
||||
record := getAPITokenRecordFromRequest(r)
|
||||
if record == nil {
|
||||
return true
|
||||
}
|
||||
for _, scope := range scopes {
|
||||
if scope != "" && record.HasScope(scope) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
respondMissingScope(w, required)
|
||||
return false
|
||||
}
|
||||
|
||||
switch {
|
||||
case path == "/email" && r.Method == http.MethodGet:
|
||||
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.GetEmailConfig(w, r)
|
||||
case path == "/email" && r.Method == http.MethodPut:
|
||||
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.UpdateEmailConfig(w, r)
|
||||
case path == "/apprise" && r.Method == http.MethodGet:
|
||||
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.GetAppriseConfig(w, r)
|
||||
case path == "/apprise" && r.Method == http.MethodPut:
|
||||
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.UpdateAppriseConfig(w, r)
|
||||
case path == "/webhooks" && r.Method == http.MethodGet:
|
||||
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.GetWebhooks(w, r)
|
||||
case path == "/webhooks" && r.Method == http.MethodPost:
|
||||
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.CreateWebhook(w, r)
|
||||
case path == "/webhooks/test" && r.Method == http.MethodPost:
|
||||
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.TestWebhook(w, r)
|
||||
case strings.HasPrefix(path, "/webhooks/") && r.Method == http.MethodPut:
|
||||
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.UpdateWebhook(w, r)
|
||||
case strings.HasPrefix(path, "/webhooks/") && r.Method == http.MethodDelete:
|
||||
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.DeleteWebhook(w, r)
|
||||
case path == "/webhook-templates" && r.Method == http.MethodGet:
|
||||
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.GetWebhookTemplates(w, r)
|
||||
case path == "/webhook-history" && r.Method == http.MethodGet:
|
||||
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.GetWebhookHistory(w, r)
|
||||
case path == "/email-providers" && r.Method == http.MethodGet:
|
||||
if !requireAnyScope(config.ScopeSettingsRead, config.ScopeSettingsRead, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.GetEmailProviders(w, r)
|
||||
case path == "/test" && r.Method == http.MethodPost:
|
||||
if !requireAnyScope(config.ScopeSettingsWrite, config.ScopeSettingsWrite) {
|
||||
return
|
||||
}
|
||||
h.TestNotification(w, r)
|
||||
default:
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
|
|
|
|||
|
|
@ -137,45 +137,54 @@ func (r *Router) setupRoutes() {
|
|||
r.mux.HandleFunc("/api/agents/docker/commands/", RequireAuth(r.config, RequireScope(config.ScopeDockerManage, r.dockerAgentHandlers.HandleCommandAck)))
|
||||
r.mux.HandleFunc("/api/agents/docker/hosts/", RequireAdmin(r.config, RequireScope(config.ScopeDockerManage, r.dockerAgentHandlers.HandleDockerHostActions)))
|
||||
r.mux.HandleFunc("/api/version", r.handleVersion)
|
||||
r.mux.HandleFunc("/api/storage/", r.handleStorage)
|
||||
r.mux.HandleFunc("/api/storage-charts", r.handleStorageCharts)
|
||||
r.mux.HandleFunc("/api/charts", r.handleCharts)
|
||||
r.mux.HandleFunc("/api/storage/", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleStorage)))
|
||||
r.mux.HandleFunc("/api/storage-charts", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleStorageCharts)))
|
||||
r.mux.HandleFunc("/api/charts", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleCharts)))
|
||||
r.mux.HandleFunc("/api/diagnostics", RequireAuth(r.config, r.handleDiagnostics))
|
||||
r.mux.HandleFunc("/api/diagnostics/temperature-proxy/register-nodes", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleDiagnosticsRegisterProxyNodes)))
|
||||
r.mux.HandleFunc("/api/diagnostics/docker/prepare-token", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleDiagnosticsDockerPrepareToken)))
|
||||
r.mux.HandleFunc("/api/install/pulse-sensor-proxy", r.handleDownloadPulseSensorProxy)
|
||||
r.mux.HandleFunc("/api/install/install-sensor-proxy.sh", r.handleDownloadInstallerScript)
|
||||
r.mux.HandleFunc("/api/install/install-docker.sh", r.handleDownloadDockerInstallerScript)
|
||||
r.mux.HandleFunc("/api/config", r.handleConfig)
|
||||
r.mux.HandleFunc("/api/backups", r.handleBackups)
|
||||
r.mux.HandleFunc("/api/backups/", r.handleBackups)
|
||||
r.mux.HandleFunc("/api/backups/unified", r.handleBackups)
|
||||
r.mux.HandleFunc("/api/backups/pve", r.handleBackupsPVE)
|
||||
r.mux.HandleFunc("/api/backups/pbs", r.handleBackupsPBS)
|
||||
r.mux.HandleFunc("/api/snapshots", r.handleSnapshots)
|
||||
r.mux.HandleFunc("/api/config", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleConfig)))
|
||||
r.mux.HandleFunc("/api/backups", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleBackups)))
|
||||
r.mux.HandleFunc("/api/backups/", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleBackups)))
|
||||
r.mux.HandleFunc("/api/backups/unified", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleBackups)))
|
||||
r.mux.HandleFunc("/api/backups/pve", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleBackupsPVE)))
|
||||
r.mux.HandleFunc("/api/backups/pbs", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleBackupsPBS)))
|
||||
r.mux.HandleFunc("/api/snapshots", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleSnapshots)))
|
||||
|
||||
// Guest metadata routes
|
||||
r.mux.HandleFunc("/api/guests/metadata", guestMetadataHandler.HandleGetMetadata)
|
||||
r.mux.HandleFunc("/api/guests/metadata/", func(w http.ResponseWriter, req *http.Request) {
|
||||
r.mux.HandleFunc("/api/guests/metadata", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, guestMetadataHandler.HandleGetMetadata)))
|
||||
r.mux.HandleFunc("/api/guests/metadata/", RequireAuth(r.config, func(w http.ResponseWriter, req *http.Request) {
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
if !ensureScope(w, req, config.ScopeMonitoringRead) {
|
||||
return
|
||||
}
|
||||
guestMetadataHandler.HandleGetMetadata(w, req)
|
||||
case http.MethodPut, http.MethodPost:
|
||||
if !ensureScope(w, req, config.ScopeMonitoringWrite) {
|
||||
return
|
||||
}
|
||||
guestMetadataHandler.HandleUpdateMetadata(w, req)
|
||||
case http.MethodDelete:
|
||||
if !ensureScope(w, req, config.ScopeMonitoringWrite) {
|
||||
return
|
||||
}
|
||||
guestMetadataHandler.HandleDeleteMetadata(w, req)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
// Update routes
|
||||
r.mux.HandleFunc("/api/updates/check", updateHandlers.HandleCheckUpdates)
|
||||
r.mux.HandleFunc("/api/updates/apply", updateHandlers.HandleApplyUpdate)
|
||||
r.mux.HandleFunc("/api/updates/status", updateHandlers.HandleUpdateStatus)
|
||||
r.mux.HandleFunc("/api/updates/plan", updateHandlers.HandleGetUpdatePlan)
|
||||
r.mux.HandleFunc("/api/updates/history", updateHandlers.HandleListUpdateHistory)
|
||||
r.mux.HandleFunc("/api/updates/history/entry", updateHandlers.HandleGetUpdateHistoryEntry)
|
||||
r.mux.HandleFunc("/api/updates/check", RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, updateHandlers.HandleCheckUpdates)))
|
||||
r.mux.HandleFunc("/api/updates/apply", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, updateHandlers.HandleApplyUpdate)))
|
||||
r.mux.HandleFunc("/api/updates/status", RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, updateHandlers.HandleUpdateStatus)))
|
||||
r.mux.HandleFunc("/api/updates/plan", RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, updateHandlers.HandleGetUpdatePlan)))
|
||||
r.mux.HandleFunc("/api/updates/history", RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, updateHandlers.HandleListUpdateHistory)))
|
||||
r.mux.HandleFunc("/api/updates/history/entry", RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, updateHandlers.HandleGetUpdateHistoryEntry)))
|
||||
|
||||
// Config management routes
|
||||
r.mux.HandleFunc("/api/config/nodes", func(w http.ResponseWriter, req *http.Request) {
|
||||
|
|
@ -860,10 +869,10 @@ func (r *Router) setupRoutes() {
|
|||
})
|
||||
|
||||
// Alert routes
|
||||
r.mux.HandleFunc("/api/alerts/", r.alertHandlers.HandleAlerts)
|
||||
r.mux.HandleFunc("/api/alerts/", RequireAuth(r.config, r.alertHandlers.HandleAlerts))
|
||||
|
||||
// Notification routes
|
||||
r.mux.HandleFunc("/api/notifications/", r.notificationHandlers.HandleNotifications)
|
||||
r.mux.HandleFunc("/api/notifications/", RequireAdmin(r.config, r.notificationHandlers.HandleNotifications))
|
||||
|
||||
// Settings routes
|
||||
r.mux.HandleFunc("/api/settings", RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, getSettings)))
|
||||
|
|
|
|||
Loading…
Reference in a new issue