1
0
Fork 0
mirror of https://github.com/Luzifer/vault-otp-ui.git synced 2024-11-08 08:10:11 +00:00

Implement pre-fetching for next codes

to provide a more seamless experience

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2018-03-23 12:32:03 +01:00
parent c120d8864d
commit 34214190f8
Signed by: luzifer
GPG key ID: DC2729FDD34BE99E
5 changed files with 326 additions and 179 deletions

View file

@ -1,5 +1,12 @@
currentTimeout = 0
clipboard = undefined
clipboard = null
preFetch = null
fetchInProgress = false
serverConnectionError = false
iterationCurrent = 'current'
iterationNext = 'next'
# document-ready function to start Javascript processing
$ ->
@ -38,20 +45,38 @@ delay = (delayMSecs, fkt) ->
window.setTimeout fkt, delayMSecs
# fetchCodes contacts the backend to receive JSON containing current codes
fetchCodes = () ->
fetchCodes = (iteration) ->
if fetchInProgress
return
fetchInProgress = true
if iteration == iterationCurrent
successFunc = updateCodes
else
successFunc = updatePreFetch
if iteration == iterationCurrent and preFetch != null
data = preFetch
preFetch = null
successFunc data
return
$.ajax
url: 'codes.json',
success: updateCodes,
url: "codes.json?it=#{iteration}",
success: successFunc,
dataType: 'json',
error: () ->
createAlert 'danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? I will retry in a few seconds.', 5000
delay 5000, fetchCodes
fetchInProgress = false
createAlert 'danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? Reload to try again.', 0
serverConnectionError = true
statusCode:
401: () ->
window.location.reload()
500: () ->
createAlert 'danger', 'Oops.', 'The server responded with an internal error. I will retry in a few seconds.', 2000
delay 2000, fetchCodes
fetchInProgress = false
createAlert 'danger', 'Oops.', 'The server responded with an internal error. Reload to try again.', 0
serverConnectionError = true
# filterChange is called when changing the filter field and matches the
# titles of all shown entries. Those not matching the given regular expression
@ -71,7 +96,7 @@ initializeApplication = () ->
$('#keylist').empty()
$('#filter').bind 'keyup', filterChange
tick 500, refreshTimerProgress
fetchCodes()
fetchCodes iterationCurrent
# refreshTimerProgress updates the top progressbar to display the
# remaining time until the one-time-passwords changes
@ -79,6 +104,10 @@ refreshTimerProgress = () ->
secondsLeft = timeLeft()
$('#timer').css 'width', "#{secondsLeft / 30 * 100}%"
if secondsLeft < 10 and preFetch == null and not serverConnectionError
# Do a pre-fetch to provide a seamless experience
fetchCodes iterationNext
# tick is a convenience wrapper to swap parameters of setInterval
tick = (delay, fkt) ->
window.setInterval fkt, delay
@ -108,4 +137,10 @@ updateCodes = (data) ->
filterChange()
delay timeLeft()*1000, fetchCodes
delay timeLeft()*1000, ->
fetchCodes iterationCurrent
fetchInProgress = false
updatePreFetch = (data) ->
preFetch = data
fetchInProgress = false

View file

