mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-30 23:33:34 +00:00 
			
		
		
		
	Access Lists
This commit is contained in:
		
							
								
								
									
										18
									
								
								TODO.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								TODO.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | # TODO | ||||||
|  |  | ||||||
|  | In order of importance, somewhat..  | ||||||
|  |  | ||||||
|  | - Manual certificate writing to disk and usage in nginx configs - MIGRATING.md | ||||||
|  | - Access Lists UI and Nginx usage | ||||||
|  | - Make modal dialogs unclosable in overlay | ||||||
|  | - Dashboard stats are caching instead of querying | ||||||
|  | - Create a nice way of importing from v1 let's encrypt certs and config data | ||||||
|  | - UI Log tail | ||||||
|  |  | ||||||
|  | Testing | ||||||
|  |  | ||||||
|  | - Access Levels | ||||||
|  | - Visibility | ||||||
|  | - Forwarding | ||||||
|  | - Cert renewals | ||||||
|  | - Custom certs | ||||||
							
								
								
									
										2
									
								
								config/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								config/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | These files are use in development and are not deployed as part of the final product. | ||||||
|  |   | ||||||
| @@ -1,8 +1,7 @@ | |||||||
| [mysqld] | [mysqld] | ||||||
| skip-innodb | skip-innodb | ||||||
| default-storage-engine=MyISAM | default-storage-engine=Aria | ||||||
| default-tmp-storage-engine=MyISAM | default-tmp-storage-engine=Aria | ||||||
| innodb=OFF | innodb=OFF | ||||||
| symbolic-links=0 | symbolic-links=0 | ||||||
| log-output=file | log-output=file | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,13 +3,14 @@ services: | |||||||
|   app: |   app: | ||||||
|     image: jc21/nginx-proxy-manager:2 |     image: jc21/nginx-proxy-manager:2 | ||||||
|     restart: always |     restart: always | ||||||
|     network_mode: host |  | ||||||
|     volumes: |     volumes: | ||||||
|       - ./config.json:/app/config/production.json |       - ./config.json:/app/config/production.json | ||||||
|       - ./data:/data |       - ./data:/data | ||||||
|       - ./letsencrypt:/etc/letsencrypt |       - ./letsencrypt:/etc/letsencrypt | ||||||
|     depends_on: |     depends_on: | ||||||
|       - db |       - db | ||||||
|  |     links: | ||||||
|  |       - db | ||||||
|   db: |   db: | ||||||
|     image: mariadb |     image: mariadb | ||||||
|     restart: always |     restart: always | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
|  |  | ||||||
| const _               = require('lodash'); | const _                   = require('lodash'); | ||||||
| const error           = require('../lib/error'); | const error               = require('../lib/error'); | ||||||
| const accessListModel = require('../models/access_list'); | const accessListModel     = require('../models/access_list'); | ||||||
|  | const accessListAuthModel = require('../models/access_list_auth'); | ||||||
|  | const internalAuditLog    = require('./audit-log'); | ||||||
|  |  | ||||||
| function omissions () { | function omissions () { | ||||||
|     return ['is_deleted']; |     return ['is_deleted']; | ||||||
| @@ -18,8 +20,51 @@ const internalAccessList = { | |||||||
|     create: (access, data) => { |     create: (access, data) => { | ||||||
|         return access.can('access_lists:create', data) |         return access.can('access_lists:create', data) | ||||||
|             .then(access_data => { |             .then(access_data => { | ||||||
|                 // TODO |                 return accessListModel | ||||||
|                 return {}; |                     .query() | ||||||
|  |                     .omit(omissions()) | ||||||
|  |                     .insertAndFetch({ | ||||||
|  |                         name:          data.name, | ||||||
|  |                         owner_user_id: access.token.get('attrs').id | ||||||
|  |                     }); | ||||||
|  |             }) | ||||||
|  |             .then(row => { | ||||||
|  |                 // Now add the items | ||||||
|  |                 let promises = []; | ||||||
|  |                 data.items.map(function (item) { | ||||||
|  |                     promises.push(accessListAuthModel | ||||||
|  |                         .query() | ||||||
|  |                         .insert({ | ||||||
|  |                             access_list_id: row.id, | ||||||
|  |                             username:       item.username, | ||||||
|  |                             password:       item.password | ||||||
|  |                         }) | ||||||
|  |                     ); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 return Promise.all(promises); | ||||||
|  |             }) | ||||||
|  |             .then(row => { | ||||||
|  |                 // re-fetch with cert | ||||||
|  |                 return internalAccessList.get(access, { | ||||||
|  |                     id:     row.id, | ||||||
|  |                     expand: ['owner', 'items'] | ||||||
|  |                 }); | ||||||
|  |             }) | ||||||
|  |             .then(row => { | ||||||
|  |                 // Audit log | ||||||
|  |                 data.meta = _.assign({}, data.meta || {}, row.meta); | ||||||
|  |  | ||||||
|  |                 // Add to audit log | ||||||
|  |                 return internalAuditLog.add(access, { | ||||||
|  |                     action:      'created', | ||||||
|  |                     object_type: 'access-list', | ||||||
|  |                     object_id:   row.id, | ||||||
|  |                     meta:        data | ||||||
|  |                 }) | ||||||
|  |                     .then(() => { | ||||||
|  |                         return row; | ||||||
|  |                     }); | ||||||
|             }); |             }); | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
| @@ -62,7 +107,7 @@ const internalAccessList = { | |||||||
|                     .query() |                     .query() | ||||||
|                     .where('is_deleted', 0) |                     .where('is_deleted', 0) | ||||||
|                     .andWhere('id', data.id) |                     .andWhere('id', data.id) | ||||||
|                     .allowEager('[owner]') |                     .allowEager('[owner,items]') | ||||||
|                     .first(); |                     .first(); | ||||||
|  |  | ||||||
|                 if (access_data.permission_visibility !== 'all') { |                 if (access_data.permission_visibility !== 'all') { | ||||||
| @@ -82,6 +127,10 @@ const internalAccessList = { | |||||||
|             }) |             }) | ||||||
|             .then(row => { |             .then(row => { | ||||||
|                 if (row) { |                 if (row) { | ||||||
|  |                     if (typeof row.items !== 'undefined' && row.items) { | ||||||
|  |                         row.items = internalAccessList.maskItems(row.items); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|                     return _.omit(row, omissions()); |                     return _.omit(row, omissions()); | ||||||
|                 } else { |                 } else { | ||||||
|                     throw new error.ItemNotFoundError(data.id); |                     throw new error.ItemNotFoundError(data.id); | ||||||
| @@ -134,7 +183,7 @@ const internalAccessList = { | |||||||
|                     .where('is_deleted', 0) |                     .where('is_deleted', 0) | ||||||
|                     .groupBy('id') |                     .groupBy('id') | ||||||
|                     .omit(['is_deleted']) |                     .omit(['is_deleted']) | ||||||
|                     .allowEager('[owner]') |                     .allowEager('[owner,items]') | ||||||
|                     .orderBy('name', 'ASC'); |                     .orderBy('name', 'ASC'); | ||||||
|  |  | ||||||
|                 if (access_data.permission_visibility !== 'all') { |                 if (access_data.permission_visibility !== 'all') { | ||||||
| @@ -153,6 +202,17 @@ const internalAccessList = { | |||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 return query; |                 return query; | ||||||
|  |             }) | ||||||
|  |             .then(rows => { | ||||||
|  |                 if (rows) { | ||||||
|  |                     rows.map(function (row, idx) { | ||||||
|  |                         if (typeof row.items !== 'undefined' && row.items) { | ||||||
|  |                             rows[idx].items = internalAccessList.maskItems(row.items); | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 return rows; | ||||||
|             }); |             }); | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
| @@ -177,6 +237,21 @@ const internalAccessList = { | |||||||
|             .then(row => { |             .then(row => { | ||||||
|                 return parseInt(row.count, 10); |                 return parseInt(row.count, 10); | ||||||
|             }); |             }); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param   {Object}  list | ||||||
|  |      * @returns {Object} | ||||||
|  |      */ | ||||||
|  |     maskItems: list => { | ||||||
|  |         if (list && typeof list.items !== 'undefined') { | ||||||
|  |             list.items.map(function (val, idx) { | ||||||
|  |                 list.items[idx].hint     = val.password.charAt(0) + ('*').repeat(val.password.length - 1); | ||||||
|  |                 list.items[idx].password = ''; | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return list; | ||||||
|     } |     } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -41,7 +41,6 @@ const internalCertificate = { | |||||||
|             return utils.exec(certbot_command + ' renew -q ' + (debug_mode ? '--staging' : '')) |             return utils.exec(certbot_command + ' renew -q ' + (debug_mode ? '--staging' : '')) | ||||||
|                 .then(result => { |                 .then(result => { | ||||||
|                     logger.info(result); |                     logger.info(result); | ||||||
|                     internalCertificate.interval_processing = false; |  | ||||||
|  |  | ||||||
|                     return internalNginx.reload() |                     return internalNginx.reload() | ||||||
|                         .then(() => { |                         .then(() => { | ||||||
| @@ -49,6 +48,42 @@ const internalCertificate = { | |||||||
|                             return result; |                             return result; | ||||||
|                         }); |                         }); | ||||||
|                 }) |                 }) | ||||||
|  |                 .then(() => { | ||||||
|  |                     // Now go and fetch all the letsencrypt certs from the db and query the files and update expiry times | ||||||
|  |                     return certificateModel | ||||||
|  |                         .query() | ||||||
|  |                         .where('is_deleted', 0) | ||||||
|  |                         .andWhere('provider', 'letsencrypt') | ||||||
|  |                         .then(certificates => { | ||||||
|  |                             if (certificates && certificates.length) { | ||||||
|  |                                 let promises = []; | ||||||
|  |  | ||||||
|  |                                 certificates.map(function (certificate) { | ||||||
|  |                                     promises.push( | ||||||
|  |                                         internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem') | ||||||
|  |                                             .then(cert_info => { | ||||||
|  |                                                 return certificateModel | ||||||
|  |                                                     .query() | ||||||
|  |                                                     .where('id', certificate.id) | ||||||
|  |                                                     .andWhere('provider', 'letsencrypt') | ||||||
|  |                                                     .patch({ | ||||||
|  |                                                         expires_on: certificateModel.raw('FROM_UNIXTIME(' + cert_info.dates.to + ')') | ||||||
|  |                                                     }); | ||||||
|  |                                             }) | ||||||
|  |                                             .catch(err => { | ||||||
|  |                                                 // Don't want to stop the train here, just log the error | ||||||
|  |                                                 logger.error(err.message); | ||||||
|  |                                             }) | ||||||
|  |                                     ); | ||||||
|  |                                 }); | ||||||
|  |  | ||||||
|  |                                 return Promise.all(promises); | ||||||
|  |                             } | ||||||
|  |                         }); | ||||||
|  |                 }) | ||||||
|  |                 .then(() => { | ||||||
|  |                     internalCertificate.interval_processing = false; | ||||||
|  |                 }) | ||||||
|                 .catch(err => { |                 .catch(err => { | ||||||
|                     logger.error(err); |                     logger.error(err); | ||||||
|                     internalCertificate.interval_processing = false; |                     internalCertificate.interval_processing = false; | ||||||
|   | |||||||
| @@ -3,9 +3,10 @@ | |||||||
|  |  | ||||||
| 'use strict'; | 'use strict'; | ||||||
|  |  | ||||||
| const db    = require('../db'); | const db             = require('../db'); | ||||||
| const Model = require('objection').Model; | const Model          = require('objection').Model; | ||||||
| const User  = require('./user'); | const User           = require('./user'); | ||||||
|  | const AccessListAuth = require('./access_list_auth'); | ||||||
|  |  | ||||||
| Model.knex(db); | Model.knex(db); | ||||||
|  |  | ||||||
| @@ -44,6 +45,17 @@ class AccessList extends Model { | |||||||
|                     qb.where('user.is_deleted', 0); |                     qb.where('user.is_deleted', 0); | ||||||
|                     qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); |                     qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); | ||||||
|                 } |                 } | ||||||
|  |             }, | ||||||
|  |             items: { | ||||||
|  |                 relation:   Model.HasManyRelation, | ||||||
|  |                 modelClass: AccessListAuth, | ||||||
|  |                 join:       { | ||||||
|  |                     from: 'access_list.id', | ||||||
|  |                     to:   'access_list_auth.access_list_id' | ||||||
|  |                 }, | ||||||
|  |                 modify:     function (qb) { | ||||||
|  |                     qb.omit(['id', 'created_on', 'modified_on']); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -34,7 +34,7 @@ class AccessListAuth extends Model { | |||||||
|         return { |         return { | ||||||
|             access_list: { |             access_list: { | ||||||
|                 relation:   Model.HasOneRelation, |                 relation:   Model.HasOneRelation, | ||||||
|                 modelClass: './access_list', |                 modelClass: require('./access_list'), | ||||||
|                 join:       { |                 join:       { | ||||||
|                     from: 'access_list_auth.access_list_id', |                     from: 'access_list_auth.access_list_id', | ||||||
|                     to:   'access_list.id' |                     to:   'access_list.id' | ||||||
|   | |||||||
| @@ -75,7 +75,7 @@ router | |||||||
|  * /api/nginx/access-lists/123 |  * /api/nginx/access-lists/123 | ||||||
|  */ |  */ | ||||||
| router | router | ||||||
|     .route('/:host_id') |     .route('/:list_id') | ||||||
|     .options((req, res) => { |     .options((req, res) => { | ||||||
|         res.sendStatus(204); |         res.sendStatus(204); | ||||||
|     }) |     }) | ||||||
| @@ -88,10 +88,10 @@ router | |||||||
|      */ |      */ | ||||||
|     .get((req, res, next) => { |     .get((req, res, next) => { | ||||||
|         validator({ |         validator({ | ||||||
|             required:             ['host_id'], |             required:             ['list_id'], | ||||||
|             additionalProperties: false, |             additionalProperties: false, | ||||||
|             properties:           { |             properties:           { | ||||||
|                 host_id: { |                 list_id: { | ||||||
|                     $ref: 'definitions#/definitions/id' |                     $ref: 'definitions#/definitions/id' | ||||||
|                 }, |                 }, | ||||||
|                 expand:  { |                 expand:  { | ||||||
| @@ -99,12 +99,12 @@ router | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }, { |         }, { | ||||||
|             host_id: req.params.host_id, |             list_id: req.params.list_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 => { |             .then(data => { | ||||||
|                 return internalAccessList.get(res.locals.access, { |                 return internalAccessList.get(res.locals.access, { | ||||||
|                     id:     parseInt(data.host_id, 10), |                     id:     parseInt(data.list_id, 10), | ||||||
|                     expand: data.expand |                     expand: data.expand | ||||||
|                 }); |                 }); | ||||||
|             }) |             }) | ||||||
| @@ -123,7 +123,7 @@ router | |||||||
|     .put((req, res, next) => { |     .put((req, res, next) => { | ||||||
|         apiValidator({$ref: 'endpoints/access-lists#/links/2/schema'}, req.body) |         apiValidator({$ref: 'endpoints/access-lists#/links/2/schema'}, req.body) | ||||||
|             .then(payload => { |             .then(payload => { | ||||||
|                 payload.id = parseInt(req.params.host_id, 10); |                 payload.id = parseInt(req.params.list_id, 10); | ||||||
|                 return internalAccessList.update(res.locals.access, payload); |                 return internalAccessList.update(res.locals.access, payload); | ||||||
|             }) |             }) | ||||||
|             .then(result => { |             .then(result => { | ||||||
| @@ -139,7 +139,7 @@ router | |||||||
|      * Update and existing access-list |      * Update and existing access-list | ||||||
|      */ |      */ | ||||||
|     .delete((req, res, next) => { |     .delete((req, res, next) => { | ||||||
|         internalAccessList.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) |         internalAccessList.delete(res.locals.access, {id: parseInt(req.params.list_id, 10)}) | ||||||
|             .then(result => { |             .then(result => { | ||||||
|                 res.status(200) |                 res.status(200) | ||||||
|                     .send(result); |                     .send(result); | ||||||
|   | |||||||
							
								
								
									
										125
									
								
								src/backend/schema/endpoints/access-lists.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/backend/schema/endpoints/access-lists.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | |||||||
|  | { | ||||||
|  |   "$schema": "http://json-schema.org/draft-07/schema#", | ||||||
|  |   "$id": "endpoints/access-lists", | ||||||
|  |   "title": "Access Lists", | ||||||
|  |   "description": "Endpoints relating to Access Lists", | ||||||
|  |   "stability": "stable", | ||||||
|  |   "type": "object", | ||||||
|  |   "definitions": { | ||||||
|  |     "id": { | ||||||
|  |       "$ref": "../definitions.json#/definitions/id" | ||||||
|  |     }, | ||||||
|  |     "created_on": { | ||||||
|  |       "$ref": "../definitions.json#/definitions/created_on" | ||||||
|  |     }, | ||||||
|  |     "modified_on": { | ||||||
|  |       "$ref": "../definitions.json#/definitions/modified_on" | ||||||
|  |     }, | ||||||
|  |     "name": { | ||||||
|  |       "type": "string", | ||||||
|  |       "description": "Name of the Access List" | ||||||
|  |     }, | ||||||
|  |     "meta": { | ||||||
|  |       "type": "object" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "properties": { | ||||||
|  |     "id": { | ||||||
|  |       "$ref": "#/definitions/id" | ||||||
|  |     }, | ||||||
|  |     "created_on": { | ||||||
|  |       "$ref": "#/definitions/created_on" | ||||||
|  |     }, | ||||||
|  |     "modified_on": { | ||||||
|  |       "$ref": "#/definitions/modified_on" | ||||||
|  |     }, | ||||||
|  |     "name": { | ||||||
|  |       "$ref": "#/definitions/name" | ||||||
|  |     }, | ||||||
|  |     "meta": { | ||||||
|  |       "$ref": "#/definitions/meta" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "links": [ | ||||||
|  |     { | ||||||
|  |       "title": "List", | ||||||
|  |       "description": "Returns a list of Access Lists", | ||||||
|  |       "href": "/nginx/access-lists", | ||||||
|  |       "access": "private", | ||||||
|  |       "method": "GET", | ||||||
|  |       "rel": "self", | ||||||
|  |       "http_header": { | ||||||
|  |         "$ref": "../examples.json#/definitions/auth_header" | ||||||
|  |       }, | ||||||
|  |       "targetSchema": { | ||||||
|  |         "type": "array", | ||||||
|  |         "items": { | ||||||
|  |           "$ref": "#/properties" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "title": "Create", | ||||||
|  |       "description": "Creates a new Access List", | ||||||
|  |       "href": "/nginx/access-list", | ||||||
|  |       "access": "private", | ||||||
|  |       "method": "POST", | ||||||
|  |       "rel": "create", | ||||||
|  |       "http_header": { | ||||||
|  |         "$ref": "../examples.json#/definitions/auth_header" | ||||||
|  |       }, | ||||||
|  |       "schema": { | ||||||
|  |         "type": "object", | ||||||
|  |         "additionalProperties": false, | ||||||
|  |         "required": [ | ||||||
|  |           "name" | ||||||
|  |         ], | ||||||
|  |         "properties": { | ||||||
|  |           "name": { | ||||||
|  |             "$ref": "#/definitions/name" | ||||||
|  |           }, | ||||||
|  |           "items": { | ||||||
|  |             "type": "array", | ||||||
|  |             "minItems": 1, | ||||||
|  |             "items": { | ||||||
|  |               "type": "object", | ||||||
|  |               "additionalProperties": false, | ||||||
|  |               "properties": { | ||||||
|  |                 "username": { | ||||||
|  |                   "type": "string", | ||||||
|  |                   "minLength": 1 | ||||||
|  |                 }, | ||||||
|  |                 "password": { | ||||||
|  |                   "type": "string", | ||||||
|  |                   "minLength": 1 | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           "meta": { | ||||||
|  |             "$ref": "#/definitions/meta" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "targetSchema": { | ||||||
|  |         "properties": { | ||||||
|  |           "$ref": "#/properties" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "title": "Delete", | ||||||
|  |       "description": "Deletes a existing Access List", | ||||||
|  |       "href": "/nginx/access-list/{definitions.identity.example}", | ||||||
|  |       "access": "private", | ||||||
|  |       "method": "DELETE", | ||||||
|  |       "rel": "delete", | ||||||
|  |       "http_header": { | ||||||
|  |         "$ref": "../examples.json#/definitions/auth_header" | ||||||
|  |       }, | ||||||
|  |       "targetSchema": { | ||||||
|  |         "type": "boolean" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | } | ||||||
| @@ -31,6 +31,9 @@ | |||||||
|     }, |     }, | ||||||
|     "certificates": { |     "certificates": { | ||||||
|       "$ref": "endpoints/certificates.json" |       "$ref": "endpoints/certificates.json" | ||||||
|  |     }, | ||||||
|  |     "access-lists": { | ||||||
|  |       "$ref": "endpoints/access-lists.json" | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,8 +12,19 @@ | |||||||
|                         <input type="text" name="name" class="form-control" value="<%- name %>" required> |                         <input type="text" name="name" class="form-control" value="<%- name %>" required> | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|  |                 <div class="col-sm-6 col-md-6"> | ||||||
|  |                     <div class="form-group"> | ||||||
|  |                         <label class="form-label"><%- i18n('str', 'username') %></label> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="col-sm-6 col-md-6"> | ||||||
|  |                     <div class="form-group"> | ||||||
|  |                         <label class="form-label"><%- i18n('str', 'password') %></label> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|  |             <div class="items"><!-- items --></div> | ||||||
|         </form> |         </form> | ||||||
|     </div> |     </div> | ||||||
|     <div class="modal-footer"> |     <div class="modal-footer"> | ||||||
|   | |||||||
| @@ -4,20 +4,28 @@ const Mn              = require('backbone.marionette'); | |||||||
| const App             = require('../../main'); | const App             = require('../../main'); | ||||||
| const AccessListModel = require('../../../models/access-list'); | const AccessListModel = require('../../../models/access-list'); | ||||||
| const template        = require('./form.ejs'); | const template        = require('./form.ejs'); | ||||||
|  | const ItemView        = require('./form/item'); | ||||||
|  |  | ||||||
| require('jquery-serializejson'); | require('jquery-serializejson'); | ||||||
| require('jquery-mask-plugin'); |  | ||||||
| require('selectize'); | const ItemsView = Mn.CollectionView.extend({ | ||||||
|  |     childView: ItemView | ||||||
|  | }); | ||||||
|  |  | ||||||
| module.exports = Mn.View.extend({ | module.exports = Mn.View.extend({ | ||||||
|     template:  template, |     template:  template, | ||||||
|     className: 'modal-dialog', |     className: 'modal-dialog', | ||||||
|  |  | ||||||
|     ui: { |     ui: { | ||||||
|         form:    'form', |         items_region: '.items', | ||||||
|         buttons: '.modal-footer button', |         form:         'form', | ||||||
|         cancel:  'button.cancel', |         buttons:      '.modal-footer button', | ||||||
|         save:    'button.save' |         cancel:       'button.cancel', | ||||||
|  |         save:         'button.save' | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     regions: { | ||||||
|  |         items_region: '@ui.items_region' | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     events: { |     events: { | ||||||
| @@ -29,11 +37,28 @@ module.exports = Mn.View.extend({ | |||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             let view = this; |             let view       = this; | ||||||
|             let data = this.ui.form.serializeJSON(); |             let form_data  = this.ui.form.serializeJSON(); | ||||||
|  |             let items_data = []; | ||||||
|  |  | ||||||
|             // Manipulate |             form_data.username.map(function (val, idx) { | ||||||
|             // ... |                 if (val.trim().length) { | ||||||
|  |                     items_data.push({ | ||||||
|  |                         username: val.trim(), | ||||||
|  |                         password: form_data.password[idx] | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             if (!items_data.length) { | ||||||
|  |                 alert('You must specify at least 1 Username and Password combination'); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             let data = { | ||||||
|  |                 name:  form_data.name, | ||||||
|  |                 items: items_data | ||||||
|  |             }; | ||||||
|  |  | ||||||
|             let method = App.Api.Nginx.AccessLists.create; |             let method = App.Api.Nginx.AccessLists.create; | ||||||
|             let is_new = true; |             let is_new = true; | ||||||
| @@ -63,6 +88,22 @@ module.exports = Mn.View.extend({ | |||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  |     onRender: function () { | ||||||
|  |         let items = this.model.get('items'); | ||||||
|  |  | ||||||
|  |         // Add empty items to the end of the list. This is cheating but hey I don't have the time to do it right | ||||||
|  |         let items_to_add = 5 - items.length; | ||||||
|  |         if (items_to_add) { | ||||||
|  |             for (let i = 0; i < items_to_add; i++) { | ||||||
|  |                 items.push({}); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.showChildView('items_region', new ItemsView({ | ||||||
|  |             collection: new Backbone.Collection(items) | ||||||
|  |         })); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|     initialize: function (options) { |     initialize: function (options) { | ||||||
|         if (typeof options.model === 'undefined' || !options.model) { |         if (typeof options.model === 'undefined' || !options.model) { | ||||||
|             this.model = new AccessListModel.Model(); |             this.model = new AccessListModel.Model(); | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								src/frontend/js/app/nginx/access/form/item.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/frontend/js/app/nginx/access/form/item.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | <div class="col-sm-6 col-md-6"> | ||||||
|  |     <div class="form-group"> | ||||||
|  |         <input type="text" name="username[]" class="form-control" value="<%- typeof username !== 'undefined' ? username : '' %>"> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | <div class="col-sm-6 col-md-6"> | ||||||
|  |     <div class="form-group"> | ||||||
|  |         <input type="password" name="password[]" class="form-control" placeholder="<%- typeof hint !== 'undefined' ? hint : '' %>" value=""> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
							
								
								
									
										9
									
								
								src/frontend/js/app/nginx/access/form/item.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/frontend/js/app/nginx/access/form/item.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const Mn       = require('backbone.marionette'); | ||||||
|  | const template = require('./item.ejs'); | ||||||
|  |  | ||||||
|  | module.exports = Mn.View.extend({ | ||||||
|  |     template:  template, | ||||||
|  |     className: 'row' | ||||||
|  | }); | ||||||
| @@ -2,6 +2,7 @@ | |||||||
|   "en": { |   "en": { | ||||||
|     "str": { |     "str": { | ||||||
|       "email-address": "Email address", |       "email-address": "Email address", | ||||||
|  |       "username": "Username", | ||||||
|       "password": "Password", |       "password": "Password", | ||||||
|       "sign-in": "Sign in", |       "sign-in": "Sign in", | ||||||
|       "sign-out": "Sign out", |       "sign-out": "Sign out", | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ const model = Backbone.Model.extend({ | |||||||
|             created_on:      null, |             created_on:      null, | ||||||
|             modified_on:     null, |             modified_on:     null, | ||||||
|             name:            '', |             name:            '', | ||||||
|  |             items:           [], | ||||||
|             // The following are expansions: |             // The following are expansions: | ||||||
|             owner:           null |             owner:           null | ||||||
|         }; |         }; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user