Merge commit '75df17476fac7c8eca6da0a69496f4a4c63ae567' into prometheus-metrics
This commit is contained in:
@@ -3,7 +3,7 @@ name: 🐛 Bug Report
|
|||||||
description: Create a report to help us improve
|
description: Create a report to help us improve
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels:
|
labels:
|
||||||
- "bug"
|
- "type: bug"
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: 🛠️ Feature Request
|
|||||||
description: Suggest an idea to help us improve
|
description: Suggest an idea to help us improve
|
||||||
title: "[Feat]: "
|
title: "[Feat]: "
|
||||||
labels:
|
labels:
|
||||||
- "enhancement"
|
- "type: feature request"
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
|
|||||||
* Automatic Light / Dark Mode
|
* Automatic Light / Dark Mode
|
||||||
* Multilanguage Support
|
* Multilanguage Support
|
||||||
* UI_TRAFFIC_STATS (default off)
|
* UI_TRAFFIC_STATS (default off)
|
||||||
* UI_SHOW_LINKS (default off)
|
* WG_ENABLE_ONE_TIME_LINKS (default off)
|
||||||
* WG_ENABLE_EXPIRES_TIME (default off)
|
* WG_ENABLE_EXPIRES_TIME (default off)
|
||||||
* Prometheus metrics support
|
* Prometheus metrics support
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ These options can be configured by setting environment variables using `-e KEY="
|
|||||||
| `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). |
|
| `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). |
|
||||||
| `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
|
| `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
|
||||||
| `UI_CHART_TYPE` | `0` | `1` | UI_CHART_TYPE=0 # Charts disabled, UI_CHART_TYPE=1 # Line chart, UI_CHART_TYPE=2 # Area chart, UI_CHART_TYPE=3 # Bar chart |
|
| `UI_CHART_TYPE` | `0` | `1` | UI_CHART_TYPE=0 # Charts disabled, UI_CHART_TYPE=1 # Line chart, UI_CHART_TYPE=2 # Area chart, UI_CHART_TYPE=3 # Bar chart |
|
||||||
| `UI_SHOW_LINKS` | `false` | `true` | Enable display of a short download link in Web UI |
|
| `WG_ENABLE_ONE_TIME_LINKS` | `false` | `true` | Enable display and generation of short one time download links (expire after 5 minutes) |
|
||||||
| `MAX_AGE` | `0` | `1440` | The maximum age of Web UI sessions in minutes. `0` means that the session will exist until the browser is closed. |
|
| `MAX_AGE` | `0` | `1440` | The maximum age of Web UI sessions in minutes. `0` means that the session will exist until the browser is closed. |
|
||||||
| `UI_ENABLE_SORT_CLIENTS` | `false` | `true` | Enable UI sort clients by name |
|
| `UI_ENABLE_SORT_CLIENTS` | `false` | `true` | Enable UI sort clients by name |
|
||||||
| `ENABLE_PROMETHEUS_METRICS` | `true` | `true` | Enable Prometheus metrics `http://0.0.0.0:51821/metrics` and `http://0.0.0.0:51821/metrics/json`|
|
| `ENABLE_PROMETHEUS_METRICS` | `true` | `true` | Enable Prometheus metrics `http://0.0.0.0:51821/metrics` and `http://0.0.0.0:51821/metrics/json`|
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ services:
|
|||||||
# - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt
|
# - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt
|
||||||
# - UI_TRAFFIC_STATS=true
|
# - UI_TRAFFIC_STATS=true
|
||||||
# - UI_CHART_TYPE=0 # (0 Charts disabled, 1 # Line chart, 2 # Area chart, 3 # Bar chart)
|
# - UI_CHART_TYPE=0 # (0 Charts disabled, 1 # Line chart, 2 # Area chart, 3 # Bar chart)
|
||||||
# - UI_SHOW_LINKS=true
|
# - WG_ENABLE_ONE_TIME_LINKS=true
|
||||||
# - UI_ENABLE_SORT_CLIENTS=true
|
# - UI_ENABLE_SORT_CLIENTS=true
|
||||||
# - WG_ENABLE_EXPIRES_TIME=true
|
# - WG_ENABLE_EXPIRES_TIME=true
|
||||||
# - ENABLE_PROMETHEUS_METRICS=false
|
# - ENABLE_PROMETHEUS_METRICS=false
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ iptables -D FORWARD -o wg0 -j ACCEPT;
|
|||||||
module.exports.LANG = process.env.LANG || 'en';
|
module.exports.LANG = process.env.LANG || 'en';
|
||||||
module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false';
|
module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false';
|
||||||
module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0;
|
module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0;
|
||||||
module.exports.UI_SHOW_LINKS = process.env.UI_SHOW_LINKS || 'false';
|
module.exports.WG_ENABLE_ONE_TIME_LINKS = process.env.WG_ENABLE_ONE_TIME_LINKS || 'false';
|
||||||
module.exports.UI_ENABLE_SORT_CLIENTS = process.env.UI_ENABLE_SORT_CLIENTS || 'false';
|
module.exports.UI_ENABLE_SORT_CLIENTS = process.env.UI_ENABLE_SORT_CLIENTS || 'false';
|
||||||
module.exports.WG_ENABLE_EXPIRES_TIME = process.env.WG_ENABLE_EXPIRES_TIME || 'false';
|
module.exports.WG_ENABLE_EXPIRES_TIME = process.env.WG_ENABLE_EXPIRES_TIME || 'false';
|
||||||
module.exports.ENABLE_PROMETHEUS_METRICS = process.env.ENABLE_PROMETHEUS_METRICS || 'true';
|
module.exports.ENABLE_PROMETHEUS_METRICS = process.env.ENABLE_PROMETHEUS_METRICS || 'true';
|
||||||
|
|||||||
+28
-7
@@ -33,7 +33,7 @@ const {
|
|||||||
LANG,
|
LANG,
|
||||||
UI_TRAFFIC_STATS,
|
UI_TRAFFIC_STATS,
|
||||||
UI_CHART_TYPE,
|
UI_CHART_TYPE,
|
||||||
UI_SHOW_LINKS,
|
WG_ENABLE_ONE_TIME_LINKS,
|
||||||
UI_ENABLE_SORT_CLIENTS,
|
UI_ENABLE_SORT_CLIENTS,
|
||||||
WG_ENABLE_EXPIRES_TIME,
|
WG_ENABLE_EXPIRES_TIME,
|
||||||
ENABLE_PROMETHEUS_METRICS,
|
ENABLE_PROMETHEUS_METRICS,
|
||||||
@@ -107,9 +107,9 @@ module.exports = class Server {
|
|||||||
return `"${UI_CHART_TYPE}"`;
|
return `"${UI_CHART_TYPE}"`;
|
||||||
}))
|
}))
|
||||||
|
|
||||||
.get('/api/ui-show-links', defineEventHandler((event) => {
|
.get('/api/wg-enable-one-time-links', defineEventHandler((event) => {
|
||||||
setHeader(event, 'Content-Type', 'application/json');
|
setHeader(event, 'Content-Type', 'application/json');
|
||||||
return `${UI_SHOW_LINKS}`;
|
return `${WG_ENABLE_ONE_TIME_LINKS}`;
|
||||||
}))
|
}))
|
||||||
|
|
||||||
.get('/api/ui-sort-clients', defineEventHandler((event) => {
|
.get('/api/ui-sort-clients', defineEventHandler((event) => {
|
||||||
@@ -133,14 +133,21 @@ module.exports = class Server {
|
|||||||
authenticated,
|
authenticated,
|
||||||
};
|
};
|
||||||
}))
|
}))
|
||||||
.get('/:clientHash', defineEventHandler(async (event) => {
|
.get('/cnf/:clientOneTimeLink', defineEventHandler(async (event) => {
|
||||||
const clientHash = getRouterParam(event, 'clientHash');
|
if (WG_ENABLE_ONE_TIME_LINKS === 'false') {
|
||||||
|
throw createError({
|
||||||
|
status: 404,
|
||||||
|
message: 'Invalid state',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const clientOneTimeLink = getRouterParam(event, 'clientOneTimeLink');
|
||||||
const clients = await WireGuard.getClients();
|
const clients = await WireGuard.getClients();
|
||||||
const client = clients.find((client) => client.hash === clientHash);
|
const client = clients.find((client) => client.oneTimeLink === clientOneTimeLink);
|
||||||
if (!client) return;
|
if (!client) return;
|
||||||
const clientId = client.id;
|
const clientId = client.id;
|
||||||
const config = await WireGuard.getClientConfiguration({ clientId });
|
const config = await WireGuard.getClientConfiguration({ clientId });
|
||||||
setHeader(event, 'Content-Disposition', `attachment; filename="${clientHash}.conf"`);
|
await WireGuard.eraseOneTimeLink({ clientId });
|
||||||
|
setHeader(event, 'Content-Disposition', `attachment; filename="${clientOneTimeLink}.conf"`);
|
||||||
setHeader(event, 'Content-Type', 'text/plain');
|
setHeader(event, 'Content-Type', 'text/plain');
|
||||||
return config;
|
return config;
|
||||||
}))
|
}))
|
||||||
@@ -253,6 +260,20 @@ module.exports = class Server {
|
|||||||
await WireGuard.enableClient({ clientId });
|
await WireGuard.enableClient({ clientId });
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}))
|
}))
|
||||||
|
.post('/api/wireguard/client/:clientId/generateOneTimeLink', defineEventHandler(async (event) => {
|
||||||
|
if (WG_ENABLE_ONE_TIME_LINKS === 'false') {
|
||||||
|
throw createError({
|
||||||
|
status: 404,
|
||||||
|
message: 'Invalid state',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
|
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
||||||
|
throw createError({ status: 403 });
|
||||||
|
}
|
||||||
|
await WireGuard.generateOneTimeLink({ clientId });
|
||||||
|
return { success: true };
|
||||||
|
}))
|
||||||
.post('/api/wireguard/client/:clientId/disable', defineEventHandler(async (event) => {
|
.post('/api/wireguard/client/:clientId/disable', defineEventHandler(async (event) => {
|
||||||
const clientId = getRouterParam(event, 'clientId');
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
||||||
|
|||||||
+35
-3
@@ -25,6 +25,7 @@ const {
|
|||||||
WG_PRE_DOWN,
|
WG_PRE_DOWN,
|
||||||
WG_POST_DOWN,
|
WG_POST_DOWN,
|
||||||
WG_ENABLE_EXPIRES_TIME,
|
WG_ENABLE_EXPIRES_TIME,
|
||||||
|
WG_ENABLE_ONE_TIME_LINKS,
|
||||||
} = require('../config');
|
} = require('../config');
|
||||||
|
|
||||||
module.exports = class WireGuard {
|
module.exports = class WireGuard {
|
||||||
@@ -152,7 +153,8 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
|
|||||||
? new Date(client.expiredAt)
|
? new Date(client.expiredAt)
|
||||||
: null,
|
: null,
|
||||||
allowedIPs: client.allowedIPs,
|
allowedIPs: client.allowedIPs,
|
||||||
hash: Math.abs(CRC32.str(clientId)).toString(16),
|
oneTimeLink: client.oneTimeLink ?? null,
|
||||||
|
oneTimeLinkExpiresAt: client.oneTimeLinkExpiresAt ?? null,
|
||||||
downloadableConfig: 'privateKey' in client,
|
downloadableConfig: 'privateKey' in client,
|
||||||
persistentKeepalive: null,
|
persistentKeepalive: null,
|
||||||
latestHandshakeAt: null,
|
latestHandshakeAt: null,
|
||||||
@@ -308,6 +310,23 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
|
|||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateOneTimeLink({ clientId }) {
|
||||||
|
const client = await this.getClient({ clientId });
|
||||||
|
const key = `${clientId}-${Math.floor(Math.random() * 1000)}`;
|
||||||
|
client.oneTimeLink = Math.abs(CRC32.str(key)).toString(16);
|
||||||
|
client.oneTimeLinkExpiresAt = new Date(Date.now() + 5 * 60 * 1000);
|
||||||
|
client.updatedAt = new Date();
|
||||||
|
await this.saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
async eraseOneTimeLink({ clientId }) {
|
||||||
|
const client = await this.getClient({ clientId });
|
||||||
|
client.oneTimeLink = null;
|
||||||
|
client.oneTimeLinkExpiresAt = null;
|
||||||
|
client.updatedAt = new Date();
|
||||||
|
await this.saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
async disableClient({ clientId }) {
|
async disableClient({ clientId }) {
|
||||||
const client = await this.getClient({ clientId });
|
const client = await this.getClient({ clientId });
|
||||||
|
|
||||||
@@ -383,8 +402,9 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
|
|||||||
|
|
||||||
async cronJobEveryMinute() {
|
async cronJobEveryMinute() {
|
||||||
const config = await this.getConfig();
|
const config = await this.getConfig();
|
||||||
if (WG_ENABLE_EXPIRES_TIME === 'true') {
|
|
||||||
let needSaveConfig = false;
|
let needSaveConfig = false;
|
||||||
|
// Expires Feature
|
||||||
|
if (WG_ENABLE_EXPIRES_TIME === 'true') {
|
||||||
for (const client of Object.values(config.clients)) {
|
for (const client of Object.values(config.clients)) {
|
||||||
if (client.enabled !== true) continue;
|
if (client.enabled !== true) continue;
|
||||||
if (client.expiredAt !== null && new Date() > new Date(client.expiredAt)) {
|
if (client.expiredAt !== null && new Date() > new Date(client.expiredAt)) {
|
||||||
@@ -394,11 +414,23 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
|
|||||||
client.updatedAt = new Date();
|
client.updatedAt = new Date();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// One Time Link Feature
|
||||||
|
if (WG_ENABLE_ONE_TIME_LINKS === 'true') {
|
||||||
|
for (const client of Object.values(config.clients)) {
|
||||||
|
if (client.oneTimeLink !== null && new Date() > new Date(client.oneTimeLinkExpiresAt)) {
|
||||||
|
debug(`Client ${client.id} One Time Link expired.`);
|
||||||
|
needSaveConfig = true;
|
||||||
|
client.oneTimeLink = null;
|
||||||
|
client.oneTimeLinkExpiresAt = null;
|
||||||
|
client.updatedAt = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (needSaveConfig) {
|
if (needSaveConfig) {
|
||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async getMetrics() {
|
async getMetrics() {
|
||||||
const clients = await this.getClients();
|
const clients = await this.getClients();
|
||||||
|
|||||||
+18
-2
@@ -256,8 +256,8 @@
|
|||||||
{{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}}
|
{{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="uiShowLinks" :ref="'client-' + client.id + '-hash'" class="text-gray-400 text-xs">
|
<div v-if="enableOneTimeLinks && client.oneTimeLink !== null && client.oneTimeLink !== ''" :ref="'client-' + client.id + '-link'" class="text-gray-400 text-xs">
|
||||||
<a :href="'./' + client.hash + ''">{{document.location.protocol}}//{{document.location.host}}/{{client.hash}}</a>
|
<a :href="'./cnf/' + client.oneTimeLink + ''">{{document.location.protocol}}//{{document.location.host}}/cnf/{{client.oneTimeLink}}</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- Expire Date -->
|
<!-- Expire Date -->
|
||||||
<div v-show="enableExpireTime" class=" block md:inline-block pb-1 md:pb-0 text-gray-500 dark:text-neutral-400 text-xs">
|
<div v-show="enableExpireTime" class=" block md:inline-block pb-1 md:pb-0 text-gray-500 dark:text-neutral-400 text-xs">
|
||||||
@@ -384,6 +384,22 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<!-- Short OneTime Link -->
|
||||||
|
<button v-if="enableOneTimeLinks" :disabled="!client.downloadableConfig"
|
||||||
|
class="align-middle inline-block bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 p-2 rounded transition"
|
||||||
|
:class="{
|
||||||
|
'hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white': client.downloadableConfig,
|
||||||
|
'is-disabled': !client.downloadableConfig
|
||||||
|
}"
|
||||||
|
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('OneTimeLink')"
|
||||||
|
@click="if(client.downloadableConfig) { showOneTimeLink(client); }">
|
||||||
|
<svg class="w-5" 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="M13.213 9.787a3.391 3.391 0 0 0-4.795 0l-3.425 3.426a3.39 3.39 0 0 0 4.795 4.794l.321-.304m-.321-4.49a3.39 3.39 0 0 0 4.795 0l3.424-3.426a3.39 3.39 0 0 0-4.794-4.795l-1.028.961"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
+9
-2
@@ -64,10 +64,10 @@ class API {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUIShowLinks() {
|
async getWGEnableOneTimeLinks() {
|
||||||
return this.call({
|
return this.call({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/ui-show-links',
|
path: '/wg-enable-one-time-links',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +132,13 @@ class API {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async showOneTimeLink({ clientId }) {
|
||||||
|
return this.call({
|
||||||
|
method: 'post',
|
||||||
|
path: `/wireguard/client/${clientId}/generateOneTimeLink`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async enableClient({ clientId }) {
|
async enableClient({ clientId }) {
|
||||||
return this.call({
|
return this.call({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
|||||||
+9
-4
@@ -92,7 +92,7 @@ new Vue({
|
|||||||
uiTrafficStats: false,
|
uiTrafficStats: false,
|
||||||
|
|
||||||
uiChartType: 0,
|
uiChartType: 0,
|
||||||
uiShowLinks: false,
|
enableOneTimeLinks: false,
|
||||||
enableSortClient: false,
|
enableSortClient: false,
|
||||||
sortClient: true, // Sort clients by name, true = asc, false = desc
|
sortClient: true, // Sort clients by name, true = asc, false = desc
|
||||||
enableExpireTime: false,
|
enableExpireTime: false,
|
||||||
@@ -312,6 +312,11 @@ new Vue({
|
|||||||
.catch((err) => alert(err.message || err.toString()))
|
.catch((err) => alert(err.message || err.toString()))
|
||||||
.finally(() => this.refresh().catch(console.error));
|
.finally(() => this.refresh().catch(console.error));
|
||||||
},
|
},
|
||||||
|
showOneTimeLink(client) {
|
||||||
|
this.api.showOneTimeLink({ clientId: client.id })
|
||||||
|
.catch((err) => alert(err.message || err.toString()))
|
||||||
|
.finally(() => this.refresh().catch(console.error));
|
||||||
|
},
|
||||||
enableClient(client) {
|
enableClient(client) {
|
||||||
this.api.enableClient({ clientId: client.id })
|
this.api.enableClient({ clientId: client.id })
|
||||||
.catch((err) => alert(err.message || err.toString()))
|
.catch((err) => alert(err.message || err.toString()))
|
||||||
@@ -436,12 +441,12 @@ new Vue({
|
|||||||
this.uiChartType = 0;
|
this.uiChartType = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.api.getUIShowLinks()
|
this.api.getWGEnableOneTimeLinks()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.uiShowLinks = res;
|
this.enableOneTimeLinks = res;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.uiShowLinks = false;
|
this.enableOneTimeLinks = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.api.getUiSortClients()
|
this.api.getUiSortClients()
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const messages = { // eslint-disable-line no-unused-vars
|
|||||||
sort: 'Sort',
|
sort: 'Sort',
|
||||||
ExpireDate: 'Expire Date',
|
ExpireDate: 'Expire Date',
|
||||||
Permanent: 'Permanent',
|
Permanent: 'Permanent',
|
||||||
|
OneTimeLink: 'Generate short one time link',
|
||||||
},
|
},
|
||||||
ua: {
|
ua: {
|
||||||
name: 'Ім`я',
|
name: 'Ім`я',
|
||||||
@@ -112,6 +113,7 @@ const messages = { // eslint-disable-line no-unused-vars
|
|||||||
sort: 'Сортировка',
|
sort: 'Сортировка',
|
||||||
ExpireDate: 'Дата истечения срока',
|
ExpireDate: 'Дата истечения срока',
|
||||||
Permanent: 'Бессрочно',
|
Permanent: 'Бессрочно',
|
||||||
|
OneTimeLink: 'Создать короткую одноразовую ссылку',
|
||||||
},
|
},
|
||||||
tr: { // Müslüm Barış Korkmazer @babico
|
tr: { // Müslüm Barış Korkmazer @babico
|
||||||
name: 'İsim',
|
name: 'İsim',
|
||||||
|
|||||||
Reference in New Issue
Block a user