commit 6a065376e1b1d8357291b23da9bb67fea635d4ef Author: Sebastian Krupinski Date: Fri Aug 7 00:56:53 2026 -0400 Initial commit for firewall_manager module diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a6c2b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Frontend development +node_modules/ +*.local +.env.local +.env.*.local +.cache/ +.vite/ +.temp/ +.tmp/ + +# Frontend build +/static/ + +# Backend development +/vendor/ +/lib/vendor/ +coverage/ +*.cache +.phpactor/ + +# Editors +.DS_Store +.vscode/ +.idea/ + +# Logs +*.log diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..94d4e98 --- /dev/null +++ b/composer.json @@ -0,0 +1,9 @@ +{ + "name": "ktxm/firewall_manager", + "type": "project", + "autoload": { + "psr-4": { + "KTXM\\FirewallManager\\": "lib/" + } + } +} diff --git a/lib/Module.php b/lib/Module.php new file mode 100644 index 0000000..1e83132 --- /dev/null +++ b/lib/Module.php @@ -0,0 +1,41 @@ + [ + 'label' => 'Access Firewall Manager', + 'description' => 'Access the firewall administration interface', + 'group' => 'Firewall Management', + ], + ]; + } + + public function registerBI(): array + { + return [ + 'handle' => $this->handle(), + 'namespace' => 'FirewallManager', + 'version' => $this->version(), + 'label' => $this->label(), + 'author' => $this->author(), + 'description' => $this->description(), + 'boot' => 'static/module.mjs', + ]; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..56ac538 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "firewall_manager", + "description": "Ktrix Firewall Manager Module", + "version": "0.0.1", + "private": true, + "license": "AGPL-3.0-or-later", + "author": "Ktrix", + "type": "module", + "scripts": { + "build": "vite build --mode production --config vite.config.ts", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "pinia": "^3.0.0", + "vue": "^3.5.13", + "vue-router": "^5.0.0", + "vuetify": "^4.0.0" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "@vitejs/plugin-vue": "^6.0.0", + "@vue/tsconfig": "^0.9.0", + "sass": "^1.77.1", + "typescript": "~6.0.0", + "vite": "^8.0.0", + "vue-tsc": "^3.0.0" + } +} diff --git a/src/integrations.ts b/src/integrations.ts new file mode 100644 index 0000000..fe21890 --- /dev/null +++ b/src/integrations.ts @@ -0,0 +1,13 @@ +import type { ModuleIntegrations } from '@KTXC/types/moduleTypes' + +const integrations: ModuleIntegrations = { + admin_settings_menu: [{ + id: 'firewall_manager', + label: 'Firewall', + path: '/firewall', + icon: 'mdi-shield-lock-outline', + priority: 90, + }], +} + +export default integrations diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..9000aba --- /dev/null +++ b/src/main.ts @@ -0,0 +1,7 @@ +import routes from '@/routes' +import integrations from '@/integrations' +import type { App as Vue } from 'vue' + +export const css = ['__CSS_FILENAME_PLACEHOLDER__'] +export { routes, integrations } +export default { install(_app: Vue) {} } diff --git a/src/pages/Main.vue b/src/pages/Main.vue new file mode 100644 index 0000000..08ff2d8 --- /dev/null +++ b/src/pages/Main.vue @@ -0,0 +1,241 @@ + + + + + diff --git a/src/routes.ts b/src/routes.ts new file mode 100644 index 0000000..9809ff5 --- /dev/null +++ b/src/routes.ts @@ -0,0 +1,8 @@ +const routes = [{ + name: 'firewall_manager', + path: '/firewall', + component: () => import('@/pages/Main.vue'), + meta: { requiresAuth: true, permission: 'firewall_manager.access' }, +}] + +export default routes diff --git a/src/services/firewallService.ts b/src/services/firewallService.ts new file mode 100644 index 0000000..76e0fb6 --- /dev/null +++ b/src/services/firewallService.ts @@ -0,0 +1,62 @@ +import type { FirewallConfiguration, FirewallLog, FirewallMetrics, FirewallRule, Page, RuleStatus, Scope } from '@/types/firewall' + +function endpoint(scope: Scope, suffix: string): string { + return scope === 'system' ? `/firewall/system${suffix}` : `/firewall${suffix}` +} + +async function request(url: string, init?: RequestInit): Promise { + const response = await fetch(url, { credentials: 'include', ...init }) + const data = await response.json().catch(() => ({})) + if (!response.ok) { + const error = data.error + throw new Error(typeof error === 'string' ? error : error?.message || `Request failed (${response.status})`) + } + return data as T +} + +function json(values: Record): RequestInit { + return { + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(Object.fromEntries(Object.entries(values).filter(([, value]) => value !== null && value !== undefined))), + } +} + +export function listRules(scope: Scope, status: RuleStatus, type?: string, action?: string, offset = 0): Promise> { + const query = new URLSearchParams({ status, limit: '25', offset: String(offset) }) + if (type) query.set('type', type) + if (action) query.set('action', action) + return request(`${endpoint(scope, '/rules')}?${query}`) +} + +export function createRule(scope: Scope, values: Record): Promise<{ rule: FirewallRule }> { + return request(endpoint(scope, '/rules'), { method: 'POST', ...json(values) }) +} + +export function updateRule(scope: Scope, ruleId: string, values: Record): Promise<{ rule: FirewallRule }> { + return request(endpoint(scope, `/rules/${encodeURIComponent(ruleId)}`), { method: 'PATCH', ...json(values) }) +} + +export function deleteRule(scope: Scope, ruleId: string, reason: string): Promise<{ rule: FirewallRule }> { + return request(endpoint(scope, `/rules/${encodeURIComponent(ruleId)}`), { method: 'DELETE', ...json({ reason }) }) +} + +export function listLogs(scope: Scope, filters: Record, offset = 0): Promise> { + const query = new URLSearchParams({ ...filters, limit: '25', offset: String(offset) }) + return request(`${endpoint(scope, '/logs')}?${query}`) +} + +export function metrics(scope: Scope): Promise { + return request(endpoint(scope, '/metrics')) +} + +export function configuration(): Promise { + return request('/firewall/configuration') +} + +export function updateConfiguration(values: FirewallConfiguration & { reason: string }): Promise<{ configuration: FirewallConfiguration }> { + return request('/firewall/configuration', { method: 'PUT', ...json(values) }) +} + +export function maintenance(): Promise> { + return request('/firewall/system/maintenance') +} diff --git a/src/types/firewall.ts b/src/types/firewall.ts new file mode 100644 index 0000000..9d594bd --- /dev/null +++ b/src/types/firewall.ts @@ -0,0 +1,39 @@ +export type Scope = 'tenant' | 'system' +export type RuleStatus = 'active' | 'disabled' | 'expired' | 'all' + +export interface FirewallRule { + id: string + tenantId: string | null + scope: Scope + type: 'ip' | 'ip_range' | 'device' + action: 'allow' | 'block' + value: string + reason: string + createdBy: string | null + createdAt: string + expiresAt: string | null + enabled: boolean + metadata?: Record +} + +export interface FirewallLog { + id: string + tenantId: string | null + ipAddress: string | null + eventType: string + result: string + ruleId: string | null + ruleScope: Scope | null + identityId: string | null + timestamp: string + metadata?: Record +} + +export interface Page { items: T[]; total: number; limit: number; offset: number } +export interface FirewallConfiguration { + enabled: boolean + maxAuthFailures: number + authFailureWindow: number + autoBlockDuration: number +} +export interface FirewallMetrics { blockedRequests: number; since?: string | null } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..cc74236 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,13 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["src/**/*", "src/**/*.vue"], + "exclude": ["node_modules"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "paths": { + "@/*": ["./src/*"], + "@KTXC/*": ["../../core/src/*"] + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..241d0e6 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,15 @@ +{ + "include": ["vite.config.*"], + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "skipLibCheck": true, + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "module": "ESNext", + "moduleResolution": "Bundler", + "moduleDetection": "force", + "types": ["node"] + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..46e2e52 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,35 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +export default defineConfig({ + plugins: [ + vue(), + { + name: 'inject-css-filename', + enforce: 'post', + generateBundle(_options, bundle) { + const cssFile = Object.keys(bundle).find(name => name.endsWith('.css')) + if (!cssFile) return + for (const fileName of Object.keys(bundle)) { + const chunk = bundle[fileName] + if (chunk.type === 'chunk' && chunk.code.includes('__CSS_FILENAME_PLACEHOLDER__')) { + chunk.code = chunk.code.replace(/__CSS_FILENAME_PLACEHOLDER__/g, `static/${cssFile}`) + } + } + }, + }, + ], + resolve: { alias: { '@': path.resolve(__dirname, './src'), '@KTXC': path.resolve(__dirname, '../../core/src') } }, + define: { 'process.env': {}, process: undefined }, + build: { + outDir: 'static', + emptyOutDir: true, + sourcemap: true, + lib: { entry: path.resolve(__dirname, 'src/main.ts'), formats: ['es'], fileName: () => 'module.mjs' }, + rollupOptions: { + external: ['vue', 'vue-router', 'pinia'], + output: { assetFileNames: info => info.name?.endsWith('.css') ? 'firewall_manager-[hash].css' : '[name]-[hash][extname]' }, + }, + }, +})