Implemented handling of client limits, and some general tweaks

- The create button is now disabled when reaching the client limit
 - The client list is now always sorted by case-insensitive config name
 - Focus moves to the form when opening a dialog
 - Fixed some inconsistent use of snake_case vs perlCase
This commit is contained in:
Erik Thuning 2025-03-04 11:14:02 +01:00
parent d8dcbc6d6b
commit 49abf8ad21
4 changed files with 82 additions and 28 deletions

@ -17,6 +17,7 @@ public_paths = [login_path, callback_path]
user_cookie = 'username'
token_cookie = 'token'
access_cookie = 'access'
limit_cookie = 'max_clients'
permitted_format = re.compile('^[A-Za-z0-9-]+$')
config = ConfigParser()
@ -62,6 +63,10 @@ def setup() -> None:
@app.after_request
def reload(response: Response) -> Response:
response.set_cookie(limit_cookie,
config['wireguard']['user_client_limit'],
secure=True,
samesite='Strict')
if app.wg.user_name:
response.set_cookie(user_cookie,
app.wg.user_name,
@ -122,7 +127,7 @@ def create_config(config_id: str) -> dict:
return fail('Id already in use')
except ClientLimitError as e:
return fail(e.message)
return get_config(config_id)
return {'result': 'success'}
@app.route('/configs/<config_id>/update', methods=['POST'])
def update_config(config_id: str) -> dict:

@ -107,6 +107,9 @@ def run_wg(*args, input: str=None):
return result.stdout
def run_command(command, *args) -> None:
# Temporarily disabling all external calls
return
# The command must be called on an absolute path so that it's possible
# to set up a safe sudoers rule for it.
command_path = Path().joinpath('commands.sh')

@ -1,7 +1,7 @@
'use strict';
(function() {
window.addEventListener('DOMContentLoaded', (event) => {
setupPage();
setup_page();
});
function close_modal(node) {
@ -44,34 +44,45 @@
'/configs/' + id + '/create',
{'name': name,
'description': description})
.then((response) => {display_configs(id)})
.then((response) => {close_modal(form)})
.then((response) => {
if(response.result == 'success') {
display_configs(id);
close_modal(form);
} else {
throw new Error(response.reason);
}
})
.catch((exception) => {
console.error('creation failed for: '+id, exception);
});
});
display_modal(template);
display_modal(template, form.name);
}
function display_configs(...config_ids) {
const configs_parent = document.querySelector('configs');
const dlprefix = 'data:text/plain;charset:utf-8,';
config_ids.forEach((config_id) => {
const old = configs_parent.querySelector('#config-'+config_id);
if(old) {
old.parentNode.removeChild(old);
}
const template = document.querySelector('template#display-config')
.content.cloneNode(true);
const config = template.querySelector('config');
make_api_request('GET', '/configs/' + config_id)
.then((data) => {
const qr = QRCode({msg: data.data,
dim: 280,
pad: 0,
ecl: 'M'});
qr.setAttribute('aria-label', 'QR code');
config.id = 'config-' + config_id;
config.querySelector('name').textContent = data.name;
config.querySelector('date').textContent = data.created;
config.querySelector('description').textContent = data.description;
config.querySelector('data').textContent = data.data;
config.querySelector('qrcode').replaceWith(
QRCode({msg: data.data,
dim: 280,
pad: 0,
ecl: 'M'}));
config.querySelector('qrcode').replaceWith(qr);
const link = config.querySelector('.conffile');
link.setAttribute('href',
dlprefix + encodeURIComponent(data.data));
@ -84,14 +95,22 @@
.addEventListener('click', (event) => {
link.click();
});
const config_children = configs_parent.children;
for(let i = 0; i < config_children.length; i++) {
const child = config_children[i];
if(child.nodeName === 'PLACEHOLDER') {
configs_parent.insertBefore(template, child);
break;
}
const name = child.querySelector('name').textContent;
if(data.name.toLowerCase() < name.toLowerCase()) {
configs_parent.insertBefore(template, child);
}
}
});
const old = configs_parent.querySelector('#config-'+config_id);
if(old) {
old.replaceWith(template);
} else {
configs_parent.appendChild(template);
}
});
update_create_button();
}
function display_edit_form(config_id, config) {
@ -102,15 +121,18 @@
form.description.value = config.description;
form.addEventListener('submit', (event) => {
event.preventDefault();
});
const save_button = form.querySelector('button.save');
save_button.addEventListener('click', (event) => {
make_api_request('POST',
'/configs/' + config_id + '/update',
{'name': form.name.value,
'description': form.description.value.trim()})
.then((response) => {display_configs(config_id)})
.then((response) => {close_modal(form)})
.then((response) => {
if(response.result == 'success') {
display_configs(config_id);
close_modal(form);
} else {
throw new Error(response.reason);
}
})
.catch((exception) => {
console.error('update failed for: '+config_id,
exception);
@ -125,17 +147,18 @@
'/configs/' + config_id + '/delete')
.then((response) => {
document.querySelector('#config-'+config_id).remove();
update_create_button();
close_modal(form);
})
.then((response) => {close_modal(form)})
.catch((exception) => {
console.error('deletion failed for: '+config_id,
exception);
});
});
display_modal(template);
display_modal(template, form.name);
}
function display_modal(fragment) {
function display_modal(fragment, focus_element) {
const modal = document.querySelector('template#modal')
.content.cloneNode(true);
modal.querySelector('wrapper').appendChild(fragment);
@ -150,9 +173,10 @@
});
document.querySelector('body').appendChild(modal);
focus_element.focus();
}
function getCookies() {
function get_cookies() {
var out = new Object();
const cookies = document.cookie.split('; ');
cookies.forEach((cookie) => {
@ -179,7 +203,7 @@
const request = new Request('/api' + path, data);
const response = await fetch(request);
if(response.status === 403) {
const cookies = getCookies();
const cookies = get_cookies();
const access = cookies['access'];
if(access === 'denied') {
throw new Error('access denied');
@ -189,10 +213,26 @@
return response.json();
}
async function setupPage(route) {
function update_create_button() {
const visible_configs = document.querySelectorAll('configs > config');
const cookies = get_cookies();
const max_clients = cookies['max_clients'];
let button_disabled = false;
let button_message = "Add a client";
if(max_clients > 0 && visible_configs.length >= max_clients) {
button_disabled = true;
button_message = "Limit of "+max_clients+" clients reached";
}
const button = document.querySelector('button#create-config');
button.disabled = button_disabled;
button.innerHTML = button_message;
}
async function setup_page(route) {
try {
const configs = await make_api_request('GET', '/configs/');
const cookies = getCookies();
const cookies = get_cookies();
document.querySelector('user#banner-userid')
.textContent = cookies['username'];
document.querySelector('button#create-config')

@ -231,6 +231,12 @@ button:hover {
background-color: #33587F;
}
button:disabled {
color: #000000;
background-color: #BABABA;
border: 2px solid #DADADA;
}
button#create-config {
font-size: 1.2rem;
}