Browse Source

Update allowed Ips from xwvike/allowed-ips

- Fix dark mode
  - adjust style to fit wg-easy theme
  - added check for valid ipv4
  - added js helper to jump on next input when '.' pressed
  - rm allowed ip from New client create for simplicity
pull/997/head
jcosmao 2 years ago
committed by Julien COSMAO
parent
commit
82dbe6fc48
No known key found for this signature in database GPG Key ID: 69153421048939E2
  1. 4
      README.md
  2. 8
      src/lib/Server.js
  3. 24
      src/lib/WireGuard.js
  4. 8
      src/www/css/app.css
  5. 157
      src/www/index.html
  6. 4
      src/www/js/api.js
  7. 14
      src/www/js/app.js
  8. 10
      src/www/js/i18n.js

4
README.md

@ -109,12 +109,12 @@ These options can be configured by setting environment variables using `-e KEY="
| `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
| `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. |
| `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will listen on that (othwise default) inside the Docker container. |
| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy)
| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy)
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
| `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. |
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. |
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. It specify which destination address will be routed to wireguard interface. |
| `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. |
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. |
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. |

8
src/lib/Server.js

@ -191,8 +191,8 @@ module.exports = class Server {
return config;
}))
.post('/api/wireguard/client', defineEventHandler(async (event) => {
const { name, allowedIps } = await readBody(event);
await WireGuard.createClient({ name, allowedIps });
const { name } = await readBody(event);
await WireGuard.createClient({ name });
return { success: true };
}))
.delete('/api/wireguard/client/:clientId', defineEventHandler(async (event) => {
@ -234,7 +234,7 @@ module.exports = class Server {
await WireGuard.updateClientAddress({ clientId, address });
return { success: true };
}))
.put('/api/wireguard/client/:clientId/allowedips', defineEventHandler(async (event) => {
.put('/api/wireguard/client/:clientId/allowedIPs', defineEventHandler(async (event) => {
const clientId = getRouterParam(event, 'clientId');
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
throw createError({ status: 403 });
@ -242,7 +242,7 @@ module.exports = class Server {
const { allowedIPs } = await readBody(event);
await WireGuard.updateClientAllowedIPs({ clientId, allowedIPs });
return { success: true };
}))
}));
const safePathJoin = (base, target) => {
// Manage web root (edge case)

24
src/lib/WireGuard.js

@ -117,7 +117,7 @@ PostDown = ${WG_POST_DOWN}
}
allowedIPs = allowedIPs.substring(1);
} else {
allowedIPs = client.address;
allowedIPs = `${client.address}/32`;
}
result += `
@ -235,7 +235,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
});
}
async createClient({ name, allowedIps }) {
async createClient({ name }) {
if (!name) {
throw new Error('Missing: Name');
}
@ -248,20 +248,16 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
});
const preSharedKey = await Util.exec('wg genpsk');
// Calculate next IP
let address;
if (allowedIps) {
address = allowedIps;
} else {
// Calculate next IP
for (let i = 2; i < 255; i++) {
const client = Object.values(config.clients).find((client) => {
return client.address.includes(WG_DEFAULT_ADDRESS.replace('x', i));
});
for (let i = 2; i < 255; i++) {
const client = Object.values(config.clients).find((client) => {
return client.address.includes(WG_DEFAULT_ADDRESS.replace('x', i));
});
if (!client) {
address = `${WG_DEFAULT_ADDRESS.replace('x', i)}`;
break;
}
if (!client) {
address = `${WG_DEFAULT_ADDRESS.replace('x', i)}`;
break;
}
}

8
src/www/css/app.css

@ -843,6 +843,10 @@ video {
height: 2rem;
}
.h-9 {
height: 2.25rem;
}
.min-h-5 {
min-height: 1.25rem;
}
@ -887,6 +891,10 @@ video {
width: 2rem;
}
.w-9 {
width: 2.25rem;
}
.w-fit {
width: -moz-fit-content;
width: fit-content;

157
src/www/index.html

@ -112,7 +112,7 @@
<span class="max-md:hidden text-sm">{{$t("backup")}}</span>
</a>
<!-- New client -->
<button @click="clientCreate = true; clientCreateName = ''; clientCreateAllowedIps = '';"
<button @click="clientCreate = true; clientCreateName = '';"
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded inline-flex items-center transition">
<svg class="w-4 mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
@ -308,7 +308,7 @@
<!-- AllowedIPs -->
<button
class="align-middle bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition"
title="Edit Allowed IPs" @click="clientEditAllowedIPs = client">
:title="$t('editAllowedIPs')" @click="clientEditAllowedIPs = client">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 icon icon-tabler icon-tabler-list" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M9 6l11 0" />
@ -467,12 +467,6 @@
type="text" v-model.trim="clientCreateName" :placeholder="$t('name')" />
</p>
</div>
<div class="mt-2">
<p class="text-sm text-gray-500">
<input class="rounded p-2 border-2 dark:bg-neutral-700 dark:text-neutral-200 border-gray-100 dark:border-neutral-600 focus:border-gray-200 focus:dark:border-neutral-500 dark:placeholder:text-neutral-400 outline-none w-full"
type="text" v-model.trim="clientCreateAllowedIps" placeholder="Allowed IPs (optional)" />
</p>
</div>
</div>
</div>
</div>
@ -562,80 +556,101 @@
</div>
</div>
</div>
</div>
<!--edit allowedIPs dialog-->
<div v-if="clientEditAllowedIPs" class="fixed z-10 inset-0 overflow-y-auto">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 bg-gray-500 dark:bg-black opacity-75 dark:opacity-50"></div>
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div
class="inline-block align-bottom bg-white dark:bg-neutral-700 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full"
role="dialog" aria-modal="true" aria-labelledby="modal-headline">
<div class="bg-white dark:bg-neutral-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full text-white bg-red-800 sm:mx-0 sm:h-10 sm:w-10">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-topology-star" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M8 18a2 2 0 1 0 -4 0a2 2 0 0 0 4 0z" />
<path d="M20 6a2 2 0 1 0 -4 0a2 2 0 0 0 4 0z" />
<path d="M8 6a2 2 0 1 0 -4 0a2 2 0 0 0 4 0z" />
<path d="M20 18a2 2 0 1 0 -4 0a2 2 0 0 0 4 0z" />
<path d="M14 12a2 2 0 1 0 -4 0a2 2 0 0 0 4 0z" />
<path d="M7.5 7.5l3 3" />
<path d="M7.5 16.5l3 -3" />
<path d="M13.5 13.5l3 3" />
<path d="M16.5 7.5l-3 3" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-neutral-200" id="modal-headline">
Edit AllowedIPs
</h3>
<div class="mt-2 flex items-center flex-col">
<div class="w-full min-h-5 flex flex-wrap">
<div :class="[item.address!==clientEditAllowedIPs.address?'pr-8':'']" class="bg-white text-gray-700 relative h-8 tracking-widest border-2 border-gray-100 text-sm items-center rounded p-1 mb-2 mr-2 px-2 cursor-pointer flex w-fit" v-for="(item,index) in clientEditAllowedIPs.allowedIPs">
{{item.address+'/'+item.cidr}}
<div v-if="item.address!==clientEditAllowedIPs.address" style="width:calc(2rem - 4px);height:calc(2rem - 4px)" class="hover:bg-red-800 hover:text-white rounded top-0 right-0 w-8 h-8 flex justify-center items-center absolute">
<svg @click="removeIP(index)" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-x" width="15" height="15" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" /></svg>
</div>
</div>
</div>
<div class="w-full flex items-center">
<div class="h-8 text-gray-700 w-fit text-sm bg-white border-2 border-gray-100 rounded flex items-center px-2">
<input maxlength="3" v-model.number="userInputIP[0]" class="w-8 text-center outline-none border-none bg-transparent"/><span></span>
<input maxlength="3" v-model.number="userInputIP[1]" class="w-8 text-center outline-none border-none bg-transparent"/><span></span>
<input maxlength="3" v-model.number="userInputIP[2]" class="w-8 text-center outline-none border-none bg-transparent"/><span></span>
<input maxlength="3" v-model.number="userInputIP[3]" class="w-8 text-center outline-none border-none bg-transparent"/><span>/</span>
<input maxlength="2" v-model.number="userInputIP[4]" class="w-8 text-center outline-none border-none bg-transparent"/>
<!-- Allowed Ips -->
<div v-if="clientEditAllowedIPs" class="fixed z-10 inset-0 overflow-y-auto">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 bg-gray-500 dark:bg-black opacity-75 dark:opacity-50"></div>
</div>
<!-- This element is to trick the browser into centering the modal contents. -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white dark:bg-neutral-700 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full"
role="dialog" aria-modal="true" aria-labelledby="modal-headline">
<div class="bg-white dark:bg-neutral-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-800 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-white" inline xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</div>
<div class="flex-grow mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-neutral-200" id="modal-headline">
{{$t("allowedIPs")}}
</h3>
<!-- Ip address input -->
<div class="w-full flex items-center mt-3">
<div class="rounded p-1 border-2 dark:bg-neutral-700 dark:text-neutral-200 border-gray-100 dark:border-neutral-600 focus:border-gray-200 focus:dark:border-neutral-500 dark:placeholder:text-neutral-400 outline-none">
<input maxlength="3" v-model.number="userInputIP[0]" placeholder="10" @keydown="handleKeyDown($event, '.', 'ip1')" class="w-8 text-center outline-none border-none bg-transparent"/>
<sub class="text-neutral-400"></sub>
<input maxlength="3" v-model.number="userInputIP[1]" placeholder="0" ref="ip1" @keydown="handleKeyDown($event, '.', 'ip2')" class="w-8 text-center outline-none border-none bg-transparent"/>
<sub class="text-neutral-400"></sub>
<input maxlength="3" v-model.number="userInputIP[2]" placeholder="0" ref="ip2" @keydown="handleKeyDown($event, '.', 'ip3')" class="w-8 text-center outline-none border-none bg-transparent"/>
<sub class="text-neutral-400"></sub>
<input maxlength="3" v-model.number="userInputIP[3]" placeholder="0" ref="ip3" @keydown="handleKeyDown($event, '/', 'ip4')" class="w-8 text-center outline-none border-none bg-transparent"/>
<span class="text-neutral-400">/</span>
<input maxlength="2" v-model.number="userInputIP[4]" placeholder="8" ref="ip4" class="w-8 text-center outline-none border-none bg-transparent"/>
</div>
<div @click="addNewIP" class="w-8 h-8 rounded bg-white hover:border-red-800 hover:bg-red-800 hover:text-white text-gray-700 border-2 border-gray-100 cursor-pointer ml-2 flex justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="18" height="18" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<!-- Ip address add -->
<button v-if="userInputIP.length == 5" @click="addNewIP"
class="w-9 h-9 rounded p-1 ml-2 bg-red-800 text-base font-medium text-white hover:bg-red-700 hover:border-red-700 dark:text-neutral-200 dark:border-neutral-600 cursor-pointer flex justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus"
width="18" height="18" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 5l0 14" />
<path d="M5 12l14 0" />
</svg>
</button>
<button v-else
class="w-9 h-9 rounded ml-2 bg-gray-200 dark:bg-neutral-400 text-white dark:text-neutral-300 border-gray-100 dark:border-neutral-600 dark:bg-neutral-500 flex justify-center items-center cursor-not-allowed">
<svg
xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus"
width="18" height="18" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 5l0 14" />
<path d="M5 12l14 0" /></svg>
<path d="M5 12l14 0" />
</svg>
</button>
</div>
<!-- Allowed ips inline display -->
<div class="mt-5">
<div class="w-full min-h-5 flex flex-wrap">
<div :class="[item.address!==clientEditAllowedIPs.address?'pr-8':'']" v-for="(item,index) in clientEditAllowedIPs.allowedIPs"
class="rounded p-1 border-2 mt-2 mb-2 mr-2 px-2 dark:bg-neutral-700 dark:text-neutral-200 border-gray-100 dark:border-neutral-600 focus:border-gray-200 focus:dark:border-neutral-500 relative h-8 tracking-widest text-sm items-center cursor-pointer flex w-fit">
{{item.address+'/'+item.cidr}}
<div v-if="item.address!==clientEditAllowedIPs.address" style="width:calc(2rem - 4px);height:calc(2rem - 4px)"
class="rounded top-0 right-0 w-8 h-8 bg-red-800 hover:bg-red-700 text-white hover:text-white hover:border-red-700 dark:text-neutral-200 dark:border-neutral-600 flex justify-center items-center absolute">
<svg @click="removeIP(index)" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-x"
width="15" height="15" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-neutral-600 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" @click="updateClientAllowedIPs(clientEditAllowedIPs); clientEditAllowedIPs = null"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 dark:bg-red-600 text-base font-medium text-white dark:text-white hover:bg-red-700 dark:hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm">
Save
</button>
<button type="button" @click="clientEditAllowedIPs = null"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-neutral-500 shadow-sm px-4 py-2 bg-white dark:bg-neutral-500 text-base font-medium text-gray-700 dark:text-neutral-50 hover:bg-gray-50 dark:hover:bg-neutral-600 dark:hover:border-neutral-600 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Cancel
</button>
<div class="bg-gray-50 dark:bg-neutral-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" @click="updateClientAllowedIPs(clientEditAllowedIPs); clientEditAllowedIPs = null"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-800 text-base font-medium text-white hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm">
{{$t("update")}}
</button>
<button type="button" @click="clientEditAllowedIPs = null"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-neutral-500 shadow-sm px-4 py-2 bg-white dark:bg-neutral-500 text-base font-medium text-gray-700 dark:text-neutral-50 hover:bg-gray-50 dark:hover:bg-neutral-600 dark:hover:border-neutral-600 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
{{$t("cancel")}}
</button>
</div>
</div>
</div>
</div>
<!-- End Allowed Ips -->
</div>
<div v-if="authenticated === false">

4
src/www/js/api.js

@ -93,11 +93,11 @@ class API {
})));
}
async createClient({ name, allowedIps }) {
async createClient({ name }) {
return this.call({
method: 'post',
path: '/wireguard/client',
body: { name, allowedIps },
body: { name },
});
}

14
src/www/js/app.js

@ -59,13 +59,12 @@ new Vue({
clientDelete: null,
clientCreate: null,
clientCreateName: '',
clientCreateAllowedIps: '',
clientEditName: null,
clientEditNameId: null,
clientEditAddress: null,
clientEditAddressId: null,
clientEditAllowedIPs: null,
userInputIP: [0, 0, 0, 0, 0],
userInputIP: [],
qrcode: null,
currentRelease: null,
@ -271,10 +270,9 @@ new Vue({
},
createClient() {
const name = this.clientCreateName;
const allowedIps = this.clientCreateAllowedIps;
if (!name) return;
this.api.createClient({ name, allowedIps })
this.api.createClient({ name })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
@ -324,13 +322,19 @@ new Vue({
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
handleKeyDown(event, key, nextInputRef) {
if (event.key === key) {
event.preventDefault();
this.$refs[nextInputRef].focus();
}
},
addNewIP() {
const address = this.userInputIP.slice(0, 4).join('.');
const allowedIPs = [...this.clientEditAllowedIPs.allowedIPs];
const obj = { type: 'ipv4', address, cidr: this.userInputIP[4] };
allowedIPs.push(obj);
this.clientEditAllowedIPs.allowedIPs = allowedIPs;
this.userInputIP = [0, 0, 0, 0, 0];
this.userInputIP = [];
},
removeIP(index) {
const allowedIPs = [...this.clientEditAllowedIPs.allowedIPs];

10
src/www/js/i18n.js

@ -34,6 +34,8 @@ const messages = { // eslint-disable-line no-unused-vars
backup: 'Backup',
titleRestoreConfig: 'Restore your configuration',
titleBackupConfig: 'Backup your configuration',
editAllowedIPs: 'Edit server AllowedIPs parameter',
allowedIPs: 'Server AllowedIPs',
},
ua: {
name: 'Ім`я',
@ -174,14 +176,14 @@ const messages = { // eslint-disable-line no-unused-vars
fr: { // github.com/clem3109
name: 'Nom',
password: 'Mot de passe',
signIn: 'Se Connecter',
signIn: 'Se connecter',
logout: 'Se déconnecter',
updateAvailable: 'Une mise à jour est disponible !',
update: 'Mise à jour',
clients: 'Clients',
new: 'Nouveau',
deleteClient: 'Supprimer ce client',
deleteDialog1: 'Êtes-vous que vous voulez supprimer',
deleteDialog1: 'Êtes-vous sûr de vouloir supprimer ?',
deleteDialog2: 'Cette action ne peut pas être annulée.',
cancel: 'Annuler',
create: 'Créer',
@ -193,7 +195,7 @@ const messages = { // eslint-disable-line no-unused-vars
disableClient: 'Désactiver ce client',
enableClient: 'Activer ce client',
noClients: 'Aucun client pour le moment.',
showQR: 'Afficher le code à réponse rapide (QR Code)',
showQR: 'Afficher le QR Code',
downloadConfig: 'Télécharger la configuration',
madeBy: 'Développé par',
donate: 'Soutenir',
@ -201,6 +203,8 @@ const messages = { // eslint-disable-line no-unused-vars
backup: 'Sauvegarder',
titleRestoreConfig: 'Restaurer votre configuration',
titleBackupConfig: 'Sauvegarder votre configuration',
editAllowedIPs: 'Editer le paramètre AllowedIPs du serveur',
allowedIPs: 'Serveur AllowedIPs',
},
de: { // github.com/florian-asche
name: 'Name',

Loading…
Cancel
Save