nord_modal.html 14 KB

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