mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
add ToolFirewall view
This commit is contained in:
parent
1284237019
commit
6f37de6d29
4 changed files with 329 additions and 4 deletions
|
@ -47,6 +47,7 @@
|
||||||
"catalog": "Catalog",
|
"catalog": "Catalog",
|
||||||
"check": "Check",
|
"check": "Check",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
|
"closed": "closed",
|
||||||
"common": {
|
"common": {
|
||||||
"firstname": "First name",
|
"firstname": "First name",
|
||||||
"lastname": "Last name"
|
"lastname": "Last name"
|
||||||
|
@ -56,8 +57,8 @@
|
||||||
"confirm_app_default": "Are you sure you want to make this app default?",
|
"confirm_app_default": "Are you sure you want to make this app default?",
|
||||||
"confirm_change_maindomain": "Are you sure you want to change the main domain?",
|
"confirm_change_maindomain": "Are you sure you want to change the main domain?",
|
||||||
"confirm_delete": "Are you sure you want to delete {name}?",
|
"confirm_delete": "Are you sure you want to delete {name}?",
|
||||||
"confirm_firewall_open": "Are you sure you want to open port %s? (protocol: %s, connection: %s)",
|
"confirm_firewall_open": "Are you sure you want to open port {port} (protocol: {protocol}, connection: {connection})",
|
||||||
"confirm_firewall_close": "Are you sure you want to close port %s? (protocol: %s, connection: %s)",
|
"confirm_firewall_close": "Are you sure you want to close port {port} (protocol: {protocol}, connection: {connection})",
|
||||||
"confirm_install_custom_app": "WARNING! Installing 3rd party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. Are you willing to take that risk?",
|
"confirm_install_custom_app": "WARNING! Installing 3rd party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. Are you willing to take that risk?",
|
||||||
"confirm_install_domain_root": "You will not be able to install any other app on %s. Continue?",
|
"confirm_install_domain_root": "You will not be able to install any other app on %s. Continue?",
|
||||||
"confirm_install_app_warning": "Warning: this application may work but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available.",
|
"confirm_install_app_warning": "Warning: this application may work but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available.",
|
||||||
|
@ -125,8 +126,8 @@
|
||||||
"username_exists": "The user '{name}' already exists",
|
"username_exists": "The user '{name}' already exists",
|
||||||
"username_syntax": "Invalid username: Must be lower-case alphanumeric and underscore characters only",
|
"username_syntax": "Invalid username: Must be lower-case alphanumeric and underscore characters only",
|
||||||
"domain_syntax": "Invalid domain name: Must be lower-case alphanumeric, dot and dash characters only",
|
"domain_syntax": "Invalid domain name: Must be lower-case alphanumeric, dot and dash characters only",
|
||||||
"dynDomain_syntax": "Invalid domain name: Must be lower-case alphanumeric and dash characters only"
|
"dynDomain_syntax": "Invalid domain name: Must be lower-case alphanumeric and dash characters only",
|
||||||
|
"firewall_port_already": "Port {port} is already {state} (protocol: {protocol}; connection: {connection})"
|
||||||
},
|
},
|
||||||
"form_input_example": "Example: %s",
|
"form_input_example": "Example: %s",
|
||||||
"from_to": "from %s to %s",
|
"from_to": "from %s to %s",
|
||||||
|
@ -214,6 +215,7 @@
|
||||||
"only_working_apps": "Only working apps",
|
"only_working_apps": "Only working apps",
|
||||||
"only_decent_quality_apps": "Only decent quality apps",
|
"only_decent_quality_apps": "Only decent quality apps",
|
||||||
"open": "Open",
|
"open": "Open",
|
||||||
|
"opened": "opened",
|
||||||
"operations": "Operations",
|
"operations": "Operations",
|
||||||
"orphaned": "Not maintained",
|
"orphaned": "Not maintained",
|
||||||
"orphaned_details": "This app has not been maintained for quite some time. It may still be working, but won't receive any upgrade until somebody volunteers to take care of it. Feel free to contribute to revive it!",
|
"orphaned_details": "This app has not been maintained for quite some time. It may still be working, but won't receive any upgrade until somebody volunteers to take care of it. Feel free to contribute to revive it!",
|
||||||
|
|
|
@ -219,6 +219,17 @@ const routes = [
|
||||||
{ name: 'tool-migrations', trad: 'migrations' }
|
{ name: 'tool-migrations', trad: 'migrations' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tool-firewall',
|
||||||
|
path: '/tools/firewall',
|
||||||
|
component: () => import(/* webpackChunkName: "views/tools" */ '@/views/tool/ToolFirewall'),
|
||||||
|
meta: {
|
||||||
|
breadcrumb: [
|
||||||
|
{ name: 'tool-list', trad: 'tools' },
|
||||||
|
{ name: 'tool-firewall', trad: 'firewall' }
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,10 @@ body {
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
// collapse icon
|
// collapse icon
|
||||||
.not-collapsed .icon {
|
.not-collapsed .icon {
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
|
|
308
app/src/views/tool/ToolFirewall.vue
Normal file
308
app/src/views/tool/ToolFirewall.vue
Normal file
|
@ -0,0 +1,308 @@
|
||||||
|
<template>
|
||||||
|
<div class="tool-log">
|
||||||
|
<!-- PORTS -->
|
||||||
|
<b-card>
|
||||||
|
<template v-slot:header>
|
||||||
|
<h2><icon iname="shield" /> {{ $t('ports') }}</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-for="(items, protocol) in protocols" :key="protocol">
|
||||||
|
<h5>{{ $t(protocol) }}</h5>
|
||||||
|
<b-table
|
||||||
|
:fields="fields" :items="items"
|
||||||
|
small striped responsive="true"
|
||||||
|
>
|
||||||
|
<!-- PORT CELL -->
|
||||||
|
<template v-slot:cell(port)="data">
|
||||||
|
{{ data.value }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- CONNECTIONS CELL -->
|
||||||
|
<template v-slot:cell()="data">
|
||||||
|
<b-checkbox
|
||||||
|
v-if="data.field.key !== 'uPnP'"
|
||||||
|
class="on-off-switch"
|
||||||
|
v-model="data.value"
|
||||||
|
switch
|
||||||
|
@change="onToggle(protocol, data.field.key, data.item.port, data.index, $event)"
|
||||||
|
>
|
||||||
|
<span :class="'btn btn-sm py-0 btn-' + (data.value ? 'danger' : 'success')">
|
||||||
|
{{ $t(data.value ? 'close' : 'open') }}
|
||||||
|
</span>
|
||||||
|
</b-checkbox>
|
||||||
|
|
||||||
|
<icon
|
||||||
|
v-else
|
||||||
|
:iname="data.value ? 'check' : 'times'"
|
||||||
|
:class="data.value ? 'text-success' : 'text-danger'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</b-table>
|
||||||
|
</div>
|
||||||
|
</b-card>
|
||||||
|
|
||||||
|
<!-- OPERATIONS -->
|
||||||
|
<b-card>
|
||||||
|
<template v-slot:header>
|
||||||
|
<h2><icon iname="cogs" /> {{ $t('operations') }}</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<b-form
|
||||||
|
id="port-form" inline class="d-flex justify-content-between"
|
||||||
|
@submit.prevent="onFormSubmit"
|
||||||
|
>
|
||||||
|
<b-input-group :prepend="$t('action')">
|
||||||
|
<b-select
|
||||||
|
id="input-action"
|
||||||
|
v-model="form.action" :options="actionChoices"
|
||||||
|
/>
|
||||||
|
</b-input-group>
|
||||||
|
|
||||||
|
<b-input-group :prepend="$t('port')">
|
||||||
|
<b-input
|
||||||
|
id="input-port" placeholder="0"
|
||||||
|
type="number" min="0" max="65535"
|
||||||
|
v-model.number="form.port"
|
||||||
|
/>
|
||||||
|
</b-input-group>
|
||||||
|
|
||||||
|
<b-input-group :prepend="$t('connection')">
|
||||||
|
<b-select
|
||||||
|
id="input-connection"
|
||||||
|
v-model="form.connection" :options="connectionChoices"
|
||||||
|
/>
|
||||||
|
</b-input-group>
|
||||||
|
|
||||||
|
<b-input-group :prepend="$t('protocol')">
|
||||||
|
<b-select
|
||||||
|
id="input-protocol"
|
||||||
|
v-model="form.protocol" :options="protocolChoices"
|
||||||
|
/>
|
||||||
|
</b-input-group>
|
||||||
|
</b-form>
|
||||||
|
|
||||||
|
<template v-slot:footer>
|
||||||
|
<b-button type="submit" form="port-form" variant="success">
|
||||||
|
{{ $t('save') }}
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
</b-card>
|
||||||
|
|
||||||
|
<!-- UPnP -->
|
||||||
|
<b-card :body-text-variant="upnpEnabled ? 'success' : 'danger'">
|
||||||
|
<template v-slot:header>
|
||||||
|
<h2><icon iname="exchange" /> {{ $t('upnp') }}</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
{{ $t(upnpEnabled ? 'upnp_enabled' : 'upnp_disabled' ) }}
|
||||||
|
|
||||||
|
<b-form-invalid-feedback :state="upnpError !== '' ? false : null">
|
||||||
|
{{ upnpError }}
|
||||||
|
</b-form-invalid-feedback>
|
||||||
|
|
||||||
|
<template v-slot:footer>
|
||||||
|
<b-button
|
||||||
|
:variant="!upnpEnabled ? 'success' : 'danger'"
|
||||||
|
v-b-modal.toggle-upnp-modal
|
||||||
|
>
|
||||||
|
{{ $t(!upnpEnabled ? 'enabled' : 'disabled' ) }}
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
</b-card>
|
||||||
|
|
||||||
|
<!-- TOGGLE PORT CONFIRM MODAL -->
|
||||||
|
<b-modal
|
||||||
|
no-close-on-backdrop centered hide-header
|
||||||
|
body-bg-variant="danger" body-text-variant="light"
|
||||||
|
@ok="togglePort(portToToggle)" ref="modal"
|
||||||
|
@cancel="onCancel"
|
||||||
|
>
|
||||||
|
{{ portToToggle ? $t('confirm_firewall_' + portToToggle.action, portToToggle) : '' }}
|
||||||
|
</b-modal>
|
||||||
|
|
||||||
|
<!-- TOGGLE UPNP CONFIRM MODAL -->
|
||||||
|
<b-modal
|
||||||
|
id="toggle-upnp-modal"
|
||||||
|
no-close-on-backdrop centered hide-header
|
||||||
|
body-bg-variant="danger" body-text-variant="light"
|
||||||
|
@ok="toggleUpnp(!upnpEnabled)"
|
||||||
|
>
|
||||||
|
{{ $t('confirm_upnp_' + (upnpEnabled ? 'disable' : 'enable')) }}
|
||||||
|
</b-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import api from '@/helpers/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ToolFirewall',
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
// Tables data
|
||||||
|
fields: [
|
||||||
|
{ key: 'port', label: this.$i18n.t('port') },
|
||||||
|
{ key: 'ipv4', label: this.$i18n.t('ipv4') },
|
||||||
|
{ key: 'ipv6', label: this.$i18n.t('ipv6') },
|
||||||
|
{ key: 'uPnP', label: this.$i18n.t('upnp') }
|
||||||
|
],
|
||||||
|
protocols: undefined,
|
||||||
|
portToToggle: undefined,
|
||||||
|
|
||||||
|
// Form data
|
||||||
|
actionChoices: [
|
||||||
|
{ value: 'open', text: this.$i18n.t('open') },
|
||||||
|
{ value: 'close', text: this.$i18n.t('close') }
|
||||||
|
],
|
||||||
|
connectionChoices: [
|
||||||
|
{ value: 'ipv4', text: this.$i18n.t('ipv4') },
|
||||||
|
{ value: 'ipv6', text: this.$i18n.t('ipv6') }
|
||||||
|
],
|
||||||
|
protocolChoices: [
|
||||||
|
{ value: 'TCP', text: this.$i18n.t('tcp') },
|
||||||
|
{ value: 'UDP', text: this.$i18n.t('udp') },
|
||||||
|
{ value: 'Both', text: this.$i18n.t('both') }
|
||||||
|
],
|
||||||
|
form: {
|
||||||
|
action: 'open',
|
||||||
|
port: undefined,
|
||||||
|
connection: 'ipv4',
|
||||||
|
protocol: 'TCP'
|
||||||
|
},
|
||||||
|
|
||||||
|
// uPnP
|
||||||
|
upnpEnabled: undefined,
|
||||||
|
upnpError: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
fetchData () {
|
||||||
|
api.get('/firewall?raw').then(data => {
|
||||||
|
const ports = Object.values(data).reduce((ports, protocols) => {
|
||||||
|
for (const type of ['TCP', 'UDP']) {
|
||||||
|
for (const port of protocols[type]) {
|
||||||
|
ports[type].add(port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ports
|
||||||
|
}, { TCP: new Set(), UDP: new Set() })
|
||||||
|
|
||||||
|
const tables = {
|
||||||
|
TCP: [],
|
||||||
|
UDP: []
|
||||||
|
}
|
||||||
|
for (const protocol of ['TCP', 'UDP']) {
|
||||||
|
for (const port of ports[protocol]) {
|
||||||
|
const row = { port }
|
||||||
|
for (const connection of ['ipv4', 'ipv6', 'uPnP']) {
|
||||||
|
row[connection] = data[connection][protocol].includes(port)
|
||||||
|
}
|
||||||
|
tables[protocol].push(row)
|
||||||
|
}
|
||||||
|
tables[protocol].sort((a, b) => a.port < b.port ? -1 : 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.protocols = tables
|
||||||
|
this.upnpEnabled = data.uPnP.enabled
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
togglePort ({ port, protocol, connection, action, index }) {
|
||||||
|
const method = action === 'open' ? 'post' : 'delete'
|
||||||
|
api[method](`/firewall/port?${connection}_only`, { port, protocol }).then(() => {
|
||||||
|
if (index === -1) this.fetchData()
|
||||||
|
this.portToToggle = undefined
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleUpnp (value) {
|
||||||
|
api.get('firewall/upnp?action=' + (value ? 'enable' : 'disable')).then(r => {
|
||||||
|
// FIXME Couldn't test when it works.
|
||||||
|
this.fetchData()
|
||||||
|
}).catch(err => {
|
||||||
|
this.upnpError = err.message
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onCancel () {
|
||||||
|
const { protocol, index, connection, value } = this.portToToggle
|
||||||
|
if (index > -1) {
|
||||||
|
this.$set(this.protocols[protocol][index], connection, !value)
|
||||||
|
}
|
||||||
|
this.portToToggle = undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
onToggle (protocol, connection, port, index, value) {
|
||||||
|
this.$set(this.protocols[protocol][index], connection, value)
|
||||||
|
this.portToToggle = {
|
||||||
|
protocol, connection, port, action: value ? 'open' : 'close', index, value
|
||||||
|
}
|
||||||
|
this.$refs.modal.show()
|
||||||
|
},
|
||||||
|
|
||||||
|
onFormSubmit (e) {
|
||||||
|
// IMPROVEMENT: could check if ports are already opened for known ports (tricky with protocol='Both')
|
||||||
|
this.portToToggle = {
|
||||||
|
...this.form,
|
||||||
|
value: this.form.action === 'open',
|
||||||
|
// set index to -1 to trigger `this.fetchData` at modal `@ok`
|
||||||
|
index: -1
|
||||||
|
}
|
||||||
|
this.$refs.modal.show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created () {
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .on-off-switch {
|
||||||
|
.custom-control-input {
|
||||||
|
&:checked ~ .custom-control-label::before {
|
||||||
|
border-color: $success;
|
||||||
|
background-color: $success;
|
||||||
|
}
|
||||||
|
&:not(:checked) ~ .custom-control-label {
|
||||||
|
&::before {
|
||||||
|
border-color: $danger;
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
background-color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus ~ .custom-control-label, &:hover {
|
||||||
|
span {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
visibility: hidden;
|
||||||
|
@include media-breakpoint-down(xs) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-bottom: -1rem;
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 1rem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in a new issue