diff --git a/src/services/api.ts b/src/services/api.ts index 9dc5c7a..2a54b43 100755 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -13,6 +13,8 @@ import type { SecurityStats, BlockedIP, ProtectedRoute, + WhitelistEntry, + ConfigEntry, DeploymentSecurityConfig, DomainConfig, ProtectedModeConfig, @@ -36,10 +38,11 @@ apiClient.interceptors.request.use((config) => { apiClient.interceptors.response.use( (response) => response, (error) => { - if (error.response?.status === 401 && !window.location.pathname.includes("/setup")) { + if (error.response?.status === 401) { const failedURL: string = error.config?.url || ""; - const isSessionEndpoint = /\/auth\/|\/users\/me(\b|\/)/.test(failedURL); - if (isSessionEndpoint) { + const isLoginAttempt = failedURL.includes("/auth/login"); + const onAuthPage = window.location.pathname.includes("/login") || window.location.pathname.includes("/setup"); + if (!isLoginAttempt && !onAuthPage) { localStorage.removeItem("auth_token"); window.location.href = "/login"; } @@ -237,6 +240,13 @@ export const settingsApi = { generateSubdomain: () => apiClient.get("/subdomain/generate"), }; +export const configApi = { + list: () => apiClient.get<{ config: ConfigEntry[]; runtime: Record }>("/config"), + get: (key: string) => apiClient.get<{ entry: ConfigEntry; runtime: boolean }>(`/config/${key}`), + set: (key: string, value: unknown) => + apiClient.put<{ entry: ConfigEntry; applied: boolean }>(`/config/${key}`, { value }), +}; + export const pluginsApi = { list: () => apiClient.get("/plugins"), get: (name: string) => apiClient.get(`/plugins/${name}`), @@ -748,6 +758,11 @@ export const securityApi = { cleanup: (days?: number) => apiClient.post<{ events_deleted: number; blocks_deleted: number }>("/security/cleanup", { days }), + getWhitelist: () => apiClient.get<{ whitelist: WhitelistEntry[] }>("/security/whitelist"), + addWhitelistEntry: (entry: { value: string; type: WhitelistEntry["type"]; reason?: string }) => + apiClient.post<{ id: number }>("/security/whitelist", entry), + removeWhitelistEntry: (id: number) => apiClient.delete<{ message: string }>(`/security/whitelist/${id}`), + getBlockedIPs: () => apiClient.get<{ blocked_ips: BlockedIP[] }>("/security/blocked-ips"), blockIP: (ip: string, reason?: string, duration?: number) => apiClient.post<{ id: number; message: string }>("/security/blocked-ips", { ip, reason, duration }), diff --git a/src/stores/security.ts b/src/stores/security.ts index 5387af5..a05e9be 100644 --- a/src/stores/security.ts +++ b/src/stores/security.ts @@ -1,13 +1,14 @@ import { defineStore } from "pinia"; import { ref } from "vue"; import { securityApi, type SecurityHealthCheck, type SecurityRefreshResponse } from "@/services/api"; -import type { SecurityEvent, SecurityStats, BlockedIP, ProtectedRoute } from "@/types"; +import type { SecurityEvent, SecurityStats, BlockedIP, ProtectedRoute, WhitelistEntry } from "@/types"; export const useSecurityStore = defineStore("security", () => { const stats = ref(null); const events = ref([]); const eventsTotal = ref(0); const blockedIPs = ref([]); + const whitelist = ref([]); const protectedRoutes = ref([]); const securityEnabled = ref(false); const realtimeCapture = ref(false); @@ -82,6 +83,39 @@ export const useSecurityStore = defineStore("security", () => { } } + async function fetchWhitelist() { + loading.value = true; + error.value = null; + try { + const response = await securityApi.getWhitelist(); + whitelist.value = response.data.whitelist || []; + } catch (e: any) { + error.value = e.response?.data?.error || e.message; + } finally { + loading.value = false; + } + } + + async function addWhitelistEntry(entry: { value: string; type: WhitelistEntry["type"]; reason?: string }) { + try { + await securityApi.addWhitelistEntry(entry); + await fetchWhitelist(); + } catch (e: any) { + error.value = e.response?.data?.error || e.message; + throw e; + } + } + + async function removeWhitelistEntry(id: number) { + try { + await securityApi.removeWhitelistEntry(id); + await fetchWhitelist(); + } catch (e: any) { + error.value = e.response?.data?.error || e.message; + throw e; + } + } + async function fetchProtectedRoutes() { loading.value = true; error.value = null; @@ -188,6 +222,7 @@ export const useSecurityStore = defineStore("security", () => { events, eventsTotal, blockedIPs, + whitelist, protectedRoutes, securityEnabled, realtimeCapture, @@ -199,6 +234,9 @@ export const useSecurityStore = defineStore("security", () => { fetchBlockedIPs, blockIP, unblockIP, + fetchWhitelist, + addWhitelistEntry, + removeWhitelistEntry, fetchProtectedRoutes, addProtectedRoute, updateProtectedRoute, diff --git a/src/types/index.ts b/src/types/index.ts index c21ed3c..195d6d4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -226,6 +226,24 @@ export interface ProtectedRoute { created_at: string; } +export interface WhitelistEntry { + id: number; + value: string; + type: "ip" | "cidr" | "path"; + reason?: string; + is_internal: boolean; + created_at: string; +} + +export interface ConfigEntry { + key: string; + type: string; + value: unknown; + default?: unknown; + description?: string; + sensitive?: boolean; +} + export interface SecurityStats { total_events: number; last_24_hours: number; @@ -327,6 +345,8 @@ export type Permission = | "apikeys:delete" | "settings:read" | "settings:write" + | "config:read" + | "config:write" | "audit:read" | "containers:read" | "containers:write" diff --git a/src/views/SecurityView.vue b/src/views/SecurityView.vue index dda5589..a1771ce 100644 --- a/src/views/SecurityView.vue +++ b/src/views/SecurityView.vue @@ -384,6 +384,48 @@ + +
+
+

Whitelisted Sources

+ +
+ +
+
+ +

No whitelist entries

+ Whitelisted IPs, networks, and paths never generate events or blocks +
+
+
+
+ {{ entry.value }} + {{ entry.reason }} +
+ {{ entry.type.toUpperCase() }} + Internal + Added {{ formatTime(entry.created_at) }} +
+
+ +
+
+
+
+
@@ -521,6 +563,46 @@
+
+
+
+

Detection Thresholds

+

+ Limits applied per source IP within the detection window. When a limit is exceeded, the IP is + automatically blocked for the configured duration. Changes apply immediately, no restart needed. +

+
+
+
+
+ + + + {{ field.hint }} +
+
+
+ +
+
+
@@ -573,6 +655,49 @@
+ + + + +