@ -1,10 +1,20 @@
// Generated by CoffeeScript 1.12.4
(function() {
var clipboard, createAlert, createOTPItem, currentTimeout, delay, fetchCodes, filterChange, initializeApplication, refreshTimerProgress, tick, timeLeft, updateCodes;
var clipboard, createAlert, createOTPItem, currentTimeout, delay, fetchCodes, fetchInProgress, filterChange, initializeApplication, iterationCurrent, iterationNext, preFetch, refreshTimerProgress, serverConnectionError, tick, timeLeft, updateCodes, updatePreFetch;
currentTimeout = 0;
clipboard = void 0;
clipboard = null;
preFetch = null;
fetchInProgress = false;
serverConnectionError = false;
iterationCurrent = 'current';
iterationNext = 'next';
$(function() {
if ($('body').hasClass('state-signedin')) {
@ -41,22 +51,40 @@
return window.setTimeout(fkt, delayMSecs);
};
fetchCodes = function() {
fetchCodes = function(iteration) {
var data, successFunc;
if (fetchInProgress) {
return;
}
fetchInProgress = true;
if (iteration === iterationCurrent) {
successFunc = updateCodes;
} else {
successFunc = updatePreFetch;
}
if (iteration === iterationCurrent && preFetch !== null) {
data = preFetch;
preFetch = null;
successFunc(data);
return;
}
return $.ajax({
url: 'codes.json',
success: updateCodes,
url: "codes.json?it=" + iteration,
success: successFunc,
dataType: 'json',
error: function() {
createAlert('danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? I will retry in a few seconds.', 5000);
return delay(5000, fetchCodes);
fetchInProgress = false;
createAlert('danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? Reload to try again.', 0);
return serverConnectionError = true;
},
statusCode: {
401: function() {
return window.location.reload();
},
500: function() {
createAlert('danger', 'Oops.', 'The server responded with an internal error. I will retry in a few seconds.', 2000);
return delay(2000, fetchCodes);
fetchInProgress = false;
createAlert('danger', 'Oops.', 'The server responded with an internal error. Reload to try again.', 0);
return serverConnectionError = true;
}
}
});
@ -78,13 +106,16 @@
$('#keylist').empty();
$('#filter').bind('keyup', filterChange);
tick(500, refreshTimerProgress);
return fetchCodes();
return fetchCodes(iterationCurrent);
};
refreshTimerProgress = function() {
var secondsLeft;
secondsLeft = timeLeft();
return $('#timer').css('width', (secondsLeft / 30 * 100) + "%");
$('#timer').css('width', (secondsLeft / 30 * 100) + "%");
if (secondsLeft < 10 && preFetch === null && !serverConnectionError) {
return fetchCodes(iterationNext);
}
};
tick = function(delay, fkt) {
@ -115,7 +146,15 @@
}
});
filterChange();
return delay(timeLeft() * 1000, fetchCodes);
delay(timeLeft() * 1000, function() {
return fetchCodes(iterationCurrent);
});
return fetchInProgress = false;
};
updatePreFetch = function(data) {
preFetch = data;
return fetchInProgress = false;
};
}).call(this);

369
assets.go

File diff suppressed because one or more lines are too long

10
main.go
View file

@ -207,7 +207,12 @@ func handleCodesJSON(res http.ResponseWriter, r *http.Request) {
}
log.WithFields(log.Fields{"token": hashSecret(tok)}).Debugf("Checked / renewed token")
tokens, err := getSecretsFromVault(tok)
pointOfTime := time.Now()
if r.URL.Query().Get("it") == "next" {
pointOfTime = pointOfTime.Add(time.Duration(30-(pointOfTime.Second()%30)) * time.Second)
}
tokens, err := getSecretsFromVault(tok, pointOfTime)
if err != nil {
log.Errorf("Unable to fetch codes: %s", err)
http.Error(res, `{"error":"Unexpected error while fetching tokens"}`, http.StatusInternalServerError)
@ -221,13 +226,12 @@ func handleCodesJSON(res http.ResponseWriter, r *http.Request) {
return
}
n := time.Now()
result := struct {
Tokens []*token `json:"tokens"`
NextWrap time.Time `json:"next_wrap"`
}{
Tokens: tokens,
NextWrap: n.Add(time.Duration(30-(n.Second()%30)) * time.Second),
NextWrap: pointOfTime.Add(time.Duration(30-(pointOfTime.Second()%30)) * time.Second),
}
res.Header().Set("Content-Type", "application/json")

View file

@ -79,7 +79,7 @@ func useOrRenewToken(tok, accessToken string) (string, error) {
}
}
func getSecretsFromVault(tok string) ([]*token, error) {
func getSecretsFromVault(tok string, pointOfTime time.Time) ([]*token, error) {
client, err := api.NewClient(&api.Config{
Address: cfg.Vault.Address,
})
@ -112,7 +112,7 @@ func getSecretsFromVault(tok string) ([]*token, error) {
case key := <-scanPool:
go scanKeyForSubKeys(client, key, scanPool, keyPoolChan, wg)
case key := <-keyPoolChan:
go fetchTokenFromKey(client, key, respChan, wg)
go fetchTokenFromKey(client, key, respChan, wg, pointOfTime)
case t := <-respChan:
resp = append(resp, t)
case <-done:
@ -159,7 +159,7 @@ func scanKeyForSubKeys(client *api.Client, key string, subKeyChan, tokenKeyChan
}
}
func fetchTokenFromKey(client *api.Client, k string, respChan chan *token, wg *sync.WaitGroup) {
func fetchTokenFromKey(client *api.Client, k string, respChan chan *token, wg *sync.WaitGroup, pointOfTime time.Time) {
defer wg.Done()
data, err := client.Logical().Read(k)
@ -182,7 +182,7 @@ func fetchTokenFromKey(client *api.Client, k string, respChan chan *token, wg *s
switch k {
case cfg.Vault.SecretField:
tok.Secret = v.(string)
tok.GenerateCode(time.Now())
tok.GenerateCode(pointOfTime)
case "code":
tok.Code = v.(string)
case "name":