Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ajax loading and bug fixes, multi user support #3

Merged
merged 10 commits into from
Feb 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,13 @@ By design, this system is very secure as far as controlling clients over SSH, bu

## Todo

* [ ] enable more than one user per machine
* [ ] add PIN protection in web GUI
* [ ] add "refresh" button per client in web GUI
* [ ] better error handling when SSH fails etc.
* [ ] support ssh keys
* [ ] support ssh keys
* [X] enable more than one user per machine
* [X] better error handling when SSH fails etc.
* [X] AJAX async loading
* [X] handle identically named users on diff servers

## Related projects

Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ services:
restart: unless-stopped
ports:
- "${TIMEKPR_IP:-0.0.0.0}:${TIMEKPR_PORT:-8080}:8080"
environment:
TZ: ${TIMEKPR_TZ:-America/Los_Angeles}
35 changes: 24 additions & 11 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import conf, re
from fabric import Connection
from paramiko.ssh_exception import AuthenticationException
from paramiko.ssh_exception import NoValidConnectionsError


def get_config():
Expand All @@ -12,25 +13,37 @@ def get_config():

def get_usage(user, computer, ssh):
# to do - maybe check if user is in timekpr first? (/usr/bin/timekpra --userlist)
timekpra_userinfo_output = str(ssh.run(
conf.ssh_timekpra_bin + ' --userinfo ' + user,
hide=True
))
global timekpra_userinfo_output
fail_json = {'time_left': 0, 'time_spent': 0, 'result': 'fail'}
try:
timekpra_userinfo_output = str(ssh.run(
conf.ssh_timekpra_bin + ' --userinfo ' + user,
hide=True
))
except NoValidConnectionsError as e:
print(f"Cannot connect to SSH server on host '{computer}'. "
f"Check address in conf.py or try again later.")
return fail_json
except AuthenticationException as e:
print(f"Wrong credentials for user '{conf.ssh_user}' on host '{computer}'. "
f"Check `ssh_user` and `ssh_password` credentials in conf.py.")
return fail_json
except Exception as e:
quit(f"Error logging in as user '{conf.ssh_user}' on host '{computer}', check conf.py. \n\n\t" + str(e))
return fail_json
search = r"(TIME_LEFT_DAY: )([0-9]+)"
time_left = re.search(search, timekpra_userinfo_output)
search = r"(TIME_SPENT_DAY: )([0-9]+)"
time_spent = re.search(search, timekpra_userinfo_output)
# todo - better handle "else" when we can't find time remaining
if not time_left or not time_left.group(2):
print(f"Error getting time left, setting to 0. ssh call result: " + str(timekpra_userinfo_output))
time_left = '0'
time_spent = '0'
return fail_json
else:
time_left = str(time_left.group(2))
time_spent = str(time_spent.group(2))

print(f"Time left for {user} at {computer}: {time_spent}")
return {'time_left': time_left, 'time_spent': time_spent}
print(f"Time left for {user} at {computer}: {time_left}")
return {'time_left': time_left, 'time_spent': time_spent, 'result': 'success'}


def get_connection(computer):
Expand All @@ -49,9 +62,9 @@ def get_connection(computer):
)
except AuthenticationException as e:
quit(f"Wrong credentials for user '{conf.ssh_user}' on host '{computer}'. "
f"Check `ssh_user` and `ssh_password` credentials in config.py.")
f"Check `ssh_user` and `ssh_password` credentials in conf.py.")
except Exception as e:
quit(f"Error logging in as user '{conf.ssh_user}' on host '{computer}', check config. \n\n\t" + str(e))
quit(f"Error logging in as user '{conf.ssh_user}' on host '{computer}', check conf.py. \n\n\t" + str(e))
finally:
return connection

Expand Down
218 changes: 137 additions & 81 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@
color: blue;
margin-left: 4px;
}
.loading {
color: lightgray;
}
.error {
color: red;
margin-left: 4px;
}
.user {
padding-bottom: 15px;
display: block;
}
.disabled {
border: 1px solid #ddd !important;
color: #ccc !important;
}
.failed_load {
height: 62px;
}

