mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-31 07:43:33 +00:00 
			
		
		
		
	404 hosts section and other fixes
This commit is contained in:
		| @@ -3,6 +3,7 @@ | ||||
| const _             = require('lodash'); | ||||
| const error         = require('../lib/error'); | ||||
| const deadHostModel = require('../models/dead_host'); | ||||
| const internalHost  = require('./host'); | ||||
|  | ||||
| function omissions () { | ||||
|     return ['is_deleted']; | ||||
| @@ -10,6 +11,199 @@ function omissions () { | ||||
|  | ||||
| const internalDeadHost = { | ||||
|  | ||||
|     /** | ||||
|      * @param   {Access}  access | ||||
|      * @param   {Object}  data | ||||
|      * @returns {Promise} | ||||
|      */ | ||||
|     create: (access, data) => { | ||||
|         return access.can('dead_hosts:create', data) | ||||
|             .then(access_data => { | ||||
|                 // Get a list of the domain names and check each of them against existing records | ||||
|                 let domain_name_check_promises = []; | ||||
|  | ||||
|                 data.domain_names.map(function (domain_name) { | ||||
|                     domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); | ||||
|                 }); | ||||
|  | ||||
|                 return Promise.all(domain_name_check_promises) | ||||
|                     .then(check_results => { | ||||
|                         check_results.map(function (result) { | ||||
|                             if (result.is_taken) { | ||||
|                                 throw new error.ValidationError(result.hostname + ' is already in use'); | ||||
|                             } | ||||
|                         }); | ||||
|                     }); | ||||
|             }) | ||||
|             .then(() => { | ||||
|                 // At this point the domains should have been checked | ||||
|                 data.owner_user_id = access.token.get('attrs').id; | ||||
|  | ||||
|                 if (typeof data.meta === 'undefined') { | ||||
|                     data.meta = {}; | ||||
|                 } | ||||
|  | ||||
|                 return deadHostModel | ||||
|                     .query() | ||||
|                     .omit(omissions()) | ||||
|                     .insertAndFetch(data); | ||||
|             }) | ||||
|             .then(row => { | ||||
|                 return _.omit(row, omissions()); | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * @param  {Access}  access | ||||
|      * @param  {Object}  data | ||||
|      * @param  {Integer} data.id | ||||
|      * @param  {String}  [data.email] | ||||
|      * @param  {String}  [data.name] | ||||
|      * @return {Promise} | ||||
|      */ | ||||
|     update: (access, data) => { | ||||
|         return access.can('dead_hosts:update', data.id) | ||||
|             .then(access_data => { | ||||
|                 // Get a list of the domain names and check each of them against existing records | ||||
|                 let domain_name_check_promises = []; | ||||
|  | ||||
|                 if (typeof data.domain_names !== 'undefined') { | ||||
|                     data.domain_names.map(function (domain_name) { | ||||
|                         domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'dead', data.id)); | ||||
|                     }); | ||||
|  | ||||
|                     return Promise.all(domain_name_check_promises) | ||||
|                         .then(check_results => { | ||||
|                             check_results.map(function (result) { | ||||
|                                 if (result.is_taken) { | ||||
|                                     throw new error.ValidationError(result.hostname + ' is already in use'); | ||||
|                                 } | ||||
|                             }); | ||||
|                         }); | ||||
|                 } | ||||
|             }) | ||||
|             .then(() => { | ||||
|                 return internalDeadHost.get(access, {id: data.id}); | ||||
|             }) | ||||
|             .then(row => { | ||||
|                 if (row.id !== data.id) { | ||||
|                     // Sanity check that something crazy hasn't happened | ||||
|                     throw new error.InternalValidationError('404 Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); | ||||
|                 } | ||||
|  | ||||
|                 return deadHostModel | ||||
|                     .query() | ||||
|                     .omit(omissions()) | ||||
|                     .patchAndFetchById(row.id, data) | ||||
|                     .then(saved_row => { | ||||
|                         saved_row.meta = internalHost.cleanMeta(saved_row.meta); | ||||
|                         return _.omit(saved_row, omissions()); | ||||
|                     }); | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * @param  {Access}   access | ||||
|      * @param  {Object}   data | ||||
|      * @param  {Integer}  data.id | ||||
|      * @param  {Array}    [data.expand] | ||||
|      * @param  {Array}    [data.omit] | ||||
|      * @return {Promise} | ||||
|      */ | ||||
|     get: (access, data) => { | ||||
|         if (typeof data === 'undefined') { | ||||
|             data = {}; | ||||
|         } | ||||
|  | ||||
|         return access.can('dead_hosts:get', data.id) | ||||
|             .then(access_data => { | ||||
|                 let query = deadHostModel | ||||
|                     .query() | ||||
|                     .where('is_deleted', 0) | ||||
|                     .andWhere('id', data.id) | ||||
|                     .allowEager('[owner]') | ||||
|                     .first(); | ||||
|  | ||||
|                 if (access_data.permission_visibility !== 'all') { | ||||
|                     query.andWhere('owner_user_id', access.token.get('attrs').id); | ||||
|                 } | ||||
|  | ||||
|                 // Custom omissions | ||||
|                 if (typeof data.omit !== 'undefined' && data.omit !== null) { | ||||
|                     query.omit(data.omit); | ||||
|                 } | ||||
|  | ||||
|                 if (typeof data.expand !== 'undefined' && data.expand !== null) { | ||||
|                     query.eager('[' + data.expand.join(', ') + ']'); | ||||
|                 } | ||||
|  | ||||
|                 return query; | ||||
|             }) | ||||
|             .then(row => { | ||||
|                 if (row) { | ||||
|                     row.meta = internalHost.cleanMeta(row.meta); | ||||
|                     return _.omit(row, omissions()); | ||||
|                 } else { | ||||
|                     throw new error.ItemNotFoundError(data.id); | ||||
|                 } | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * @param {Access}  access | ||||
|      * @param {Object}  data | ||||
|      * @param {Integer} data.id | ||||
|      * @param {String}  [data.reason] | ||||
|      * @returns {Promise} | ||||
|      */ | ||||
|     delete: (access, data) => { | ||||
|         return access.can('dead_hosts:delete', data.id) | ||||
|             .then(() => { | ||||
|                 return internalDeadHost.get(access, {id: data.id}); | ||||
|             }) | ||||
|             .then(row => { | ||||
|                 if (!row) { | ||||
|                     throw new error.ItemNotFoundError(data.id); | ||||
|                 } | ||||
|  | ||||
|                 return deadHostModel | ||||
|                     .query() | ||||
|                     .where('id', row.id) | ||||
|                     .patch({ | ||||
|                         is_deleted: 1 | ||||
|                     }); | ||||
|             }) | ||||
|             .then(() => { | ||||
|                 return true; | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * @param   {Access}  access | ||||
|      * @param   {Object}  data | ||||
|      * @param   {Integer} data.id | ||||
|      * @param   {Object}  data.files | ||||
|      * @returns {Promise} | ||||
|      */ | ||||
|     setCerts: (access, data) => { | ||||
|         return internalDeadHost.get(access, {id: data.id}) | ||||
|             .then(row => { | ||||
|                 _.map(data.files, (file, name) => { | ||||
|                     if (internalHost.allowed_ssl_files.indexOf(name) !== -1) { | ||||
|                         row.meta[name] = file.data.toString(); | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 return internalDeadHost.update(access, { | ||||
|                     id:   data.id, | ||||
|                     meta: row.meta | ||||
|                 }); | ||||
|             }) | ||||
|             .then(row => { | ||||
|                 return _.pick(row.meta, internalHost.allowed_ssl_files); | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * All Hosts | ||||
|      * | ||||
| @@ -26,6 +220,7 @@ const internalDeadHost = { | ||||
|                     .where('is_deleted', 0) | ||||
|                     .groupBy('id') | ||||
|                     .omit(['is_deleted']) | ||||
|                     .allowEager('[owner]') | ||||
|                     .orderBy('domain_names', 'ASC'); | ||||
|  | ||||
|                 if (access_data.permission_visibility !== 'all') { | ||||
| @@ -44,6 +239,13 @@ const internalDeadHost = { | ||||
|                 } | ||||
|  | ||||
|                 return query; | ||||
|             }) | ||||
|             .then(rows => { | ||||
|                 rows.map(row => { | ||||
|                     row.meta = internalHost.cleanMeta(row.meta); | ||||
|                 }); | ||||
|  | ||||
|                 return rows; | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|   | ||||
| @@ -115,10 +115,6 @@ const internalProxyHost = { | ||||
|             data = {}; | ||||
|         } | ||||
|  | ||||
|         if (typeof data.id === 'undefined' || !data.id) { | ||||
|             data.id = access.token.get('attrs').id; | ||||
|         } | ||||
|  | ||||
|         return access.can('proxy_hosts:get', data.id) | ||||
|             .then(access_data => { | ||||
|                 let query = proxyHostModel | ||||
|   | ||||
| @@ -115,10 +115,6 @@ const internalRedirectionHost = { | ||||
|             data = {}; | ||||
|         } | ||||
|  | ||||
|         if (typeof data.id === 'undefined' || !data.id) { | ||||
|             data.id = access.token.get('attrs').id; | ||||
|         } | ||||
|  | ||||
|         return access.can('redirection_hosts:get', data.id) | ||||
|             .then(access_data => { | ||||
|                 let query = redirectionHostModel | ||||
|   | ||||
| @@ -11,7 +11,137 @@ function omissions () { | ||||
| const internalStream = { | ||||
|  | ||||
|     /** | ||||
|      * All Hosts | ||||
|      * @param   {Access}  access | ||||
|      * @param   {Object}  data | ||||
|      * @returns {Promise} | ||||
|      */ | ||||
|     create: (access, data) => { | ||||
|         return access.can('streams:create', data) | ||||
|             .then(access_data => { | ||||
|                 // TODO: At this point the existing ports should have been checked | ||||
|                 data.owner_user_id = access.token.get('attrs').id; | ||||
|  | ||||
|                 if (typeof data.meta === 'undefined') { | ||||
|                     data.meta = {}; | ||||
|                 } | ||||
|  | ||||
|                 return streamModel | ||||
|                     .query() | ||||
|                     .omit(omissions()) | ||||
|                     .insertAndFetch(data); | ||||
|             }) | ||||
|             .then(row => { | ||||
|                 return _.omit(row, omissions()); | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * @param  {Access}  access | ||||
|      * @param  {Object}  data | ||||
|      * @param  {Integer} data.id | ||||
|      * @param  {String}  [data.email] | ||||
|      * @param  {String}  [data.name] | ||||
|      * @return {Promise} | ||||
|      */ | ||||
|     update: (access, data) => { | ||||
|         return access.can('streams:update', data.id) | ||||
|             .then(access_data => { | ||||
|                 // TODO: at this point the existing streams should have been checked | ||||
|                 return internalStream.get(access, {id: data.id}); | ||||
|             }) | ||||
|             .then(row => { | ||||
|                 if (row.id !== data.id) { | ||||
|                     // Sanity check that something crazy hasn't happened | ||||
|                     throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); | ||||
|                 } | ||||
|  | ||||
|                 return streamModel | ||||
|                     .query() | ||||
|                     .omit(omissions()) | ||||
|                     .patchAndFetchById(row.id, data) | ||||
|                     .then(saved_row => { | ||||
|                         return _.omit(saved_row, omissions()); | ||||
|                     }); | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * @param  {Access}   access | ||||
|      * @param  {Object}   data | ||||
|      * @param  {Integer}  data.id | ||||
|      * @param  {Array}    [data.expand] | ||||
|      * @param  {Array}    [data.omit] | ||||
|      * @return {Promise} | ||||
|      */ | ||||
|     get: (access, data) => { | ||||
|         if (typeof data === 'undefined') { | ||||
|             data = {}; | ||||
|         } | ||||
|  | ||||
|         return access.can('streams:get', data.id) | ||||
|             .then(access_data => { | ||||
|                 let query = streamModel | ||||
|                     .query() | ||||
|                     .where('is_deleted', 0) | ||||
|                     .andWhere('id', data.id) | ||||
|                     .allowEager('[owner]') | ||||
|                     .first(); | ||||
|  | ||||
|                 if (access_data.permission_visibility !== 'all') { | ||||
|                     query.andWhere('owner_user_id', access.token.get('attrs').id); | ||||
|                 } | ||||
|  | ||||
|                 // Custom omissions | ||||
|                 if (typeof data.omit !== 'undefined' && data.omit !== null) { | ||||
|                     query.omit(data.omit); | ||||
|                 } | ||||
|  | ||||
|                 if (typeof data.expand !== 'undefined' && data.expand !== null) { | ||||
|                     query.eager('[' + data.expand.join(', ') + ']'); | ||||
|                 } | ||||
|  | ||||
|                 return query; | ||||
|             }) | ||||
|             .then(row => { | ||||
|                 if (row) { | ||||
|                     return _.omit(row, omissions()); | ||||
|                 } else { | ||||
|                     throw new error.ItemNotFoundError(data.id); | ||||
|                 } | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * @param {Access}  access | ||||
|      * @param {Object}  data | ||||
|      * @param {Integer} data.id | ||||
|      * @param {String}  [data.reason] | ||||
|      * @returns {Promise} | ||||
|      */ | ||||
|     delete: (access, data) => { | ||||
|         return access.can('streams:delete', data.id) | ||||
|             .then(() => { | ||||
|                 return internalStream.get(access, {id: data.id}); | ||||
|             }) | ||||
|             .then(row => { | ||||
|                 if (!row) { | ||||
|                     throw new error.ItemNotFoundError(data.id); | ||||
|                 } | ||||
|  | ||||
|                 return streamModel | ||||
|                     .query() | ||||
|                     .where('id', row.id) | ||||
|                     .patch({ | ||||
|                         is_deleted: 1 | ||||
|                     }); | ||||
|             }) | ||||
|             .then(() => { | ||||
|                 return true; | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * All Streams | ||||
|      * | ||||
|      * @param   {Access}  access | ||||
|      * @param   {Array}   [expand] | ||||
| @@ -26,6 +156,7 @@ const internalStream = { | ||||
|                     .where('is_deleted', 0) | ||||
|                     .groupBy('id') | ||||
|                     .omit(['is_deleted']) | ||||
|                     .allowEager('[owner]') | ||||
|                     .orderBy('incoming_port', 'ASC'); | ||||
|  | ||||
|                 if (access_data.permission_visibility !== 'all') { | ||||
|   | ||||
							
								
								
									
										23
									
								
								src/backend/lib/access/dead_hosts-create.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/backend/lib/access/dead_hosts-create.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| { | ||||
|   "anyOf": [ | ||||
|     { | ||||
|       "$ref": "roles#/definitions/admin" | ||||
|     }, | ||||
|     { | ||||
|       "type": "object", | ||||
|       "required": ["permission_dead_hosts", "roles"], | ||||
|       "properties": { | ||||
|         "permission_dead_hosts": { | ||||
|           "$ref": "perms#/definitions/manage" | ||||
|         }, | ||||
|         "roles": { | ||||
|           "type": "array", | ||||
|           "items": { | ||||
|             "type": "string", | ||||
|             "enum": ["user"] | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/backend/lib/access/dead_hosts-delete.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/backend/lib/access/dead_hosts-delete.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| { | ||||
|   "anyOf": [ | ||||
|     { | ||||
|       "$ref": "roles#/definitions/admin" | ||||
|     }, | ||||
|     { | ||||
|       "type": "object", | ||||
|       "required": ["permission_dead_hosts", "roles"], | ||||
|       "properties": { | ||||
|         "permission_dead_hosts": { | ||||
|           "$ref": "perms#/definitions/manage" | ||||
|         }, | ||||
|         "roles": { | ||||
|           "type": "array", | ||||
|           "items": { | ||||
|             "type": "string", | ||||
|             "enum": ["user"] | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/backend/lib/access/dead_hosts-get.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/backend/lib/access/dead_hosts-get.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| { | ||||
|   "anyOf": [ | ||||
|     { | ||||
|       "$ref": "roles#/definitions/admin" | ||||
|     }, | ||||
|     { | ||||
|       "type": "object", | ||||
|       "required": ["permission_dead_hosts", "roles"], | ||||
|       "properties": { | ||||
|         "permission_dead_hosts": { | ||||
|           "$ref": "perms#/definitions/view" | ||||
|         }, | ||||
|         "roles": { | ||||
|           "type": "array", | ||||
|           "items": { | ||||
|             "type": "string", | ||||
|             "enum": ["user"] | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/backend/lib/access/dead_hosts-update.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/backend/lib/access/dead_hosts-update.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| { | ||||
|   "anyOf": [ | ||||
|     { | ||||
|       "$ref": "roles#/definitions/admin" | ||||
|     }, | ||||
|     { | ||||
|       "type": "object", | ||||
|       "required": ["permission_dead_hosts", "roles"], | ||||
|       "properties": { | ||||
|         "permission_dead_hosts": { | ||||
|           "$ref": "perms#/definitions/manage" | ||||
|         }, | ||||
|         "roles": { | ||||
|           "type": "array", | ||||
|           "items": { | ||||
|             "type": "string", | ||||
|             "enum": ["user"] | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/backend/lib/access/streams-create.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/backend/lib/access/streams-create.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| { | ||||
|   "anyOf": [ | ||||
|     { | ||||
|       "$ref": "roles#/definitions/admin" | ||||
|     }, | ||||
|     { | ||||
|       "type": "object", | ||||
|       "required": ["permission_streams", "roles"], | ||||
|       "properties": { | ||||
|         "permission_streams": { | ||||
|           "$ref": "perms#/definitions/manage" | ||||
|         }, | ||||
|         "roles": { | ||||
|           "type": "array", | ||||
|           "items": { | ||||
|             "type": "string", | ||||
|             "enum": ["user"] | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/backend/lib/access/streams-delete.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/backend/lib/access/streams-delete.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| { | ||||
|   "anyOf": [ | ||||
|     { | ||||
|       "$ref": "roles#/definitions/admin" | ||||
|     }, | ||||
|     { | ||||
|       "type": "object", | ||||
|       "required": ["permission_streams", "roles"], | ||||
|       "properties": { | ||||
|         "permission_streams": { | ||||
|           "$ref": "perms#/definitions/manage" | ||||
|         }, | ||||
|         "roles": { | ||||
|           "type": "array", | ||||
|           "items": { | ||||
|             "type": "string", | ||||
|             "enum": ["user"] | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/backend/lib/access/streams-get.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/backend/lib/access/streams-get.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| { | ||||
|   "anyOf": [ | ||||
|     { | ||||
|       "$ref": "roles#/definitions/admin" | ||||
|     }, | ||||
|     { | ||||
|       "type": "object", | ||||
|       "required": ["permission_streams", "roles"], | ||||
|       "properties": { | ||||
|         "permission_streams": { | ||||
|           "$ref": "perms#/definitions/view" | ||||
|         }, | ||||
|         "roles": { | ||||
|           "type": "array", | ||||
|           "items": { | ||||
|             "type": "string", | ||||
|             "enum": ["user"] | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/backend/lib/access/streams-update.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/backend/lib/access/streams-update.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| { | ||||
|   "anyOf": [ | ||||
|     { | ||||
|       "$ref": "roles#/definitions/admin" | ||||
|     }, | ||||
|     { | ||||
|       "type": "object", | ||||
|       "required": ["permission_streams", "roles"], | ||||
|       "properties": { | ||||
|         "permission_streams": { | ||||
|           "$ref": "perms#/definitions/manage" | ||||
|         }, | ||||
|         "roles": { | ||||
|           "type": "array", | ||||
|           "items": { | ||||
|             "type": "string", | ||||
|             "enum": ["user"] | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -93,6 +93,7 @@ exports.up = function (knex/*, Promise*/) { | ||||
|                 table.integer('preserve_path').notNull().unsigned().defaultTo(0); | ||||
|                 table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); | ||||
|                 table.string('ssl_provider').notNull().defaultTo(''); | ||||
|                 table.integer('ssl_forced').notNull().unsigned().defaultTo(0); | ||||
|                 table.integer('block_exploits').notNull().unsigned().defaultTo(0); | ||||
|                 table.json('meta').notNull(); | ||||
|             }); | ||||
| @@ -109,6 +110,7 @@ exports.up = function (knex/*, Promise*/) { | ||||
|                     table.json('domain_names').notNull(); | ||||
|                     table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); | ||||
|                     table.string('ssl_provider').notNull().defaultTo(''); | ||||
|                     table.integer('ssl_forced').notNull().unsigned().defaultTo(0); | ||||
|                     table.json('meta').notNull(); | ||||
|                 }); | ||||
|         }) | ||||
|   | ||||
| @@ -104,7 +104,7 @@ router | ||||
|         }) | ||||
|             .then(data => { | ||||
|                 return internalDeadHost.get(res.locals.access, { | ||||
|                     id:     data.host_id, | ||||
|                     id:     parseInt(data.host_id, 10), | ||||
|                     expand: data.expand | ||||
|                 }); | ||||
|             }) | ||||
| @@ -123,7 +123,7 @@ router | ||||
|     .put((req, res, next) => { | ||||
|         apiValidator({$ref: 'endpoints/dead-hosts#/links/2/schema'}, req.body) | ||||
|             .then(payload => { | ||||
|                 payload.id = req.params.host_id; | ||||
|                 payload.id = parseInt(req.params.host_id, 10); | ||||
|                 return internalDeadHost.update(res.locals.access, payload); | ||||
|             }) | ||||
|             .then(result => { | ||||
| @@ -139,7 +139,7 @@ router | ||||
|      * Update and existing dead-host | ||||
|      */ | ||||
|     .delete((req, res, next) => { | ||||
|         internalDeadHost.delete(res.locals.access, {id: req.params.host_id}) | ||||
|         internalDeadHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) | ||||
|             .then(result => { | ||||
|                 res.status(200) | ||||
|                     .send(result); | ||||
| @@ -147,4 +147,38 @@ router | ||||
|             .catch(next); | ||||
|     }); | ||||
|  | ||||
| /** | ||||
|  * Specific dead-host Certificates | ||||
|  * | ||||
|  * /api/nginx/dead-hosts/123/certificates | ||||
|  */ | ||||
| router | ||||
|     .route('/:host_id/certificates') | ||||
|     .options((req, res) => { | ||||
|         res.sendStatus(204); | ||||
|     }) | ||||
|     .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes | ||||
|  | ||||
|     /** | ||||
|      * POST /api/nginx/dead-hosts/123/certificates | ||||
|      * | ||||
|      * Upload certifications | ||||
|      */ | ||||
|     .post((req, res, next) => { | ||||
|         if (!req.files) { | ||||
|             res.status(400) | ||||
|                 .send({error: 'No files were uploaded'}); | ||||
|         } else { | ||||
|             internalDeadHost.setCerts(res.locals.access, { | ||||
|                 id:    parseInt(req.params.host_id, 10), | ||||
|                 files: req.files | ||||
|             }) | ||||
|                 .then(result => { | ||||
|                     res.status(200) | ||||
|                         .send(result); | ||||
|                 }) | ||||
|                 .catch(next); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
| module.exports = router; | ||||
|   | ||||
| @@ -94,17 +94,17 @@ router | ||||
|                 stream_id: { | ||||
|                     $ref: 'definitions#/definitions/id' | ||||
|                 }, | ||||
|                 expand:  { | ||||
|                 expand:    { | ||||
|                     $ref: 'definitions#/definitions/expand' | ||||
|                 } | ||||
|             } | ||||
|         }, { | ||||
|             stream_id: req.params.stream_id, | ||||
|             expand:  (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) | ||||
|             expand:    (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) | ||||
|         }) | ||||
|             .then(data => { | ||||
|                 return internalStream.get(res.locals.access, { | ||||
|                     id:     data.stream_id, | ||||
|                     id:     parseInt(data.stream_id, 10), | ||||
|                     expand: data.expand | ||||
|                 }); | ||||
|             }) | ||||
| @@ -123,7 +123,7 @@ router | ||||
|     .put((req, res, next) => { | ||||
|         apiValidator({$ref: 'endpoints/streams#/links/2/schema'}, req.body) | ||||
|             .then(payload => { | ||||
|                 payload.id = req.params.stream_id; | ||||
|                 payload.id = parseInt(req.params.stream_id, 10); | ||||
|                 return internalStream.update(res.locals.access, payload); | ||||
|             }) | ||||
|             .then(result => { | ||||
| @@ -139,7 +139,7 @@ router | ||||
|      * Update and existing stream | ||||
|      */ | ||||
|     .delete((req, res, next) => { | ||||
|         internalStream.delete(res.locals.access, {id: req.params.stream_id}) | ||||
|         internalStream.delete(res.locals.access, {id: parseInt(req.params.stream_id, 10)}) | ||||
|             .then(result => { | ||||
|                 res.status(200) | ||||
|                     .send(result); | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| { | ||||
|   "$schema": "http://json-schema.org/draft-07/schema#", | ||||
|   "$id": "endpoints/dead-hosts", | ||||
|   "title": "Users", | ||||
|   "description": "Endpoints relating to Dead Hosts", | ||||
|   "title": "404 Hosts", | ||||
|   "description": "Endpoints relating to 404 Hosts", | ||||
|   "stability": "stable", | ||||
|   "type": "object", | ||||
|   "definitions": { | ||||
| @@ -15,49 +15,63 @@ | ||||
|     "modified_on": { | ||||
|       "$ref": "../definitions.json#/definitions/modified_on" | ||||
|     }, | ||||
|     "name": { | ||||
|       "description": "Name", | ||||
|       "example": "Jamie Curnow", | ||||
|       "type": "string", | ||||
|       "minLength": 2, | ||||
|       "maxLength": 100 | ||||
|     "domain_names": { | ||||
|       "$ref": "../definitions.json#/definitions/domain_names" | ||||
|     }, | ||||
|     "nickname": { | ||||
|       "description": "Nickname", | ||||
|       "example": "Jamie", | ||||
|       "type": "string", | ||||
|       "minLength": 2, | ||||
|       "maxLength": 50 | ||||
|     "ssl_enabled": { | ||||
|       "$ref": "../definitions.json#/definitions/ssl_enabled" | ||||
|     }, | ||||
|     "email": { | ||||
|       "$ref": "../definitions.json#/definitions/email" | ||||
|     "ssl_forced": { | ||||
|       "$ref": "../definitions.json#/definitions/ssl_forced" | ||||
|     }, | ||||
|     "avatar": { | ||||
|       "description": "Avatar", | ||||
|       "example": "http://somewhere.jpg", | ||||
|       "type": "string", | ||||
|       "minLength": 2, | ||||
|       "maxLength": 150, | ||||
|       "readOnly": true | ||||
|     "ssl_provider": { | ||||
|       "$ref": "../definitions.json#/definitions/ssl_provider" | ||||
|     }, | ||||
|     "roles": { | ||||
|       "description": "Roles", | ||||
|       "example": [ | ||||
|         "admin" | ||||
|       ], | ||||
|       "type": "array" | ||||
|     "meta": { | ||||
|       "type": "object", | ||||
|       "additionalProperties": false, | ||||
|       "properties": { | ||||
|         "letsencrypt_email": { | ||||
|           "type": "string", | ||||
|           "format": "email" | ||||
|         }, | ||||
|         "letsencrypt_agree": { | ||||
|           "type": "boolean" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "properties": { | ||||
|     "id": { | ||||
|       "$ref": "#/definitions/id" | ||||
|     }, | ||||
|     "is_disabled": { | ||||
|       "description": "Is Disabled", | ||||
|       "example": false, | ||||
|       "type": "boolean" | ||||
|     "created_on": { | ||||
|       "$ref": "#/definitions/created_on" | ||||
|     }, | ||||
|     "modified_on": { | ||||
|       "$ref": "#/definitions/modified_on" | ||||
|     }, | ||||
|     "domain_names": { | ||||
|       "$ref": "#/definitions/domain_names" | ||||
|     }, | ||||
|     "ssl_enabled": { | ||||
|       "$ref": "#/definitions/ssl_enabled" | ||||
|     }, | ||||
|     "ssl_forced": { | ||||
|       "$ref": "#/definitions/ssl_forced" | ||||
|     }, | ||||
|     "ssl_provider": { | ||||
|       "$ref": "#/definitions/ssl_provider" | ||||
|     }, | ||||
|     "meta": { | ||||
|       "$ref": "#/definitions/meta" | ||||
|     } | ||||
|   }, | ||||
|   "links": [ | ||||
|     { | ||||
|       "title": "List", | ||||
|       "description": "Returns a list of Users", | ||||
|       "href": "/users", | ||||
|       "description": "Returns a list of 404 Hosts", | ||||
|       "href": "/nginx/dead-hosts", | ||||
|       "access": "private", | ||||
|       "method": "GET", | ||||
|       "rel": "self", | ||||
| @@ -73,8 +87,8 @@ | ||||
|     }, | ||||
|     { | ||||
|       "title": "Create", | ||||
|       "description": "Creates a new User", | ||||
|       "href": "/users", | ||||
|       "description": "Creates a new 404 Host", | ||||
|       "href": "/nginx/dead-hosts", | ||||
|       "access": "private", | ||||
|       "method": "POST", | ||||
|       "rel": "create", | ||||
| @@ -83,34 +97,25 @@ | ||||
|       }, | ||||
|       "schema": { | ||||
|         "type": "object", | ||||
|         "additionalProperties": false, | ||||
|         "required": [ | ||||
|           "name", | ||||
|           "nickname", | ||||
|           "email" | ||||
|           "domain_names" | ||||
|         ], | ||||
|         "properties": { | ||||
|           "name": { | ||||
|             "$ref": "#/definitions/name" | ||||
|           "domain_names": { | ||||
|             "$ref": "#/definitions/domain_names" | ||||
|           }, | ||||
|           "nickname": { | ||||
|             "$ref": "#/definitions/nickname" | ||||
|           "ssl_enabled": { | ||||
|             "$ref": "#/definitions/ssl_enabled" | ||||
|           }, | ||||
|           "email": { | ||||
|             "$ref": "#/definitions/email" | ||||
|           "ssl_forced": { | ||||
|             "$ref": "#/definitions/ssl_forced" | ||||
|           }, | ||||
|           "roles": { | ||||
|             "$ref": "#/definitions/roles" | ||||
|           "ssl_provider": { | ||||
|             "$ref": "#/definitions/ssl_provider" | ||||
|           }, | ||||
|           "is_disabled": { | ||||
|             "$ref": "#/definitions/is_disabled" | ||||
|           }, | ||||
|           "auth": { | ||||
|             "type": "object", | ||||
|             "description": "Auth Credentials", | ||||
|             "example": { | ||||
|               "type": "password", | ||||
|               "secret": "bigredhorsebanana" | ||||
|             } | ||||
|           "meta": { | ||||
|             "$ref": "#/definitions/meta" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
| @@ -122,8 +127,8 @@ | ||||
|     }, | ||||
|     { | ||||
|       "title": "Update", | ||||
|       "description": "Updates a existing User", | ||||
|       "href": "/users/{definitions.identity.example}", | ||||
|       "description": "Updates a existing 404 Host", | ||||
|       "href": "/nginx/dead-hosts/{definitions.identity.example}", | ||||
|       "access": "private", | ||||
|       "method": "PUT", | ||||
|       "rel": "update", | ||||
| @@ -132,21 +137,22 @@ | ||||
|       }, | ||||
|       "schema": { | ||||
|         "type": "object", | ||||
|         "additionalProperties": false, | ||||
|         "properties": { | ||||
|           "name": { | ||||
|             "$ref": "#/definitions/name" | ||||
|           "domain_names": { | ||||
|             "$ref": "#/definitions/domain_names" | ||||
|           }, | ||||
|           "nickname": { | ||||
|             "$ref": "#/definitions/nickname" | ||||
|           "ssl_enabled": { | ||||
|             "$ref": "#/definitions/ssl_enabled" | ||||
|           }, | ||||
|           "email": { | ||||
|             "$ref": "#/definitions/email" | ||||
|           "ssl_forced": { | ||||
|             "$ref": "#/definitions/ssl_forced" | ||||
|           }, | ||||
|           "roles": { | ||||
|             "$ref": "#/definitions/roles" | ||||
|           "ssl_provider": { | ||||
|             "$ref": "#/definitions/ssl_provider" | ||||
|           }, | ||||
|           "is_disabled": { | ||||
|             "$ref": "#/definitions/is_disabled" | ||||
|           "meta": { | ||||
|             "$ref": "#/definitions/meta" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
| @@ -158,8 +164,8 @@ | ||||
|     }, | ||||
|     { | ||||
|       "title": "Delete", | ||||
|       "description": "Deletes a existing User", | ||||
|       "href": "/users/{definitions.identity.example}", | ||||
|       "description": "Deletes a existing 404 Host", | ||||
|       "href": "/nginx/dead-hosts/{definitions.identity.example}", | ||||
|       "access": "private", | ||||
|       "method": "DELETE", | ||||
|       "rel": "delete", | ||||
| @@ -170,34 +176,5 @@ | ||||
|         "type": "boolean" | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   "properties": { | ||||
|     "id": { | ||||
|       "$ref": "#/definitions/id" | ||||
|     }, | ||||
|     "created_on": { | ||||
|       "$ref": "#/definitions/created_on" | ||||
|     }, | ||||
|     "modified_on": { | ||||
|       "$ref": "#/definitions/modified_on" | ||||
|     }, | ||||
|     "name": { | ||||
|       "$ref": "#/definitions/name" | ||||
|     }, | ||||
|     "nickname": { | ||||
|       "$ref": "#/definitions/nickname" | ||||
|     }, | ||||
|     "email": { | ||||
|       "$ref": "#/definitions/email" | ||||
|     }, | ||||
|     "avatar": { | ||||
|       "$ref": "#/definitions/avatar" | ||||
|     }, | ||||
|     "roles": { | ||||
|       "$ref": "#/definitions/roles" | ||||
|     }, | ||||
|     "is_disabled": { | ||||
|       "$ref": "#/definitions/is_disabled" | ||||
|     } | ||||
|   } | ||||
|   ] | ||||
| } | ||||
|   | ||||
| @@ -130,6 +130,7 @@ | ||||
|       }, | ||||
|       "schema": { | ||||
|         "type": "object", | ||||
|         "additionalProperties": false, | ||||
|         "required": [ | ||||
|           "domain_names", | ||||
|           "forward_ip", | ||||
| @@ -186,6 +187,7 @@ | ||||
|       }, | ||||
|       "schema": { | ||||
|         "type": "object", | ||||
|         "additionalProperties": false, | ||||
|         "properties": { | ||||
|           "domain_names": { | ||||
|             "$ref": "#/definitions/domain_names" | ||||
|   | ||||
| @@ -117,6 +117,7 @@ | ||||
|       }, | ||||
|       "schema": { | ||||
|         "type": "object", | ||||
|         "additionalProperties": false, | ||||
|         "required": [ | ||||
|           "domain_names", | ||||
|           "forward_domain_name" | ||||
| @@ -166,6 +167,7 @@ | ||||
|       }, | ||||
|       "schema": { | ||||
|         "type": "object", | ||||
|         "additionalProperties": false, | ||||
|         "properties": { | ||||
|           "domain_names": { | ||||
|             "$ref": "#/definitions/domain_names" | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|   "$schema": "http://json-schema.org/draft-07/schema#", | ||||
|   "$id": "endpoints/streams", | ||||
|   "title": "Users", | ||||
|   "title": "Streams", | ||||
|   "description": "Endpoints relating to Streams", | ||||
|   "stability": "stable", | ||||
|   "type": "object", | ||||
| @@ -15,49 +15,64 @@ | ||||
|     "modified_on": { | ||||
|       "$ref": "../definitions.json#/definitions/modified_on" | ||||
|     }, | ||||
|     "name": { | ||||
|       "description": "Name", | ||||
|       "example": "Jamie Curnow", | ||||
|     "incoming_port": { | ||||
|       "type": "integer", | ||||
|       "minimum": 1, | ||||
|       "maximum": 65535 | ||||
|     }, | ||||
|     "forward_ip": { | ||||
|       "type": "string", | ||||
|       "minLength": 2, | ||||
|       "maxLength": 100 | ||||
|       "format": "ipv4" | ||||
|     }, | ||||
|     "nickname": { | ||||
|       "description": "Nickname", | ||||
|       "example": "Jamie", | ||||
|       "type": "string", | ||||
|       "minLength": 2, | ||||
|       "maxLength": 50 | ||||
|     "forwarding_port": { | ||||
|       "type": "integer", | ||||
|       "minimum": 1, | ||||
|       "maximum": 65535 | ||||
|     }, | ||||
|     "email": { | ||||
|       "$ref": "../definitions.json#/definitions/email" | ||||
|     }, | ||||
|     "avatar": { | ||||
|       "description": "Avatar", | ||||
|       "example": "http://somewhere.jpg", | ||||
|       "type": "string", | ||||
|       "minLength": 2, | ||||
|       "maxLength": 150, | ||||
|       "readOnly": true | ||||
|     }, | ||||
|     "roles": { | ||||
|       "description": "Roles", | ||||
|       "example": [ | ||||
|         "admin" | ||||
|       ], | ||||
|       "type": "array" | ||||
|     }, | ||||
|     "is_disabled": { | ||||
|       "description": "Is Disabled", | ||||
|       "example": false, | ||||
|     "tcp_forwarding": { | ||||
|       "type": "boolean" | ||||
|     }, | ||||
|     "udp_forwarding": { | ||||
|       "type": "boolean" | ||||
|     }, | ||||
|     "meta": { | ||||
|       "type": "object" | ||||
|     } | ||||
|   }, | ||||
|   "properties": { | ||||
|     "id": { | ||||
|       "$ref": "#/definitions/id" | ||||
|     }, | ||||
|     "created_on": { | ||||
|       "$ref": "#/definitions/created_on" | ||||
|     }, | ||||
|     "modified_on": { | ||||
|       "$ref": "#/definitions/modified_on" | ||||
|     }, | ||||
|     "incoming_port": { | ||||
|       "$ref": "#/definitions/incoming_port" | ||||
|     }, | ||||
|     "forward_ip": { | ||||
|       "$ref": "#/definitions/forward_ip" | ||||
|     }, | ||||
|     "forwarding_port": { | ||||
|       "$ref": "#/definitions/forwarding_port" | ||||
|     }, | ||||
|     "tcp_forwarding": { | ||||
|       "$ref": "#/definitions/tcp_forwarding" | ||||
|     }, | ||||
|     "udp_forwarding": { | ||||
|       "$ref": "#/definitions/udp_forwarding" | ||||
|     }, | ||||
|     "meta": { | ||||
|       "$ref": "#/definitions/meta" | ||||
|     } | ||||
|   }, | ||||
|   "links": [ | ||||
|     { | ||||
|       "title": "List", | ||||
|       "description": "Returns a list of Users", | ||||
|       "href": "/users", | ||||
|       "description": "Returns a list of Steams", | ||||
|       "href": "/nginx/streams", | ||||
|       "access": "private", | ||||
|       "method": "GET", | ||||
|       "rel": "self", | ||||
| @@ -73,8 +88,8 @@ | ||||
|     }, | ||||
|     { | ||||
|       "title": "Create", | ||||
|       "description": "Creates a new User", | ||||
|       "href": "/users", | ||||
|       "description": "Creates a new Stream", | ||||
|       "href": "/nginx/streams", | ||||
|       "access": "private", | ||||
|       "method": "POST", | ||||
|       "rel": "create", | ||||
| @@ -83,34 +98,30 @@ | ||||
|       }, | ||||
|       "schema": { | ||||
|         "type": "object", | ||||
|         "additionalProperties": false, | ||||
|         "required": [ | ||||
|           "name", | ||||
|           "nickname", | ||||
|           "email" | ||||
|           "incoming_port", | ||||
|           "forward_ip", | ||||
|           "forwarding_port" | ||||
|         ], | ||||
|         "properties": { | ||||
|           "name": { | ||||
|             "$ref": "#/definitions/name" | ||||
|           "incoming_port": { | ||||
|             "$ref": "#/definitions/incoming_port" | ||||
|           }, | ||||
|           "nickname": { | ||||
|             "$ref": "#/definitions/nickname" | ||||
|           "forward_ip": { | ||||
|             "$ref": "#/definitions/forward_ip" | ||||
|           }, | ||||
|           "email": { | ||||
|             "$ref": "#/definitions/email" | ||||
|           "forwarding_port": { | ||||
|             "$ref": "#/definitions/forwarding_port" | ||||
|           }, | ||||
|           "roles": { | ||||
|             "$ref": "#/definitions/roles" | ||||
|           "tcp_forwarding": { | ||||
|             "$ref": "#/definitions/tcp_forwarding" | ||||
|           }, | ||||
|           "is_disabled": { | ||||
|             "$ref": "#/definitions/is_disabled" | ||||
|           "udp_forwarding": { | ||||
|             "$ref": "#/definitions/udp_forwarding" | ||||
|           }, | ||||
|           "auth": { | ||||
|             "type": "object", | ||||
|             "description": "Auth Credentials", | ||||
|             "example": { | ||||
|               "type": "password", | ||||
|               "secret": "bigredhorsebanana" | ||||
|             } | ||||
|           "meta": { | ||||
|             "$ref": "#/definitions/meta" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
| @@ -122,8 +133,8 @@ | ||||
|     }, | ||||
|     { | ||||
|       "title": "Update", | ||||
|       "description": "Updates a existing User", | ||||
|       "href": "/users/{definitions.identity.example}", | ||||
|       "description": "Updates a existing Stream", | ||||
|       "href": "/nginx/streams/{definitions.identity.example}", | ||||
|       "access": "private", | ||||
|       "method": "PUT", | ||||
|       "rel": "update", | ||||
| @@ -132,21 +143,25 @@ | ||||
|       }, | ||||
|       "schema": { | ||||
|         "type": "object", | ||||
|         "additionalProperties": false, | ||||
|         "properties": { | ||||
|           "name": { | ||||
|             "$ref": "#/definitions/name" | ||||
|           "incoming_port": { | ||||
|             "$ref": "#/definitions/incoming_port" | ||||
|           }, | ||||
|           "nickname": { | ||||
|             "$ref": "#/definitions/nickname" | ||||
|           "forward_ip": { | ||||
|             "$ref": "#/definitions/forward_ip" | ||||
|           }, | ||||
|           "email": { | ||||
|             "$ref": "#/definitions/email" | ||||
|           "forwarding_port": { | ||||
|             "$ref": "#/definitions/forwarding_port" | ||||
|           }, | ||||
|           "roles": { | ||||
|             "$ref": "#/definitions/roles" | ||||
|           "tcp_forwarding": { | ||||
|             "$ref": "#/definitions/tcp_forwarding" | ||||
|           }, | ||||
|           "is_disabled": { | ||||
|             "$ref": "#/definitions/is_disabled" | ||||
|           "udp_forwarding": { | ||||
|             "$ref": "#/definitions/udp_forwarding" | ||||
|           }, | ||||
|           "meta": { | ||||
|             "$ref": "#/definitions/meta" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
| @@ -158,8 +173,8 @@ | ||||
|     }, | ||||
|     { | ||||
|       "title": "Delete", | ||||
|       "description": "Deletes a existing User", | ||||
|       "href": "/users/{definitions.identity.example}", | ||||
|       "description": "Deletes a existing Stream", | ||||
|       "href": "/nginx/streams/{definitions.identity.example}", | ||||
|       "access": "private", | ||||
|       "method": "DELETE", | ||||
|       "rel": "delete", | ||||
| @@ -170,34 +185,5 @@ | ||||
|         "type": "boolean" | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   "properties": { | ||||
|     "id": { | ||||
|       "$ref": "#/definitions/id" | ||||
|     }, | ||||
|     "created_on": { | ||||
|       "$ref": "#/definitions/created_on" | ||||
|     }, | ||||
|     "modified_on": { | ||||
|       "$ref": "#/definitions/modified_on" | ||||
|     }, | ||||
|     "name": { | ||||
|       "$ref": "#/definitions/name" | ||||
|     }, | ||||
|     "nickname": { | ||||
|       "$ref": "#/definitions/nickname" | ||||
|     }, | ||||
|     "email": { | ||||
|       "$ref": "#/definitions/email" | ||||
|     }, | ||||
|     "avatar": { | ||||
|       "$ref": "#/definitions/avatar" | ||||
|     }, | ||||
|     "roles": { | ||||
|       "$ref": "#/definitions/roles" | ||||
|     }, | ||||
|     "is_disabled": { | ||||
|       "$ref": "#/definitions/is_disabled" | ||||
|     } | ||||
|   } | ||||
|   ] | ||||
| } | ||||
|   | ||||
| @@ -215,18 +215,31 @@ module.exports = { | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Nginx Stream Form | ||||
|      * Stream Form | ||||
|      * | ||||
|      * @param [model] | ||||
|      */ | ||||
|     showNginxStreamForm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('streams')) { | ||||
|             require(['./main', './nginx/stream/form'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Stream Delete Confirm | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showNginxStreamDeleteConfirm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('streams')) { | ||||
|             require(['./main', './nginx/stream/delete'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Nginx Dead Hosts | ||||
|      */ | ||||
| @@ -241,6 +254,32 @@ module.exports = { | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Dead Host Form | ||||
|      * | ||||
|      * @param [model] | ||||
|      */ | ||||
|     showNginxDeadForm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) { | ||||
|             require(['./main', './nginx/dead/form'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Dead Host Delete Confirm | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showNginxDeadDeleteConfirm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) { | ||||
|             require(['./main', './nginx/dead/delete'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Nginx Access | ||||
|      */ | ||||
|   | ||||
							
								
								
									
										23
									
								
								src/frontend/js/app/nginx/dead/delete.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/frontend/js/app/nginx/dead/delete.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| <div class="modal-content"> | ||||
|     <div class="modal-header"> | ||||
|         <h5 class="modal-title"><%- i18n('dead-hosts', 'delete') %></h5> | ||||
|         <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal"> </button> | ||||
|     </div> | ||||
|     <div class="modal-body"> | ||||
|         <form> | ||||
|             <div class="row"> | ||||
|                 <div class="col-sm-12 col-md-12"> | ||||
|                     <%= i18n('dead-hosts', 'delete-confirm', {domains: domain_names.join(', ')}) %> | ||||
|                     <% if (ssl_enabled) { %> | ||||
|                         <br><br> | ||||
|                         <%- i18n('ssl', 'delete-ssl') %> | ||||
|                     <% } %> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </form> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|         <button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button> | ||||
|         <button type="button" class="btn btn-danger save"><%- i18n('str', 'sure') %></button> | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										36
									
								
								src/frontend/js/app/nginx/dead/delete.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/frontend/js/app/nginx/dead/delete.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const Mn       = require('backbone.marionette'); | ||||
| const App      = require('../../main'); | ||||
| const template = require('./delete.ejs'); | ||||
|  | ||||
| require('jquery-serializejson'); | ||||
|  | ||||
| module.exports = Mn.View.extend({ | ||||
|     template:  template, | ||||
|     className: 'modal-dialog', | ||||
|  | ||||
|     ui: { | ||||
|         form:    'form', | ||||
|         buttons: '.modal-footer button', | ||||
|         cancel:  'button.cancel', | ||||
|         save:    'button.save' | ||||
|     }, | ||||
|  | ||||
|     events: { | ||||
|  | ||||
|         'click @ui.save': function (e) { | ||||
|             e.preventDefault(); | ||||
|  | ||||
|             App.Api.Nginx.DeadHosts.delete(this.model.get('id')) | ||||
|                 .then(() => { | ||||
|                     App.Controller.showNginxDead(); | ||||
|                     App.UI.closeModal(); | ||||
|                 }) | ||||
|                 .catch(err => { | ||||
|                     alert(err.message); | ||||
|                     this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); | ||||
|                 }); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										110
									
								
								src/frontend/js/app/nginx/dead/form.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/frontend/js/app/nginx/dead/form.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| <div class="modal-content"> | ||||
|     <div class="modal-header"> | ||||
|         <h5 class="modal-title"><%- i18n('dead-hosts', 'form-title', {id: id}) %></h5> | ||||
|         <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal"> </button> | ||||
|     </div> | ||||
|     <div class="modal-body has-tabs"> | ||||
|         <form> | ||||
|             <ul class="nav nav-tabs" role="tablist"> | ||||
|                 <li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active"><i class="fe fe-zap"></i> <%- i18n('all-hosts', 'details') %></a></li> | ||||
|                 <li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> <%- i18n('str', 'ssl') %></a></li> | ||||
|             </ul> | ||||
|             <div class="tab-content"> | ||||
|                 <!-- Details --> | ||||
|                 <div role="tabpanel" class="tab-pane active" id="details"> | ||||
|                     <div class="row"> | ||||
|  | ||||
|                         <div class="col-sm-12 col-md-12"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label"><%- i18n('all-hosts', 'domain-names') %> <span class="form-required">*</span></label> | ||||
|                                 <input type="text" name="domain_names" class="form-control" id="input-domains" value="<%- domain_names.join(',') %>" required> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 <!-- SSL --> | ||||
|                 <div role="tabpanel" class="tab-pane" id="ssl-options"> | ||||
|                     <div class="row"> | ||||
|                         <div class="col-sm-6 col-md-6"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="custom-switch"> | ||||
|                                     <input type="checkbox" class="custom-switch-input" name="ssl_enabled" value="1"<%- ssl_enabled ? ' checked' : '' %>> | ||||
|                                     <span class="custom-switch-indicator"></span> | ||||
|                                     <span class="custom-switch-description"><%- i18n('all-hosts', 'enable-ssl') %></span> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-6 col-md-6"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="custom-switch"> | ||||
|                                     <input type="checkbox" class="custom-switch-input" name="ssl_forced" value="1"<%- ssl_forced ? ' checked' : '' %><%- ssl_enabled ? '' : ' disabled' %>> | ||||
|                                     <span class="custom-switch-indicator"></span> | ||||
|                                     <span class="custom-switch-description"><%- i18n('all-hosts', 'force-ssl') %></span> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-12 col-md-12"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label"><%- i18n('all-hosts', 'cert-provider') %></label> | ||||
|                                 <div class="selectgroup w-100"> | ||||
|                                     <label class="selectgroup-item"> | ||||
|                                         <input type="radio" name="ssl_provider" value="letsencrypt" class="selectgroup-input"<%- ssl_provider !== 'other' ? ' checked' : '' %>> | ||||
|                                         <span class="selectgroup-button"><%- i18n('ssl', 'letsencrypt') %></span> | ||||
|                                     </label> | ||||
|                                     <label class="selectgroup-item"> | ||||
|                                         <input type="radio" name="ssl_provider" value="other" class="selectgroup-input"<%- ssl_provider === 'other' ? ' checked' : '' %>> | ||||
|                                         <span class="selectgroup-button"><%- i18n('ssl', 'other') %></span> | ||||
|                                     </label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|  | ||||
|                         <!-- Lets encrypt --> | ||||
|                         <div class="col-sm-12 col-md-12 letsencrypt-ssl"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label"><%- i18n('ssl', 'letsencrypt-email') %> <span class="form-required">*</span></label> | ||||
|                                 <input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-12 col-md-12 letsencrypt-ssl"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="custom-switch"> | ||||
|                                     <input type="checkbox" class="custom-switch-input" name="meta[letsencrypt_agree]" value="1" required<%- getLetsencryptAgree() ? ' checked' : '' %>> | ||||
|                                     <span class="custom-switch-indicator"></span> | ||||
|                                     <span class="custom-switch-description"><%= i18n('ssl', 'letsencrypt-agree', {url: 'https://letsencrypt.org/repository/'}) %> <span class="form-required">*</span></span> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                         </div> | ||||
|  | ||||
|                         <!-- Other --> | ||||
|                         <div class="col-sm-12 col-md-12 other-ssl"> | ||||
|                             <div class="form-group"> | ||||
|                                 <div class="form-label"><%- i18n('all-hosts', 'other-certificate') %></div> | ||||
|                                 <div class="custom-file"> | ||||
|                                     <input type="file" class="custom-file-input" name="meta[other_ssl_certificate]" id="other_ssl_certificate"> | ||||
|                                     <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-12 col-md-12 other-ssl"> | ||||
|                             <div class="form-group"> | ||||
|                                 <div class="form-label"><%- i18n('all-hosts', 'other-certificate-key') %></div> | ||||
|                                 <div class="custom-file"> | ||||
|                                     <input type="file" class="custom-file-input" name="meta[other_ssl_certificate_key]" id="other_ssl_certificate_key"> | ||||
|                                     <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|  | ||||
|         </form> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|         <button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button> | ||||
|         <button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button> | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										181
									
								
								src/frontend/js/app/nginx/dead/form.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								src/frontend/js/app/nginx/dead/form.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const _             = require('underscore'); | ||||
| const Mn            = require('backbone.marionette'); | ||||
| const App           = require('../../main'); | ||||
| const DeadHostModel = require('../../../models/dead-host'); | ||||
| const template      = require('./form.ejs'); | ||||
|  | ||||
| require('jquery-serializejson'); | ||||
| require('selectize'); | ||||
|  | ||||
| module.exports = Mn.View.extend({ | ||||
|     template:      template, | ||||
|     className:     'modal-dialog', | ||||
|     max_file_size: 5120, | ||||
|  | ||||
|     ui: { | ||||
|         form:                      'form', | ||||
|         domain_names:              'input[name="domain_names"]', | ||||
|         buttons:                   '.modal-footer button', | ||||
|         cancel:                    'button.cancel', | ||||
|         save:                      'button.save', | ||||
|         ssl_enabled:               'input[name="ssl_enabled"]', | ||||
|         ssl_options:               '#ssl-options input', | ||||
|         ssl_provider:              'input[name="ssl_provider"]', | ||||
|         other_ssl_certificate:     '#other_ssl_certificate', | ||||
|         other_ssl_certificate_key: '#other_ssl_certificate_key', | ||||
|  | ||||
|         // SSL hiding and showing | ||||
|         all_ssl:         '.letsencrypt-ssl, .other-ssl', | ||||
|         letsencrypt_ssl: '.letsencrypt-ssl', | ||||
|         other_ssl:       '.other-ssl' | ||||
|     }, | ||||
|  | ||||
|     events: { | ||||
|         'change @ui.ssl_enabled': function () { | ||||
|             let enabled = this.ui.ssl_enabled.prop('checked'); | ||||
|             this.ui.ssl_options.not(this.ui.ssl_enabled).prop('disabled', !enabled).parents('.form-group').css('opacity', enabled ? 1 : 0.5); | ||||
|             this.ui.ssl_provider.trigger('change'); | ||||
|         }, | ||||
|  | ||||
|         'change @ui.ssl_provider': function () { | ||||
|             let enabled  = this.ui.ssl_enabled.prop('checked'); | ||||
|             let provider = this.ui.ssl_provider.filter(':checked').val(); | ||||
|             this.ui.all_ssl.hide().find('input').prop('disabled', true); | ||||
|             this.ui[provider + '_ssl'].show().find('input').prop('disabled', !enabled); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.save': function (e) { | ||||
|             e.preventDefault(); | ||||
|  | ||||
|             if (!this.ui.form[0].checkValidity()) { | ||||
|                 $('<input type="submit">').hide().appendTo(this.ui.form).click().remove(); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let view = this; | ||||
|             let data = this.ui.form.serializeJSON(); | ||||
|  | ||||
|             // Manipulate | ||||
|             data.ssl_enabled = !!data.ssl_enabled; | ||||
|             data.ssl_forced  = !!data.ssl_forced; | ||||
|  | ||||
|             if (typeof data.meta !== 'undefined' && typeof data.meta.letsencrypt_agree !== 'undefined') { | ||||
|                 data.meta.letsencrypt_agree = !!data.meta.letsencrypt_agree; | ||||
|             } | ||||
|  | ||||
|             if (typeof data.domain_names === 'string' && data.domain_names) { | ||||
|                 data.domain_names = data.domain_names.split(','); | ||||
|             } | ||||
|  | ||||
|             let require_ssl_files = typeof data.ssl_enabled !== 'undefined' && data.ssl_enabled && typeof data.ssl_provider !== 'undefined' && data.ssl_provider === 'other'; | ||||
|             let ssl_files         = []; | ||||
|             let method            = App.Api.Nginx.DeadHosts.create; | ||||
|             let is_new            = true; | ||||
|  | ||||
|             let must_require_ssl_files = require_ssl_files && !view.model.hasSslFiles('other'); | ||||
|  | ||||
|             if (this.model.get('id')) { | ||||
|                 // edit | ||||
|                 is_new  = false; | ||||
|                 method  = App.Api.Nginx.DeadHosts.update; | ||||
|                 data.id = this.model.get('id'); | ||||
|             } | ||||
|  | ||||
|             // check files are attached | ||||
|             if (require_ssl_files) { | ||||
|                 if (!this.ui.other_ssl_certificate[0].files.length || !this.ui.other_ssl_certificate[0].files[0].size) { | ||||
|                     if (must_require_ssl_files) { | ||||
|                         alert('certificate file is not attached'); | ||||
|                         return; | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (this.ui.other_ssl_certificate[0].files[0].size > this.max_file_size) { | ||||
|                         alert('certificate file is too large (> 5kb)'); | ||||
|                         return; | ||||
|                     } | ||||
|                     ssl_files.push({name: 'other_certificate', file: this.ui.other_ssl_certificate[0].files[0]}); | ||||
|                 } | ||||
|  | ||||
|                 if (!this.ui.other_ssl_certificate_key[0].files.length || !this.ui.other_ssl_certificate_key[0].files[0].size) { | ||||
|                     if (must_require_ssl_files) { | ||||
|                         alert('certificate key file is not attached'); | ||||
|                         return; | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (this.ui.other_ssl_certificate_key[0].files[0].size > this.max_file_size) { | ||||
|                         alert('certificate key file is too large (> 5kb)'); | ||||
|                         return; | ||||
|                     } | ||||
|                     ssl_files.push({name: 'other_certificate_key', file: this.ui.other_ssl_certificate_key[0].files[0]}); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); | ||||
|             method(data) | ||||
|                 .then(result => { | ||||
|                     view.model.set(result); | ||||
|  | ||||
|                     // Now upload the certs if we need to | ||||
|                     if (ssl_files.length) { | ||||
|                         let form_data = new FormData(); | ||||
|  | ||||
|                         ssl_files.map(function (file) { | ||||
|                             form_data.append(file.name, file.file); | ||||
|                         }); | ||||
|  | ||||
|                         return App.Api.Nginx.DeadHosts.setCerts(view.model.get('id'), form_data) | ||||
|                             .then(result => { | ||||
|                                 view.model.set('meta', _.assign({}, view.model.get('meta'), result)); | ||||
|                             }); | ||||
|                     } | ||||
|                 }) | ||||
|                 .then(() => { | ||||
|                     App.UI.closeModal(function () { | ||||
|                         if (is_new) { | ||||
|                             App.Controller.showNginxDead(); | ||||
|                         } | ||||
|                     }); | ||||
|                 }) | ||||
|                 .catch(err => { | ||||
|                     alert(err.message); | ||||
|                     this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); | ||||
|                 }); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     templateContext: { | ||||
|         getLetsencryptEmail: function () { | ||||
|             return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : App.Cache.User.get('email'); | ||||
|         }, | ||||
|  | ||||
|         getLetsencryptAgree: function () { | ||||
|             return typeof this.meta.letsencrypt_agree !== 'undefined' ? this.meta.letsencrypt_agree : false; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     onRender: function () { | ||||
|         this.ui.ssl_enabled.trigger('change'); | ||||
|         this.ui.ssl_provider.trigger('change'); | ||||
|  | ||||
|         this.ui.domain_names.selectize({ | ||||
|             delimiter:    ',', | ||||
|             persist:      false, | ||||
|             maxOptions:   15, | ||||
|             create:       function (input) { | ||||
|                 return { | ||||
|                     value: input, | ||||
|                     text:  input | ||||
|                 }; | ||||
|             }, | ||||
|             createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/ | ||||
|         }); | ||||
|     }, | ||||
|  | ||||
|     initialize: function (options) { | ||||
|         if (typeof options.model === 'undefined' || !options.model) { | ||||
|             this.model = new DeadHostModel.Model(); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
| @@ -1,32 +1,34 @@ | ||||
| <td class="text-center"> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- avatar || '/images/default-avatar.jpg' %>)"> | ||||
|         <span class="avatar-status <%- is_disabled ? 'bg-red' : 'bg-green' %>"></span> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> | ||||
|         <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
|     <div><%- name %></div> | ||||
|     <div> | ||||
|         <% domain_names.map(function(host) { | ||||
|             %> | ||||
|             <span class="tag"><%- host %></span> | ||||
|             <% | ||||
|         }); | ||||
|         %> | ||||
|     </div> | ||||
|     <div class="small text-muted"> | ||||
|         Created: <%- formatDbDate(created_on, 'Do MMMM YYYY') %> | ||||
|         <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
|     <div><%- email %></div> | ||||
| </td> | ||||
| <td> | ||||
|     <div><%- roles.join(', ') %></div> | ||||
|     <div><%- ssl_enabled && ssl_provider ? i18n('ssl', ssl_provider) : i18n('ssl', 'none') %></div> | ||||
| </td> | ||||
| <% if (canManage) { %> | ||||
| <td class="text-center"> | ||||
|     <div class="item-action dropdown"> | ||||
|         <a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a> | ||||
|         <div class="dropdown-menu dropdown-menu-right"> | ||||
|             <a href="#" class="edit-user dropdown-item"><i class="dropdown-icon fe fe-edit"></i> Edit Details</a> | ||||
|             <a href="#" class="edit-permissions dropdown-item"><i class="dropdown-icon fe fe-shield"></i> Edit Permissions</a> | ||||
|             <a href="#" class="set-password dropdown-item"><i class="dropdown-icon fe fe-lock"></i> Set Password</a> | ||||
|             <% if (!isSelf()) { %> | ||||
|             <a href="#" class="login dropdown-item"><i class="dropdown-icon fe fe-log-in"></i> Sign in as User</a> | ||||
|             <a href="#" class="edit dropdown-item"><i class="dropdown-icon fe fe-edit"></i> <%- i18n('str', 'edit') %></a> | ||||
|             <a href="#" class="logs dropdown-item"><i class="dropdown-icon fe fe-book"></i> <%- i18n('str', 'logs') %></a> | ||||
|             <div class="dropdown-divider"></div> | ||||
|             <a href="#" class="delete-user dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> Delete User</a> | ||||
|             <% } %> | ||||
|             <a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a> | ||||
|         </div> | ||||
|     </div> | ||||
| </td> | ||||
| <% } %> | ||||
| @@ -1,69 +1,32 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const Mn         = require('backbone.marionette'); | ||||
| const Controller = require('../../../controller'); | ||||
| const Api        = require('../../../api'); | ||||
| const Cache      = require('../../../cache'); | ||||
| const Tokens     = require('../../../tokens'); | ||||
| const template   = require('./item.ejs'); | ||||
| const Mn       = require('backbone.marionette'); | ||||
| const App      = require('../../../main'); | ||||
| const template = require('./item.ejs'); | ||||
|  | ||||
| module.exports = Mn.View.extend({ | ||||
|     template: template, | ||||
|     tagName:  'tr', | ||||
|  | ||||
|     ui: { | ||||
|         edit:        'a.edit-user', | ||||
|         permissions: 'a.edit-permissions', | ||||
|         password:    'a.set-password', | ||||
|         login:       'a.login', | ||||
|         delete:      'a.delete-user' | ||||
|         edit:   'a.edit', | ||||
|         delete: 'a.delete' | ||||
|     }, | ||||
|  | ||||
|     events: { | ||||
|         'click @ui.edit': function (e) { | ||||
|             e.preventDefault(); | ||||
|             Controller.showUserForm(this.model); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.permissions': function (e) { | ||||
|             e.preventDefault(); | ||||
|             Controller.showUserPermissions(this.model); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.password': function (e) { | ||||
|             e.preventDefault(); | ||||
|             Controller.showUserPasswordForm(this.model); | ||||
|             App.Controller.showNginxDeadForm(this.model); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.delete': function (e) { | ||||
|             e.preventDefault(); | ||||
|             Controller.showUserDeleteConfirm(this.model); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.login': function (e) { | ||||
|             e.preventDefault(); | ||||
|  | ||||
|             if (Cache.User.get('id') !== this.model.get('id')) { | ||||
|                 this.ui.login.prop('disabled', true).addClass('btn-disabled'); | ||||
|  | ||||
|                 Api.Users.loginAs(this.model.get('id')) | ||||
|                     .then(res => { | ||||
|                         Tokens.addToken(res.token, res.user.nickname || res.user.name); | ||||
|                         window.location = '/'; | ||||
|                         window.location.reload(); | ||||
|                     }) | ||||
|                     .catch(err => { | ||||
|                         alert(err.message); | ||||
|                         this.ui.login.prop('disabled', false).removeClass('btn-disabled'); | ||||
|                     }); | ||||
|             } | ||||
|             App.Controller.showNginxDeadDeleteConfirm(this.model); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     templateContext: { | ||||
|         isSelf: function () { | ||||
|             return Cache.User.get('id') === this.id; | ||||
|         } | ||||
|         canManage: App.Cache.User.canManage('dead_hosts') | ||||
|     }, | ||||
|  | ||||
|     initialize: function () { | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| <thead> | ||||
|     <th width="30"> </th> | ||||
|     <th>Name</th> | ||||
|     <th>Email</th> | ||||
|     <th>Roles</th> | ||||
|     <th><%- i18n('str', 'source') %></th> | ||||
|     <th><%- i18n('str', 'ssl') %></th> | ||||
|     <% if (canManage) { %> | ||||
|     <th> </th> | ||||
|     <% } %> | ||||
| </thead> | ||||
| <tbody> | ||||
|     <!-- items --> | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const Mn         = require('backbone.marionette'); | ||||
| const ItemView   = require('./item'); | ||||
| const template   = require('./main.ejs'); | ||||
| const Mn       = require('backbone.marionette'); | ||||
| const App      = require('../../../main'); | ||||
| const ItemView = require('./item'); | ||||
| const template = require('./main.ejs'); | ||||
|  | ||||
| const TableBody = Mn.CollectionView.extend({ | ||||
|     tagName:   'tbody', | ||||
| @@ -21,6 +22,10 @@ module.exports = Mn.View.extend({ | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     templateContext: { | ||||
|         canManage: App.Cache.User.canManage('dead_hosts') | ||||
|     }, | ||||
|  | ||||
|     onRender: function () { | ||||
|         this.showChildView('body', new TableBody({ | ||||
|             collection: this.collection | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| <div class="card"> | ||||
|     <div class="card-status bg-danger"></div> | ||||
|     <div class="card-header"> | ||||
|         <h3 class="card-title">404 Hosts</h3> | ||||
|         <h3 class="card-title"><%- i18n('dead-hosts', 'title') %></h3> | ||||
|         <div class="card-options"> | ||||
|             <% if (showAddButton) { %> | ||||
|             <a href="#" class="btn btn-outline-danger btn-sm ml-2 add-item">Add 404 Host</a> | ||||
|             <a href="#" class="btn btn-outline-danger btn-sm ml-2 add-item"><%- i18n('dead-hosts', 'add') %></a> | ||||
|             <% } %> | ||||
|         </div> | ||||
|     </div> | ||||
|   | ||||
| @@ -1,14 +1,12 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const Mn            = require('backbone.marionette'); | ||||
| const App           = require('../../main'); | ||||
| const DeadHostModel = require('../../../models/dead-host'); | ||||
| const Api           = require('../../api'); | ||||
| const Cache         = require('../../cache'); | ||||
| const Controller    = require('../../controller'); | ||||
| const ListView      = require('./list/main'); | ||||
| const ErrorView     = require('../../error/main'); | ||||
| const template      = require('./main.ejs'); | ||||
| const EmptyView     = require('../../empty/main'); | ||||
| const template      = require('./main.ejs'); | ||||
|  | ||||
| module.exports = Mn.View.extend({ | ||||
|     id:       'nginx-dead', | ||||
| @@ -27,18 +25,18 @@ module.exports = Mn.View.extend({ | ||||
|     events: { | ||||
|         'click @ui.add': function (e) { | ||||
|             e.preventDefault(); | ||||
|             Controller.showNginxDeadForm(); | ||||
|             App.Controller.showNginxDeadForm(); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     templateContext: { | ||||
|         showAddButton: Cache.User.canManage('dead_hosts') | ||||
|         showAddButton: App.Cache.User.canManage('dead_hosts') | ||||
|     }, | ||||
|  | ||||
|     onRender: function () { | ||||
|         let view = this; | ||||
|  | ||||
|         Api.Nginx.DeadHosts.getAll() | ||||
|         App.Api.Nginx.DeadHosts.getAll(['owner']) | ||||
|             .then(response => { | ||||
|                 if (!view.isDestroyed()) { | ||||
|                     if (response && response.length) { | ||||
| @@ -46,15 +44,16 @@ module.exports = Mn.View.extend({ | ||||
|                             collection: new DeadHostModel.Collection(response) | ||||
|                         })); | ||||
|                     } else { | ||||
|                         let manage = Cache.User.canManage('dead_hosts'); | ||||
|                         let manage = App.Cache.User.canManage('dead_hosts'); | ||||
|  | ||||
|                         view.showChildView('list_region', new EmptyView({ | ||||
|                             title:     'There are no 404 Hosts', | ||||
|                             subtitle:  manage ? 'Why don\'t you create one?' : 'And you don\'t have permission to create one.', | ||||
|                             link:      manage ? 'Add 404 Host' : null, | ||||
|                             btn_color: 'danger', | ||||
|                             action:    function () { | ||||
|                                 Controller.showNginxDeadForm(); | ||||
|                             title:      App.i18n('dead-hosts', 'empty'), | ||||
|                             subtitle:   App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), | ||||
|                             link:       manage ? App.i18n('dead-hosts', 'add') : null, | ||||
|                             btn_color:  'danger', | ||||
|                             permission: 'dead_hosts', | ||||
|                             action:     function () { | ||||
|                                 App.Controller.showNginxDeadForm(); | ||||
|                             } | ||||
|                         })); | ||||
|                     } | ||||
| @@ -65,7 +64,7 @@ module.exports = Mn.View.extend({ | ||||
|                     code:    err.code, | ||||
|                     message: err.message, | ||||
|                     retry:   function () { | ||||
|                         Controller.showNginxDead(); | ||||
|                         App.Controller.showNginxDead(); | ||||
|                     } | ||||
|                 })); | ||||
|  | ||||
|   | ||||
| @@ -11,8 +11,8 @@ require('jquery-mask-plugin'); | ||||
| require('selectize'); | ||||
|  | ||||
| module.exports = Mn.View.extend({ | ||||
|     template:  template, | ||||
|     className: 'modal-dialog', | ||||
|     template:      template, | ||||
|     className:     'modal-dialog', | ||||
|     max_file_size: 5120, | ||||
|  | ||||
|     ui: { | ||||
| @@ -60,19 +60,15 @@ module.exports = Mn.View.extend({ | ||||
|             let data = this.ui.form.serializeJSON(); | ||||
|  | ||||
|             // Manipulate | ||||
|             data.forward_port = parseInt(data.forward_port, 10); | ||||
|             _.map(data, function (item, idx) { | ||||
|                 if (typeof item === 'string' && item === '1') { | ||||
|                     item = true; | ||||
|                 } else if (typeof item === 'object' && item !== null) { | ||||
|                     _.map(item, function (item2, idx2) { | ||||
|                         if (typeof item2 === 'string' && item2 === '1') { | ||||
|                             item[idx2] = true; | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|                 data[idx] = item; | ||||
|             }); | ||||
|             data.forward_port    = parseInt(data.forward_port, 10); | ||||
|             data.block_exploits  = !!data.block_exploits; | ||||
|             data.caching_enabled = !!data.caching_enabled; | ||||
|             data.ssl_enabled     = !!data.ssl_enabled; | ||||
|             data.ssl_forced      = !!data.ssl_forced; | ||||
|  | ||||
|             if (typeof data.meta !== 'undefined' && typeof data.meta.letsencrypt_agree !== 'undefined') { | ||||
|                 data.meta.letsencrypt_agree = !!data.meta.letsencrypt_agree; | ||||
|             } | ||||
|  | ||||
|             if (typeof data.domain_names === 'string' && data.domain_names) { | ||||
|                 data.domain_names = data.domain_names.split(','); | ||||
|   | ||||
| @@ -58,18 +58,14 @@ module.exports = Mn.View.extend({ | ||||
|             let data = this.ui.form.serializeJSON(); | ||||
|  | ||||
|             // Manipulate | ||||
|             _.map(data, function (item, idx) { | ||||
|                 if (typeof item === 'string' && item === '1') { | ||||
|                     item = true; | ||||
|                 } else if (typeof item === 'object' && item !== null) { | ||||
|                     _.map(item, function (item2, idx2) { | ||||
|                         if (typeof item2 === 'string' && item2 === '1') { | ||||
|                             item[idx2] = true; | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|                 data[idx] = item; | ||||
|             }); | ||||
|             data.block_exploits = !!data.block_exploits; | ||||
|             data.preserve_path  = !!data.preserve_path; | ||||
|             data.ssl_enabled    = !!data.ssl_enabled; | ||||
|             data.ssl_forced     = !!data.ssl_forced; | ||||
|  | ||||
|             if (typeof data.meta !== 'undefined' && typeof data.meta.letsencrypt_agree !== 'undefined') { | ||||
|                 data.meta.letsencrypt_agree = !!data.meta.letsencrypt_agree; | ||||
|             } | ||||
|  | ||||
|             if (typeof data.domain_names === 'string' && data.domain_names) { | ||||
|                 data.domain_names = data.domain_names.split(','); | ||||
|   | ||||
| @@ -9,7 +9,7 @@ | ||||
|                 <div class="col-sm-12 col-md-12"> | ||||
|                     <div class="form-group"> | ||||
|                         <label class="form-label"><%- i18n('streams', 'incoming-port') %> <span class="form-required">*</span></label> | ||||
|                         <input name="incoming_port" type="number" class="form-control text-monospace" placeholder="8080" value="<%- incoming_port %>" required> | ||||
|                         <input name="incoming_port" type="number" class="form-control text-monospace" placeholder="eg: 8080" value="<%- incoming_port %>" required> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="col-sm-8 col-md-8"> | ||||
| @@ -21,7 +21,7 @@ | ||||
|                 <div class="col-sm-4 col-md-4"> | ||||
|                     <div class="form-group"> | ||||
|                         <label class="form-label"><%- i18n('streams', 'forwarding-port') %> <span class="form-required">*</span></label> | ||||
|                         <input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="80" value="<%- forwarding_port %>" required> | ||||
|                         <input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="eg: 80" value="<%- forwarding_port %>" required> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="col-sm-6 col-md-6"> | ||||
| @@ -38,10 +38,13 @@ | ||||
|                         <label class="custom-switch"> | ||||
|                             <input type="checkbox" class="custom-switch-input" name="udp_forwarding" value="1"<%- udp_forwarding ? ' checked' : '' %>> | ||||
|                             <span class="custom-switch-indicator"></span> | ||||
|                             <span class="custom-switch-description"><%- i18n('streams', 'udp_forwarding') %></span> | ||||
|                             <span class="custom-switch-description"><%- i18n('streams', 'udp-forwarding') %></span> | ||||
|                         </label> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="col-sm-12 col-md-12"> | ||||
|                     <div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </form> | ||||
|     </div> | ||||
|   | ||||
| @@ -11,19 +11,24 @@ require('jquery-mask-plugin'); | ||||
| require('selectize'); | ||||
|  | ||||
| module.exports = Mn.View.extend({ | ||||
|     template:      template, | ||||
|     className:     'modal-dialog', | ||||
|     max_file_size: 5120, | ||||
|     template:  template, | ||||
|     className: 'modal-dialog', | ||||
|  | ||||
|     ui: { | ||||
|         form:       'form', | ||||
|         forward_ip: 'input[name="forward_ip"]', | ||||
|         type_error: '.forward-type-error', | ||||
|         buttons:    '.modal-footer button', | ||||
|         switches:   '.custom-switch-input', | ||||
|         cancel:     'button.cancel', | ||||
|         save:       'button.save' | ||||
|     }, | ||||
|  | ||||
|     events: { | ||||
|         'change @ui.switches': function () { | ||||
|             this.ui.type_error.hide(); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.save': function (e) { | ||||
|             e.preventDefault(); | ||||
|  | ||||
| @@ -35,20 +40,16 @@ module.exports = Mn.View.extend({ | ||||
|             let view = this; | ||||
|             let data = this.ui.form.serializeJSON(); | ||||
|  | ||||
|             if (!data.tcp_forwarding && !data.udp_forwarding) { | ||||
|                 this.ui.type_error.show(); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Manipulate | ||||
|             data.forward_port = parseInt(data.forward_port, 10); | ||||
|             _.map(data, function (item, idx) { | ||||
|                 if (typeof item === 'string' && item === '1') { | ||||
|                     item = true; | ||||
|                 } else if (typeof item === 'object' && item !== null) { | ||||
|                     _.map(item, function (item2, idx2) { | ||||
|                         if (typeof item2 === 'string' && item2 === '1') { | ||||
|                             item[idx2] = true; | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|                 data[idx] = item; | ||||
|             }); | ||||
|             data.incoming_port   = parseInt(data.incoming_port, 10); | ||||
|             data.forwarding_port = parseInt(data.forwarding_port, 10); | ||||
|             data.tcp_forwarding  = !!data.tcp_forwarding; | ||||
|             data.udp_forwarding  = !!data.udp_forwarding; | ||||
|  | ||||
|             let method = App.Api.Nginx.Streams.create; | ||||
|             let is_new = true; | ||||
|   | ||||
| @@ -4,26 +4,25 @@ | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
|     <div> | ||||
|         <% domain_names.map(function(host) { | ||||
|             %> | ||||
|             <span class="tag"><%- host %></span> | ||||
|             <% | ||||
|         }); | ||||
|         %> | ||||
|     <div class="text-monospace"> | ||||
|         <%- incoming_port %> | ||||
|     </div> | ||||
|     <div class="small text-muted"> | ||||
|         <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
|     <div class="text-monospace"><%- forward_ip %>:<%- forward_port %></div> | ||||
|     <div class="text-monospace"><%- forward_ip %>:<%- forwarding_port %></div> | ||||
| </td> | ||||
| <td> | ||||
|     <div><%- ssl_enabled && ssl_provider ? i18n('ssl', ssl_provider) : i18n('ssl', 'none') %></div> | ||||
| </td> | ||||
| <td> | ||||
|     <div><%- access_list_id ? access_list.name : i18n('str', 'public') %></div> | ||||
|     <div> | ||||
|         <% if (tcp_forwarding) { %> | ||||
|             <span class="tag"><%- i18n('streams', 'tcp') %></span> | ||||
|         <% } | ||||
|         if (udp_forwarding) { %> | ||||
|             <span class="tag"><%- i18n('streams', 'udp') %></span> | ||||
|         <% } %> | ||||
|     </div> | ||||
| </td> | ||||
| <% if (canManage) { %> | ||||
| <td class="text-center"> | ||||
| @@ -31,7 +30,6 @@ | ||||
|         <a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a> | ||||
|         <div class="dropdown-menu dropdown-menu-right"> | ||||
|             <a href="#" class="edit dropdown-item"><i class="dropdown-icon fe fe-edit"></i> <%- i18n('str', 'edit') %></a> | ||||
|             <a href="#" class="logs dropdown-item"><i class="dropdown-icon fe fe-book"></i> <%- i18n('str', 'logs') %></a> | ||||
|             <div class="dropdown-divider"></div> | ||||
|             <a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a> | ||||
|         </div> | ||||
|   | ||||
| @@ -16,17 +16,17 @@ module.exports = Mn.View.extend({ | ||||
|     events: { | ||||
|         'click @ui.edit': function (e) { | ||||
|             e.preventDefault(); | ||||
|             App.Controller.showNginxProxyForm(this.model); | ||||
|             App.Controller.showNginxStreamForm(this.model); | ||||
|         }, | ||||
|  | ||||
|         'click @ui.delete': function (e) { | ||||
|             e.preventDefault(); | ||||
|             App.Controller.showNginxProxyDeleteConfirm(this.model); | ||||
|             App.Controller.showNginxStreamDeleteConfirm(this.model); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     templateContext: { | ||||
|         canManage: App.Cache.User.canManage('proxy_hosts') | ||||
|         canManage: App.Cache.User.canManage('streams') | ||||
|     }, | ||||
|  | ||||
|     initialize: function () { | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| <thead> | ||||
|     <th width="30"> </th> | ||||
|     <th><%- i18n('str', 'source') %></th> | ||||
|     <th><%- i18n('streams', 'incoming-port') %></th> | ||||
|     <th><%- i18n('str', 'destination') %></th> | ||||
|     <th><%- i18n('str', 'ssl') %></th> | ||||
|     <th><%- i18n('str', 'access') %></th> | ||||
|     <th><%- i18n('streams', 'protocol') %></th> | ||||
|     <% if (canManage) { %> | ||||
|     <th> </th> | ||||
|     <% } %> | ||||
|   | ||||
| @@ -23,7 +23,7 @@ module.exports = Mn.View.extend({ | ||||
|     }, | ||||
|  | ||||
|     templateContext: { | ||||
|         canManage: App.Cache.User.canManage('proxy_hosts') | ||||
|         canManage: App.Cache.User.canManage('streams') | ||||
|     }, | ||||
|  | ||||
|     onRender: function () { | ||||
|   | ||||
| @@ -94,12 +94,27 @@ | ||||
|       "delete-confirm": "Are you sure you want to delete the Redirection host for: <strong>{domains}</strong>?" | ||||
|     }, | ||||
|     "dead-hosts": { | ||||
|       "title": "404 Hosts" | ||||
|       "title": "404 Hosts", | ||||
|       "empty": "There are no 404 Hosts", | ||||
|       "add": "Add 404 Host", | ||||
|       "form-title": "{id, select, undefined{New} other{Edit}} 404 Host" | ||||
|     }, | ||||
|     "streams": { | ||||
|       "title": "Streams", | ||||
|       "empty": "There are no Streams", | ||||
|       "add": "Add Stream" | ||||
|       "add": "Add Stream", | ||||
|       "form-title": "{id, select, undefined{New} other{Edit}} Stream", | ||||
|       "incoming-port": "Incoming Port", | ||||
|       "forward-ip": "Forward IP", | ||||
|       "forwarding-port": "Forward Port", | ||||
|       "tcp-forwarding": "TCP Forwarding", | ||||
|       "udp-forwarding": "UDP Forwarding", | ||||
|       "forward-type-error": "At least one type of protocol must be enabled", | ||||
|       "protocol": "Protocol", | ||||
|       "tcp": "TCP", | ||||
|       "udp": "UDP", | ||||
|       "delete": "Delete Stream", | ||||
|       "delete-confirm": "Are you sure you want to delete this Stream?" | ||||
|     }, | ||||
|     "access-lists": { | ||||
|       "title": "Access Lists", | ||||
|   | ||||
| @@ -10,11 +10,13 @@ const model = Backbone.Model.extend({ | ||||
|             id:           0, | ||||
|             created_on:   null, | ||||
|             modified_on:  null, | ||||
|             owner:        null, | ||||
|             domain_name:  '', | ||||
|             domain_names: [], | ||||
|             ssl_enabled:  false, | ||||
|             ssl_provider: false, | ||||
|             meta:         [] | ||||
|             ssl_forced:   false, | ||||
|             meta:         {}, | ||||
|             // The following are expansions: | ||||
|             owner:        null | ||||
|         }; | ||||
|     } | ||||
| }); | ||||
|   | ||||
| @@ -11,9 +11,9 @@ const model = Backbone.Model.extend({ | ||||
|             created_on:      null, | ||||
|             modified_on:     null, | ||||
|             owner:           null, | ||||
|             incoming_port:   3000, | ||||
|             forward_ip:      '', | ||||
|             forwarding_port: 3000, | ||||
|             incoming_port:   null, | ||||
|             forward_ip:      null, | ||||
|             forwarding_port: null, | ||||
|             tcp_forwarding:  true, | ||||
|             udp_forwarding:  false, | ||||
|             meta:            {} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user