Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/controller/xray_setting.go28
-rw-r--r--web/html/modals/nord_modal.html306
-rw-r--r--web/html/settings/xray/basics.html19
-rw-r--r--web/html/settings/xray/outbounds.html2
-rw-r--r--web/html/xray.html22
-rw-r--r--web/service/nord.go145
-rw-r--r--web/service/setting.go9
-rw-r--r--web/translation/translate.ar_EG.toml12
-rw-r--r--web/translation/translate.en_US.toml12
-rw-r--r--web/translation/translate.es_ES.toml12
-rw-r--r--web/translation/translate.fa_IR.toml12
-rw-r--r--web/translation/translate.id_ID.toml12
-rw-r--r--web/translation/translate.ja_JP.toml12
-rw-r--r--web/translation/translate.pt_BR.toml12
-rw-r--r--web/translation/translate.ru_RU.toml14
-rw-r--r--web/translation/translate.tr_TR.toml12
-rw-r--r--web/translation/translate.uk_UA.toml12
-rw-r--r--web/translation/translate.vi_VN.toml12
-rw-r--r--web/translation/translate.zh_CN.toml12
-rw-r--r--web/translation/translate.zh_TW.toml12
20 files changed, 688 insertions, 1 deletions
diff --git a/web/controller/xray_setting.go b/web/controller/xray_setting.go
index 5b7a0e26..0c382fb9 100644
--- a/web/controller/xray_setting.go
+++ b/web/controller/xray_setting.go
@@ -17,6 +17,7 @@ type XraySettingController struct {
OutboundService service.OutboundService
XrayService service.XrayService
WarpService service.WarpService
+ NordService service.NordService
}
// NewXraySettingController creates a new XraySettingController and initializes its routes.
@@ -35,6 +36,7 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
g.POST("/", a.getXraySetting)
g.POST("/warp/:action", a.warp)
+ g.POST("/nord/:action", a.nord)
g.POST("/update", a.updateSetting)
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
g.POST("/testOutbound", a.testOutbound)
@@ -123,6 +125,32 @@ func (a *XraySettingController) warp(c *gin.Context) {
jsonObj(c, resp, err)
}
+// nord handles NordVPN-related operations based on the action parameter.
+func (a *XraySettingController) nord(c *gin.Context) {
+ action := c.Param("action")
+ var resp string
+ var err error
+ switch action {
+ case "countries":
+ resp, err = a.NordService.GetCountries()
+ case "servers":
+ countryId := c.PostForm("countryId")
+ resp, err = a.NordService.GetServers(countryId)
+ case "reg":
+ token := c.PostForm("token")
+ resp, err = a.NordService.GetCredentials(token)
+ case "setKey":
+ key := c.PostForm("key")
+ resp, err = a.NordService.SetKey(key)
+ case "data":
+ resp, err = a.NordService.GetNordData()
+ case "del":
+ err = a.NordService.DelNordData()
+ }
+
+ jsonObj(c, resp, err)
+}
+
// getOutboundsTraffic retrieves the traffic statistics for outbounds.
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
diff --git a/web/html/modals/nord_modal.html b/web/html/modals/nord_modal.html
new file mode 100644
index 00000000..c86d8ed9
--- /dev/null
+++ b/web/html/modals/nord_modal.html
@@ -0,0 +1,306 @@
+{{define "modals/nordModal"}}
+<a-modal id="nord-modal" v-model="nordModal.visible" title="NordVPN NordLynx"
+ :confirm-loading="nordModal.confirmLoading" :closable="true" :mask-closable="true"
+ :footer="null" :class="themeSwitcher.currentTheme">
+ <template v-if="nordModal.nordData == null">
+ <a-tabs default-active-key="token" :class="themeSwitcher.currentTheme">
+ <a-tab-pane key="token" tab='{{ i18n "pages.xray.outbound.accessToken" }}'>
+ <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }">
+ <a-form-item label='{{ i18n "pages.xray.outbound.accessToken" }}'>
+ <a-input v-model="nordModal.token" placeholder='{{ i18n "pages.xray.outbound.accessToken" }}'></a-input>
+ <div :style="{ marginTop: '10px' }">
+ <a-button type="primary" icon="login" @click="login()" :loading="nordModal.confirmLoading">{{ i18n "login" }}</a-button>
+ </div>
+ </a-form-item>
+ </a-form>
+ </a-tab-pane>
+ <a-tab-pane key="key" tab='{{ i18n "pages.xray.outbound.privateKey" }}'>
+ <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }">
+ <a-form-item label='{{ i18n "pages.xray.outbound.privateKey" }}'>
+ <a-input v-model="nordModal.manualKey" placeholder='{{ i18n "pages.xray.outbound.privateKey" }}'></a-input>
+ <div :style="{ marginTop: '10px' }">
+ <a-button type="primary" icon="save" @click="saveKey()" :loading="nordModal.confirmLoading">{{ i18n "save" }}</a-button>
+ </div>
+ </a-form-item>
+ </a-form>
+ </a-tab-pane>
+ </a-tabs>
+ </template>
+ <template v-else>
+ <table :style="{ margin: '5px 0', width: '100%' }">
+ <tr class="client-table-odd-row" v-if="nordModal.nordData.token">
+ <td>{{ i18n "pages.xray.outbound.accessToken" }}</td>
+ <td>[[ nordModal.nordData.token ]]</td>
+ </tr>
+ <tr>
+ <td>{{ i18n "pages.xray.outbound.privateKey" }}</td>
+ <td>[[ nordModal.nordData.private_key ]]</td>
+ </tr>
+ </table>
+ <a-button @click="logout" :loading="nordModal.confirmLoading" type="danger">{{ i18n "logout" }}</a-button>
+ <a-divider :style="{ margin: '0' }">{{ i18n "pages.xray.outbound.settings" }}</a-divider>
+ <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '10px' }">
+ <a-form-item label='{{ i18n "pages.xray.outbound.country" }}'>
+ <a-select v-model="nordModal.countryId" @change="fetchServers" show-search option-filter-prop="label">
+ <a-select-option v-for="c in nordModal.countries" :key="c.id" :value="c.id" :label="c.name">
+ [[ c.name ]] ([[ c.code ]])
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+ <a-form-item label='{{ i18n "pages.xray.outbound.city" }}' v-if="nordModal.cities.length > 0">
+ <a-select v-model="nordModal.cityId" @change="onCityChange" show-search option-filter-prop="label">
+ <a-select-option :key="0" :value="null" label='{{ i18n "pages.xray.outbound.allCities" }}'>
+ {{ i18n "pages.xray.outbound.allCities" }}
+ </a-select-option>
+ <a-select-option v-for="c in nordModal.cities" :key="c.id" :value="c.id" :label="c.name">
+ [[ c.name ]]
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+ <a-form-item label='{{ i18n "pages.xray.outbound.server" }}' v-if="filteredServers.length > 0">
+ <a-select v-model="nordModal.serverId">
+ <a-select-option v-for="s in filteredServers" :key="s.id" :value="s.id">
+ [[ s.cityName ]] - [[ s.name ]] ({{ i18n "pages.xray.outbound.load" }}: [[ s.load ]]%)
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+ </a-form>
+ <a-divider :style="{ margin: '10px 0' }">{{ i18n "pages.xray.outbound.outboundStatus" }}</a-divider>
+ <a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+ <template v-if="nordOutboundIndex>=0">
+ <a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag>
+ <a-button @click="resetOutbound" :loading="nordModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button>
+ </template>
+ <template v-else>
+ <a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag>
+ <a-button @click="addOutbound" :disabled="!nordModal.serverId" :loading="nordModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
+ </template>
+ </a-form>
+ </template>
+</a-modal>
+
+<script>
+ const nordModal = {
+ visible: false,
+ confirmLoading: false,
+ nordData: null,
+ token: '',
+ manualKey: '',
+ countries: [],
+ countryId: null,
+ cities: [],
+ cityId: null,
+ servers: [],
+ serverId: null,
+ show() {
+ this.visible = true;
+ this.getData();
+ },
+ close() {
+ this.visible = false;
+ },
+ loading(loading = true) {
+ this.confirmLoading = loading;
+ },
+ async getData() {
+ this.loading(true);
+ const msg = await HttpUtil.post('/panel/xray/nord/data');
+ if (msg.success) {
+ this.nordData = msg.obj ? JSON.parse(msg.obj) : null;
+ if (this.nordData) {
+ await this.fetchCountries();
+ }
+ }
+ this.loading(false);
+ },
+ async login() {
+ this.loading(true);
+ const msg = await HttpUtil.post('/panel/xray/nord/reg', { token: this.token });
+ if (msg.success) {
+ this.nordData = JSON.parse(msg.obj);
+ await this.fetchCountries();
+ }
+ this.loading(false);
+ },
+ async saveKey() {
+ this.loading(true);
+ const msg = await HttpUtil.post('/panel/xray/nord/setKey', { key: this.manualKey });
+ if (msg.success) {
+ this.nordData = JSON.parse(msg.obj);
+ await this.fetchCountries();
+ }
+ this.loading(false);
+ },
+ async logout(index) {
+ this.loading(true);
+ const msg = await HttpUtil.post('/panel/xray/nord/del');
+ if (msg.success) {
+ this.delOutbound(index);
+ this.delRouting();
+ this.nordData = null;
+ this.token = '';
+ this.manualKey = '';
+ this.countries = [];
+ this.cities = [];
+ this.servers = [];
+ this.countryId = null;
+ this.cityId = null;
+ }
+ this.loading(false);
+ },
+ async fetchCountries() {
+ const msg = await HttpUtil.post('/panel/xray/nord/countries');
+ if (msg.success) {
+ this.countries = JSON.parse(msg.obj);
+ }
+ },
+ async fetchServers() {
+ this.loading(true);
+ this.servers = [];
+ this.cities = [];
+ this.serverId = null;
+ this.cityId = null;
+ const msg = await HttpUtil.post('/panel/xray/nord/servers', { countryId: this.countryId });
+ if (msg.success) {
+ const data = JSON.parse(msg.obj);
+ const locations = data.locations || [];
+ const locToCity = {};
+ const citiesMap = new Map();
+ locations.forEach(loc => {
+ if (loc.country && loc.country.city) {
+ citiesMap.set(loc.country.city.id, loc.country.city);
+ locToCity[loc.id] = loc.country.city;
+ }
+ });
+ this.cities = Array.from(citiesMap.values()).sort((a, b) => a.name.localeCompare(b.name));
+
+ this.servers = (data.servers || []).map(s => {
+ const firstLocId = (s.location_ids || [])[0];
+ const city = locToCity[firstLocId];
+ s.cityId = city ? city.id : null;
+ s.cityName = city ? city.name : 'Unknown';
+ return s;
+ }).sort((a, b) => a.load - b.load);
+
+ if (this.servers.length > 0) {
+ this.serverId = this.servers[0].id;
+ }
+
+ if (this.servers.length === 0) {
+ app.$message.warning('No servers found for the selected country');
+ }
+ }
+ this.loading(false);
+ },
+ addOutbound() {
+ const server = this.servers.find(s => s.id === this.serverId);
+ if (!server) return;
+
+ const tech = server.technologies.find(t => t.id === 35);
+ const publicKey = tech.metadata.find(m => m.name === 'public_key').value;
+
+ const outbound = {
+ tag: `nord-${server.hostname}`,
+ protocol: 'wireguard',
+ settings: {
+ secretKey: this.nordData.private_key,
+ address: ['10.5.0.2/32'],
+ peers: [{
+ publicKey: publicKey,
+ endpoint: server.station + ':51820'
+ }],
+ noKernelTun: false
+ }
+ };
+
+ app.templateSettings.outbounds.push(outbound);
+ app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
+ this.close();
+ app.$message.success('NordVPN outbound added');
+ },
+ resetOutbound(index) {
+ const server = this.servers.find(s => s.id === this.serverId);
+ if (!server || index === -1) return;
+
+ const tech = server.technologies.find(t => t.id === 35);
+ const publicKey = tech.metadata.find(m => m.name === 'public_key').value;
+
+ const oldTag = app.templateSettings.outbounds[index].tag;
+ const newTag = `nord-${server.hostname}`;
+
+ const outbound = {
+ tag: newTag,
+ protocol: 'wireguard',
+ settings: {
+ secretKey: this.nordData.private_key,
+ address: ['10.5.0.2/32'],
+ peers: [{
+ publicKey: publicKey,
+ endpoint: server.station + ':51820'
+ }],
+ noKernelTun: false
+ }
+ };
+ app.templateSettings.outbounds[index] = outbound;
+
+ // Sync routing rules
+ app.templateSettings.routing.rules.forEach(r => {
+ if (r.outboundTag === oldTag) {
+ r.outboundTag = newTag;
+ }
+ });
+
+ app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
+ this.close();
+ app.$message.success('NordVPN outbound updated');
+ },
+ delOutbound(index) {
+ if (index !== -1) {
+ app.templateSettings.outbounds.splice(index, 1);
+ app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
+ }
+ },
+ delRouting() {
+ if (app.templateSettings && app.templateSettings.routing) {
+ app.templateSettings.routing.rules = app.templateSettings.routing.rules.filter(r => !r.outboundTag.startsWith("nord-"));
+ }
+ }
+ };
+
+ new Vue({
+ delimiters: ['[[', ']]'],
+ el: '#nord-modal',
+ data: {
+ nordModal: nordModal,
+ },
+ methods: {
+ login: () => nordModal.login(),
+ saveKey: () => nordModal.saveKey(),
+ logout() { nordModal.logout(this.nordOutboundIndex) },
+ fetchServers: () => nordModal.fetchServers(),
+ addOutbound: () => nordModal.addOutbound(),
+ resetOutbound() { nordModal.resetOutbound(this.nordOutboundIndex) },
+ onCityChange() {
+ if (this.filteredServers.length > 0) {
+ this.nordModal.serverId = this.filteredServers[0].id;
+ } else {
+ this.nordModal.serverId = null;
+ }
+ }
+ },
+ computed: {
+ nordOutboundIndex: {
+ get: function () {
+ return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag.startsWith("nord-")) : -1;
+ }
+ },
+ filteredServers: function() {
+ if (!this.nordModal.cityId) {
+ return this.nordModal.servers;
+ }
+ return this.nordModal.servers.filter(s => s.cityId === this.nordModal.cityId);
+ }
+ }
+ });
+</script>
+{{end}}
diff --git a/web/html/settings/xray/basics.html b/web/html/settings/xray/basics.html
index 9a31038a..c637e30a 100644
--- a/web/html/settings/xray/basics.html
+++ b/web/html/settings/xray/basics.html
@@ -313,6 +313,25 @@
</template>
</template>
</a-setting-list-item>
+ <a-setting-list-item paddings="small">
+ <template #title>{{ i18n "pages.xray.nordRouting" }}</template>
+ <template #control>
+ <template v-if="NordExist">
+ <a-select mode="tags" :style="{ width: '100%' }"
+ v-model="nordDomains"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option :value="p.value" :label="p.label"
+ v-for="p in settingsData.ServicesOptions">
+ <span>[[ p.label ]]</span>
+ </a-select-option>
+ </a-select>
+ </template>
+ <template v-else>
+ <a-button type="primary" icon="api"
+ @click="showNord()">{{ i18n "pages.xray.outbound.nordvpn" }}</a-button>
+ </template>
+ </template>
+ </a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel key="6"
header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
diff --git a/web/html/settings/xray/outbounds.html b/web/html/settings/xray/outbounds.html
index 3995760f..232fe55e 100644
--- a/web/html/settings/xray/outbounds.html
+++ b/web/html/settings/xray/outbounds.html
@@ -9,6 +9,8 @@
</a-button>
<a-button type="primary" icon="cloud"
@click="showWarp()">WARP</a-button>
+ <a-button type="primary" icon="api"
+ @click="showNord()">NordVPN</a-button>
</a-space>
</a-col>
<a-col :xs="12" :sm="12" :lg="12" :style="{ textAlign: 'right' }">
diff --git a/web/html/xray.html b/web/html/xray.html
index 040ae2ee..01b4e4e2 100644
--- a/web/html/xray.html
+++ b/web/html/xray.html
@@ -163,6 +163,7 @@
{{template "modals/dnsPresetsModal"}}
{{template "modals/fakednsModal"}}
{{template "modals/warpModal"}}
+{{template "modals/nordModal"}}
<script>
const rulesColumns = [
{ title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } },
@@ -1056,6 +1057,9 @@
showWarp() {
warpModal.show();
},
+ showNord() {
+ nordModal.show();
+ },
async loadCustomGeoAliases() {
try {
const msg = await HttpUtil.get('/panel/api/custom-geo/aliases');
@@ -1429,6 +1433,19 @@
this.templateRuleSetter({ outboundTag: "warp", property: "domain", data: newValue });
}
},
+ nordTag: {
+ get: function () {
+ return this.templateSettings ? (this.templateSettings.outbounds.find((o) => o.tag.startsWith("nord-")) || { tag: "nord" }).tag : "nord";
+ }
+ },
+ nordDomains: {
+ get: function () {
+ return this.templateRuleGetter({ outboundTag: this.nordTag, property: "domain" });
+ },
+ set: function (newValue) {
+ this.templateRuleSetter({ outboundTag: this.nordTag, property: "domain", data: newValue });
+ }
+ },
torrentSettings: {
get: function () {
return ArrayUtils.doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
@@ -1446,6 +1463,11 @@
return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag == "warp") >= 0 : false;
},
},
+ NordExist: {
+ get: function () {
+ return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag.startsWith("nord-")) >= 0 : false;
+ },
+ },
enableDNS: {
get: function () {
return this.templateSettings ? this.templateSettings.dns != null : false;
diff --git a/web/service/nord.go b/web/service/nord.go
new file mode 100644
index 00000000..db0a48f9
--- /dev/null
+++ b/web/service/nord.go
@@ -0,0 +1,145 @@
+package service
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/mhsanaei/3x-ui/v2/util/common"
+)
+
+type NordService struct {
+ SettingService
+}
+
+var nordHTTPClient = &http.Client{Timeout: 15 * time.Second}
+
+// maxResponseSize limits the maximum size of NordVPN API responses (10 MB).
+const maxResponseSize = 10 << 20
+
+func (s *NordService) GetCountries() (string, error) {
+ resp, err := nordHTTPClient.Get("https://api.nordvpn.com/v1/countries")
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
+ if err != nil {
+ return "", err
+ }
+ return string(body), nil
+}
+
+func (s *NordService) GetServers(countryId string) (string, error) {
+ // Validate countryId is numeric to prevent URL injection
+ for _, c := range countryId {
+ if c < '0' || c > '9' {
+ return "", common.NewError("invalid country ID")
+ }
+ }
+ url := fmt.Sprintf("https://api.nordvpn.com/v2/servers?limit=0&filters[servers_technologies][id]=35&filters[country_id]=%s", countryId)
+ resp, err := nordHTTPClient.Get(url)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
+ if err != nil {
+ return "", err
+ }
+ var data map[string]any
+ if err := json.Unmarshal(body, &data); err != nil {
+ return string(body), nil
+ }
+
+ servers, ok := data["servers"].([]any)
+ if !ok {
+ return string(body), nil
+ }
+
+ var filtered []any
+ for _, s := range servers {
+ if server, ok := s.(map[string]any); ok {
+ if load, ok := server["load"].(float64); ok && load > 7 {
+ filtered = append(filtered, s)
+ }
+ }
+ }
+ data["servers"] = filtered
+
+ result, _ := json.Marshal(data)
+ return string(result), nil
+}
+
+func (s *NordService) SetKey(privateKey string) (string, error) {
+ if privateKey == "" {
+ return "", common.NewError("private key cannot be empty")
+ }
+ nordData := map[string]string{
+ "private_key": privateKey,
+ "token": "",
+ }
+ data, _ := json.Marshal(nordData)
+ err := s.SettingService.SetNord(string(data))
+ if err != nil {
+ return "", err
+ }
+ return string(data), nil
+}
+
+func (s *NordService) GetCredentials(token string) (string, error) {
+ url := "https://api.nordvpn.com/v1/users/services/credentials"
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return "", err
+ }
+ req.SetBasicAuth("token", token)
+
+ client := &http.Client{Timeout: 10 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return "", common.NewErrorf("NordVPN API error: %s", resp.Status)
+ }
+
+ body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
+ if err != nil {
+ return "", err
+ }
+
+ var creds map[string]any
+ if err := json.Unmarshal(body, &creds); err != nil {
+ return "", err
+ }
+
+ privateKey, ok := creds["nordlynx_private_key"].(string)
+ if !ok || privateKey == "" {
+ return "", common.NewError("failed to retrieve NordLynx private key")
+ }
+
+ nordData := map[string]string{
+ "private_key": privateKey,
+ "token": token,
+ }
+ data, _ := json.Marshal(nordData)
+ err = s.SettingService.SetNord(string(data))
+ if err != nil {
+ return "", err
+ }
+
+ return string(data), nil
+}
+
+func (s *NordService) GetNordData() (string, error) {
+ return s.SettingService.GetNord()
+}
+
+func (s *NordService) DelNordData() error {
+ return s.SettingService.SetNord("")
+}
diff --git a/web/service/setting.go b/web/service/setting.go
index 468a7960..04d8f6a8 100644
--- a/web/service/setting.go
+++ b/web/service/setting.go
@@ -80,6 +80,7 @@ var defaultValueMap = map[string]string{
"subJsonRules": "",
"datepicker": "gregorian",
"warp": "",
+ "nord": "",
"externalTrafficInformEnable": "false",
"externalTrafficInformURI": "",
"xrayOutboundTestUrl": "https://www.google.com/generate_204",
@@ -598,6 +599,14 @@ func (s *SettingService) SetWarp(data string) error {
return s.setString("warp", data)
}
+func (s *SettingService) GetNord() (string, error) {
+ return s.getString("nord")
+}
+
+func (s *SettingService) SetNord(data string) error {
+ return s.setString("nord", data)
+}
+
func (s *SettingService) GetExternalTrafficInformEnable() (bool, error) {
return s.getBool("externalTrafficInformEnable")
}
diff --git a/web/translation/translate.ar_EG.toml b/web/translation/translate.ar_EG.toml
index ace753e1..f9216621 100644
--- a/web/translation/translate.ar_EG.toml
+++ b/web/translation/translate.ar_EG.toml
@@ -4,6 +4,8 @@
"confirm" = "تأكيد"
"cancel" = "إلغاء"
"close" = "إغلاق"
+"save" = "حفظ"
+"logout" = "تسجيل خروج"
"create" = "إنشاء"
"update" = "تحديث"
"copy" = "نسخ"
@@ -496,6 +498,8 @@
"ipv4RoutingDesc" = "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر IPv4."
"warpRouting" = "توجيه WARP"
"warpRoutingDesc" = "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر WARP."
+"nordRouting" = "توجيه NordVPN"
+"nordRoutingDesc" = "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر NordVPN."
"Template" = "قالب إعدادات Xray المتقدم"
"TemplateDesc" = "ملف إعدادات Xray النهائي هيتولد بناءً على القالب ده."
"FreedomStrategy" = "استراتيجية بروتوكول الحرية"
@@ -573,6 +577,14 @@
"testSuccess" = "الاختبار ناجح"
"testFailed" = "فشل الاختبار"
"testError" = "فشل اختبار المخرج"
+"nordvpn" = "NordVPN"
+"accessToken" = "رمز الوصول"
+"country" = "الدولة"
+"server" = "الخادم"
+"city" = "المدينة"
+"allCities" = "كل المدن"
+"privateKey" = "المفتاح الخاص"
+"load" = "الحمل"
[pages.xray.balancer]
"addBalancer" = "أضف موازن تحميل"
diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml
index 4cf1be07..91580a29 100644
--- a/web/translation/translate.en_US.toml
+++ b/web/translation/translate.en_US.toml
@@ -4,6 +4,8 @@
"confirm" = "Confirm"
"cancel" = "Cancel"
"close" = "Close"
+"save" = "Save"
+"logout" = "Log Out"
"create" = "Create"
"update" = "Update"
"copy" = "Copy"
@@ -496,6 +498,8 @@
"ipv4RoutingDesc" = "These options will route traffic based on a specific destination via IPv4."
"warpRouting" = "WARP Routing"
"warpRoutingDesc" = "These options will route traffic based on a specific destination via WARP."
+"nordRouting" = "NordVPN Routing"
+"nordRoutingDesc" = "These options will route traffic based on a specific destination via NordVPN."
"Template" = "Advanced Xray Configuration Template"
"TemplateDesc" = "The final Xray config file will be generated based on this template."
"FreedomStrategy" = "Freedom Protocol Strategy"
@@ -573,6 +577,14 @@
"testSuccess" = "Test successful"
"testFailed" = "Test failed"
"testError" = "Failed to test outbound"
+"nordvpn" = "NordVPN"
+"accessToken" = "Access Token"
+"country" = "Country"
+"server" = "Server"
+"city" = "City"
+"allCities" = "All Cities"
+"privateKey" = "Private Key"
+"load" = "Load"
[pages.xray.balancer]
"addBalancer" = "Add Balancer"
diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml
index 8e76fba3..3dfe3ade 100644
--- a/web/translation/translate.es_ES.toml
+++ b/web/translation/translate.es_ES.toml
@@ -4,6 +4,8 @@
"confirm" = "Confirmar"
"cancel" = "Cancelar"
"close" = "Cerrar"
+"save" = "Guardar"
+"logout" = "Cerrar Sesión"
"create" = "Crear"
"update" = "Actualizar"
"copy" = "Copiar"
@@ -496,6 +498,8 @@
"ipv4RoutingDesc" = "Estas opciones solo enrutarán a los dominios objetivo a través de IPv4."
"warpRouting" = "Enrutamiento WARP"
"warpRoutingDesc" = "Precaución: Antes de usar estas opciones, instale WARP en modo de proxy socks5 en su servidor siguiendo los pasos en el GitHub del panel. WARP enrutará el tráfico a los sitios web a través de los servidores de Cloudflare."
+"nordRouting" = "Enrutamiento NordVPN"
+"nordRoutingDesc" = "Estas opciones enrutarán el tráfico basado en un destino específico a través de NordVPN."
"Template" = "Plantilla de Configuración de Xray"
"TemplateDesc" = "Genera el archivo de configuración final de Xray basado en esta plantilla."
"FreedomStrategy" = "Configurar Estrategia para el Protocolo Freedom"
@@ -573,6 +577,14 @@
"testSuccess" = "Prueba exitosa"
"testFailed" = "Prueba fallida"
"testError" = "Error al probar la salida"
+"nordvpn" = "NordVPN"
+"accessToken" = "Token de acceso"
+"country" = "País"
+"server" = "Servidor"
+"city" = "Ciudad"
+"allCities" = "Todas las ciudades"
+"privateKey" = "Clave privada"
+"load" = "Carga"
[pages.xray.balancer]
"addBalancer" = "Agregar equilibrador"
diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml
index c6ea9a84..5569239f 100644
--- a/web/translation/translate.fa_IR.toml
+++ b/web/translation/translate.fa_IR.toml
@@ -4,6 +4,8 @@
"confirm" = "تایید"
"cancel" = "انصراف"
"close" = "بستن"
+"save" = "ذخیره"
+"logout" = "خروج"
"create" = "ایجاد"
"update" = "به‌روزرسانی"
"copy" = "کپی"
@@ -496,6 +498,8 @@
"ipv4RoutingDesc" = "این گزینه‌ها ترافیک را از طریق آی‌پی نسخه4 سرور، به مقصد هدایت می‌کند"
"warpRouting" = "WARP مسیریابی"
"warpRoutingDesc" = "این گزینه‌ها ترافیک‌ را از طریق وارپ کلادفلر به مقصد هدایت می‌کند"
+"nordRouting" = "مسیریابی NordVPN"
+"nordRoutingDesc" = "این گزینه‌ها ترافیک را بر اساس مقصد خاص از طریق NordVPN مسیریابی می‌کنند."
"Template" = "‌پیکربندی پیشرفته الگو ایکس‌ری"
"TemplateDesc" = "فایل پیکربندی نهایی ایکس‌ری بر اساس این الگو ایجاد می‌شود"
"FreedomStrategy" = "Freedom استراتژی پروتکل"
@@ -573,6 +577,14 @@
"testSuccess" = "تست موفقیت‌آمیز"
"testFailed" = "تست ناموفق"
"testError" = "خطا در تست خروجی"
+"nordvpn" = "NordVPN"
+"accessToken" = "توکن دسترسی"
+"country" = "کشور"
+"server" = "سرور"
+"privateKey" = "کلید خصوصی"
+"city" = "شهر"
+"allCities" = "همه شهرها"
+"load" = "فشار سرور"
[pages.xray.balancer]
"addBalancer" = "افزودن بالانسر"
diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml
index 32f79d5f..c53b6a9c 100644
--- a/web/translation/translate.id_ID.toml
+++ b/web/translation/translate.id_ID.toml
@@ -4,6 +4,8 @@
"confirm" = "Konfirmasi"