form.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. const Mn = require('backbone.marionette');
  2. const App = require('../../main');
  3. const ProxyHostModel = require('../../../models/proxy-host');
  4. const ProxyLocationModel = require('../../../models/proxy-host-location');
  5. const template = require('./form.ejs');
  6. const certListItemTemplate = require('../certificates-list-item.ejs');
  7. const accessListItemTemplate = require('./access-list-item.ejs');
  8. const CustomLocation = require('./location');
  9. const Helpers = require('../../../lib/helpers');
  10. require('jquery-serializejson');
  11. require('selectize');
  12. module.exports = Mn.View.extend({
  13. template: template,
  14. className: 'modal-dialog',
  15. locationsCollection: new ProxyLocationModel.Collection(),
  16. ui: {
  17. form: 'form',
  18. domain_names: 'input[name="domain_names"]',
  19. forward_host: 'input[name="forward_host"]',
  20. buttons: '.modal-footer button',
  21. cancel: 'button.cancel',
  22. save: 'button.save',
  23. add_location_btn: 'button.add_location',
  24. locations_container:'.locations_container',
  25. certificate_select: 'select[name="certificate_id"]',
  26. access_list_select: 'select[name="access_list_id"]',
  27. ssl_forced: 'input[name="ssl_forced"]',
  28. hsts_enabled: 'input[name="hsts_enabled"]',
  29. hsts_subdomains: 'input[name="hsts_subdomains"]',
  30. http2_support: 'input[name="http2_support"]',
  31. cloudflare_switch: 'input[name="meta[cloudflare_use]"]',
  32. cloudflare_token: 'input[name="meta[cloudflare_token]"',
  33. cloudflare: '.cloudflare',
  34. forward_scheme: 'select[name="forward_scheme"]',
  35. letsencrypt: '.letsencrypt'
  36. },
  37. regions: {
  38. locations_regions: '@ui.locations_container'
  39. },
  40. events: {
  41. 'change @ui.certificate_select': function () {
  42. let id = this.ui.certificate_select.val();
  43. if (id === 'new') {
  44. this.ui.letsencrypt.show().find('input').prop('disabled', false);
  45. this.ui.cloudflare.hide();
  46. } else {
  47. this.ui.letsencrypt.hide().find('input').prop('disabled', true);
  48. }
  49. let enabled = id === 'new' || parseInt(id, 10) > 0;
  50. let inputs = this.ui.ssl_forced.add(this.ui.http2_support);
  51. inputs
  52. .prop('disabled', !enabled)
  53. .parents('.form-group')
  54. .css('opacity', enabled ? 1 : 0.5);
  55. if (!enabled) {
  56. inputs.prop('checked', false);
  57. }
  58. inputs.trigger('change');
  59. },
  60. 'change @ui.ssl_forced': function () {
  61. let checked = this.ui.ssl_forced.prop('checked');
  62. this.ui.hsts_enabled
  63. .prop('disabled', !checked)
  64. .parents('.form-group')
  65. .css('opacity', checked ? 1 : 0.5);
  66. if (!checked) {
  67. this.ui.hsts_enabled.prop('checked', false);
  68. }
  69. this.ui.hsts_enabled.trigger('change');
  70. },
  71. 'change @ui.hsts_enabled': function () {
  72. let checked = this.ui.hsts_enabled.prop('checked');
  73. this.ui.hsts_subdomains
  74. .prop('disabled', !checked)
  75. .parents('.form-group')
  76. .css('opacity', checked ? 1 : 0.5);
  77. if (!checked) {
  78. this.ui.hsts_subdomains.prop('checked', false);
  79. }
  80. },
  81. 'change @ui.cloudflare_switch': function() {
  82. let checked = this.ui.cloudflare_switch.prop('checked');
  83. if (checked) {
  84. this.ui.cloudflare_token.prop('required', 'required');
  85. this.ui.cloudflare.show();
  86. } else {
  87. this.ui.cloudflare_token.prop('required', false);
  88. this.ui.cloudflare.hide();
  89. }
  90. },
  91. 'click @ui.add_location_btn': function (e) {
  92. e.preventDefault();
  93. const model = new ProxyLocationModel.Model();
  94. this.locationsCollection.add(model);
  95. },
  96. 'click @ui.save': function (e) {
  97. e.preventDefault();
  98. if (!this.ui.form[0].checkValidity()) {
  99. $('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
  100. return;
  101. }
  102. let view = this;
  103. let data = this.ui.form.serializeJSON();
  104. // Add locations
  105. data.locations = [];
  106. this.locationsCollection.models.forEach((location) => {
  107. data.locations.push(location.toJSON());
  108. });
  109. // Serialize collects path from custom locations
  110. // This field must be removed from root object
  111. delete data.path;
  112. // Manipulate
  113. data.forward_port = parseInt(data.forward_port, 10);
  114. data.block_exploits = !!data.block_exploits;
  115. data.caching_enabled = !!data.caching_enabled;
  116. data.allow_websocket_upgrade = !!data.allow_websocket_upgrade;
  117. data.http2_support = !!data.http2_support;
  118. data.hsts_enabled = !!data.hsts_enabled;
  119. data.hsts_subdomains = !!data.hsts_subdomains;
  120. data.ssl_forced = !!data.ssl_forced;
  121. if (typeof data.domain_names === 'string' && data.domain_names) {
  122. data.domain_names = data.domain_names.split(',');
  123. }
  124. // Check for any domain names containing wildcards, which are not allowed with letsencrypt
  125. if (data.certificate_id === 'new') {
  126. let domain_err = false;
  127. if (!data.meta.cloudflare_use) {
  128. data.domain_names.map(function (name) {
  129. if (name.match(/\*/im)) {
  130. domain_err = true;
  131. }
  132. });
  133. }
  134. if (domain_err) {
  135. alert('Cannot request Let\'s Encrypt Certificate for wildcard domains without CloudFlare DNS.');
  136. return;
  137. }
  138. data.meta.cloudflare_use = data.meta.cloudflare_use === '1';
  139. data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1';
  140. } else {
  141. data.certificate_id = parseInt(data.certificate_id, 10);
  142. }
  143. let method = App.Api.Nginx.ProxyHosts.create;
  144. let is_new = true;
  145. if (this.model.get('id')) {
  146. // edit
  147. is_new = false;
  148. method = App.Api.Nginx.ProxyHosts.update;
  149. data.id = this.model.get('id');
  150. }
  151. this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
  152. this.ui.save.addClass('btn-loading');
  153. method(data)
  154. .then(result => {
  155. view.model.set(result);
  156. App.UI.closeModal(function () {
  157. if (is_new) {
  158. App.Controller.showNginxProxy();
  159. }
  160. });
  161. })
  162. .catch(err => {
  163. alert(err.message);
  164. this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
  165. this.ui.save.removeClass('btn-loading');
  166. });
  167. }
  168. },
  169. templateContext: {
  170. getLetsencryptEmail: function () {
  171. return App.Cache.User.get('email');
  172. }
  173. },
  174. onRender: function () {
  175. let view = this;
  176. this.ui.ssl_forced.trigger('change');
  177. this.ui.hsts_enabled.trigger('change');
  178. // Domain names
  179. this.ui.domain_names.selectize({
  180. delimiter: ',',
  181. persist: false,
  182. maxOptions: 15,
  183. create: function (input) {
  184. return {
  185. value: input,
  186. text: input
  187. };
  188. },
  189. createFilter: /^(?:\.)?(?:[^.*]+\.?)+[^.]$/
  190. });
  191. // Access Lists
  192. this.ui.access_list_select.selectize({
  193. valueField: 'id',
  194. labelField: 'name',
  195. searchField: ['name'],
  196. create: false,
  197. preload: true,
  198. allowEmptyOption: true,
  199. render: {
  200. option: function (item) {
  201. item.i18n = App.i18n;
  202. item.formatDbDate = Helpers.formatDbDate;
  203. return accessListItemTemplate(item);
  204. }
  205. },
  206. load: function (query, callback) {
  207. App.Api.Nginx.AccessLists.getAll(['items', 'clients'])
  208. .then(rows => {
  209. callback(rows);
  210. })
  211. .catch(err => {
  212. console.error(err);
  213. callback();
  214. });
  215. },
  216. onLoad: function () {
  217. view.ui.access_list_select[0].selectize.setValue(view.model.get('access_list_id'));
  218. }
  219. });
  220. // Certificates
  221. this.ui.letsencrypt.hide();
  222. this.ui.certificate_select.selectize({
  223. valueField: 'id',
  224. labelField: 'nice_name',
  225. searchField: ['nice_name', 'domain_names'],
  226. create: false,
  227. preload: true,
  228. allowEmptyOption: true,
  229. render: {
  230. option: function (item) {
  231. item.i18n = App.i18n;
  232. item.formatDbDate = Helpers.formatDbDate;
  233. return certListItemTemplate(item);
  234. }
  235. },
  236. load: function (query, callback) {
  237. App.Api.Nginx.Certificates.getAll()
  238. .then(rows => {
  239. callback(rows);
  240. })
  241. .catch(err => {
  242. console.error(err);
  243. callback();
  244. });
  245. },
  246. onLoad: function () {
  247. view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id'));
  248. }
  249. });
  250. },
  251. initialize: function (options) {
  252. if (typeof options.model === 'undefined' || !options.model) {
  253. this.model = new ProxyHostModel.Model();
  254. }
  255. this.locationsCollection = new ProxyLocationModel.Collection();
  256. // Custom locations
  257. this.showChildView('locations_regions', new CustomLocation.LocationCollectionView({
  258. collection: this.locationsCollection
  259. }));
  260. // Check wether there are any location defined
  261. if (options.model && Array.isArray(options.model.attributes.locations)) {
  262. options.model.attributes.locations.forEach((location) => {
  263. let m = new ProxyLocationModel.Model(location);
  264. this.locationsCollection.add(m);
  265. });
  266. }
  267. }
  268. });