From d682bc6d07833c3213d17709054c78089c14cfd2 Mon Sep 17 00:00:00 2001 From: piwind Date: Tue, 16 Sep 2025 21:46:33 +0800 Subject: [PATCH 1/9] add basePath --- src/app.js | 39 +++++++++++++++++++++++++-------- src/controllers/auth.js | 3 ++- src/routes/index.js | 16 +++++++++----- src/views/controller_layout.pug | 8 +++---- src/views/dns.pug | 2 +- src/views/head_layout.pug | 12 +++++----- src/views/index.pug | 2 +- src/views/ipAssignmentPools.pug | 6 ++--- src/views/ipAssignments.pug | 6 ++--- src/views/login.pug | 2 +- src/views/login_layout.pug | 2 +- src/views/member_delete.pug | 4 ++-- src/views/member_detail.pug | 4 ++-- src/views/network_delete.pug | 2 +- src/views/network_detail.pug | 6 ++--- src/views/network_layout.pug | 2 +- src/views/networks.pug | 2 +- src/views/not_implemented.pug | 2 +- src/views/password.pug | 2 +- src/views/routes.pug | 6 ++--- src/views/user_delete.pug | 2 +- src/views/users.pug | 6 ++--- src/views/users_layout.pug | 8 +++---- 23 files changed, 86 insertions(+), 58 deletions(-) diff --git a/src/app.js b/src/app.js index 5bbde10..fecca9e 100644 --- a/src/app.js +++ b/src/app.js @@ -22,6 +22,18 @@ const zt_controller = require('./routes/zt_controller'); const app = express(); +// Base path support for reverse proxy subpaths (e.g., Nginx location) +// Example: BASE_PATH=/ztncui -> app is served under http(s)://host/ztncui +const rawBasePath = process.env.BASE_PATH || ''; +const basePath = (function normalizeBasePath(p) { + if (!p) return ''; + if (!p.startsWith('/')) p = '/' + p; + // trim trailing slash except root + if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1); + return p; +})(rawBasePath); +app.locals.basePath = basePath; + const session_secret = Math.random().toString(36).substring(2,12); // view engine setup @@ -29,7 +41,8 @@ app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); app.use(helmet()); -app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); +// Mount favicon and static assets under base path +app.use(basePath, favicon(path.join(__dirname, 'public', 'favicon.ico'))); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); @@ -40,15 +53,23 @@ app.use(session({ })); app.use(expressValidator()); app.use(cookieParser()); -app.use(express.static(path.join(__dirname, 'public'))); -app.use('/fonts', express.static(path.join(__dirname, 'node_modules/bootstrap/fonts'))); -app.use('/bscss', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/css'))); -app.use('/jqjs', express.static(path.join(__dirname, 'node_modules/jquery/dist'))); -app.use('/bsjs', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/js'))); +// Inject basePath into templates +app.use(function(req, res, next) { + res.locals.basePath = basePath; + next(); +}); -app.use('/', index); -app.use('/users', users); -app.use('/controller', zt_controller); +// Static mounts under base path +app.use(basePath, express.static(path.join(__dirname, 'public'))); +app.use(basePath + '/fonts', express.static(path.join(__dirname, 'node_modules/bootstrap/fonts'))); +app.use(basePath + '/bscss', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/css'))); +app.use(basePath + '/jqjs', express.static(path.join(__dirname, 'node_modules/jquery/dist'))); +app.use(basePath + '/bsjs', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/js'))); + +// Route mounts under base path +app.use(basePath + '/', index); +app.use(basePath + '/users', users); +app.use(basePath + '/controller', zt_controller); // catch 404 and forward to error handler app.use(function(req, res, next) { diff --git a/src/controllers/auth.js b/src/controllers/auth.js index 8e11ecb..5453869 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -42,7 +42,8 @@ exports.restrict = function(req, res, next) { if (req.session.user) { next(); } else { + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; req.session.error = 'Access denied!'; - res.redirect('/login?redirect=' + encodeURIComponent(req.originalUrl)); + res.redirect(basePath + '/login?redirect=' + encodeURIComponent(req.originalUrl)); } } diff --git a/src/routes/index.js b/src/routes/index.js index cc255fb..6c45e06 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -13,7 +13,8 @@ const router = express.Router(); /** Redirect logged user to controler page */ function guest_only(req, res, next) { if (req.session.user) { - res.redirect('/controller'); + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + res.redirect(basePath + '/controller'); } else { next(); } @@ -26,7 +27,8 @@ router.get('/', guest_only, function(req, res, next) { router.get('/logout', function(req, res) { req.session.destroy(function() { - res.redirect('/'); + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + res.redirect(basePath + '/'); }); }); @@ -49,14 +51,18 @@ router.post('/login', async function(req, res) { req.session.user = user; req.session.success = 'Authenticated as ' + user.name; if (user.pass_set) { - res.redirect(req.query.redirect || '/controller'); + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const redirectTarget = req.query.redirect || (basePath + '/controller'); + res.redirect(redirectTarget); } else { - res.redirect('/users/' + user.name + '/password'); + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + res.redirect(basePath + '/users/' + user.name + '/password'); } }); } else { req.session.error = 'Authentication failed, please check your username and password.' - res.redirect('/login'); + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + res.redirect(basePath + '/login'); } }); }); diff --git a/src/views/controller_layout.pug b/src/views/controller_layout.pug index b0f72ee..57c59e6 100644 --- a/src/views/controller_layout.pug +++ b/src/views/controller_layout.pug @@ -6,10 +6,10 @@ extends head_layout block nav_items - +nav_item('controller_home', 'Home', '/controller') - +nav_item('users', 'Users', '/users') - +nav_item('networks', 'Networks', '/controller/networks') - +nav_item('add_network', 'Add network', '/controller/network/create') + +nav_item('controller_home', 'Home', basePath + '/controller') + +nav_item('users', 'Users', basePath + '/users') + +nav_item('networks', 'Networks', basePath + '/controller/networks') + +nav_item('add_network', 'Add network', basePath + '/controller/network/create') block body_content .container(style='margin: 50px auto 20px') diff --git a/src/views/dns.pug b/src/views/dns.pug index 87b08a4..19f008a 100644 --- a/src/views/dns.pug +++ b/src/views/dns.pug @@ -47,7 +47,7 @@ block net_content .col-sm-12 button.btn.btn-primary(type='submit') Submit = ' ' - a.btn.btn-default(href=('/controller/network/' + network.nwid) name='cancel' role='button') Cancel + a.btn.btn-default(href=(basePath + '/controller/network/' + network.nwid) name='cancel' role='button') Cancel if errors .row diff --git a/src/views/head_layout.pug b/src/views/head_layout.pug index c2be79c..97ae0c5 100644 --- a/src/views/head_layout.pug +++ b/src/views/head_layout.pug @@ -19,10 +19,10 @@ html(lang='en') title= title meta(charset='utf-8') meta(name='viewport', content='width=device-width, initial-scale=1') - link(rel='stylesheet', href='/bscss/bootstrap.min.css') - link(rel='stylesheet', href='/stylesheets/style.css') - script(src='/jqjs/jquery.min.js') - script(src='/bsjs/bootstrap.min.js') + link(rel='stylesheet', href=(basePath + '/bscss/bootstrap.min.css')) + link(rel='stylesheet', href=(basePath + '/stylesheets/style.css')) + script(src=(basePath + '/jqjs/jquery.min.js')) + script(src=(basePath + '/bsjs/bootstrap.min.js')) body nav.navbar.navbar-inverse.navbar-fixed-top .container-fluid @@ -32,7 +32,7 @@ html(lang='en') span.icon-bar span.icon-bar a.navbar-brand(href='https://key-networks.com' target='_blank') - img(src='/images/key-logo.svg' alt='Key Networks logo' height='25px' width='25px' style='display: inline') + img(src=(basePath + '/images/key-logo.svg') alt='Key Networks logo' height='25px' width='25px' style='display: inline') | Key Networks .collapse.navbar-collapse(id='BarNav') ul.nav.navbar-nav @@ -40,7 +40,7 @@ html(lang='en') ul.nav.navbar-nav.navbar-right li block nav_login - a(href='/logout') + a(href=(basePath + '/logout')) span.glyphicon.glyphicon-log-out | Logout block body_content diff --git a/src/views/index.pug b/src/views/index.pug index 6a77369..aae3f9f 100644 --- a/src/views/index.pug +++ b/src/views/index.pug @@ -19,4 +19,4 @@ block content h4 This network controller has a ZeroTier address of #{zt_status.address} h4 ZeroTier version #{zt_status.version} h4 - a(href='/controller/networks') List all networks on this network controller + a(href=(basePath + '/controller/networks')) List all networks on this network controller diff --git a/src/views/ipAssignmentPools.pug b/src/views/ipAssignmentPools.pug index 7af58bf..6487358 100644 --- a/src/views/ipAssignmentPools.pug +++ b/src/views/ipAssignmentPools.pug @@ -16,7 +16,7 @@ block net_content each ipAssignmentPool in network.ipAssignmentPools tr td(width='3%') - a(href='/controller/network/' + network.nwid + '/ipAssignmentPools/' + ipAssignmentPool.ipRangeStart + '/' + ipAssignmentPool.ipRangeEnd + '/delete') + a(href=(basePath + '/controller/network/' + network.nwid + '/ipAssignmentPools/' + ipAssignmentPool.ipRangeStart + '/' + ipAssignmentPool.ipRangeEnd + '/delete')) i.glyphicon.glyphicon-trash td= ipAssignmentPool.ipRangeStart td= ipAssignmentPool.ipRangeEnd @@ -25,7 +25,7 @@ block net_content .col-sm-12 h3 Add new IP Assignment Pool: - form(method='POST' action='/controller/network/' + network.nwid + '/ipAssignmentPools') + form(method='POST' action=(basePath + '/controller/network/' + network.nwid + '/ipAssignmentPools')) .form-group.row .col-sm-2 label(for='ipRangeStart') IP range start: @@ -42,7 +42,7 @@ block net_content .col-sm-12 button.btn.btn-primary(type='submit') Submit = ' ' - a.btn.btn-default(href='/controller/network/' + network.nwid name='cancel' role='button') Cancel + a.btn.btn-default(href=(basePath + '/controller/network/' + network.nwid) name='cancel' role='button') Cancel if errors .row diff --git a/src/views/ipAssignments.pug b/src/views/ipAssignments.pug index 26c3ee1..f2a7d90 100644 --- a/src/views/ipAssignments.pug +++ b/src/views/ipAssignments.pug @@ -32,7 +32,7 @@ block net_content each ipAssignment, index in member.ipAssignments tr td(width='3%') - a.btn.btn-link(role='button' href='/controller/network/' + network.nwid + '/member/' + member.id + '/ipAssignments/' + index + '/delete') + a.btn.btn-link(role='button' href=(basePath + '/controller/network/' + network.nwid + '/member/' + member.id + '/ipAssignments/' + index + '/delete')) i.glyphicon.glyphicon-trash td each digit in ipAssignment @@ -47,7 +47,7 @@ block net_content .row .col-sm-12 - a(href='/controller/network/' + network.nwid + '/routes') + a(href=(basePath + '/controller/network/' + network.nwid + '/routes')) h3 Managed routes table.table.table-responsive.table-striped.table-hover tr @@ -57,7 +57,7 @@ block net_content each route in network.routes tr td(width='3%') - a.btn.btn-link(role='button' href='/controller/network/' + network.nwid + '/routes/' + route.target + '/delete') + a.btn.btn-link(role='button' href=(basePath + '/controller/network/' + network.nwid + '/routes/' + route.target + '/delete')) i.glyphicon.glyphicon-trash td= route.target td= route.via diff --git a/src/views/login.pug b/src/views/login.pug index 9a131ab..392fdd3 100644 --- a/src/views/login.pug +++ b/src/views/login.pug @@ -42,4 +42,4 @@ block login_content .col-sm-10 button.btn.btn-primary(type='submit') Login = ' ' - a.btn.btn-default(href='/' name='cancel' role='button') Cancel + a.btn.btn-default(href=(basePath + '/') name='cancel' role='button') Cancel diff --git a/src/views/login_layout.pug b/src/views/login_layout.pug index a9fcb5c..8cce467 100644 --- a/src/views/login_layout.pug +++ b/src/views/login_layout.pug @@ -6,7 +6,7 @@ extends head_layout block nav_login - a(href='/login') + a(href=(basePath + '/login')) span.glyphicon.glyphicon-log-in | Login diff --git a/src/views/member_delete.pug b/src/views/member_delete.pug index e6001ea..c623162 100644 --- a/src/views/member_delete.pug +++ b/src/views/member_delete.pug @@ -9,7 +9,7 @@ block net_content if member.deleted .alert.alert-success strong #{member.name} (#{member.id}) was deleted - a.btn.btn-default(href=('/controller/network/' + network.nwid + '#members') name='networks' role='button') Members + a.btn.btn-default(href=(basePath + '/controller/network/' + network.nwid + '#members') name='networks' role='button') Members else .alert.alert-info @@ -21,7 +21,7 @@ block net_content form(method='POST' action='') button.btn.btn-primary(type='submit', name='delete') Delete #{member.name} (#{member.id}) = ' ' - a.btn.btn-default(href='/controller/network/' + network.nwid + '#members', + a.btn.btn-default(href=(basePath + '/controller/network/' + network.nwid + '#members'), name='cancel', role='button') Cancel if errors diff --git a/src/views/member_detail.pug b/src/views/member_detail.pug index 8d1369a..03a681b 100644 --- a/src/views/member_detail.pug +++ b/src/views/member_detail.pug @@ -12,9 +12,9 @@ block net_content each value, key in member .row .col-sm-2 - a(href=('/controller/network/' + member.nwid + '/member/' + member.address + '/' + key)) #{key}: + a(href=(basePath + '/controller/network/' + member.nwid + '/member/' + member.address + '/' + key)) #{key}: .col-sm-10 +json_value(value) - a.btn.btn-default(href=('/controller/network/' + member.nwid + "#members") name='networks' role='button' style='margin-top: 10px;') + a.btn.btn-default(href=(basePath + '/controller/network/' + member.nwid + "#members") name='networks' role='button' style='margin-top: 10px;') | Members diff --git a/src/views/network_delete.pug b/src/views/network_delete.pug index 00bba12..9a20733 100644 --- a/src/views/network_delete.pug +++ b/src/views/network_delete.pug @@ -16,7 +16,7 @@ block net_content form(method='POST' action='') button.btn.btn-danger(type='submit', name='delete') Delete #{network.name} (#{network.nwid}) = ' ' - a.btn.btn-default(href='/controller/networks', name='cancel', role='button') Cancel + a.btn.btn-default(href=(basePath + '/controller/networks'), name='cancel', role='button') Cancel if errors ul diff --git a/src/views/network_detail.pug b/src/views/network_detail.pug index 127dd6a..cd85764 100644 --- a/src/views/network_detail.pug +++ b/src/views/network_detail.pug @@ -19,7 +19,7 @@ block network_title | (#{network.nwid}): script. $(function() { - var nwurl = '/controller/network/#{network.nwid}'; + var nwurl = '#{basePath}/controller/network/#{network.nwid}'; var name = !{JSON.stringify(network.name)}; function toggleNameEditor(show) { @@ -51,7 +51,7 @@ block network_title }); block net_content - - const nwurl = '/controller/network/' + network.nwid; + - const nwurl = basePath + '/controller/network/' + network.nwid; a.btn.btn-primary(style="margin: 5px" href=(nwurl + '/private') role='button') = network.private ? "Private" : "Public" @@ -158,4 +158,4 @@ block net_content .col-sm-10 +json_value(value) - a.btn.btn-default(href='/controller/networks' name='networks' role='button' style='margin-top: 10px;') Networks + a.btn.btn-default(href=(basePath + '/controller/networks') name='networks' role='button' style='margin-top: 10px;') Networks diff --git a/src/views/network_layout.pug b/src/views/network_layout.pug index 2004c85..ff4c839 100644 --- a/src/views/network_layout.pug +++ b/src/views/network_layout.pug @@ -14,7 +14,7 @@ block content block network_title h2 | Network - a(href='/controller/network/' + network.nwid) #{network.name} + a(href=(basePath + '/controller/network/' + network.nwid)) #{network.name} | (#{network.nwid}): block title if title diff --git a/src/views/networks.pug b/src/views/networks.pug index 6a9e5d4..7b11ee3 100644 --- a/src/views/networks.pug +++ b/src/views/networks.pug @@ -26,7 +26,7 @@ block content th(width='37%') = '' each network in networks - - const nwurl = '/controller/network/' + network.nwid; + - const nwurl = basePath + '/controller/network/' + network.nwid; tr td a(href=nwurl + '/delete') diff --git a/src/views/not_implemented.pug b/src/views/not_implemented.pug index 8e61eca..85ee1b3 100644 --- a/src/views/not_implemented.pug +++ b/src/views/not_implemented.pug @@ -16,5 +16,5 @@ block net_content h4 | Note that you may be able to edit some properties on the strong - a(href='/controller/network/' + network.nwid + '#members') Members + a(href=(basePath + '/controller/network/' + network.nwid + '#members')) Members | page. diff --git a/src/views/password.pug b/src/views/password.pug index 09b9369..cdef7ca 100644 --- a/src/views/password.pug +++ b/src/views/password.pug @@ -51,7 +51,7 @@ block users_content .col-sm-10 button.btn.btn-primary(type='submit') Set password = ' ' - a.btn.btn-default(href='/users' name='cancel' role='button') Cancel + a.btn.btn-default(href=(basePath + '/users') name='cancel' role='button') Cancel if errors .form-group.row diff --git a/src/views/routes.pug b/src/views/routes.pug index d80c5e4..390579f 100644 --- a/src/views/routes.pug +++ b/src/views/routes.pug @@ -16,7 +16,7 @@ block net_content each route in network.routes tr td(width='3%') - a(href='/controller/network/' + network.nwid + '/routes/' + route.target + '/delete') + a(href=(basePath + '/controller/network/' + network.nwid + '/routes/' + route.target + '/delete')) i.glyphicon.glyphicon-trash td= route.target td= route.via @@ -25,7 +25,7 @@ block net_content .col-sm-12 h3 Add new route: - form(method='POST' action='/controller/network/' + network.nwid + '/routes') + form(method='POST' action=(basePath + '/controller/network/' + network.nwid + '/routes')) .form-group.row .col-sm-12 label(for='target') Target: @@ -42,7 +42,7 @@ block net_content .col-sm-12 button.btn.btn-primary(type='submit') Submit = ' ' - a.btn.btn-default(href=('/controller/network/' + network.nwid) name='cancel' role='button') Cancel + a.btn.btn-default(href=(basePath + '/controller/network/' + network.nwid) name='cancel' role='button') Cancel if errors .row diff --git a/src/views/user_delete.pug b/src/views/user_delete.pug index d9d74b6..458eda3 100644 --- a/src/views/user_delete.pug +++ b/src/views/user_delete.pug @@ -24,7 +24,7 @@ block users_content form(method='POST' action='') button.btn.btn-danger(type='submit', name='delete' value='delete') Delete #{user.name} = ' ' - a.btn.btn-default(href='/users', name='cancel', role='button') Cancel + a.btn.btn-default(href=(basePath + '/users'), name='cancel', role='button') Cancel if errors ul diff --git a/src/views/users.pug b/src/views/users.pug index 5cef684..b8451cb 100644 --- a/src/views/users.pug +++ b/src/views/users.pug @@ -10,12 +10,12 @@ block users_content each user in users tr td(width='3%') - a(href='/users/' + user.name + '/delete') + a(href=(basePath + '/users/' + user.name + '/delete')) i.glyphicon.glyphicon-trash td(width='15%') - a(href='/users/' + user.name + '/password') #{user.name} + a(href=(basePath + '/users/' + user.name + '/password')) #{user.name} td(width='82%') - a(href='/users/' + user.name + '/password') set password + a(href=(basePath + '/users/' + user.name + '/password')) set password else .alert.alert-info diff --git a/src/views/users_layout.pug b/src/views/users_layout.pug index bf5e73d..c0633e4 100644 --- a/src/views/users_layout.pug +++ b/src/views/users_layout.pug @@ -6,10 +6,10 @@ extends head_layout block nav_items - +nav_item('controller_home', 'Home', '/controller') - +nav_item('users', 'Users', '/users') - +nav_item('networks', 'Networks', '/controller/networks') - +nav_item('create_user', 'Create user', '/users/create') + +nav_item('controller_home', 'Home', basePath + '/controller') + +nav_item('users', 'Users', basePath + '/users') + +nav_item('networks', 'Networks', basePath + '/controller/networks') + +nav_item('create_user', 'Create user', basePath + '/users/create') block body_content .container(style='margin-top:50px') From 4dc9cd00f4de241c0382f2f4e8982d72b7c60289 Mon Sep 17 00:00:00 2001 From: piwind Date: Tue, 16 Sep 2025 22:55:57 +0800 Subject: [PATCH 2/9] add basePath for favicon.ico --- src/views/head_layout.pug | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/views/head_layout.pug b/src/views/head_layout.pug index 97ae0c5..4451741 100644 --- a/src/views/head_layout.pug +++ b/src/views/head_layout.pug @@ -21,6 +21,9 @@ html(lang='en') meta(name='viewport', content='width=device-width, initial-scale=1') link(rel='stylesheet', href=(basePath + '/bscss/bootstrap.min.css')) link(rel='stylesheet', href=(basePath + '/stylesheets/style.css')) + //- Explicit favicon with basePath to avoid root-level request + link(rel='icon', type='image/x-icon', href=(basePath + '/favicon.ico')) + link(rel='shortcut icon', type='image/x-icon', href=(basePath + '/favicon.ico')) script(src=(basePath + '/jqjs/jquery.min.js')) script(src=(basePath + '/bsjs/bootstrap.min.js')) body From 0d44dffed141c5b412b325f2f06300a103d95567 Mon Sep 17 00:00:00 2001 From: piwind Date: Tue, 16 Sep 2025 23:40:55 +0800 Subject: [PATCH 3/9] add basePath for back button --- src/views/network_easy.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/network_easy.pug b/src/views/network_easy.pug index 209428e..8a1aec8 100644 --- a/src/views/network_easy.pug +++ b/src/views/network_easy.pug @@ -80,7 +80,7 @@ block net_content .form-group(style='padding-top: 10px') button.btn.btn-primary(type='submit') Submit = ' ' - a.btn.btn-default(href=('/controller/network/' + network.nwid) name='cancel' role='button') Cancel + a.btn.btn-default(href=(basePath + '/controller/network/' + network.nwid) name='cancel' role='button') Cancel if errors ul From 7c14a28c66f9241916a8cbef2e1ad965242e12f7 Mon Sep 17 00:00:00 2001 From: piwind Date: Tue, 16 Sep 2025 23:41:38 +0800 Subject: [PATCH 4/9] add basePath for networkController.js --- src/controllers/networkController.js | 56 ++++++++++++++++++---------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/controllers/networkController.js b/src/controllers/networkController.js index 50c10ee..047a876 100644 --- a/src/controllers/networkController.js +++ b/src/controllers/networkController.js @@ -92,10 +92,11 @@ exports.network_list = async function(req, res) { // Display detail page for specific network exports.network_detail = async function(req, res) { + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; const navigate = { active: 'networks', - whence: '/controller/networks' + whence: basePath + '/controller/networks' } try { @@ -144,7 +145,8 @@ exports.network_create_post = async function(req, res) { } else { try { const network = await zt.network_create(name); - res.redirect('/controller/network/' + network.nwid); + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + res.redirect(basePath + '/controller/network/' + network.nwid); } catch (err) { res.render('network_detail', {title: 'Create Network - error', navigate: navigate, error: 'Error creating network ' + name.name}); } @@ -153,10 +155,11 @@ exports.network_create_post = async function(req, res) { // Display Network delete form on GET exports.network_delete_get = async function(req, res) { + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; const navigate = { active: 'networks', - whence: '/controller/networks' + whence: basePath + '/controller/networks' } try { @@ -170,10 +173,11 @@ exports.network_delete_get = async function(req, res) { // Handle Network delete on POST exports.network_delete_post = async function(req, res) { + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; const navigate = { active: 'networks', - whence: '/controller/networks' + whence: basePath + '/controller/networks' } try { @@ -194,7 +198,8 @@ exports.network_object = async function(req, res) { try { const network = await zt.network_detail(req.params.nwid); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + navigate.whence = basePath + '/controller/network/' + network.nwid; res.render(req.params.object, {title: req.params.object, navigate: navigate, network: network}, function(err, html) { if (err) { if (err.message.indexOf('Failed to lookup view') !== -1 ) { @@ -211,10 +216,11 @@ exports.network_object = async function(req, res) { // Handle Network rename form on POST exports.name = async function(req, res) { + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; const navigate = { active: 'networks', - whence: '/controller/networks' + whence: basePath + '/controller/networks' } req.checkBody('name', 'Network name required').notEmpty(); @@ -234,7 +240,7 @@ exports.name = async function(req, res) { console.error("Error renaming network " + req.params.nwid, err); } } - res.redirect('/controller/network/' + req.params.nwid); + res.redirect(basePath + '/controller/network/' + req.params.nwid); }; // ipAssignmentPools POST @@ -265,7 +271,8 @@ exports.ipAssignmentPools = async function(req, res) { if (errors) { try { const network = await zt.network_detail(req.params.nwid); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('ipAssignmentPools', {title: 'ipAssignmentPools', navigate: navigate, ipAssignmentPool: ipAssignmentPool, network: network, errors: errors}); } catch (err) { res.render('ipAssignmentPools', {title: 'ipAssignmentPools', navigate: navigate, error: 'Error resolving network detail for network ' + req.params.nwid + ': ' + err}); @@ -340,7 +347,8 @@ exports.routes = async function (req, res) { } else { try { const network = await zt.routes(req.params.nwid, route, 'add'); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('routes', {title: 'routes', navigate: navigate, route: route, network: network}); } catch (err) { res.render('routes', {title: 'routes', navigate: navigate, error: 'Error adding route for network ' + req.params.nwid + ': ' + err}); @@ -366,7 +374,8 @@ exports.route_delete = async function (req, res) { try { const network = await zt.routes(req.params.nwid, route, 'delete'); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('routes', {title: 'routes', navigate: navigate, route: route, network: network}); } catch (err) { res.render('routes', {title: 'routes', navigate: navigate, error: 'Error deleting route for network ' + req.params.nwid + ': ' + err}); @@ -390,7 +399,8 @@ exports.ipAssignmentPool_delete = async function (req, res) { try { const network = await zt.ipAssignmentPools(req.params.nwid, ipAssignmentPool, 'delete'); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('ipAssignmentPools', {title: 'ipAssignmentPools', navigate: navigate, ipAssignmentPool: ipAssignmentPool, network: network}); } catch (err) { res.render('ipAssignmentPools', {title: 'ipAssignmentPools', navigate: navigate, error: 'Error deleting IP Assignment Pool for network ' + req.params.nwid + ': ' + err}); @@ -412,7 +422,8 @@ exports.private = async function (req, res) { try { const network = await zt.network_object(req.params.nwid, private); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('private', {title: 'private', navigate: navigate, network: network}); } catch (err) { res.render('private', {title: 'private', navigate: navigate, error: 'Error applying private for network ' + req.params.nwid + ': ' + err}); @@ -434,7 +445,8 @@ exports.v4AssignMode = async function (req, res) { try { const network = await zt.network_object(req.params.nwid, v4AssignMode); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('v4AssignMode', {title: 'v4AssignMode', navigate: navigate, network: network}); } catch (err) { res.render('v4AssignMode', {title: 'v4AssignMode', navigate: navigate, error: 'Error applying v4AssignMode for network ' + req.params.nwid + ': ' + err}); @@ -461,7 +473,8 @@ exports.v6AssignMode = async function (req, res) { try { const network = await zt.network_object(req.params.nwid, v6AssignMode); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('v6AssignMode', {title: 'v6AssignMode', navigate: navigate, network: network}); } catch (err) { res.render('v6AssignMode', {title: 'v6AssignMode', navigate: navigate, error: 'Error applying v6AssignMode for network ' + req.params.nwid + ': ' + err}); @@ -490,7 +503,8 @@ exports.dns = async function (req, res) { try { const network = await zt.network_object(req.params.nwid, dns); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('dns', {title: 'dns', navigate: navigate, network: network}); } catch (err) { res.render('dns', {title: 'dns', navigate: navigate, error: 'Error updating dns for network ' + req.params.nwid + ': ' + err}); @@ -507,7 +521,8 @@ exports.member_detail = async function(req, res) { try { const {network, member} = await get_network_member(req.params.nwid, req.params.id); - navigate.whence = '/controller/network/' + network.nwid + '#members'; + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + navigate.whence = basePath + '/controller/network/' + network.nwid + '#members'; res.render('member_detail', {title: 'Network member detail', navigate: navigate, network: network, member: member}); } catch (err) { console.error(err); @@ -525,7 +540,8 @@ exports.member_object = async function(req, res) { try { const {network, member} = await get_network_member(req.params.nwid, req.params.id); - navigate.whence = '/controller/network/' + network.nwid + '#members'; + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + navigate.whence = basePath + '/controller/network/' + network.nwid + '#members'; res.render(req.params.object, {title: req.params.object, navigate: navigate, network: network, member: member}, function(err, html) { if (err) { if (err.message.indexOf('Failed to lookup view') !== -1 ) { @@ -542,10 +558,11 @@ exports.member_object = async function(req, res) { // Easy network setup GET exports.easy_get = async function(req, res) { + const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; const navigate = { active: 'networks', - whence: '/controller/network/' + req.params.nwid + whence: basePath + '/controller/network/' + req.params.nwid } try { @@ -558,10 +575,11 @@ exports.easy_get = async function(req, res) { // Easy network setup POST exports.easy_post = async function(req, res) { + const basePath2 = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; const navigate = { active: 'networks', - whence: '/controller/networks' + whence: basePath2 + '/controller/networks' } req.checkBody('networkCIDR', 'Network address is required').notEmpty(); From 0a246bca3e48b3fe52625b52b775858fa9081a7f Mon Sep 17 00:00:00 2001 From: piwind Date: Tue, 16 Sep 2025 23:42:15 +0800 Subject: [PATCH 5/9] add basePath for userController.js --- src/controllers/usersController.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/usersController.js b/src/controllers/usersController.js index 4ac89fb..cd70922 100644 --- a/src/controllers/usersController.js +++ b/src/controllers/usersController.js @@ -150,7 +150,8 @@ exports.user_create_post = async function(req, res) { active: 'create_user', } - res.redirect(307, '/users/' + req.body.username + '/password'); + const basePath = (res && res.locals && res.locals.basePath) ? res.locals.basePath : ''; + res.redirect(307, basePath + '/users/' + req.body.username + '/password'); } exports.user_delete = async function(req, res) { From 4045ac8a3bac714a5023547d4d68482144fc2804 Mon Sep 17 00:00:00 2001 From: piwind Date: Wed, 17 Sep 2025 01:23:59 +0800 Subject: [PATCH 6/9] =?UTF-8?q?=E6=89=8B=E5=B7=A5=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E9=81=97=E6=BC=8F=E7=9A=84basePath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/networkController.js | 65 ++++++++++++++++------------ src/views/head_layout.pug | 1 - 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/controllers/networkController.js b/src/controllers/networkController.js index 047a876..23a9698 100644 --- a/src/controllers/networkController.js +++ b/src/controllers/networkController.js @@ -12,6 +12,11 @@ const util = require('util'); storage.initSync({dir: 'etc/storage'}); +// Unified helper to get basePath consistently across the controller +function getBasePath(req) { + return (req.app && req.app.locals ? (req.app.locals.basePath || '') : ''); +} + async function get_network_with_members(nwid) { const [network, peers, members] = await Promise.all([ zt.network_detail(nwid), @@ -92,7 +97,7 @@ exports.network_list = async function(req, res) { // Display detail page for specific network exports.network_detail = async function(req, res) { - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); const navigate = { active: 'networks', @@ -145,7 +150,7 @@ exports.network_create_post = async function(req, res) { } else { try { const network = await zt.network_create(name); - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); res.redirect(basePath + '/controller/network/' + network.nwid); } catch (err) { res.render('network_detail', {title: 'Create Network - error', navigate: navigate, error: 'Error creating network ' + name.name}); @@ -155,7 +160,7 @@ exports.network_create_post = async function(req, res) { // Display Network delete form on GET exports.network_delete_get = async function(req, res) { - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); const navigate = { active: 'networks', @@ -173,7 +178,7 @@ exports.network_delete_get = async function(req, res) { // Handle Network delete on POST exports.network_delete_post = async function(req, res) { - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); const navigate = { active: 'networks', @@ -198,7 +203,7 @@ exports.network_object = async function(req, res) { try { const network = await zt.network_detail(req.params.nwid); - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); navigate.whence = basePath + '/controller/network/' + network.nwid; res.render(req.params.object, {title: req.params.object, navigate: navigate, network: network}, function(err, html) { if (err) { @@ -216,7 +221,7 @@ exports.network_object = async function(req, res) { // Handle Network rename form on POST exports.name = async function(req, res) { - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); const navigate = { active: 'networks', @@ -271,7 +276,7 @@ exports.ipAssignmentPools = async function(req, res) { if (errors) { try { const network = await zt.network_detail(req.params.nwid); - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('ipAssignmentPools', {title: 'ipAssignmentPools', navigate: navigate, ipAssignmentPool: ipAssignmentPool, network: network, errors: errors}); } catch (err) { @@ -280,7 +285,8 @@ exports.ipAssignmentPools = async function(req, res) { } else { try { const network = await zt.ipAssignmentPools(req.params.nwid, ipAssignmentPool, 'add'); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = getBasePath(req); + navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('ipAssignmentPools', {title: 'ipAssignmentPools', navigate: navigate, ipAssignmentPool: ipAssignmentPool, network: network}); } catch (err) { res.render('ipAssignmentPools', {title: 'ipAssignmentPools', navigate: navigate, error: 'Error applying IP Assignment Pools for network ' + req.params.nwid + ': ' + err}); @@ -339,7 +345,8 @@ exports.routes = async function (req, res) { if (errors) { try { const network = await zt.network_detail(req.params.nwid); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = getBasePath(req); + navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('routes', {title: 'routes', navigate: navigate, route: route, network: network, errors: errors}); } catch (err) { res.render('routes', {title: 'routes', navigate: navigate, error: 'Error resolving network detail'}); @@ -347,7 +354,7 @@ exports.routes = async function (req, res) { } else { try { const network = await zt.routes(req.params.nwid, route, 'add'); - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('routes', {title: 'routes', navigate: navigate, route: route, network: network}); } catch (err) { @@ -374,7 +381,7 @@ exports.route_delete = async function (req, res) { try { const network = await zt.routes(req.params.nwid, route, 'delete'); - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('routes', {title: 'routes', navigate: navigate, route: route, network: network}); } catch (err) { @@ -399,7 +406,7 @@ exports.ipAssignmentPool_delete = async function (req, res) { try { const network = await zt.ipAssignmentPools(req.params.nwid, ipAssignmentPool, 'delete'); - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('ipAssignmentPools', {title: 'ipAssignmentPools', navigate: navigate, ipAssignmentPool: ipAssignmentPool, network: network}); } catch (err) { @@ -422,7 +429,7 @@ exports.private = async function (req, res) { try { const network = await zt.network_object(req.params.nwid, private); - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('private', {title: 'private', navigate: navigate, network: network}); } catch (err) { @@ -445,7 +452,7 @@ exports.v4AssignMode = async function (req, res) { try { const network = await zt.network_object(req.params.nwid, v4AssignMode); - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('v4AssignMode', {title: 'v4AssignMode', navigate: navigate, network: network}); } catch (err) { @@ -473,7 +480,7 @@ exports.v6AssignMode = async function (req, res) { try { const network = await zt.network_object(req.params.nwid, v6AssignMode); - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('v6AssignMode', {title: 'v6AssignMode', navigate: navigate, network: network}); } catch (err) { @@ -503,7 +510,7 @@ exports.dns = async function (req, res) { try { const network = await zt.network_object(req.params.nwid, dns); - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('dns', {title: 'dns', navigate: navigate, network: network}); } catch (err) { @@ -521,7 +528,7 @@ exports.member_detail = async function(req, res) { try { const {network, member} = await get_network_member(req.params.nwid, req.params.id); - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); navigate.whence = basePath + '/controller/network/' + network.nwid + '#members'; res.render('member_detail', {title: 'Network member detail', navigate: navigate, network: network, member: member}); } catch (err) { @@ -540,7 +547,7 @@ exports.member_object = async function(req, res) { try { const {network, member} = await get_network_member(req.params.nwid, req.params.id); - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); navigate.whence = basePath + '/controller/network/' + network.nwid + '#members'; res.render(req.params.object, {title: req.params.object, navigate: navigate, network: network, member: member}, function(err, html) { if (err) { @@ -558,7 +565,7 @@ exports.member_object = async function(req, res) { // Easy network setup GET exports.easy_get = async function(req, res) { - const basePath = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); const navigate = { active: 'networks', @@ -575,11 +582,11 @@ exports.easy_get = async function(req, res) { // Easy network setup POST exports.easy_post = async function(req, res) { - const basePath2 = req.app && req.app.locals ? (req.app.locals.basePath || '') : ''; + const basePath = getBasePath(req); const navigate = { active: 'networks', - whence: basePath2 + '/controller/networks' + whence: basePath + '/controller/networks' } req.checkBody('networkCIDR', 'Network address is required').notEmpty(); @@ -649,7 +656,7 @@ exports.members = async function(req, res) { const navigate = { active: 'networks', - whence: '/controller/networks' + whence: getBasePath(req) + '/controller/networks' } let errors = null; @@ -713,7 +720,8 @@ exports.members = async function(req, res) { } } } else { // GET - res.redirect("/controller/network/" + req.params.nwid + "#members"); + const basePath = getBasePath(req); + res.redirect(basePath + "/controller/network/" + req.params.nwid + "#members"); } } @@ -740,7 +748,8 @@ exports.member_delete = async function(req, res) { } member.name = name || ''; - navigate.whence = '/controller/network/' + network.nwid; + const basePath = getBasePath(req); + navigate.whence = basePath + '/controller/network/' + network.nwid; res.render('member_delete', {title: 'Delete member from ' + network.name, navigate: navigate, network: network, member: member}); } catch (err) { @@ -761,12 +770,13 @@ exports.delete_ip = async function(req, res) { try { const network = await zt.network_detail(req.params.nwid); let member = await zt.member_detail(req.params.nwid, req.params.id); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = getBasePath(req); + navigate.whence = basePath + '/controller/network/' + network.nwid; member.name = await storage.getItem(member.id) | ''; if (req.params.index) { member = await zt.ipAssignmentDelete(network.nwid, member.id, req.params.index); - res.redirect('/controller/network/' + network.nwid + '/member/' + + res.redirect(basePath + '/controller/network/' + network.nwid + '/member/' + member.id + '/ipAssignments'); } res.render('ipAssignments', {title: 'ipAssignments ' + network.name, @@ -820,7 +830,8 @@ exports.assign_ip = async function(req, res) { try { let member = await zt.member_detail(req.params.nwid, req.params.id); - navigate.whence = '/controller/network/' + network.nwid; + const basePath = getBasePath(req); + navigate.whence = basePath + '/controller/network/' + network.nwid; if (!errors) { member = await zt.ipAssignmentAdd(network.nwid, member.id, ipAssignment); diff --git a/src/views/head_layout.pug b/src/views/head_layout.pug index 4451741..56da6f2 100644 --- a/src/views/head_layout.pug +++ b/src/views/head_layout.pug @@ -21,7 +21,6 @@ html(lang='en') meta(name='viewport', content='width=device-width, initial-scale=1') link(rel='stylesheet', href=(basePath + '/bscss/bootstrap.min.css')) link(rel='stylesheet', href=(basePath + '/stylesheets/style.css')) - //- Explicit favicon with basePath to avoid root-level request link(rel='icon', type='image/x-icon', href=(basePath + '/favicon.ico')) link(rel='shortcut icon', type='image/x-icon', href=(basePath + '/favicon.ico')) script(src=(basePath + '/jqjs/jquery.min.js')) From 9911bedd2aae27d4b254a3fdcbd58d3c694b0851 Mon Sep 17 00:00:00 2001 From: piwind Date: Wed, 17 Sep 2025 01:25:03 +0800 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20=E8=AE=BE=E7=BD=AEmember=20name?= =?UTF-8?q?=E6=97=B6=E7=9A=84POST=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/network_detail.pug | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/views/network_detail.pug b/src/views/network_detail.pug index cd85764..0005fb9 100644 --- a/src/views/network_detail.pug +++ b/src/views/network_detail.pug @@ -75,6 +75,18 @@ block net_content $('.text').on('change', function() { $.post(url, {'id': this.name, 'name': this.value}); }); + // Prevent form submit on Enter which would POST to the page URL + $('form').on('submit', function(e) { + e.preventDefault(); + return false; + }); + // If Enter is pressed inside the name input, prevent submit and trigger change + $('.text').on('keypress', function(e) { + if (e.which == 13) { + e.preventDefault(); + $(this).blur(); + } + }); }); h3#members Members (#{members.length}) form(method='POST' action='') @@ -154,7 +166,7 @@ block net_content each value, key in network .row(style='margin: 5px 0;') .col-sm-2 - a(href= network.nwid + '/' + key) #{key}: + a(href= nwurl + '/' + key) #{key}: .col-sm-10 +json_value(value) From 417fd4902f302b88525c25a8683b3e5c046253c7 Mon Sep 17 00:00:00 2001 From: piwind Date: Wed, 17 Sep 2025 01:27:30 +0800 Subject: [PATCH 8/9] update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index b5c8ca2..11aa4da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.swp Release/ Staging/ + +node_modules/ From b3112f7cb6f413c87fe99512e44cd9ad5881b4ec Mon Sep 17 00:00:00 2001 From: piwind Date: Wed, 17 Sep 2025 01:41:53 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=92=8CAJAX=E6=9B=B4=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=93=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/networkController.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/controllers/networkController.js b/src/controllers/networkController.js index 23a9698..3d5b947 100644 --- a/src/controllers/networkController.js +++ b/src/controllers/networkController.js @@ -719,6 +719,14 @@ exports.members = async function(req, res) { } } } + + // Respond based on validation result + if (errors) { + return res.status(400).json({ errors }); + } + + // No content needed for AJAX updates + return res.sendStatus(204); } else { // GET const basePath = getBasePath(req); res.redirect(basePath + "/controller/network/" + req.params.nwid + "#members");