feat: реализовать EvoFirewall V1 control plane
Build and Push EvoFirewall Docker Image / build-and-push (push) Failing after 25s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

API, UI, Linux/MikroTik agents, IP lists, политики, stats, CI и интеграция с auth-portal/EvoBGP.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-20 19:50:54 +07:00
co-authored by Cursor
parent d71b45d86f
commit ebadf70e2b
107 changed files with 15196 additions and 99 deletions
+34
View File
@@ -0,0 +1,34 @@
import { getToken, clearToken, redirectToPortalLogin, isAuthEnabled } from './auth'
const API_BASE = import.meta.env.VITE_API_URL ?? ''
export async function apiFetch<T = unknown>(
path: string,
init: RequestInit = {},
): Promise<T> {
const headers = new Headers(init.headers)
if (!headers.has('Content-Type') && init.body) {
headers.set('Content-Type', 'application/json')
}
const token = getToken()
if (token) headers.set('Authorization', `Bearer ${token}`)
const res = await fetch(`${API_BASE}${path}`, { ...init, headers })
if (res.status === 401 && isAuthEnabled()) {
clearToken()
redirectToPortalLogin(`${window.location.origin}/auth/callback`)
throw new Error('Unauthorized')
}
if (!res.ok) {
let message = res.statusText
try {
const body = (await res.json()) as { error?: { message?: string } }
message = body.error?.message ?? message
} catch {
/* ignore */
}
throw new Error(message)
}
if (res.status === 204) return undefined as T
return (await res.json()) as T
}
+126
View File
@@ -0,0 +1,126 @@
/** Portal JWT storage for EvoFirewall (app id `fw`). */
const TOKEN_KEY = 'fw_auth_token'
const HANDOFF_AT_KEY = 'fw_portal_handoff_at'
const HANDOFF_COOLDOWN_MS = 12_000
const API_BASE = import.meta.env.VITE_API_URL ?? ''
export type AccessClaims = {
sub: string
email: string
name: string
apps: string[]
permissions: string[]
is_admin?: boolean
exp?: number
}
export type RuntimeAuthConfig = {
required: boolean
portalUrl: string
}
let runtimeConfig: RuntimeAuthConfig | null = null
function viteAuthEnabled(): boolean {
return (
import.meta.env.VITE_AUTH_ENABLED === 'true' ||
import.meta.env.VITE_AUTH_ENABLED === '1'
)
}
function vitePortalUrl(): string {
return (import.meta.env.VITE_AUTH_PORTAL_URL ?? 'http://localhost:5175').replace(
/\/$/,
'',
)
}
export async function ensureAuthConfig(): Promise<RuntimeAuthConfig> {
if (runtimeConfig) return runtimeConfig
try {
const res = await fetch(`${API_BASE}/api/auth/config`)
if (res.ok) {
const data = (await res.json()) as {
required?: boolean
portal_url?: string
}
runtimeConfig = {
required: Boolean(data.required) || viteAuthEnabled(),
portalUrl: (data.portal_url || vitePortalUrl()).replace(/\/$/, ''),
}
return runtimeConfig
}
} catch {
/* fallback */
}
runtimeConfig = {
required: viteAuthEnabled(),
portalUrl: vitePortalUrl(),
}
return runtimeConfig
}
export function getToken(): string | null {
return localStorage.getItem(TOKEN_KEY)
}
export function setToken(token: string) {
localStorage.setItem(TOKEN_KEY, token)
}
export function clearToken() {
localStorage.removeItem(TOKEN_KEY)
}
export function isAuthEnabled(): boolean {
return runtimeConfig?.required ?? viteAuthEnabled()
}
export function authPortalUrl(): string {
return runtimeConfig?.portalUrl ?? vitePortalUrl()
}
export function isPortalHandoffCoolingDown(): boolean {
const raw = sessionStorage.getItem(HANDOFF_AT_KEY)
if (!raw) return false
return Date.now() - Number(raw) < HANDOFF_COOLDOWN_MS
}
export function markPortalHandoff(): void {
sessionStorage.setItem(HANDOFF_AT_KEY, String(Date.now()))
}
export function parseClaims(token: string): AccessClaims | null {
try {
const payload = token.split('.')[1]
if (!payload) return null
const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
return JSON.parse(json) as AccessClaims
} catch {
return null
}
}
export function isTokenValid(): boolean {
const t = getToken()
if (!t) return false
const c = parseClaims(t)
if (!c?.exp) return !!t
return c.exp * 1000 > Date.now() + 5_000
}
export function redirectToPortalLogin(returnTo: string) {
if (isPortalHandoffCoolingDown()) return
markPortalHandoff()
const portal = authPortalUrl()
const url = `${portal}/?return_to=${encodeURIComponent(returnTo)}`
window.location.href = url
}
export function logout() {
clearToken()
window.location.href = `${authPortalUrl()}/logout`
}
export const CURRENT_APP_ID = 'fw'