mirror of
https://github.com/Luzifer/vault-otp-ui.git
synced 2024-11-09 16:50:05 +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:
parent
c120d8864d
commit
34214190f8
5 changed files with 326 additions and 179 deletions
|
@ -1,5 +1,12 @@
|
||||||
currentTimeout = 0
|
currentTimeout = 0
|
||||||
clipboard = undefined
|
clipboard = null
|
||||||
|
preFetch = null
|
||||||
|
|
||||||
|
fetchInProgress = false
|
||||||
|
serverConnectionError = false
|
||||||
|
|
||||||
|
iterationCurrent = 'current'
|
||||||
|
iterationNext = 'next'
|
||||||
|
|
||||||
# document-ready function to start Javascript processing
|
# document-ready function to start Javascript processing
|
||||||
$ ->
|
$ ->
|
||||||
|
@ -38,20 +45,38 @@ delay = (delayMSecs, fkt) ->
|
||||||
window.setTimeout fkt, delayMSecs
|
window.setTimeout fkt, delayMSecs
|
||||||
|
|
||||||
# fetchCodes contacts the backend to receive JSON containing current codes
|
# 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
|
$.ajax
|
||||||
url: 'codes.json',
|
url: "codes.json?it=#{iteration}",
|
||||||
success: updateCodes,
|
success: successFunc,
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
error: () ->
|
error: () ->
|
||||||
createAlert 'danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? I will retry in a few seconds.', 5000
|
fetchInProgress = false
|
||||||
delay 5000, fetchCodes
|
createAlert 'danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? Reload to try again.', 0
|
||||||
|
serverConnectionError = true
|
||||||
statusCode:
|
statusCode:
|
||||||
401: () ->
|
401: () ->
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
500: () ->
|
500: () ->
|
||||||
createAlert 'danger', 'Oops.', 'The server responded with an internal error. I will retry in a few seconds.', 2000
|
fetchInProgress = false
|
||||||
delay 2000, fetchCodes
|
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
|
# filterChange is called when changing the filter field and matches the
|
||||||
# titles of all shown entries. Those not matching the given regular expression
|
# titles of all shown entries. Those not matching the given regular expression
|
||||||
|
@ -71,7 +96,7 @@ initializeApplication = () ->
|
||||||
$('#keylist').empty()
|
$('#keylist').empty()
|
||||||
$('#filter').bind 'keyup', filterChange
|
$('#filter').bind 'keyup', filterChange
|
||||||
tick 500, refreshTimerProgress
|
tick 500, refreshTimerProgress
|
||||||
fetchCodes()
|
fetchCodes iterationCurrent
|
||||||
|
|
||||||
# refreshTimerProgress updates the top progressbar to display the
|
# refreshTimerProgress updates the top progressbar to display the
|
||||||
# remaining time until the one-time-passwords changes
|
# remaining time until the one-time-passwords changes
|
||||||
|
@ -79,6 +104,10 @@ refreshTimerProgress = () ->
|
||||||
secondsLeft = timeLeft()
|
secondsLeft = timeLeft()
|
||||||
$('#timer').css 'width', "#{secondsLeft / 30 * 100}%"
|
$('#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 is a convenience wrapper to swap parameters of setInterval
|
||||||
tick = (delay, fkt) ->
|
tick = (delay, fkt) ->
|
||||||
window.setInterval fkt, delay
|
window.setInterval fkt, delay
|
||||||
|
@ -108,4 +137,10 @@ updateCodes = (data) ->
|
||||||
|
|
||||||
filterChange()
|
filterChange()
|
||||||
|
|
||||||
delay timeLeft()*1000, fetchCodes
|
delay timeLeft()*1000, ->
|
||||||
|
fetchCodes iterationCurrent
|
||||||
|
fetchInProgress = false
|
||||||
|
|
||||||
|
updatePreFetch = (data) ->
|
||||||
|
preFetch = data
|
||||||
|
fetchInProgress = false
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
// Generated by CoffeeScript 1.12.4
|
// Generated by CoffeeScript 1.12.4
|
||||||
(function() {
|
(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;
|
currentTimeout = 0;
|
||||||
|
|
||||||
clipboard = void 0;
|
clipboard = null;
|
||||||
|
|
||||||
|
preFetch = null;
|
||||||
|
|
||||||
|
fetchInProgress = false;
|
||||||
|
|
||||||
|
serverConnectionError = false;
|
||||||
|
|
||||||
|
iterationCurrent = 'current';
|
||||||
|
|
||||||
|
iterationNext = 'next';
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
if ($('body').hasClass('state-signedin')) {
|
if ($('body').hasClass('state-signedin')) {
|
||||||
|
@ -41,22 +51,40 @@
|
||||||
return window.setTimeout(fkt, delayMSecs);
|
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({
|
return $.ajax({
|
||||||
url: 'codes.json',
|
url: "codes.json?it=" + iteration,
|
||||||
success: updateCodes,
|
success: successFunc,
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
error: function() {
|
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);
|
fetchInProgress = false;
|
||||||
return delay(5000, fetchCodes);
|
createAlert('danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? Reload to try again.', 0);
|
||||||
|
return serverConnectionError = true;
|
||||||
},
|
},
|
||||||
statusCode: {
|
statusCode: {
|
||||||
401: function() {
|
401: function() {
|
||||||
return window.location.reload();
|
return window.location.reload();
|
||||||
},
|
},
|
||||||
500: function() {
|
500: function() {
|
||||||
createAlert('danger', 'Oops.', 'The server responded with an internal error. I will retry in a few seconds.', 2000);
|
fetchInProgress = false;
|
||||||
return delay(2000, fetchCodes);
|
createAlert('danger', 'Oops.', 'The server responded with an internal error. Reload to try again.', 0);
|
||||||
|
return serverConnectionError = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -78,13 +106,16 @@
|
||||||
$('#keylist').empty();
|
$('#keylist').empty();
|
||||||
$('#filter').bind('keyup', filterChange);
|
$('#filter').bind('keyup', filterChange);
|
||||||
tick(500, refreshTimerProgress);
|
tick(500, refreshTimerProgress);
|
||||||
return fetchCodes();
|
return fetchCodes(iterationCurrent);
|
||||||
};
|
};
|
||||||
|
|
||||||
refreshTimerProgress = function() {
|
refreshTimerProgress = function() {
|
||||||
var secondsLeft;
|
var secondsLeft;
|
||||||
secondsLeft = timeLeft();
|
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) {
|
tick = function(delay, fkt) {
|
||||||
|
@ -115,7 +146,15 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
filterChange();
|
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);
|
}).call(this);
|
||||||
|
|
369
assets.go
369
assets.go
File diff suppressed because one or more lines are too long
10
main.go
10
main.go
|
@ -207,7 +207,12 @@ func handleCodesJSON(res http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
log.WithFields(log.Fields{"token": hashSecret(tok)}).Debugf("Checked / renewed token")
|
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 {
|
if err != nil {
|
||||||
log.Errorf("Unable to fetch codes: %s", err)
|
log.Errorf("Unable to fetch codes: %s", err)
|
||||||
http.Error(res, `{"error":"Unexpected error while fetching tokens"}`, http.StatusInternalServerError)
|
http.Error(res, `{"error":"Unexpected error while fetching tokens"}`, http.StatusInternalServerError)
|
||||||
|
@ -221,13 +226,12 @@ func handleCodesJSON(res http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
n := time.Now()
|
|
||||||
result := struct {
|
result := struct {
|
||||||
Tokens []*token `json:"tokens"`
|
Tokens []*token `json:"tokens"`
|
||||||
NextWrap time.Time `json:"next_wrap"`
|
NextWrap time.Time `json:"next_wrap"`
|
||||||
}{
|
}{
|
||||||
Tokens: tokens,
|
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")
|
res.Header().Set("Content-Type", "application/json")
|
||||||
|
|
8
token.go
8
token.go
|
@ -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{
|
client, err := api.NewClient(&api.Config{
|
||||||
Address: cfg.Vault.Address,
|
Address: cfg.Vault.Address,
|
||||||
})
|
})
|
||||||
|
@ -112,7 +112,7 @@ func getSecretsFromVault(tok string) ([]*token, error) {
|
||||||
case key := <-scanPool:
|
case key := <-scanPool:
|
||||||
go scanKeyForSubKeys(client, key, scanPool, keyPoolChan, wg)
|
go scanKeyForSubKeys(client, key, scanPool, keyPoolChan, wg)
|
||||||
case key := <-keyPoolChan:
|
case key := <-keyPoolChan:
|
||||||
go fetchTokenFromKey(client, key, respChan, wg)
|
go fetchTokenFromKey(client, key, respChan, wg, pointOfTime)
|
||||||
case t := <-respChan:
|
case t := <-respChan:
|
||||||
resp = append(resp, t)
|
resp = append(resp, t)
|
||||||
case <-done:
|
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()
|
defer wg.Done()
|
||||||
|
|
||||||
data, err := client.Logical().Read(k)
|
data, err := client.Logical().Read(k)
|
||||||
|
@ -182,7 +182,7 @@ func fetchTokenFromKey(client *api.Client, k string, respChan chan *token, wg *s
|
||||||
switch k {
|
switch k {
|
||||||
case cfg.Vault.SecretField:
|
case cfg.Vault.SecretField:
|
||||||
tok.Secret = v.(string)
|
tok.Secret = v.(string)
|
||||||
tok.GenerateCode(time.Now())
|
tok.GenerateCode(pointOfTime)
|
||||||
case "code":
|
case "code":
|
||||||
tok.Code = v.(string)
|
tok.Code = v.(string)
|
||||||
case "name":
|
case "name":
|
||||||
|
|
Loading…
Reference in a new issue