1
0

nord_modal.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. {{define "modals/nordModal"}}
  2. <a-modal id="nord-modal" v-model="nordModal.visible" title="NordVPN NordLynx"
  3. :confirm-loading="nordModal.confirmLoading" :closable="true" :mask-closable="true"
  4. :footer="null" :class="themeSwitcher.currentTheme">
  5. <template v-if="nordModal.nordData == null">
  6. <a-tabs default-active-key="token" :class="themeSwitcher.currentTheme">
  7. <a-tab-pane key="token" tab='{{ i18n "pages.xray.outbound.accessToken" }}'>
  8. <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }">
  9. <a-form-item label='{{ i18n "pages.xray.outbound.accessToken" }}'>
  10. <a-input v-model="nordModal.token" placeholder='{{ i18n "pages.xray.outbound.accessToken" }}'></a-input>
  11. <div :style="{ marginTop: '10px' }">
  12. <a-button type="primary" icon="login" @click="login()" :loading="nordModal.confirmLoading">{{ i18n "login" }}</a-button>
  13. </div>
  14. </a-form-item>
  15. </a-form>
  16. </a-tab-pane>
  17. <a-tab-pane key="key" tab='{{ i18n "pages.xray.outbound.privateKey" }}'>
  18. <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }">
  19. <a-form-item label='{{ i18n "pages.xray.outbound.privateKey" }}'>
  20. <a-input v-model="nordModal.manualKey" placeholder='{{ i18n "pages.xray.outbound.privateKey" }}'></a-input>
  21. <div :style="{ marginTop: '10px' }">
  22. <a-button type="primary" icon="save" @click="saveKey()" :loading="nordModal.confirmLoading">{{ i18n "save" }}</a-button>
  23. </div>
  24. </a-form-item>
  25. </a-form>
  26. </a-tab-pane>
  27. </a-tabs>
  28. </template>
  29. <template v-else>
  30. <table :style="{ margin: '5px 0', width: '100%' }">
  31. <tr class="client-table-odd-row" v-if="nordModal.nordData.token">
  32. <td>{{ i18n "pages.xray.outbound.accessToken" }}</td>
  33. <td>[[ nordModal.nordData.token ]]</td>
  34. </tr>
  35. <tr>
  36. <td>{{ i18n "pages.xray.outbound.privateKey" }}</td>
  37. <td>[[ nordModal.nordData.private_key ]]</td>
  38. </tr>
  39. </table>
  40. <a-button @click="logout" :loading="nordModal.confirmLoading" type="danger">{{ i18n "logout" }}</a-button>
  41. <a-divider :style="{ margin: '0' }">{{ i18n "pages.xray.outbound.settings" }}</a-divider>
  42. <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '10px' }">
  43. <a-form-item label='{{ i18n "pages.xray.outbound.country" }}'>
  44. <a-select v-model="nordModal.countryId" @change="fetchServers" show-search option-filter-prop="label">
  45. <a-select-option v-for="c in nordModal.countries" :key="c.id" :value="c.id" :label="c.name">
  46. [[ c.name ]] ([[ c.code ]])
  47. </a-select-option>
  48. </a-select>
  49. </a-form-item>
  50. <a-form-item label='{{ i18n "pages.xray.outbound.city" }}' v-if="nordModal.cities.length > 0">
  51. <a-select v-model="nordModal.cityId" @change="onCityChange" show-search option-filter-prop="label">
  52. <a-select-option :key="0" :value="null" label='{{ i18n "pages.xray.outbound.allCities" }}'>
  53. {{ i18n "pages.xray.outbound.allCities" }}
  54. </a-select-option>
  55. <a-select-option v-for="c in nordModal.cities" :key="c.id" :value="c.id" :label="c.name">
  56. [[ c.name ]]
  57. </a-select-option>
  58. </a-select>
  59. </a-form-item>
  60. <a-form-item label='{{ i18n "pages.xray.outbound.server" }}' v-if="filteredServers.length > 0">
  61. <a-select v-model="nordModal.serverId">
  62. <a-select-option v-for="s in filteredServers" :key="s.id" :value="s.id">
  63. [[ s.cityName ]] - [[ s.name ]] ({{ i18n "pages.xray.outbound.load" }}: [[ s.load ]]%)
  64. </a-select-option>
  65. </a-select>
  66. </a-form-item>
  67. </a-form>
  68. <a-divider :style="{ margin: '10px 0' }">{{ i18n "pages.xray.outbound.outboundStatus" }}</a-divider>
  69. <a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
  70. <template v-if="nordOutboundIndex>=0">
  71. <a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag>
  72. <a-button @click="resetOutbound" :loading="nordModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button>
  73. </template>
  74. <template v-else>
  75. <a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag>
  76. <a-button @click="addOutbound" :disabled="!nordModal.serverId" :loading="nordModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
  77. </template>
  78. </a-form>
  79. </template>
  80. </a-modal>
  81. <script>
  82. const nordModal = {
  83. visible: false,
  84. confirmLoading: false,
  85. nordData: null,
  86. token: '',
  87. manualKey: '',
  88. countries: [],
  89. countryId: null,
  90. cities: [],
  91. cityId: null,
  92. servers: [],
  93. serverId: null,
  94. show() {
  95. this.visible = true;
  96. this.getData();
  97. },
  98. close() {
  99. this.visible = false;
  100. },
  101. loading(loading = true) {
  102. this.confirmLoading = loading;
  103. },
  104. async getData() {
  105. this.loading(true);
  106. const msg = await HttpUtil.post('/panel/xray/nord/data');
  107. if (msg.success) {
  108. this.nordData = msg.obj ? JSON.parse(msg.obj) : null;
  109. if (this.nordData) {
  110. await this.fetchCountries();
  111. }
  112. }
  113. this.loading(false);
  114. },
  115. async login() {
  116. this.loading(true);
  117. const msg = await HttpUtil.post('/panel/xray/nord/reg', { token: this.token });
  118. if (msg.success) {
  119. this.nordData = JSON.parse(msg.obj);
  120. await this.fetchCountries();
  121. }
  122. this.loading(false);
  123. },
  124. async saveKey() {
  125. this.loading(true);
  126. const msg = await HttpUtil.post('/panel/xray/nord/setKey', { key: this.manualKey });
  127. if (msg.success) {
  128. this.nordData = JSON.parse(msg.obj);
  129. await this.fetchCountries();
  130. }
  131. this.loading(false);
  132. },
  133. async logout(index) {
  134. this.loading(true);
  135. const msg = await HttpUtil.post('/panel/xray/nord/del');
  136. if (msg.success) {
  137. this.delOutbound(index);
  138. this.delRouting();
  139. this.nordData = null;
  140. this.token = '';
  141. this.manualKey = '';
  142. this.countries = [];
  143. this.cities = [];
  144. this.servers = [];
  145. this.countryId = null;
  146. this.cityId = null;
  147. }
  148. this.loading(false);
  149. },
  150. async fetchCountries() {
  151. const msg = await HttpUtil.post('/panel/xray/nord/countries');
  152. if (msg.success) {
  153. this.countries = JSON.parse(msg.obj);
  154. }
  155. },
  156. async fetchServers() {
  157. this.loading(true);
  158. this.servers = [];
  159. this.cities = [];
  160. this.serverId = null;
  161. this.cityId = null;
  162. const msg = await HttpUtil.post('/panel/xray/nord/servers', { countryId: this.countryId });
  163. if (msg.success) {
  164. const data = JSON.parse(msg.obj);
  165. const locations = data.locations || [];
  166. const locToCity = {};
  167. const citiesMap = new Map();
  168. locations.forEach(loc => {
  169. if (loc.country && loc.country.city) {
  170. citiesMap.set(loc.country.city.id, loc.country.city);
  171. locToCity[loc.id] = loc.country.city;
  172. }
  173. });
  174. this.cities = Array.from(citiesMap.values()).sort((a, b) => a.name.localeCompare(b.name));
  175. this.servers = (data.servers || []).map(s => {
  176. const firstLocId = (s.location_ids || [])[0];
  177. const city = locToCity[firstLocId];
  178. s.cityId = city ? city.id : null;
  179. s.cityName = city ? city.name : 'Unknown';
  180. return s;
  181. }).sort((a, b) => a.load - b.load);
  182. if (this.servers.length > 0) {
  183. this.serverId = this.servers[0].id;
  184. }
  185. if (this.servers.length === 0) {
  186. app.$message.warning('No servers found for the selected country');
  187. }
  188. }
  189. this.loading(false);
  190. },
  191. addOutbound() {
  192. const server = this.servers.find(s => s.id === this.serverId);
  193. if (!server) return;
  194. const tech = server.technologies.find(t => t.id === 35);
  195. const publicKey = tech.metadata.find(m => m.name === 'public_key').value;
  196. const outbound = {
  197. tag: `nord-${server.hostname}`,
  198. protocol: 'wireguard',
  199. settings: {
  200. secretKey: this.nordData.private_key,
  201. address: ['10.5.0.2/32'],
  202. peers: [{
  203. publicKey: publicKey,
  204. endpoint: server.station + ':51820'
  205. }],
  206. noKernelTun: false
  207. }
  208. };
  209. app.templateSettings.outbounds.push(outbound);
  210. app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
  211. this.close();
  212. app.$message.success('NordVPN outbound added');
  213. },
  214. resetOutbound(index) {
  215. const server = this.servers.find(s => s.id === this.serverId);
  216. if (!server || index === -1) return;
  217. const tech = server.technologies.find(t => t.id === 35);
  218. const publicKey = tech.metadata.find(m => m.name === 'public_key').value;
  219. const oldTag = app.templateSettings.outbounds[index].tag;
  220. const newTag = `nord-${server.hostname}`;
  221. const outbound = {
  222. tag: newTag,
  223. protocol: 'wireguard',
  224. settings: {
  225. secretKey: this.nordData.private_key,
  226. address: ['10.5.0.2/32'],
  227. peers: [{
  228. publicKey: publicKey,
  229. endpoint: server.station + ':51820'
  230. }],
  231. noKernelTun: false
  232. }
  233. };
  234. app.templateSettings.outbounds[index] = outbound;
  235. // Sync routing rules
  236. app.templateSettings.routing.rules.forEach(r => {
  237. if (r.outboundTag === oldTag) {
  238. r.outboundTag = newTag;
  239. }
  240. });
  241. app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
  242. this.close();
  243. app.$message.success('NordVPN outbound updated');
  244. },
  245. delOutbound(index) {
  246. if (index !== -1) {
  247. app.templateSettings.outbounds.splice(index, 1);
  248. app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
  249. }
  250. },
  251. delRouting() {
  252. if (app.templateSettings && app.templateSettings.routing) {
  253. app.templateSettings.routing.rules = app.templateSettings.routing.rules.filter(r => !r.outboundTag.startsWith("nord-"));
  254. }
  255. }
  256. };
  257. new Vue({
  258. delimiters: ['[[', ']]'],
  259. el: '#nord-modal',
  260. data: {
  261. nordModal: nordModal,
  262. },
  263. methods: {
  264. login: () => nordModal.login(),
  265. saveKey: () => nordModal.saveKey(),
  266. logout() { nordModal.logout(this.nordOutboundIndex) },
  267. fetchServers: () => nordModal.fetchServers(),
  268. addOutbound: () => nordModal.addOutbound(),
  269. resetOutbound() { nordModal.resetOutbound(this.nordOutboundIndex) },
  270. onCityChange() {
  271. if (this.filteredServers.length > 0) {
  272. this.nordModal.serverId = this.filteredServers[0].id;
  273. } else {
  274. this.nordModal.serverId = null;
  275. }
  276. }
  277. },
  278. computed: {
  279. nordOutboundIndex: {
  280. get: function () {
  281. return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag.startsWith("nord-")) : -1;
  282. }
  283. },
  284. filteredServers: function() {
  285. if (!this.nordModal.cityId) {
  286. return this.nordModal.servers;
  287. }
  288. return this.nordModal.servers.filter(s => s.cityId === this.nordModal.cityId);
  289. }
  290. }
  291. });
  292. </script>
  293. {{end}}