/* ==========================================================================
Fork Me on Github Stuffs
Expand Down Expand Up @@ -103,105 +121,143 @@
</header>

<div class="b-example-divider"></div>
<div id="people"></div>
<div id="people"></div>
</div>
<script src="{{ url_for('static', filename='bootstrap.5.1.3.min.js') }}"></script>
<script src="{{ url_for('static', filename='jquery-3.6.3.min.js') }}"></script>
<script>
$.getJSON( "/config", function( data ) {
const items = [];
const second_options = [300, 900, 1800, 2700, 3600];
$.each( data, function( computer, user ) {
let usage = $.parseJSON(get_usage(user, computer));
items.push(
"<span class='user' id='"+ user + "'>" +
"<p class='h3'>" + user + "<span class='notify'>Updated!</span><p>" +
"<span>Unused: <span class='time' id='time_left'>" + secondsToHms(usage.time_left) + "</span> </span>" +
"<span>Used: <span class='time' id='time_spent'>" + secondsToHms(usage.time_spent) + "</span></span><br/>" +
"<div class='btn-group' role='group' aria-label='Action'>" +
"<input type='radio' class='btn-check' id='add' value='add' name='action' />" +
"<label class='btn btn-outline-primary' for='add'>Add</label>" +
"<input type='radio' class='btn-check' id='remove' value='remove' name='action' />" +
"<label class='btn btn-outline-primary' for='remove'>Remove</label> " +
"</div> " +
"<div class='btn-group' role='group' aria-label='Times'>"
$.getJSON( "/config", function( data ) {
$.each( data, function( computer, user ) {
let rand_dom = '_' + uuidv4();
let times = get_times(rand_dom);
// todo - click doesn't work and radio spans accross users'
$("#people").append(
"<span class='user' id='" + user + rand_dom + "'>" +
"<p class='h3'>" + user + "<span class='notify loading'></span></p>" +
"<span class='time_wrapper'>" +
"Unused: <span class='time time_left'>...</span> " +
"Used: " +
"<span class='time time_spent'>...</span>" +
"</span><br/>" +
"<div class='btn-group' role='group' aria-label='Action'>" +
"<input type='radio' class='btn-check' id='add" + rand_dom + "' value='add' name='action' />" +
"<label class='btn btn-outline-primary' for='add" + rand_dom + "'>Add</label>" +
"<input type='radio' class='btn-check' id='remove" + rand_dom + "' value='remove' name='action' />" +
"<label class='btn btn-outline-primary' for='remove" + rand_dom + "'>Remove</label> " +
"</div> " +
"<div class='btn-group' role='group' aria-label='Times'>" +
times +
"</div>" +
"<div class='btn-group' role='group' aria-label='save'>" +
"<button class='btn btn-outline-primary save_me' " +
"value='save' user='" + user + "' computer='" + computer + "' >Save</button>" +
"</div> " +
"</span>"
);
$.each(second_options, function(index, seconds) {
items.push(
"<input type='radio' class='btn-check' value='" + seconds + "' id='the" + seconds + "' name='seconds' />" +
"<label class='btn btn-outline-primary' for='the" + seconds + "'>" + secondsToHms(seconds) + "</label>");

$('#' + user + rand_dom + " .notify").show().html("Loading...");
$('#' + user + rand_dom + ' .btn').addClass("disabled");
$('#' + user + rand_dom + ' .btn-check').addClass("disabled");

setInterval(function () {
$('#' + user + rand_dom + " .loading").fadeIn(1300).fadeOut(1300);
}, 1400);
$.getJSON( "/get_usage/" + computer + "/" + user, function( usage ) {
render_user_to_dom(user, computer, usage, rand_dom);
});
});
items.push(
"</div>" +
"<div class='btn-group' role='group' aria-label='save'>" +
"<button class='btn btn-outline-primary' " +
"id='save_me' value='save' user='" + user + "' computer='" + computer + "' >Save</button>" +
"</div> " +
"</span>"
);
get_usage();
});

$("#people").html(items.join( "" ));

$("#save_me" ).click(function() {
const user = $( this ).attr('user');
const computer = $( this ).attr('computer');
const action = $( '#' + user + ' input[name="action"]:checked').val();
const seconds = $( '#' + user + ' input[name="seconds"]:checked').val();
if(action !== undefined && seconds !== undefined){
$('input').prop('checked', false);
add_or_remove_time(user, computer, action, seconds);
$( '#' + user + ' .notify').fadeIn(function() {
function get_times(rand_dom){
const second_options = [300, 900, 1800, 2700, 3600];
let times = '';
$.each(second_options, function (index, seconds) {
times = times + "<input type='radio' class='btn-check' value='" + seconds + "' id='" + seconds + rand_dom + "' name='seconds' />" +
"<label class='btn btn-outline-primary' for='" + seconds + rand_dom + "'>" + secondsToHms(seconds) + "</label>";
});
return times;
}
function uuidv4() {
return 'xxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}

function add_click_handler(user, rand_dom){
$("#" + user + rand_dom + " .save_me" ).click(function() {
const computer = $( this ).attr('computer');
const action = $( '#' + user + rand_dom + ' input[name="action"]:checked').val();
const seconds = $( '#' + user + rand_dom + ' input[name="seconds"]:checked').val();
if(action !== undefined && seconds !== undefined){
$('input').prop('checked', false);
add_or_remove_time(user, computer, action, seconds, rand_dom);
$( '#' + user + rand_dom + ' .notify').html('Updated!').fadeIn(function() {
setTimeout(function() {
$('#' + user + rand_dom + ' .notify').fadeOut("slow");
}, 1000);
});
} else {
alert("Choose 'Add' or 'Remove' and choose an amount of time to add.");
}
});
}

function render_user_to_dom(user, computer, usage, rand_dom){
if(usage.result === 'success') {
$( '#' + user + rand_dom + ' .notify').fadeOut().html('Loaded!').fadeIn(function() {
setTimeout(function() {
$('#' + user + ' .notify').fadeOut("slow");
$('#' + user + rand_dom + ' .notify').fadeOut("slow");
}, 1000);
});
$('#' + user + rand_dom + " .time_left").html(secondsToHms(usage.time_left));
$('#' + user + rand_dom + " .time_spent").html(secondsToHms(usage.time_spent));
$('#' + user + rand_dom + " .loading").removeClass('loading');
$('#' + user + rand_dom + ' .btn').removeClass("disabled");
$('#' + user + rand_dom + ' .btn-check').removeClass("disabled");
add_click_handler(user, rand_dom);
} else {
alert("Choose 'Add' or 'Remove' and choose an amount of time to add.");
$('#' + user + rand_dom + " .time_wrapper").html(computer + " failed. Off? Bad Auth?");
$('#' + user + rand_dom + " .notify").addClass("error").html("Error");
}
});
});

function add_or_remove_time(user, computer, action, seconds){
let form;
if(action === 'add'){
form = 'increase_time'
} else {
form = 'decrease_time'
}
console.log("add_or_remove_time callded: form", form,"computer", computer,"user",user,"seconds",seconds);
$.getJSON("/" + form + "/" + computer + "/" + user + "/" + seconds, function( data ) {
$('#' + user + " #time_left").html(secondsToHms(data.time_left));
$('#' + user + " #time_spent").html(secondsToHms(data.time_spent));
});
}

function get_usage(user, computer){
return $.ajax({
type: "GET",
url: "/get_usage/" + computer + "/" + user,
async: false
}).responseText;
}

// thanks https://stackoverflow.com/a/37096512 !
function secondsToHms(d) {
if (Number(d) === 0){
return "none";

function get_buttons(user, computer, state = 'enabled'){

}

function add_or_remove_time(user, computer, action, seconds, rand_dom){
let form;
if(action === 'add'){
form = 'increase_time'
} else {
form = 'decrease_time'
}
$.getJSON("/" + form + "/" + computer + "/" + user + "/" + seconds, function( data ) {
$('#' + user + rand_dom + " .time_left").html(secondsToHms(data.time_left));
$('#' + user + rand_dom + " .time_spent").html(secondsToHms(data.time_spent));
});
}
d = Number(d);
var h = Math.floor(d / 3600);
var m = Math.floor(d % 3600 / 60);
var s = Math.floor(d % 3600 % 60);

var hDisplay = h > 0 ? h + (h === 1 ? " hr " : " hrs ") : "";
var mDisplay = m > 0 ? m + (m === 1 ? " min " : " mins ") : "";
var sDisplay = s > 0 ? s + (s === 1 ? " sec" : " secs") : "";
return hDisplay + mDisplay + sDisplay;
}
// thanks https://stackoverflow.com/a/37096512 !
function secondsToHms(d) {
if (Number(d) === 0){
return "none";
}
d = Number(d);
var h = Math.floor(d / 3600);
var m = Math.floor(d % 3600 / 60);
var s = Math.floor(d % 3600 % 60);

var hDisplay = h > 0 ? h + (h === 1 ? " hr " : " hrs ") : "";
var mDisplay = m > 0 ? m + (m === 1 ? " min " : " mins ") : "";
var sDisplay = s > 0 ? s + (s === 1 ? " sec" : " secs") : "";
return hDisplay + mDisplay + sDisplay;
}

</script>
<script src="{{ url_for('static', filename='bootstrap.5.1.3.min.js') }}"></script>
<script src="{{ url_for('static', filename='jquery-3.6.3.min.js') }}"></script>
</body>
</html>

Expand Down
Binary file modified timekpr-next-remote.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion timekpr-next-web.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def get_usage(computer, user):
return validate_request(computer, user), 500
ssh = main.get_connection(computer)
usage = main.get_usage(user, computer, ssh)
return {'result': "success", "time_left": usage['time_left'], "time_spent": usage['time_spent']}, 200
return {'result': usage['result'], "time_left": usage['time_left'], "time_spent": usage['time_spent']}, 200


@app.route("/increase_time/<computer>/<user>/<seconds>")
Expand Down