first commit

This commit is contained in:
yann
2025-11-01 17:25:35 +01:00
commit 155791d05a
85 changed files with 3776 additions and 0 deletions

2
ui/.browserslistrc Normal file
View File

@@ -0,0 +1,2 @@
> 1%
last 2 versions

19
ui/README.md Normal file
View File

@@ -0,0 +1,19 @@
# ui
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

27
ui/package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "ui",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"axios": "^0.19.2",
"is-cidr": "^3.1.0",
"moment": "^2.24.0",
"vue": "^2.6.10",
"vue-moment": "^4.1.0",
"vue-router": "^3.1.6",
"vuetify": "^2.2.18"
},
"devDependencies": {
"@vue/cli-plugin-router": "^4.2.3",
"@vue/cli-service": "^4.2.3",
"sass": "^1.26.3",
"sass-loader": "^8.0.0",
"vue-cli-plugin-vuetify": "^2.0.5",
"vue-template-compiler": "^2.6.10",
"vuetify-loader": "^1.3.0"
}
}

BIN
ui/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

19
ui/public/index.html Normal file
View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.png">
<title>ui</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
</head>
<body>
<noscript>
<strong>We're sorry but ui doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

97
ui/src/App.vue Normal file
View File

@@ -0,0 +1,97 @@
<template>
<v-app id="inspire">
<v-app-bar app>
<img class="mr-3" :src="require('./assets/logo.png')" height="50" alt="Wg Gen Web"/>
<v-toolbar-title to="/">Configuration Wireguard VPN</v-toolbar-title>
<v-spacer />
<v-toolbar-items>
<v-btn to="/clients">
Clients
<v-icon right dark>mdi-account-network-outline</v-icon>
</v-btn>
<v-btn to="/server">
Serveur
<v-icon right dark>mdi-vpn</v-icon>
</v-btn>
</v-toolbar-items>
</v-app-bar>
<v-content>
<v-container>
<router-view />
</v-container>
<Notification v-bind:notification="notification"/>
</v-content>
<v-footer app>
<v-row justify="start" no-gutters>
<v-col cols="12" lg="6" md="12" sm="12">
<div :align="$vuetify.breakpoint.smAndDown ? 'center' : 'left'">
<small>{{ new Date().getFullYear() }} </small>
<small><a class="pr-1 pl-1" href="http://www.wtfpl.net/" target="_blank">Licence WTFPL</a></small>
</div>
</v-col>
</v-row>
<v-row justify="end" no-gutters>
<v-col cols="12" lg="6" md="12" sm="12">
<div :align="$vuetify.breakpoint.smAndDown ? 'center' : 'right'">
<small>Sources </small>
<a href="https://gitea.rnmkcy.eu/yann/wg-webui-fr">Gitea</a>
</div>
</v-col>
</v-row>
</v-footer>
</v-app>
</template>
<script>
import {ApiService} from "./services/ApiService";
import Notification from './components/Notification'
export default {
name: 'App',
components: {
Notification
},
data: () => ({
api: null,
version:'N/A',
notification: {
show: false,
color: '',
text: '',
},
}),
mounted() {
this.api = new ApiService();
this.getVersion()
},
created () {
this.$vuetify.theme.dark = true
},
methods: {
getVersion() {
this.api.get('/server/version').then((res) => {
this.version = res.version;
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
notify(color, msg) {
this.notification.show = true;
this.notification.color = color;
this.notification.text = msg;
}
}
};
</script>

BIN
ui/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,476 @@
<template>
<v-container>
<v-row>
<v-col cols="12">
<v-card dark>
<v-list-item>
<v-list-item-content>
<v-list-item-title class="headline">Clients</v-list-item-title>
</v-list-item-content>
<v-btn
color="success"
@click="startAddClient"
>
Ajout nouveau client
<v-icon right dark>mdi-account-multiple-plus-outline</v-icon>
</v-btn>
</v-list-item>
<v-row>
<v-col
v-for="(client, i) in clients"
:key="i"
sm12 lg6
>
<v-card
:color="client.enable ? '#1F7087' : 'warning'"
class="mx-auto"
raised
shaped
>
<v-list-item>
<v-list-item-content>
<v-list-item-title class="headline">{{ client.name }}</v-list-item-title>
<v-list-item-subtitle>{{ client.email }}</v-list-item-subtitle>
<v-list-item-subtitle>Créé: {{ client.created | formatDate }}</v-list-item-subtitle>
<v-list-item-subtitle>Modifié: {{ client.updated | formatDate }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-avatar
tile
size="150"
>
<v-img :src="`${apiBaseUrl}/client/${client.id}/config?qrcode=true`"/>
</v-list-item-avatar>
</v-list-item>
<v-card-text class="text--primary">
<v-chip
v-for="(ip, i) in client.address"
:key="i"
color="indigo"
text-color="white"
>
<v-icon left>mdi-ip-network</v-icon>
{{ ip }}
</v-chip>
</v-card-text>
<v-card-actions>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
text
:href="`${apiBaseUrl}/client/${client.id}/config?qrcode=false`"
v-on="on"
>
<span class="d-none d-lg-flex">Télécharger</span>
<v-icon right dark>mdi-cloud-download-outline</v-icon>
</v-btn>
</template>
<span>Télécharger</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
text
@click.stop="startUpdateClient(client)"
v-on="on"
>
<span class="d-none d-lg-flex">Editer</span>
<v-icon right dark>mdi-square-edit-outline</v-icon>
</v-btn>
</template>
<span>Editer</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
text
@click="sendEmailClient(client.id)"
v-on="on"
>
<span class="d-none d-lg-flex">Envoyer message</span>
<v-icon right dark>mdi-email-outline</v-icon>
</v-btn>
</template>
<span>Envoyer message</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
text
@click="deleteClient(client)"
v-on="on"
>
<span class="d-none d-lg-flex">Supprimer</span>
<v-icon right dark>mdi-trash-can-outline</v-icon>
</v-btn>
</template>
<span>Supprimer</span>
</v-tooltip>
<v-spacer/>
<v-tooltip right>
<template v-slot:activator="{ on }">
<v-switch
dark
v-on="on"
color="success"
v-model="client.enable"
v-on:change="updateClient(client)"
/>
</template>
<span> {{client.enable ? 'Désactiver' : 'Activer'}} ce client</span>
</v-tooltip>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-card>
</v-col>
</v-row>
<v-dialog
v-if="client"
v-model="dialogAddClient"
max-width="550"
>
<v-card>
<v-card-title class="headline">Ajout nouveau client</v-card-title>
<v-card-text>
<v-row>
<v-col
cols="12"
>
<v-form
ref="form"
v-model="valid"
>
<v-text-field
v-model="client.name"
label="Nom convivial"
:rules="[ v => !!v || 'Nom client est requis', ]"
required
/>
<v-text-field
v-model="client.email"
label="Client messagerie"
:rules="[ v => (/.+@.+\..+/.test(v) || v === '') || 'Adresse non valide',]"
/>
<v-select
v-model="client.address"
:items="server.address"
label="Adresse IP client choisie parmi les réseaux suivants"
:rules="[ v => !!v || 'Réseau requis', ]"
multiple
chips
persistent-hint
required
/>
<v-combobox
v-model="client.allowedIPs"
chips
hint="Saisir le CIDR IPv4 ou IPv6 et appuyez sur touche Entrée"
label="IP autorisés"
multiple
dark
>
<template v-slot:selection="{ attrs, item, select, selected }">
<v-chip
v-bind="attrs"
:input-value="selected"
close
@click="select"
@click:close="client.allowedIPs.splice(client.allowedIPs.indexOf(item), 1)"
>
<strong>{{ item }}</strong>&nbsp;
</v-chip>
</template>
</v-combobox>
<v-switch
v-model="client.enable"
color="red"
inset
:label="client.enable ? 'Activer client après création': 'Désactiver client après création'"
/>
<v-switch
v-model="client.ignorePersistentKeepalive"
color="red"
inset
:label="'Ignorer le keepalive persistant global: ' + (client.ignorePersistentKeepalive ? 'Oui': 'NON')"
/>
</v-form>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn
:disabled="!valid"
color="success"
@click="addClient(client)"
>
Valider
<v-icon right dark>mdi-check-outline</v-icon>
</v-btn>
<v-btn
color="primary"
@click="dialogAddClient = false"
>
Abandon
<v-icon right dark>mdi-close-circle-outline</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-if="client"
v-model="dialogEditClient"
max-width="550"
>
<v-card>
<v-card-title class="headline">Edition client</v-card-title>
<v-card-text>
<v-row>
<v-col
cols="12"
>
<v-form
ref="form"
v-model="valid"
>
<v-text-field
v-model="client.name"
label="Nom convivial"
:rules="[ v => !!v || 'Nom client est requis',]"
required
/>
<v-text-field
v-model="client.email"
label="Messagerie"
:rules="[ v => (/.+@.+\..+/.test(v) || v === '') || 'Adresse non valide',]"
required
/>
<v-combobox
v-model="client.address"
chips
hint="Saisir le CIDR IPv4 ou IPv6 et appuyez sur touche Entrée"
label="Adresses"
multiple
dark
>
<template v-slot:selection="{ attrs, item, select, selected }">
<v-chip
v-bind="attrs"
:input-value="selected"
close
@click="select"
@click:close="client.address.splice(client.address.indexOf(item), 1)"
>
<strong>{{ item }}</strong>&nbsp;
</v-chip>
</template>
</v-combobox>
<v-combobox
v-model="client.allowedIPs"
chips
hint="Saisir le CIDR IPv4 ou IPv6 et appuyez sur touche Entrée"
label="IP autorisés"
multiple
dark
>
<template v-slot:selection="{ attrs, item, select, selected }">
<v-chip
v-bind="attrs"
:input-value="selected"
close
@click="select"
@click:close="client.allowedIPs.splice(client.allowedIPs.indexOf(item), 1)"
>
<strong>{{ item }}</strong>&nbsp;
</v-chip>
</template>
</v-combobox>
<v-switch
v-model="client.ignorePersistentKeepalive"
color="red"
inset
:label="'Ignorer le keepalive persistant global: ' + (client.ignorePersistentKeepalive ? 'Oui': 'NON')"
/>
</v-form>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn
:disabled="!valid"
color="success"
@click="updateClient(client)"
>
Valider
<v-icon right dark>mdi-check-outline</v-icon>
</v-btn>
<v-btn
color="primary"
@click="dialogEditClient = false"
>
Abandon
<v-icon right dark>mdi-close-circle-outline</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<Notification v-bind:notification="notification"/>
</v-container>
</template>
<script>
import {ApiService, API_BASE_URL} from '../services/ApiService'
import Notification from '../components/Notification'
export default {
name: 'Clients',
components: {
Notification
},
data: () => ({
api: null,
apiBaseUrl: API_BASE_URL,
clients: [],
notification: {
show: false,
color: '',
text: '',
},
dialogAddClient: false,
dialogEditClient: false,
client: null,
server: null,
valid: false,
}),
mounted () {
this.api = new ApiService();
this.getClients();
this.getServer()
},
methods: {
getClients() {
this.api.get('/client').then((res) => {
this.clients = res
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
getServer() {
this.api.get('/server').then((res) => {
this.server = res;
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
startAddClient() {
this.dialogAddClient = true;
this.client = {
name: "",
email: "",
enable: true,
allowedIPs: ["0.0.0.0/0", "::/0"],
address: this.server.address,
}
},
addClient(client) {
if (client.allowedIPs.length < 1) {
this.notify('error', 'Veuillez fournir au moins une adresse CIDR valide pour les adresses IP autorisées du client.');
return;
}
for (let i = 0; i < client.allowedIPs.length; i++){
if (this.$isCidr(client.allowedIPs[i]) === 0) {
this.notify('error', 'Un CIDR invalide a été détecté, veuillez corriger avant de soumettre');
return
}
}
this.dialogAddClient = false;
this.api.post('/client', client).then((res) => {
this.notify('success', `Client ${res.name} ajouté avec succès`);
this.getClients()
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
deleteClient(client) {
if(confirm(`Voulez-vous vraiment supprimer ${client.name} ?`)){
this.api.delete(`/client/${client.id}`).then((res) => {
this.notify('success', "Client supprimé avec succès");
this.getClients()
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
}
},
sendEmailClient(id) {
this.api.get(`/client/${id}/email`).then((res) => {
this.notify('success', "Courriel envoyé avec succès");
this.getClients()
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
startUpdateClient(client) {
this.client = client;
this.dialogEditClient = true;
},
updateClient(client) {
// check allowed IPs
if (client.allowedIPs.length < 1) {
this.notify('error', 'Veuillez fournir au moins une adresse CIDR valide pour les adresses IP autorisées du client');
return;
}
for (let i = 0; i < client.allowedIPs.length; i++){
if (this.$isCidr(client.allowedIPs[i]) === 0) {
this.notify('error', 'Un CIDR invalide a été détecté, veuillez corriger avant de soumettre');
return
}
}
// check address
if (client.address.length < 1) {
this.notify('error', 'Veuillez fournir au moins une adresse CIDR valide pour le client');
return;
}
for (let i = 0; i < client.address.length; i++){
if (this.$isCidr(client.address[i]) === 0) {
this.notify('error', 'Un CIDR invalide a été détecté, veuillez corriger avant de soumettre');
return
}
}
// all good, submit
this.dialogEditClient = false;
this.api.patch(`/client/${client.id}`, client).then((res) => {
this.notify('success', `Client ${res.name} mise à jour réussie`);
this.getClients()
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
notify(color, msg) {
this.notification.show = true;
this.notification.color = color;
this.notification.text = msg;
}
}
};
</script>

View File

@@ -0,0 +1,25 @@
<template>
<v-snackbar
v-model="notification.show"
:right="true"
:top="true"
:color="notification.color"
>
{{ notification.text }}
<v-btn
dark
text
@click="notification.show = false"
>
Fermer
</v-btn>
</v-snackbar>
</template>
<script>
export default {
name: 'Notification',
props: ['notification'],
data: () => ({
}),
};
</script>

View File

@@ -0,0 +1,235 @@
<template>
<v-container v-if="server">
<v-row>
<v-col cols="12">
<v-card dark>
<v-list-item>
<v-list-item-content>
<v-list-item-title class="headline">Interface configuration serveur</v-list-item-title>
</v-list-item-content>
</v-list-item>
<div class="d-flex flex-no-wrap justify-space-between">
<v-col cols="12">
<v-text-field
v-model="server.publicKey"
label="Clé publique"
disabled
/>
<v-text-field
v-model="server.listenPort"
type="number"
:rules="[
v => !!v || 'Le port en écoute est requis',
]"
label="Port en écoute"
required
/>
<v-combobox
v-model="server.address"
chips
hint="Saisir le CIDR IPv4 ou IPv6 et appuyez sur touche Entrée"
label="Adresse interfaces serveur"
multiple
dark
>
<template v-slot:selection="{ attrs, item, select, selected }">
<v-chip
v-bind="attrs"
:input-value="selected"
close
@click="select"
@click:close="server.address.splice(server.address.indexOf(item), 1)"
>
<strong>{{ item }}</strong>&nbsp;
</v-chip>
</template>
</v-combobox>
</v-col>
</div>
</v-card>
</v-col>
<v-col cols="12">
<v-card dark>
<v-list-item>
<v-list-item-content>
<v-list-item-title class="headline">Configuration globale du client</v-list-item-title>
</v-list-item-content>
</v-list-item>
<div class="d-flex flex-no-wrap justify-space-between">
<v-col cols="12">
<v-text-field
v-model="server.endpoint"
label="Point accès public connexion client"
:rules="[
v => !!v || 'Point accès public connexion client est requis',
]"
required
/>
<v-combobox
v-model="server.dns"
chips
hint="Saisir adresse IPv4 ou IPv6 et appui sur touche Entrée"
label="Serveurs DNS pour les clients"
multiple
dark
>
<template v-slot:selection="{ attrs, item, select, selected }">
<v-chip
v-bind="attrs"
:input-value="selected"
close
@click="select"
@click:close="server.dns.splice(server.dns.indexOf(item), 1)"
>
<strong>{{ item }}</strong>&nbsp;
</v-chip>
</template>
</v-combobox>
<v-text-field
type="number"
v-model="server.mtu"
label="Définir le MTU global"
hint="Laisser à 0 et laisser wg-quick gérer le MTU"
/>
<v-text-field
type="number"
v-model="server.persistentKeepalive"
label="Keepalive persistant"
hint="Laissez à 0 pour ne pas spécifier un keepalive persistant"
/>
</v-col>
</div>
</v-card>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-card dark>
<v-list-item>
<v-list-item-content>
<v-list-item-title class="headline">Interface configuration hooks</v-list-item-title>
</v-list-item-content>
</v-list-item>
<div class="d-flex flex-no-wrap justify-space-between">
<v-col cols="12">
<v-text-field
v-model="server.preUp"
label="PreUp: extraits de scripts qui seront exécutés par bash avant la mise en place de l'interface"
/>
<v-text-field
v-model="server.postUp"
label="PostUp: extraits de scripts qui seront exécutés par bash après la mise en place de l'interface"
/>
<v-text-field
v-model="server.preDown"
label="PreDown: extraits de scripts qui seront exécutés par bash avant la mise en place de l'interface"
/>
<v-text-field
v-model="server.postDown "
label="PostDown : extraits de scripts qui seront exécutés par bash après la mise en place de l'interface"
/>
</v-col>
</div>
</v-card>
</v-col>
</v-row>
<v-row>
<v-divider dark/>
<v-btn
class="ma-2"
color="success"
:href="`${apiBaseUrl}/server/config`"
>
Télécharger la configuration du serveur
<v-icon right dark>mdi-cloud-download-outline</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-btn
class="ma-2"
color="warning"
@click="updateServer"
>
Mise à jour configuration serveur
<v-icon right dark>mdi-update</v-icon>
</v-btn>
<v-divider dark/>
</v-row>
<Notification v-bind:notification="notification"/>
</v-container>
</template>
<script>
import {API_BASE_URL, ApiService} from "../services/ApiService";
import Notification from '../components/Notification'
export default {
name: 'Server',
components: {
Notification
},
data: () => ({
api: null,
server: null,
apiBaseUrl: API_BASE_URL,
notification: {
show: false,
color: '',
text: '',
},
}),
mounted () {
this.api = new ApiService();
this.getServer()
},
methods: {
getServer() {
this.api.get('/server').then((res) => {
this.server = res;
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
updateServer () {
// convert int values
this.server.listenPort = parseInt(this.server.listenPort, 10);
this.server.persistentKeepalive = parseInt(this.server.persistentKeepalive, 10);
this.server.mtu = parseInt(this.server.mtu, 10);
// check server addresses
if (this.server.address.length < 1) {
this.notify('error', 'Veuillez fournir au moins une adresse CIDR valide pour le serveur');
return;
}
for (let i = 0; i < this.server.address.length; i++){
if (this.$isCidr(this.server.address[i]) === 0) {
this.notify('error', `Un CIDR invalide a été détecté, veuillez corriger ${this.server.address[i]} avant de soumettre`);
return
}
}
// check DNS correct
for (let i = 0; i < this.server.dns.length; i++){
if (this.$isCidr(this.server.dns[i] + '/32') === 0) {
this.notify('error', `IP invalide détectée, veuillez corriger ${this.server.dns[i]} avant de soumettre`);
return
}
}
this.api.patch('/server', this.server).then((res) => {
this.notify('success', "Mise à jour du serveur réussie");
this.server = res;
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
notify(color, msg) {
this.notification.show = true;
this.notification.color = color;
this.notification.text = msg;
}
}
};
</script>

14
ui/src/main.js Normal file
View File

@@ -0,0 +1,14 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify';
import './plugins/moment';
import './plugins/cidr'
Vue.config.productionTip = false
new Vue({
router,
vuetify,
render: function (h) { return h(App) }
}).$mount('#app')

11
ui/src/plugins/cidr.js Normal file
View File

@@ -0,0 +1,11 @@
import Vue from 'vue'
const isCidr = require('is-cidr');
const plugin = {
install () {
Vue.isCidr = isCidr;
Vue.prototype.$isCidr = isCidr
}
};
Vue.use(plugin);

15
ui/src/plugins/moment.js Normal file
View File

@@ -0,0 +1,15 @@
import Vue from 'vue'
import moment from 'moment';
import VueMoment from 'vue-moment'
moment.locale('en');
Vue.use(VueMoment, {
moment
});
// $moment() accessible in project
Vue.filter('formatDate', function (value) {
if (!value) return '';
return moment(String(value)).format('DD/MM/YYYY HH:mm')
});

View File

@@ -0,0 +1,7 @@
import Vue from 'vue';
import Vuetify from 'vuetify/lib';
Vue.use(Vuetify);
export default new Vuetify({
});

36
ui/src/router/index.js Normal file
View File

@@ -0,0 +1,36 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter);
const routes = [
{
path: '/',
name: 'index',
component: function () {
return import(/* webpackChunkName: "Index" */ '../views/Index.vue')
},
},
{
path: '/clients',
name: 'clients',
component: function () {
return import(/* webpackChunkName: "Clients" */ '../views/Clients.vue')
},
},
{
path: '/server',
name: 'server',
component: function () {
return import(/* webpackChunkName: "Server" */ '../views/Server.vue')
},
}
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
});
export default router

View File

@@ -0,0 +1,40 @@
import axios from 'axios'
let baseUrl = "/api/v1.0";
if (process.env.NODE_ENV === "development"){
baseUrl = process.env.VUE_APP_API_BASE_URL
}
export const API_BASE_URL = baseUrl;
export class ApiService {
get(resource) {
return axios
.get(`${API_BASE_URL}${resource}`)
.then(response => response.data)
};
post(resource, data) {
return axios
.post(`${API_BASE_URL}${resource}`, data)
.then(response => response.data)
};
put(resource, data) {
return axios
.put(`${API_BASE_URL}${resource}`, data)
.then(response => response.data)
};
patch(resource, data) {
return axios
.patch(`${API_BASE_URL}${resource}`, data)
.then(response => response.data)
};
delete(resource) {
return axios
.delete(`${API_BASE_URL}${resource}`)
.then(response => response.data)
};
}

16
ui/src/views/Clients.vue Normal file
View File

@@ -0,0 +1,16 @@
<template>
<v-content>
<Clients/>
</v-content>
</template>
<script>
import Clients from '../components/Clients'
export default {
name: 'clients',
components: {
Clients
}
}
</script>

10
ui/src/views/Index.vue Normal file
View File

@@ -0,0 +1,10 @@
<template>
</template>
<script>
export default {
created () {
this.$router.replace({ name: 'clients' })
}
}
</script>

16
ui/src/views/Server.vue Normal file
View File

@@ -0,0 +1,16 @@
<template>
<v-content>
<Server/>
</v-content>
</template>
<script>
import Server from '../components/Server'
export default {
name: 'server',
components: {
Server
}
}
</script>

9
ui/vue.config.js Normal file
View File

@@ -0,0 +1,9 @@
module.exports = {
devServer: {
port: 8081,
disableHostCheck: true,
},
"transpileDependencies": [
"vuetify"
]
};