Merge branch 'main' into feat-custom-multiselect

This commit is contained in:
Samuel Lorch 2023-04-11 19:42:46 +02:00 committed by GitHub
commit 1b2a1ec6e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 3050 additions and 969 deletions

View file

@ -12,6 +12,7 @@
},
"dependencies": {
"@intlify/unplugin-vue-i18n": "^0.8.2",
"@vee-validate/zod": "^4.8.4",
"@vueuse/core": "^9.13.0",
"@vueuse/head": "^1.1.15",
"axios": "^1.3.4",
@ -25,7 +26,9 @@
"vue": "^3.2.45",
"vue-i18n": "9",
"vue-router": "4",
"ws": "^8.13.0"
"vue-toast-notification": "^3.0",
"ws": "^8.13.0",
"zod": "^3.21.4"
},
"devDependencies": {
"@iconify/json": "^2.2.30",

1380
client/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -5,9 +5,12 @@ import { authenticate, logout, checkAuthentication, setup } from "./api";
import IDashboard from '~icons/ri/dashboard-2-line';
import IRule from '~icons/material-symbols/rule-folder-outline-sharp';
import IAddress from '~icons/eos-icons/ip';
import IEthernet from '~icons/bi/ethernet';
import IService from '~icons/material-symbols/home-repair-service';
import ISNAT from '~icons/mdi/arrow-expand-right';
import IDNAT from '~icons/mdi/arrow-collapse-right';
import IConfig from '~icons/grommet-icons/document-config';
import IStaticRoutes from '~icons/material-symbols/drive-folder-upload-outline-sharp';
enum NavState { Open, Reduced, Collapsed };
const NavStateCount = 3;
@ -17,8 +20,11 @@ const navRoutes = {
"/firewall/forwardrules": { icon: IRule, caption: "Rules" },
"/firewall/sourcenatrules": { icon: ISNAT, caption: "SNAT" },
"/firewall/destinationnatrules": { icon: IDNAT, caption: "DNAT" },
"/network/interfaces": { icon: IEthernet, caption: "Interfaces" },
"/network/staticroutes": { icon: IStaticRoutes, caption: "Static Routes" },
"/object/addresses": { icon: IAddress, caption: "Addresses" },
"/object/services": { icon: IService, caption: "Services" },
"/config/config": { icon: IConfig, caption: "Config" },
};
enum AuthState { Unauthenticated, MfaRequired, Authenticated };

View file

@ -1,6 +1,9 @@
// import WebSocketServer from 'ws';
import JsonRPC from 'simple-jsonrpc-js';
import axios from "axios";
import { useToast } from 'vue-toast-notification';
const $toast = useToast();
let jrpc = new JsonRPC.connect_xhr('/api');
// let socket = new WebSocket("ws://"+ window.location.host +"/ws/api");
@ -21,6 +24,7 @@ export async function apiCall(method: string, params: Record<string, any>): Prom
if (ex.code === 401) {
UnauthorizedCallback();
} else {
$toast.error(method+ ': ' + ex.message);
console.debug("api call epic fail", ex);
}
return { Data: null, Error: ex};

View file

@ -0,0 +1,87 @@
<script setup lang="ts">
const props = defineModel<{
title: string
validationSchema: Record<string, string | Function>,
sections: {
title: string
fields: {
key: string,
label: string,
as: string,
props: any,
default: any,
enabled?: (values: Record<string, any>) => Boolean,
rules?: (value: any) => true | string,
}[],
}[],
modelValue: any,
submit: (value: any) => boolean,
discard: () => void,
}>();
let { sections, submit, discard, validationSchema } = $(props);
</script>
<template>
<ValidationForm as="div" v-slot="{ values, handleSubmit }" @submit="submit" :validationSchema="validationSchema">
<template v-for="(section, index) in sections" :key="index">
<h4 v-if="section.title">{{ section.title }}</h4>
<div class="section">
<template v-for="(field, index) in section.fields" :key="index">
<template v-if="field.enabled ? field.enabled(values) : true">
<label :for="field.key" v-text="field.label" />
<Field v-if="field.as == 'NumberBox'" :name="field.key" :as="field.as" :rules="field.rules" v-bind="field.props" @update:modelValue="values[field.key] = Number(values[field.key])"/>
<Field v-else :name="field.key" :as="field.as" :rules="field.rules" v-bind="field.props"/>
<ErrorMessage :name="field.key" />
</template>
</template>
</div>
</template>
<div class="actions">
<div class="flex-grow"/>
<button @click="handleSubmit($event, submit)">Submit</button>
<div class="space"/>
<button @click="discard">Discard</button>
<div class="flex-grow"/>
</div>
<p>{{ values }}</p>
</ValidationForm>
</template>
<style scoped>
form {
display: flex;
flex-direction: column;
}
.section {
display: grid;
grid-template-columns: auto 1fr;
padding: 0.5rem;
gap: 0.5rem;
}
.actions {
flex-direction: row;
}
.space {
padding: 0.2rem;
}
.actions > button {
flex-grow: true;
}
h4,
p {
grid-column: 1 / 3;
}
h4 {
background-color: var(--cl-bg-hl);
padding: 0.3rem;
padding-left: 0.5rem;
;
}
</style>

View file

@ -15,15 +15,20 @@ const props = defineModel<{
sortSelf?: boolean,
sortBy?: string,
sortDesc?: boolean,
selection?: number[],
draggable?: boolean,
}>();
let { columns, data, sort, sortSelf, sortBy, sortDesc } = $(props);
let { columns, data, sort, sortSelf, sortBy, sortDesc, selection, draggable } = $(props);
const emit = defineEmits<{
(event: 'rowAction', index: number): void,
(event: 'selectionChanged'): void
(event: 'draggedRow', draggedRow: number, draggedOverRow: number): void,
}>();
let selection = $ref([] as number[]);
if (selection == undefined) {
selection = [];
}
const displayData = $computed(() => (sortSelf && sortBy !== '')
? data?.sort((a, b) => {
@ -97,7 +102,9 @@ function dragDropRow() {
data.splice(draggedRow, 1);
data.splice(draggedOverRow, 0, row);
data = data;
emit("draggedRow", draggedRow, draggedOverRow);
}
// Reset drag data
draggedRow = 0;
draggedOverRow = 0;
@ -123,7 +130,7 @@ function dragDropRow() {
</thead>
<tbody>
<tr v-for="(row, index) in displayData" :key="index"
draggable="true"
:draggable="draggable"
@click="() => rowSelection(index)"
@dblclick="() => emit('rowAction', index)"
@dragstart="() => draggedRow = index"

View file

@ -0,0 +1,36 @@
<script setup lang="ts">
const props = defineModel<{
title: string,
loading: boolean,
columns?: {
heading: string,
path: string,
component?: Component,
}[],
data: Record<string, any>[],
tableProps: any,
selection?: number[],
}>();
let { title, loading, columns, data, selection, tableProps } = $(props);
const emit = defineEmits<{
(event: 'draggedRow', draggedRow: number, draggedOverRow: number): void,
}>();
async function draggedRow(draggedRow: number, draggedOverRow: number) {
emit("draggedRow", draggedRow, draggedOverRow);
}
</script>
<template>
<div>
<PageHeader :title="title">
<slot/>
</PageHeader>
<div v-if="loading" >Loading...</div>
<NiceTable v-else :columns="columns" v-model:selection="selection" @draggedRow="draggedRow" v-bind="tableProps" :data="data"/>
</div>
</template>

View file

@ -0,0 +1,21 @@
<script setup lang="ts">
const props = defineModel<{
modelValue: boolean,
}>();
let { modelValue } = $(props);
</script>
<template>
<div @click="() => modelValue = !modelValue">
<i-material-symbols-check-box-outline v-if="modelValue"/>
<i-material-symbols-check-box-outline-blank v-else/>
</div>
</template>
<style scoped>
div {
cursor: pointer;
}
</style>

View file

@ -0,0 +1,18 @@
<script setup lang="ts">
const props = defineModel<{
modelValue: string,
}>();
let { modelValue } = $(props);
</script>
<template>
<textarea v-model="modelValue" rows="5"/>
</template>
<style scoped>
textarea {
resize: vertical;
}
</style>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
const props = defineModel<{
modelValue: number,
min?: number,
max?: number,
}>();
let { modelValue, min, max } = $(props);
</script>
<template>
<input type="number" v-model.number="modelValue" :min="min" :max="max">
</template>
<style scoped>
</style>

View file

@ -0,0 +1,51 @@
<script setup lang="ts">
const props = defineModel<{
options: {
name: string,
key: string,
icon: Component,
}[],
modelValue: number | string,
useIndex: boolean,
}>();
let { options, modelValue, useIndex } = $(props);
function setSelection(option: any, index: number){
if (useIndex) {
modelValue = index
} else {
modelValue = option.key
}
}
onMounted(async() => {
if (modelValue === undefined) {
if (useIndex) {
modelValue = 0
} else {
modelValue = options[0].key
}
}
});
</script>
<template>
<div>
<button class="option" v-for="(option, index) in options" :key="index" :class="{selected: modelValue == index || modelValue == option.key}" @click="setSelection(option, index)">
<i class="material-icons" v-if="option.icon">{{ option.icon }}</i>
{{ option.name }}
</button>
</div>
</template>
<style scoped>
div {
flex-flow: nowrap;
}
.selected {
background-color: var(--cl-bg-sl);
}
</style>

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
const props = defineModel<{
modelValue: string,
}>();
let { modelValue } = $(props);
</script>
<template>
<input v-model="modelValue">
</template>
<style scoped>
</style>

View file

@ -1,44 +0,0 @@
<script setup lang="ts">
const props = defineModel<{
options: {
name: string,
icon: Component,
selected: boolean,
}[],
}>();
let { options } = $(props);
const emit = defineEmits<{
(event: 'selectionChanged'): void
}>();
function select(option: any) {
for(let opt of options) {
opt.selected = false;
}
option.selected = true;
emit('selectionChanged');
console.debug("selected", options);
}
</script>
<template>
<div>
<button class="option" v-for="(option, index) in options" :key="index" :class="{selected:option.selected}" @click="select(option)">
<i class="material-icons" v-if="option.icon">{{ option.icon }}</i>
{{ option.name }}
</button>
</div>
</template>
<style scoped>
div {
flex-flow: nowrap;
}
.selected {
background-color: var(--cl-bg-sl);
}
</style>

84
client/src/definitions.ts Normal file
View file

@ -0,0 +1,84 @@
import { toFormValidator } from '@vee-validate/zod';
import * as zod from 'zod';
export const editTypes: { [key: string]: { [key: string]: any } } = {
"firewall": {
name: "Firewall",
"forwardrules": {
name: "ForwardRule",
validationSchema: toFormValidator(
zod.object({
name: zod.string(),
verdict: zod.string(),
counter: zod.boolean(),
comment: zod.string().optional(),
}),
),
sections: [
{
fields: [
{ key: "name", label: "Name", as: "TextBox" },
{ key: "verdict", label: "Verdict", as: "PillBar", props: { options: [{ name: 'Accept', key: 'accept' }, { name: 'Drop', key: 'drop' }, { name: 'Continue', key: 'continue' }] } },
{ key: "counter", label: "Counter", as: "CheckBox", },
{ key: "comment", label: "Comment", as: "MultilineTextBox", },
],
},
],
},
},
"network": {
name: "Network",
"interfaces": {
name: "Interface",
validationSchema: toFormValidator(
zod.object({
name: zod.string(),
type: zod.string(),
hardware_interface: zod.string().optional(),
vlan_id: zod.number().optional(),
comment: zod.string().optional(),
}),
),
sections: [
{
fields: [
{ key: "name", label: "Name", as: "TextBox", default: "placeholder" },
{ key: "type", label: "Type", as: "PillBar", props: { options: [{ name: 'Hardware', key: 'hardware' }, { name: 'VLAN', key: 'vlan' }, { name: 'Bond', key: 'bond' }, { name: 'Bridge', key: 'bridge' }] } },
{ key: "hardware_device", label: "Hardware Device", as: "TextBox", enabled: (values: any) => (values["type"] == 'hardware') },
{ key: "vlan_parent", label: "VLAN Parent", as: "TextBox", enabled: (values: any) => (values["type"] == 'vlan') },
{ key: "vlan_id", label: "VLAN ID", as: "NumberBox", props: { min: 1, max: 4094 }, enabled: (values: any) => (values["type"] == 'vlan') },
{ key: "bond_members", label: "Bond Members", as: "TextBox", enabled: (values: any) => (values["type"] == 'bond') },
{ key: "bridge_members", label: "Bridge Members", as: "TextBox", enabled: (values: any) => (values["type"] == 'bridge') },
],
},
{
title: "Addressing",
fields: [
{ key: "addressing_mode", label: "Addressing Mode", as: "PillBar", props: { options: [{ name: 'None', key: 'none' }, { name: 'Static', key: 'static' }, { name: 'DHCP', key: 'dhcp' }] } },
{ key: "address", label: "Address", as: "TextBox", enabled: (values: any) => (values["addressing_mode"] == 'static') },
{ key: "comment", label: "Comment", as: "MultilineTextBox" },
],
}
],
},
"staticroutes": {
name: "StaticRoute",
validationSchema: toFormValidator(
zod.object({
name: zod.string(),
}),
),
sections: [
{
fields: [
{ key: "name", label: "Name", as: "TextBox", },
{ key: "interface", label: "Interface", as: "TextBox" },
{ key: "gateway", label: "Gateway", as: "TextBox" },
{ key: "destination", label: "Destination", as: "TextBox" },
{ key: "metric", label: "Metric", as: "NumberBox" },
],
},
],
},
},
};

View file

@ -3,4 +3,5 @@
.pad { padding: 0.5rem; }
.gap { gap: 0.5rem; }
.flex-grow { flex-grow: 1; }
.flex-row { flex-direction: row; }
.flex-row { flex-direction: row; }
.scroll { overflow-y:auto; }

View file

@ -47,7 +47,7 @@ th:hover {
}
th, td {
padding: 0.5rem;
border: 0.125rem solid var(--cl-fg);
border: 0.125rem solid var(--cl-bg-el);
}
th > *{
justify-content: center;
@ -77,6 +77,11 @@ button, .button {
background-color: var(--cl-bg-hl);
}
.button:disabled, button:disabled, .disabled {
background-color: var(--cl-bg-hl);
pointer-events: none;
}
input, textarea {
background-color: var(--cl-bg-hl);
border: 1px solid var(--cl-fg);

View file

@ -3,11 +3,21 @@ import './global-styles/components.css';
import './global-styles/colors.css';
import './global-styles/mlfe.css';
import './global-styles/transitions.css';
import 'vue-toast-notification/dist/theme-default.css';
import PillBar from "./components/inputs/PillBar.vue";
import TextBox from "./components/inputs/TextBox.vue";
import NumberBox from "./components/inputs/NumberBox.vue";
import MultilineTextBox from "./components/inputs/MultilineTextBox.vue";
import CheckBox from "./components/inputs/CheckBox.vue";
import { Form, Field, ErrorMessage } from 'vee-validate';
import App from './App.vue';
import { createHead } from '@vueuse/head';
import { createRouter, createWebHistory } from 'vue-router';
import routes from '~pages';
import ToastPlugin from 'vue-toast-notification';
const app = createApp(App);
const head = createHead();
@ -18,5 +28,16 @@ const router = createRouter({
app.use(router);
app.use(head);
app.use(ToastPlugin);
// Global Components
app.component('PillBar', PillBar);
app.component('TextBox', TextBox);
app.component('NumberBox', NumberBox);
app.component('MultilineTextBox', MultilineTextBox);
app.component('CheckBox', CheckBox);
app.component('ValidationForm', Form);
app.component('Field', Field);
app.component('ErrorMessage', ErrorMessage);
app.mount('#app');

View file

@ -1,15 +0,0 @@
<script setup lang="ts">
const props = $defineProps<{entity: string, id: string}>();
const { entity, id } = $(props);
const pageTypes: { [key: string]: any } = {
"rules": { title: "Rules" },
"addresses": { title: "Addresses"},
};
</script>
<template>
<div>
<PageHeader :title="pageTypes[entity].title"/>
{{ entity }} {{ id }}
</div>
</template>

View file

@ -1,90 +0,0 @@
<script setup lang="ts">
const props = $defineProps<{entity: string}>();
const { entity } = $(props);
const pageTypes: { [key: string]: any } = {
"rules": { title: "Rules" },
"addresses": { title: "Addresses"},
};
let searchTerm = $ref("");
</script>
<template>
<div>
<PageHeader :title="pageTypes[entity].title">
<input class="search-bar" placeholder="Search..." v-model="searchTerm"/>
<button>
<i-material-symbols-add/>
</button>
</PageHeader>
<NiceTable :columns="{fname: 'First Name', lname: 'Last Name'}" :sort-self="true" :data="[
{
fname: 'Haynes',
lname: 'Chavez'
}, {
fname: 'Brennan',
lname: 'Bradley'
}, {
fname: 'Blanchard',
lname: 'Thornton'
}, {
fname: 'Benjamin',
lname: 'Nash'
}, {
fname: 'Jan',
lname: 'Bradford'
}, {
fname: 'Zelma',
lname: 'Spears'
}, {
fname: 'Freeman',
lname: 'Page'
}, {
fname: 'Wilson',
lname: 'Carlson'
}, {
fname: 'Lewis',
lname: 'Fuentes'
}, {
fname: 'Vega',
lname: 'Villarreal'
}, {
fname: 'Carolyn',
lname: 'Cardenas'
}, {
fname: 'Angie',
lname: 'Adams'
}, {
fname: 'Richards',
lname: 'Leon'
}, {
fname: 'Velma',
lname: 'Fields'
}, {
fname: 'Witt',
lname: 'Lowe'
}, {
fname: 'Waters',
lname: 'Leblanc'
}, {
fname: 'Henry',
lname: 'Lloyd'
}, {
fname: 'Boone',
lname: 'Greer'
}, {
fname: 'Willis',
lname: 'Stark'
}, {
fname: 'Dickson',
lname: 'Spencer'
}
].filter(x => (`${x.fname} ${x.lname}`).toLowerCase().includes(searchTerm.toLowerCase()))"/>
</div>
</template>
<style scoped>
.page-content {
overflow-y: auto;
}
</style>

View file

@ -0,0 +1,29 @@
<script setup lang="ts">
import { editTypes } from "../../../../definitions";
import getPlugins from '../../../../plugins';
const p = getPlugins();
const props = $defineProps<{subsystem: string, entity: string, id: string}>();
const { subsystem, entity, id } = $(props);
let data = $ref({} as {});
async function update() {
}
</script>
<template>
<div v-if="editTypes[subsystem][entity]">
<PageHeader :title="'Edit ' + editTypes[subsystem][entity].name">
<button @click="update">Update</button>
<button @click="$router.go(-1)">Discard</button>
</PageHeader>
<NiceForm class="scroll cl-secondary" :sections="editTypes[subsystem][entity].sections" v-model="data"/>
</div>
<div v-else>
<PageHeader title="Error"/>
No editType for this Entity
</div>
</template>

View file

@ -0,0 +1,34 @@
<script setup lang="ts">
import { editTypes } from "../../../../definitions";
import { apiCall } from "../../../../api";
import getPlugins from '../../../../plugins';
const p = getPlugins();
const props = $defineProps<{subsystem: string, entity: string}>();
const { subsystem, entity } = $(props);
let data = $ref({});
async function create(value: any) {
console.debug("value", value);
let res = await apiCall(editTypes[subsystem].name +".Create"+ editTypes[subsystem][entity].name, value);
if (res.Error === null) {
p.toast.success("Created " + editTypes[subsystem][entity].name);
p.router.go(-1);
} else {
console.debug("error", res);
}
}
</script>
<template>
<div v-if="editTypes[subsystem][entity]">
<PageHeader :title="'Create ' + editTypes[subsystem][entity].name">
</PageHeader>
<NiceForm class="scroll cl-secondary" :submit="create" :discard="() => $router.go(-1)" :sections="editTypes[subsystem][entity].sections" v-model="data"/>
</div>
<div v-else>
<PageHeader title="Error"/>
No editType for this Entity
</div>
</template>

View file

@ -0,0 +1,77 @@
<script setup lang="ts">
import { apiCall } from "../../api";
import getPlugins from '../../plugins';
const p = getPlugins();
let changelog = $ref([]);
let loading = $ref(false);
const columns = [
{heading: 'Path', path: 'path'},
{heading: 'Type', path: 'type'},
{heading: 'From', path: 'from'},
{heading: 'To', path: 'to'},
];
const displayData = $computed(() => {
let data: any;
data = [];
for (const change of changelog) {
data.push({
path: change.path,
type: change.type,
from: change.from,
to: change.to,
});
}
return data;
});
async function load(){
loading = true
let res = await apiCall("Config.GetPendingChangelog", {});
if (res.Error === null) {
console.debug("changelog", res.Data.Changelog);
changelog = res.Data.Changelog;
} else {
console.debug("error", res);
}
loading = false
}
async function apply(){
let res = await apiCall("Config.ApplyPendingChanges", {});
if (res.Error === null) {
console.debug("apply");
p.toast.success("Applied Pending Config");
} else {
console.debug("error", res);
}
load()
}
async function discard(){
let res = await apiCall("Config.DiscardPendingChanges", {});
if (res.Error === null) {
console.debug("discard");
p.toast.success("Discarded Pending Config");
} else {
console.debug("error", res);
}
load()
}
onMounted(async() => {
load();
});
</script>
<template>
<TableView title="Pending Changes" :columns="columns" :loading="loading" v-model:data="displayData" :table-props="{sort:true, sortSelf: true}">
<button @click="load">Refresh</button>
<button @click="apply">Apply</button>
<button @click="discard">Discard</button>
</TableView>
</template>

View file

@ -1,38 +1,67 @@
<script setup lang="ts">
import { apiCall } from "../../api";
import getPlugins from '../../plugins';
const p = getPlugins();
let rules = $ref([]);
let loading = $ref(false);
let selection = $ref([] as number[]);
const columns = [
{heading: 'Name', path: 'name'},
{heading: 'Source', path: 'match.source_addresses'},
{heading: 'Destination', path: 'match.destination_addresses'},
{heading: 'Service', path: 'match.services'},
{heading: 'Verdict', path: 'verdict'},
{heading: 'Counter', path: 'counter'},
{heading: 'Comment', path: 'comment'},
];
async function loadRules(){
async function load(){
let res = await apiCall("Firewall.GetDestinationNATRules", {});
if (res.Error === null) {
rules = res.Data.DestinationNATRules;
rules = res.Data.destination_nat_rules;
console.debug("rules", rules);
} else {
console.debug("error", res);
}
}
async function deleteRule(){
let res = await apiCall("Firewall.DeleteDestinationNATRule", {index: selection[0]});
if (res.Error === null) {
console.debug("deleted rule");
p.toast.success("Deleted Rule");
} else {
console.debug("error", res);
}
load();
}
async function draggedRow(draggedRow: number, draggedOverRow: number) {
console.log("dragged", draggedRow, draggedOverRow);
let res = await apiCall("Firewall.MoveDestinationNATRule", {index: draggedRow, to_index: draggedOverRow});
if (res.Error === null) {
console.debug("moved rule");
p.toast.success("Moved Rule");
} else {
console.debug("error", res);
}
load();
}
onMounted(async() => {
loadRules();
load();
});
</script>
<template>
<div>
<PageHeader title="DNAT Rules">
<button @click="loadRules">Load Rules</button>
</PageHeader>
<NiceTable :columns="columns" v-model:data="rules"/>
<TableView title="DNAT Rules" :columns="columns" :loading="loading" @draggedRow="draggedRow" v-model:selection="selection" v-model:data="rules" :table-props="{sort:true, sortSelf: true, draggable: true}">
<button @click="load">Refresh</button>
<router-link class="button" to="/firewall/destinationnatrules/edit">Create</router-link>
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/firewall/destinationnatrules/edit/' + selection[0]">Edit</router-link>
<button @click="deleteRule" :disabled="selection.length != 1">Delete</button>
</TableView>
</div>
</template>

View file

@ -1,7 +1,12 @@
<script setup lang="ts">
import { apiCall } from "../../api";
import getPlugins from '../../plugins';
const p = getPlugins();
let rules = $ref([]);
let loading = $ref(false);
let selection = $ref([] as number[]);
const columns = [
{heading: 'Name', path: 'name'},
{heading: 'Source', path: 'match.source_addresses'},
@ -12,27 +17,52 @@ const columns = [
{heading: 'Comment', path: 'comment'},
];
async function loadRules(){
async function load(){
let res = await apiCall("Firewall.GetForwardRules", {});
if (res.Error === null) {
rules = res.Data.ForwardRules;
rules = res.Data.forward_rules;
console.debug("rules", rules);
} else {
console.debug("error", res);
}
}
async function deleteRule(){
let res = await apiCall("Firewall.DeleteForwardRule", {index: selection[0]});
if (res.Error === null) {
console.debug("deleted rule");
p.toast.success("Deleted Rule");
} else {
console.debug("error", res);
}
load();
}
async function draggedRow(draggedRow: number, draggedOverRow: number) {
console.log("dragged", draggedRow, draggedOverRow);
let res = await apiCall("Firewall.MoveForwardRule", {index: draggedRow, to_index: draggedOverRow});
if (res.Error === null) {
console.debug("moved rule");
p.toast.success("Moved Rule");
} else {
console.debug("error", res);
}
load();
}
onMounted(async() => {
loadRules();
load();
});
</script>
<template>
<div>
<PageHeader title="Forward Rules">
<button @click="loadRules">Load Rules</button>
</PageHeader>
<NiceTable :columns="columns" v-model:data="rules"/>
<TableView title="Forward Rules" :columns="columns" :loading="loading" @draggedRow="draggedRow" v-model:selection="selection" v-model:data="rules" :table-props="{sort:true, sortSelf: true, draggable: true}">
<button @click="load">Refresh</button>
<router-link class="button" to="/firewall/forwardrules/edit">Create</router-link>
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/firewall/forwardrules/edit/' + selection[0]">Edit</router-link>
<button @click="deleteRule" :disabled="selection.length != 1">Delete</button>
</TableView>
</div>
</template>

View file

@ -1,38 +1,67 @@
<script setup lang="ts">
import { apiCall } from "../../api";
import getPlugins from '../../plugins';
const p = getPlugins();
let rules = $ref([]);
let loading = $ref(false);
let selection = $ref([] as number[]);
const columns = [
{heading: 'Name', path: 'name'},
{heading: 'Source', path: 'match.source_addresses'},
{heading: 'Destination', path: 'match.destination_addresses'},
{heading: 'Service', path: 'match.services'},
{heading: 'Verdict', path: 'verdict'},
{heading: 'Counter', path: 'counter'},
{heading: 'Comment', path: 'comment'},
];
async function loadRules(){
async function load(){
let res = await apiCall("Firewall.GetSourceNATRules", {});
if (res.Error === null) {
rules = res.Data.SourceNATRules;
rules = res.Data.source_nat_rules;
console.debug("rules", rules);
} else {
console.debug("error", res);
}
}
async function deleteRule(){
let res = await apiCall("Firewall.DeleteSourceNATRule", {index: selection[0]});
if (res.Error === null) {
console.debug("deleted rule");
p.toast.success("Deleted Rule");
} else {
console.debug("error", res);
}
load();
}
async function draggedRow(draggedRow: number, draggedOverRow: number) {
console.log("dragged", draggedRow, draggedOverRow);
let res = await apiCall("Firewall.MoveSourceNATRule", {index: draggedRow, to_index: draggedOverRow});
if (res.Error === null) {
console.debug("moved rule");
p.toast.success("Moved Rule");
} else {
console.debug("error", res);
}
load();
}
onMounted(async() => {
loadRules();
load();
});
</script>
<template>
<div>
<PageHeader title="SNAT Rules">
<button @click="loadRules">Load Rules</button>
</PageHeader>
<NiceTable :columns="columns" v-model:data="rules"/>
<TableView title="SNAT Rules" :columns="columns" :loading="loading" @draggedRow="draggedRow" v-model:selection="selection" v-model:data="rules" :table-props="{sort:true, sortSelf: true, draggable: true}">
<button @click="load">Refresh</button>
<router-link class="button" to="/firewall/sourcenatrules/edit">Create</router-link>
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/firewall/sourcenatrules/edit/' + selection[0]">Edit</router-link>
<button @click="deleteRule" :disabled="selection.length != 1">Delete</button>
</TableView>
</div>
</template>

View file

@ -1,39 +1,39 @@
<script setup lang="ts">
import { apiCall } from "../api";
async function doShit(){
apiCall("Firewall.GetForwardRules", {});
let links = $ref([]);
let loading = $ref(false);
async function load(){
loading = true
let res = await apiCall("Network.GetLinks", {});
if (res.Error === null) {
console.debug("links", res.Data.Links);
links = res.Data.Links;
} else {
console.debug("error", res);
}
loading = false
}
let name = $ref("");
let comment = $ref("");
let counter = $ref(false);
let options = $ref([{name: 'Accept'}, {name: 'Drop'}, {name: 'Continue'}]);
onMounted(async() => {
load();
});
</script>
<template>
<div style="overflow-y: auto;">
<PageHeader title="Dashboard">
<button @click="doShit">Example Buttons</button>
</PageHeader>
<form @submit="$event => $event.preventDefault()" class="cl-secondary">
<h3>Create Rule</h3>
<label for="name" v-text="'Name'"/>
<input name="name" v-model="name"/>
<label for="counter" v-text="'Counter'"/>
<input name="counter" type="checkbox" v-model="counter"/>
<label for="comment" v-text="'Comment'"/>
<textarea name="comment" v-model="comment"></textarea>
<label for="verdict" v-text="'Verdict'"/>
<pillbar :options="options" name="verdict" ></pillbar>
<button>Submit</button>
</form>
<Multiselect/>
asd
<div v-if="!loading" v-for="(link, index) in links" :key="index">
<p>{{ link.name }} {{ link.carrier_state }} {{ link.operational_state }}</p>
</div>
<div v-else>
Loading...
</div>
</div>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,66 @@
<script setup lang="ts">
import { apiCall } from "../../api";
let interfaces = $ref({});
let loading = $ref(false);
let selection = $ref([] as number[]);
const columns = [
{heading: 'Name', path: 'name'},
{heading: 'Type', path: 'type'},
{heading: 'Members', path: 'members'},
{heading: 'Addressing Mode', path: 'addressing_mode'},
{heading: 'Address', path: 'address'},
];
const displayData = $computed(() => {
let data: any;
data = [];
for (const name in interfaces) {
data.push({
name,
type: interfaces[name].type,
addressing_mode: interfaces[name].addressing_mode,
address: interfaces[name].address,
comment: interfaces[name].comment,
});
}
return data;
});
async function load(){
loading = true
let res = await apiCall("Network.GetInterfaces", {});
if (res.Error === null) {
console.debug("interfaces", res.Data.Interfaces);
interfaces = res.Data.Interfaces;
} else {
console.debug("error", res);
}
loading = false
}
async function deleteInterface(){
let res = await apiCall("Network.DeleteInterface", {name: displayData[selection[0]].name});
if (res.Error === null) {
console.debug("deleted interface");
} else {
console.debug("error", res);
}
load();
}
onMounted(async() => {
load();
});
</script>
<template>
<TableView title="Interfaces" :columns="columns" :loading="loading" v-model:selection="selection" v-model:data="displayData" :table-props="{sort:true, sortSelf: true}">
<button @click="load">Refresh</button>
<router-link class="button" to="/network/interfaces/edit">Create</router-link>
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/network/interfaces/edit/' + selection[0]">Edit</router-link>
<button @click="deleteInterface" :disabled="selection.length != 1">Delete</button>
</TableView>
</template>

View file

@ -0,0 +1,51 @@
<script setup lang="ts">
import { apiCall } from "../../api";
let staticRoutes = $ref([]);
let loading = $ref(false);
let selection = $ref([] as number[]);
const columns = [
{heading: 'Name', path: 'name'},
{heading: 'Interface', path: 'interface'},
{heading: 'Gateway', path: 'gateway'},
{heading: 'Destination', path: 'destination'},
{heading: 'Metric', path: 'metric'},
];
async function load(){
loading = true
let res = await apiCall("Network.GetStaticRoutes", {});
if (res.Error === null) {
console.debug("staticRoutes", res.Data.StaticRoutes);
staticRoutes = res.Data.StaticRoutes;
} else {
console.debug("error", res);
}
loading = false
}
async function deleteStaticRoutes(){
let res = await apiCall("Network.DeleteStaticRoute", {index: selection[0]});
if (res.Error === null) {
console.debug("deleted static routes");
} else {
console.debug("error", res);
}
load();
}
onMounted(async() => {
load();
});
</script>
<template>
<TableView title="Static Routes" :columns="columns" :loading="loading" v-model:selection="selection" v-model:data="staticRoutes" :table-props="{sort:true, sortSelf: true}">
<button @click="load">Refresh</button>
<router-link class="button" to="/network/staticroutes/edit">Create</router-link>
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/network/staticroutes/edit/' + selection[0]">Edit</router-link>
<button @click="deleteStaticRoutes" :disabled="selection.length != 1">Delete</button>
</TableView>
</template>

View file

@ -2,6 +2,9 @@
import { apiCall } from "../../api";
let addresses = $ref([]);
let loading = $ref(false);
let selection = $ref([] as number[]);
const columns = [
{heading: 'Name', path: 'name'},
{heading: 'Type', path: 'type'},
@ -10,6 +13,7 @@ const columns = [
];
async function load(){
loading = true
let res = await apiCall("Object.GetAddresses", {});
if (res.Error === null) {
addresses = res.Data.Addresses;
@ -17,6 +21,52 @@ async function load(){
} else {
console.debug("error", res);
}
loading = false
}
const displayData = $computed(() => {
let data: any;
data = [];
for (const name in addresses) {
data.push({
name,
value: getAddressValue(addresses[name]),
type: addresses[name].type,
comment: addresses[name].comment,
});
}
return data;
});
function getAddressValue(s: any): string {
let value: string;
switch (s.type) {
case "host":
value = s.host;
break;
case "range":
value = s.range;
break;
case "network":
value = s.network;
break;
case "group":
value = s.children;
break;
default:
value = "unkown";
}
return value;
}
async function deleteAddress(){
let res = await apiCall("Object.DeleteAddress", {name: displayData[selection[0]].name});
if (res.Error === null) {
console.debug("deleted address");
} else {
console.debug("error", res);
}
load();
}
onMounted(async() => {
@ -26,10 +76,10 @@ onMounted(async() => {
</script>
<template>
<div>
<PageHeader title="Addresses">
<button @click="load">Load Addresses</button>
</PageHeader>
<NiceTable :columns="columns" v-model:data="addresses"/>
</div>
<TableView title="Addresses" :columns="columns" :loading="loading" v-model:selection="selection" v-model:data="displayData" :table-props="{sort:true, sortSelf: true}">
<button @click="load">Refresh</button>
<router-link class="button" to="/object/addresses/edit">Create</router-link>
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/object/addresses/edit/' + selection[0]">Edit</router-link>
<button @click="deleteAddress" :disabled="selection.length != 1">Delete</button>
</TableView>
</template>

View file

@ -1,7 +1,10 @@
<script setup lang="ts">
import { apiCall } from "../../api";
let services = $ref([]);
let services = $ref({});
let loading = $ref(false);
let selection = $ref([] as number[]);
const columns = [
{heading: 'Name', path: 'name'},
{heading: 'Type', path: 'type'},
@ -9,14 +12,66 @@ const columns = [
{heading: 'Comment', path: 'comment'},
];
const displayData = $computed(() => {
let data: any;
data = [];
for (const name in services) {
data.push({
name,
value: getServiceValue(services[name]),
type: services[name].type,
comment: services[name].comment,
});
}
return data;
});
function getServiceValue(s: any): string {
let value: string;
switch (s.type) {
case "tcp":
case "udp":
value = getServicePortRange(s);
break;
case "icmp":
value = "icmp";
break;
case "group":
value = s.children;
break;
default:
value = "unkown";
}
return value;
}
function getServicePortRange(s:any): string {
if (s.dport_end) {
return s.dport_start + "-" + s.dport_end;
}
return s.dport_start;
}
async function load(){
loading = true
let res = await apiCall("Object.GetServices", {});
if (res.Error === null) {
console.debug("services", res.Data.Services);
services = res.Data.Services;
console.debug("services", services);
} else {
console.debug("error", res);
}
loading = false
}
async function deleteService(){
let res = await apiCall("Object.DeleteService", {name: displayData[selection[0]].name});
if (res.Error === null) {
console.debug("deleted service");
} else {
console.debug("error", res);
}
load();
}
onMounted(async() => {
@ -26,10 +81,10 @@ onMounted(async() => {
</script>
<template>
<div>
<PageHeader title="services">
<button @click="load">Load Services</button>
</PageHeader>
<NiceTable :columns="columns" v-model:data="services"/>
</div>
<TableView title="Services" :columns="columns" :loading="loading" v-model:selection="selection" v-model:data="displayData" :table-props="{sort:true, sortSelf: true}">
<button @click="load">Refresh</button>
<router-link class="button" to="/object/services/edit">Create</router-link>
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/object/services/edit/' + selection[0]">Edit</router-link>
<button @click="deleteService" :disabled="selection.length != 1">Delete</button>
</TableView>
</template>

9
client/src/plugins.ts Normal file
View file

@ -0,0 +1,9 @@
import { useRouter } from 'vue-router';
import { useToast } from "vue-toast-notification";
export default function initiateCommonPlugins() {
const router = useRouter();
const toast = useToast();
return { router, toast };
}