Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added SSH Configuration #8

Merged
merged 11 commits into from
Jul 12, 2023
Prev Previous commit
Next Next commit
feat(ssh): added ssh configuration component
  • Loading branch information
Tbaile committed Jul 12, 2023
commit 2f04cc3a37683380d177ba0963bba3ef07dc6084
14 changes: 13 additions & 1 deletion public/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"http_404": "Resource not found",
"http_500": "Server error",
"required": "Required",
"validation_failed": "Validation failed",
"invalid_hostname": "Invalid hostname",
"hostname_is_too_long": "Hostname has too many characters",
"cannot_save_configuration": "Cannot save configuration",
Expand Down Expand Up @@ -107,7 +108,18 @@
"title": "Services"
},
"ssh": {
"title": "SSH"
"title": "SSH",
"ssh_access": {
"description": "Dropbear offers SSH network shell access and an integrated SCP server",
"title": "SSH Access",
"tcp_port_label": "TCP Port",
"allow_ssh_password_auth": "Allow SSH password authentication",
"allow_root_login_with_password": "Allow the root user to login with password",
"allow_remote_host_connection": "Allow remote hosts to connect to local SSH forwarded ports",
"tcp_port_too_low": "TCP port must be greater than 1",
"tcp_port_too_high": "TCP port must be less than 65535",
"tcp_port_invalid": "TCP port is invalid"
}
},
"backup_and_restore": {
"title": "Backup & restore"
Expand Down
4 changes: 4 additions & 0 deletions src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,7 @@ body {
transform: translateY(-15px);
opacity: 0;
}

.page-title {
@apply font-medium leading-normal text-gray-900 dark:text-gray-100 text-2xl mb-8;
}
151 changes: 151 additions & 0 deletions src/components/standalone/ssh/SshConfig.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<script lang="ts" setup>
import type { Ref } from 'vue'
import { onMounted, ref } from 'vue'
import {
getAxiosErrorMessage,
NeButton,
NeCheckbox,
NeInlineNotification,
NeSkeleton,
NeTextInput
} from '@nethserver/vue-tailwind-lib'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import InputLayout from '@/components/standalone/InputLayout.vue'
import { useI18n } from 'vue-i18n'
import { ubusCall } from '@/lib/standalone/ubus'
import { AxiosError } from 'axios'
import { useUciPendingChangesStore } from '@/stores/standalone/uciPendingChanges'
import { MessageBag, ValidationError } from '@/lib/validation'

const { t } = useI18n()
const uciChangesStore = useUciPendingChangesStore()

type SshConfigResponse = {
data: {
values: {
cfg014dd4: {
Tbaile marked this conversation as resolved.
Show resolved Hide resolved
Port: string
PasswordAuth: string
RootPasswordAuth: string
GatewayPorts: string
}
}
}
}

const port = ref(22)
const passwordAuth = ref(false)
const rootPasswordAuth = ref(false)
const gatewayPorts = ref(false)
const loading = ref(true)
const validationErrors = ref(new MessageBag())
const error: Ref<Error | undefined> = ref(undefined)

onMounted(() => {
load()
})

function submit() {
error.value = undefined
validate()
if (validationErrors.value.size > 0) {
error.value = new ValidationError()
} else {
loading.value = true
ubusCall('uci', 'set', {
config: 'dropbear',
section: 'cfg014dd4',
Tbaile marked this conversation as resolved.
Show resolved Hide resolved
values: {
Port: port.value,
PasswordAuth: passwordAuth.value ? 'on' : 'off',
RootPasswordAuth: rootPasswordAuth.value ? 'on' : 'off',
GatewayPorts: gatewayPorts.value ? 'on' : 'off'
}
})
.then(() => {
uciChangesStore.getChanges()
})
.catch((exception: AxiosError) => {
error.value = new Error(getAxiosErrorMessage(exception))
})
.finally(() => {
loading.value = false
})
}
}

function validate() {
validationErrors.value = new MessageBag()
if (Number.isNaN(Number(port.value))) {
validationErrors.value.set('port', [t('standalone.ssh.ssh_access.tcp_port_invalid')])
}
if (Number(port.value) < 1) {
validationErrors.value.set('port', [t('standalone.ssh.ssh_access.tcp_port_too_low')])
}
if (Number(port.value) > 65535) {
validationErrors.value.set('port', [t('standalone.ssh.ssh_access.tcp_port_too_high')])
}
}

function load() {
error.value = undefined
loading.value = true
ubusCall('uci', 'get', { config: 'dropbear' })
Tbaile marked this conversation as resolved.
Show resolved Hide resolved
.then((response: SshConfigResponse) => {
port.value = Number(response.data.values.cfg014dd4.Port)
passwordAuth.value = response.data.values.cfg014dd4.PasswordAuth == 'on'
rootPasswordAuth.value = response.data.values.cfg014dd4.RootPasswordAuth == 'on'
gatewayPorts.value = response.data.values.cfg014dd4.GatewayPorts == 'on'
})
.catch((exception: AxiosError) => {
error.value = new Error(getAxiosErrorMessage(exception))
})
.finally(() => {
loading.value = false
})
}
</script>

<template>
<NeInlineNotification v-if="error != undefined" :title="t(error!.message)" kind="error" />
<NeSkeleton v-if="loading" :lines="10"></NeSkeleton>
<InputLayout
v-else
:description="t('standalone.ssh.ssh_access.description')"
:title="t('standalone.ssh.ssh_access.title')"
>
<form>
<!-- Form -->
<div class="flex flex-col gap-y-4 mb-8">
<NeTextInput
v-model.number="port"
:invalid-message="validationErrors.get('port')?.shift()"
:label="t('standalone.ssh.ssh_access.tcp_port_label')"
type="number"
/>
<h3 class="font-medium">Options</h3>
<NeCheckbox
v-model="passwordAuth"
:label="t('standalone.ssh.ssh_access.allow_ssh_password_auth')"
/>
<NeCheckbox
v-model="rootPasswordAuth"
:label="t('standalone.ssh.ssh_access.allow_root_login_with_password')"
/>
<NeCheckbox
v-model="gatewayPorts"
:label="t('standalone.ssh.ssh_access.allow_remote_host_connection')"
/>
</div>
</form>
<!-- Save Button -->
<div class="flex justify-end">
<NeButton kind="primary" @click.prevent="submit()">
<template #prefix>
<FontAwesomeIcon :icon="['fas', 'floppy-disk']" aria-hidden="true" />
</template>
{{ t('common.save') }}
</NeButton>
</div>
</InputLayout>
</template>