diff --git a/lib/Vroom/Constants.pm b/lib/Vroom/Constants.pm index 0fae109..5dbda81 100644 --- a/lib/Vroom/Constants.pm +++ b/lib/Vroom/Constants.pm @@ -97,6 +97,7 @@ use constant MOH => { use constant API_ACTIONS => { admin => { get_room_list => 1, + get_event_list => 1, set_persistent => 1 }, owner => { diff --git a/public/js/vroom.js b/public/js/vroom.js index f796ff8..8232bf7 100644 --- a/public/js/vroom.js +++ b/public/js/vroom.js @@ -720,6 +720,136 @@ function initAdminRooms(){ }); } +// Audit page +function initAdminAudit(){ + var eventList = {}; + var matches = 0; + + // Update display of event list + function updateEventList(filter, min, max){ + $('#loading-icon').show(); + $('#eventList').html(''); + var filterRe = new RegExp(filter, "gi"); + var i = 0; + matches = 0; + $.each(eventList, function (index, obj){ + if (filter === '' || + ( obj.event.match(filterRe) || + obj.from_ip.match(filterRe) || + obj.user.match(filterRe) || + obj.message.match(filterRe)) + ){ + matches ++; + if (i >= min && i < max){ + var t = obj.date.split(/[- :]/); + var date = utc2Local(new Date(t[0], t[1]-1, t[2], t[3], t[4], t[5])).toLocaleString(); + $('#eventList').append($('') + .append($('').html(stringEscape(obj.id)).addClass('hidden-xs')) + .append($('').html(stringEscape(date))) + .append($('').html(stringEscape(obj.from_ip))) + .append($('').html(stringEscape(obj.event))) + .append($('').html(stringEscape(obj.user)).addClass('hidden-xs')) + .append($('').html(stringEscape(obj.message))) + ); + } + i++; + } + }); + $('#loadingIcon').hide(); + $('.tablesorter').trigger('update'); + } + + function updatePagination(){ + if (matches <= itemPerPage){ + $('#pagination').hide(200); + return; + } + var total = Math.ceil(matches / itemPerPage); + if (total === 0){ + total = 1; + } + $('#pagination').bootpag({ + total: total, + maxVisible: 10, + page: 1 + }).on('page', function(e, page){ + var min = itemPerPage * (page - 1); + var max = min + itemPerPage; + updateEventList($('#searchEvent').val(), min, max); + }); + $('#pagination').show(200); + } + + function reloadEvents(start,end){ + $.ajax({ + data: { + req: JSON.stringify({ + action: 'get_event_list', + param: { + start: start, + end: end + } + }) + }, + error: function(data) { + showApiError(data); + }, + success: function(data){ + eventList = data.events; + matches = Object.keys(eventList).length; + updateEventList($('#eventSearch').val(), 0, itemPerPage); + updatePagination(); + //$('.tablesorter').tablesorter({textSorter: $.tablesorter.sortText}); + } + }); + } + + // Intercept form submission + $('#eventSearch').submit(function(e){ + e.preventDefault(); + var startObj = new Date($('#dateStart').val()); + var endObj = new Date($('#dateEnd').val()); + if (!$('#dateStart').val().match(dateRe)){ + $('#dateStart').notify(localize('ERROR_DATE_INVALID'), { + class: 'error', + position: 'bottom center' + }); + return false; + } + else if (!$('#dateEnd').val().match(dateRe)){ + $('#dateEnd').notify(localize('ERROR_DATE_INVALID'), { + class: 'error', + position: 'bottom center' + }); + return false; + } + else if (startObj > endObj){ + $('#dateEnd').notify(localize('ERROR_END_MUST_BE_AFTER_START'), { + class: 'error', + position: 'bottom center' + }); + return false; + } + else{ + reloadEvents($('#dateStart').val(),$('#dateEnd').val()); + return; + } + }); + + $('#searchEvent').on('input', function(){ + var lastInput = +new Date; + setTimeout(function(){ + if (lastInput + 500 < +new Date){ + $('#loading-icon').show(); + $('#pagination').html(''); + updateEventList($('#searchEvent').val(), 0, itemPerPage); + updatePagination(); + } + }, 600); + }); + + reloadEvents($('#dateStart').val(),$('#dateEnd').val()); +} // Started when entering a room function initJoin(room){ // Auth input if access is protected diff --git a/vroom.pl b/vroom.pl index a179243..ec9ac35 100755 --- a/vroom.pl +++ b/vroom.pl @@ -125,6 +125,18 @@ helper valid_email => sub { return Email::Valid->address($email); }; +# Validate a date in YYYY-MM-DD format +# Also accept YYYY-MM-DD hh:mm:ss +helper valid_date => sub { + my $self = shift; + my ($date) = @_; + if ($date =~ m/^\d{4}\-\d{1,2}\-\d{1,2}(\s+\d{1,2}:\d{1,2}:\d{1,2})?$/){ + return 1; + } + $self->app->log->debug("$date is not a valid date"); + return 0; +}; + ########################## # Various helpers # ########################## @@ -167,6 +179,36 @@ helper log_event => sub { return 1; }; +# Return a list of event between 2 dates +helper get_event_list => sub { + my $self = shift; + my ($start,$end) = @_; + # Check both start and end dates seems valid + if (!$self->valid_date($start) || !$self->valid_date($end)){ + $self->app->log->debug("Invalid date submitted while looking for events"); + return 0; + } + my $sth; + $sth = eval { + $self->db->prepare('SELECT * FROM `audit` + WHERE `date`>=? + AND `date`<=?'); + }; + if ($@){ + $self->app->log->debug("DB error: $@"); + return 0; + } + # We want both dates to be inclusive, as the default time is 00:00:00 + # if not given, append 23:59:59 to the end date + $sth->execute($start,$end . ' 23:59:59'); + if ($sth->err){ + $self->app->log->debug("DB error: " . $sth->errstr . " (code " . $sth->err . ")"); + return 0; + } + # Everything went fine, return the list of event as a hashref + return $sth->fetchall_hashref('id'); +}; + # Generate and manage rotation of session keys # used to sign cookies helper update_session_keys => sub { @@ -1570,6 +1612,41 @@ any '/api' => sub { } ); } + elsif ($req->{action} eq 'get_event_list'){ + my $start = $req->{param}->{start}; + my $end = $req->{param}->{end}; + if ($start eq ''){ + $start = DateTime->now->ymd; + } + if ($end eq ''){ + $end = DateTime->now->ymd; + } + # Validate input + if (!$self->valid_date($start) || !$self->valid_date($end)){ + return $self->render( + json => { + err => 'ERROR_INPUT_INVALID', + msg => $self->l('ERROR_INPUT_INVALID'), + status => 'error' + }, + ); + } + my $events = $self->get_event_list($start,$end); + foreach my $event (keys %{$events}){ + # Init NULL values to empty strings + foreach (qw(date from_ip event user message)){ + if (!$events->{$event}->{$_}){ + $events->{$event}->{$_} = ''; + } + } + } + # And send the list of event as a json object + return $self->render( + json => { + events => $events + } + ); + } # And here anonymous method, which do not require an API Key elsif ($req->{action} eq 'create_room'){ $req->{param}->{room} ||= $self->get_random_name();