Add OTP feature to redesign

Framework7
Skylar Ittner 6 years ago
parent 0b85c8a7f6
commit d7c00945a7

@ -26,12 +26,14 @@
<script src="vendor/js/fontawesome/fontawesome.min.js"></script>
<script src="vendor/js/fontawesome/solid.min.js"></script>
<script src="vendor/js/fontawesome/regular.min.js"></script>
<script src="vendor/js/jsOTP.min.js"></script>
<script src="js/polyfills.js"></script>
<script src="js/material-palette.js"></script>
<script src="js/notifications.js"></script>
<script src="js/accounts.js"></script>
<script src="js/home.js"></script>
<script src="js/otp.js"></script>
<script src="js/app.js"></script>
<script src="routes.js"></script>

@ -0,0 +1,188 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
var totp = new jsOTP.totp();
function parseOTP(jsontext, reload) {
var keys = [];
if (jsontext !== null && jsontext != "") {
var keys = JSON.parse(jsontext || "[]");
if (keys.length > 0) {
//$("#nokeys").css("display", "none");
}
var tmpldata = [];
for (var i = 0; i < keys.length; i++) {
var code = totp.getOtp(keys[i]["secret"]);
// Escape HTML characters
var label = $('<div/>').html(keys[i]["label"]).html();
var issuer = $('<div/>').text(keys[i]["issuer"]).html();
tmpldata.push({
code: code,
secret: keys[i]["secret"],
label: label,
issuer: issuer,
index: i
});
}
router.navigate("/otp", {
context: {
otpcodes: tmpldata
},
reloadCurrent: (reload == true ? true : false)
});
setInterval(function () {
refreshCountdown();
refreshCodes();
}, 1000);
refreshCountdown();
}
}
function loadOTPPage(reload) {
var ls_text = localStorage.getItem("otp");
if (ls_text === null || ls_text == "") {
// Recover from NativeStorage
NativeStorage.getItem("otp", function (data) {
localStorage.setItem("otp");
parseOTP(data, reload);
}, function (error) {
parseOTP("[]", reload);
});
} else {
parseOTP(ls_text, reload);
}
}
function refreshCountdown() {
var percent = ((30 - ((new Date).getSeconds() % 30)) / 30) * 100;
$("#countdown").css("width", percent + "%");
}
function refreshCodes() {
$(".otpcard").each(function () {
var code = totp.getOtp($(this).data("secret"));
$(".otpcode", this).text(code);
});
}
function deleteOTPCode(index) {
var label = $(".otpcard[data-index=" + index + "] .otplabel").text();
navigator.notification.confirm("Delete auth key? This cannot be undone, so make sure you don't need this key to login anymore!", function (result) {
if (result != 1) {
return;
}
var keys = JSON.parse(localStorage.getItem("otp"));
keys.splice(index, 1);
localStorage.setItem("otp", JSON.stringify(keys));
NativeStorage.setItem("otp", JSON.stringify(keys));
loadOTPPage(true);
}, "Delete " + label + "?");
}
function addOTPCode(key, label, issuer) {
if (key == "") {
navigator.notification.alert("Missing secret key.", null, "Error", 'Dismiss');
return;
}
key = key.toUpperCase();
/* Thanks to https://stackoverflow.com/a/27362880 for the regex */
if (!key.match(/^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$/)) {
navigator.notification.alert("Secret key is not valid base32.", null, "Error", 'Dismiss');
return;
}
if (label == "") {
navigator.notification.alert("Missing label.", null, "Error", 'Dismiss');
return;
}
var ls_text = localStorage.getItem("otp");
var keys = [];
if (ls_text != null && ls_text != "") {
keys = JSON.parse(ls_text || "[]");
}
keys.push({"secret": key, "label": label, "issuer": issuer});
localStorage.setItem("otp", JSON.stringify(keys));
NativeStorage.setItem("otp", JSON.stringify(keys));
app.toast.create({
text: '2-factor key saved.',
closeButton: true,
}).open();
loadOTPPage(true);
}
function scanOTPCode() {
try {
cordova.plugins.barcodeScanner.scan(
function (result) {
if (!result.cancelled) {
try {
var url = decodeURI(result.text);
} catch (e) {
navigator.notification.alert("Could not decode OTP URI.", null, "Error", 'Dismiss');
return;
}
if (!url.startsWith("otpauth://")) {
navigator.notification.alert("Invalid OTP code. Try again.", null, "Error", 'Dismiss');
return;
}
if (!url.startsWith("otpauth://totp/")) {
navigator.notification.alert("Unsupported key type.", null, "Error", 'Dismiss');
return;
}
var stripped = url.replace("otpauth://totp/", "");
var params = stripped.split("?")[1].split("&");
var label = stripped.split("?")[0];
var secret = "";
var issuer = "";
for (var i = 0; i < params.length; i++) {
var param = params[i].split("=");
if (param[0] == "secret") {
secret = param[1].toUpperCase();
} else if (param[0] == "issuer") {
issuer = param[1];
} else if (param[0] == "algorithm" && param[1].toLowerCase() != "sha1") {
navigator.notification.alert("Unsupported hash algorithm.", null, "Error", 'Dismiss');
return;
} else if (param[0] == "digits" && param[1] != "6") {
navigator.notification.alert("Unsupported digit count.", null, "Error", 'Dismiss');
return;
} else if (param[0] == "period" && param[1] != "30") {
navigator.notification.alert("Unsupported period.", null, "Error", 'Dismiss');
return;
}
}
try {
secret = decodeURIComponent(secret);
issuer = decodeURIComponent(issuer);
label = decodeURIComponent(label);
} catch (e) {
navigator.notification.alert("Could not decode OTP URI.", null, "Error", 'Dismiss');
return;
}
addOTPCode(secret, label, issuer);
}
},
function (error) {
navigator.notification.alert("Scanning failed: " + error, null, "Error", 'Dismiss');
},
{
"showFlipCameraButton": false,
"prompt": "Scan OTP QR code."
}
);
} catch (ex) {
navigator.notification.alert(ex.message, null, "Error", 'Dismiss');
}
}
function addManualOTP() {
var label = $("#addotppopup #label").val();
var secret = $("#addotppopup #secret").val().replace(/\s+/g, '');
app.popup.close("#addotppopup");
addOTPCode(secret, label, "");
}

@ -1,149 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<div class="panel panel-blue">
<div class="panel-body">
<p></p>
<span class="btn btn-primary btn-lg" onclick="scanCode()" id="scancodebtn">
<i class="fa fa-qrcode"></i> Scan QR Code
</span>
<span class="btn btn-link" onclick="manualshow()" id="manualaddbtn">
Manual Entry
</span>
<div id="manual_add" class="well" style="display: none;">
<input type="text" id="key" class="form-control" placeholder="Secret key" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /> <br />
<input type="text" id="label" class="form-control" placeholder="Label" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /> <br />
<input type="text" id="issuer" class="form-control" placeholder="Issuer" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
<br />
<div class="btn btn-primary" onclick="manualadd()">
Continue
</div>
</div>
</div>
</div>
<script>
$("#key").on("keyup", function () {
if (window.getSelection().toString() !== '') {
return;
}
var text = $('#key').val().replace(/\s+/g, '');
var formatted = "";
for (var i = 1; i <= text.length; i++) {
formatted = formatted + text[i - 1];
if (i % 4 == 0 && i > 1 && i < text.length) {
// add a space every 5 characters,
// unless it's the first character
// or the last character
formatted = formatted + " ";
}
}
$('#key').val(formatted.toUpperCase());
});
function manualadd() {
var key = $('#key').val().replace(/\s+/g, '');
var label = $('#label').val();
var issuer = $('#issuer').val();
addOTP(key, label, issuer);
}
function manualshow() {
$('#manual_add').css('display', 'block');
}
function addOTP(key, label, issuer) {
if (key == "") {
navigator.notification.alert("Missing secret key.", null, "Error", 'Dismiss');
return;
}
key = key.toUpperCase();
/* Thanks to https://stackoverflow.com/a/27362880 for the regex */
if (!key.match(/^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$/)) {
navigator.notification.alert("Secret key is not valid base32.", null, "Error", 'Dismiss');
return;
}
if (label == "") {
navigator.notification.alert("Missing label.", null, "Error", 'Dismiss');
return;
}
var ls_text = localStorage.getItem("otp");
var keys = [];
if (ls_text != null && ls_text != "") {
keys = JSON.parse(ls_text || "[]");
}
keys.push({"secret": key, "label": label, "issuer": issuer});
localStorage.setItem("otp", JSON.stringify(keys));
NativeStorage.setItem("otp", JSON.stringify(keys));
navigator.notification.alert("2-factor key saved.", null, "Key added", 'Dismiss');
openscreen("otp");
}
function scanCode() {
try {
cordova.plugins.barcodeScanner.scan(
function (result) {
if (!result.cancelled) {
try {
var url = decodeURI(result.text);
} catch (e) {
navigator.notification.alert("Could not decode OTP URI.", null, "Error", 'Dismiss');
return;
}
if (!url.startsWith("otpauth://")) {
navigator.notification.alert("Invalid OTP code. Try again.", null, "Error", 'Dismiss');
return;
}
if (!url.startsWith("otpauth://totp/")) {
navigator.notification.alert("Unsupported key type.", null, "Error", 'Dismiss');
return;
}
var stripped = url.replace("otpauth://totp/", "");
var params = stripped.split("?")[1].split("&");
var label = stripped.split("?")[0];
var secret = "";
var issuer = "";
for (var i = 0; i < params.length; i++) {
var param = params[i].split("=");
if (param[0] == "secret") {
secret = param[1].toUpperCase();
} else if (param[0] == "issuer") {
issuer = param[1];
} else if (param[0] == "algorithm" && param[1].toLowerCase() != "sha1") {
navigator.notification.alert("Unsupported hash algorithm.", null, "Error", 'Dismiss');
return;
} else if (param[0] == "digits" && param[1] != "6") {
navigator.notification.alert("Unsupported digit count.", null, "Error", 'Dismiss');
return;
} else if (param[0] == "period" && param[1] != "30") {
navigator.notification.alert("Unsupported period.", null, "Error", 'Dismiss');
return;
}
}
try {
secret = decodeURIComponent(secret);
issuer = decodeURIComponent(issuer);
label = decodeURIComponent(label);
} catch (e) {
navigator.notification.alert("Could not decode OTP URI.", null, "Error", 'Dismiss');
return;
}
addOTP(secret, label, issuer);
}
},
function (error) {
navigator.notification.alert("Scanning failed: " + error, null, "Error", 'Dismiss');
},
{
"showFlipCameraButton": false,
"prompt": "Scan OTP QR code."
}
);
} catch (ex) {
navigator.notification.alert(ex.message, null, "Error", 'Dismiss');
}
}
setnavbar("app", "Add Auth Key", "otp");
</script>

@ -1,91 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<div class="progress">
<div class="progress-bar" id="countdown" style="width: 0%;"></div>
</div>
<div class="circle-btn btn btn-light-blue" onclick="openscreen('addotp', 'FADE');">
<img src="icons/ic_add.svg" />
</div>
<div id="nokeys">
<div class="app-dock-container">
<div class="app-dock" id="app-dock">
<div style="margin-top: 50px; text-align: center; font-size: 120%;">
<img src="img/nokeys.svg" alt="" style="max-width: 80%; max-height: 25%;" />
<br /><br />
<p style="max-width: 80%; margin: 0 auto;">You haven't added any authentication keys yet. Press <i class="fa fa-plus"></i> to add one.</p>
</div>
</div>
</div>
</div>
<div class="list-group" id="codelist">
</div>
<script src="js/jsOTP.min.js"></script>
<script>
setnavbar("otp", "", "home");
var totp = new jsOTP.totp();
function load(jsontext) {
var keys = [];
if (jsontext !== null && jsontext != "") {
var keys = JSON.parse(jsontext || "[]");
if (keys.length > 0) {
$("#nokeys").css("display", "none");
}
for (var i = 0; i < keys.length; i++) {
var code = totp.getOtp(keys[i]["secret"]);
// Escape HTML characters
var label = $('<div/>').html(keys[i]["label"]).html();
var issuer = $('<div/>').text(keys[i]["issuer"]).html();
$("#codelist").append("<div class=\"list-group-item\" id=\"codeitem_" + i + "\">"
+ "<span class=\"pull-right\" style=\"color: red;\" onclick=\"deleteCode(" + i + ")\"><i class=\"fa fa-trash-o\"></i></span>"
+ "<p class=\"h6\">" + label + "</p>"
+ "<div class=\"h3 code\" style=\"font-weight: bold;\">" + code + "</div>"
+ "<p class=\"small\">" + issuer + "</p>"
+ "</div>");
}
}
}
var ls_text = localStorage.getItem("otp");
if (ls_text === null || ls_text == "") {
// Recover from NativeStorage
NativeStorage.getItem("otp", function (data) {
localStorage.setItem("otp");
load(data);
});
} else {
load(ls_text);
}
function refreshCountdown() {
var percent = ((30 - ((new Date).getSeconds() % 30)) / 30) * 100;
$("#countdown").css("width", percent + "%");
}
function refreshCodes() {
for (var i = 0; i < keys.length; i++) {
var code = totp.getOtp(keys[i]["secret"]);
$("#codelist #codeitem_" + i + " .code").text(code);
}
}
function deleteCode(index) {
navigator.notification.confirm("Delete auth key? This cannot be undone, so make sure you don't need this key to login anymore!", function (result) {
if (result != 1) {
return;
}
keys.splice(index, 1);
localStorage.setItem("otp", JSON.stringify(keys));
openscreen("otp");
}, "Delete " + keys[index]["label"] + "?");
}
setInterval(function () {
refreshCountdown();
refreshCodes();
}, 1000);
refreshCountdown();
</script>

@ -1,44 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<br />
<div class="panel panel-blue">
<div class="panel-heading">
<h3 class="panel-title">Setup</h3>
</div>
<div class="panel-body">
<p>Almost done!
<br /><br />
Please enter your password, then press Finish.
</p>
<input type="password" id="passbox" class="form-control" placeholder="Password" style="display: block;" />
<br />
<div class="btn btn-primary" onclick="savePassword()"><i class="fa fa-check"></i> Finish</div>
</div>
</div>
<script>
function savePassword() {
$.post(setupsyncurl, {
username: setupusername,
key: setupsynckey,
password: $('#passbox').val(),
action: "check_password"
}, function (data) {
if (data.status === 'OK') {
setuppassword = $('#passbox').val();
var accid = addaccount(setupusername, setuppassword, setupsyncurl, setupsynckey);
switchaccount(accid);
localStorage.setItem("firstrun", "1");
navigator.notification.alert("Account connected!", null, "Success", 'Continue');
restartApplication();
} else {
navigator.notification.alert(data.msg, null, "Error", 'Dismiss');
}
}, "json").fail(function () {
navigator.notification.alert("Could not connect to the server. Try again later.", null, "Error", 'Dismiss');
});
}
setnavbar("setup");
</script>

@ -6,6 +6,12 @@
<div class="navbar">
<div class="navbar-inner">
<div class="title">Business</div>
<div class="right">
<a href="#" onclick="loadOTPPage()" class="link icon-only">
<i class="fas fa-key"></i>
</a>
</div>
</div>
</div>

@ -0,0 +1,89 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<div class="page" data-name="otp">
<div class="navbar">
<div class="navbar-inner">
<div class="left">
<a href="#" class="link icon-only back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="title">Auth Keys</div>
</div>
</div>
<div class="popup" id="addotppopup">
<div class="view">
<div class="page">
<div class="page-content">
<div class="block">
<div class="list">
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-input-wrap">
<input type="text" id="label" placeholder="Label" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-input-wrap">
<input type="text" id="secret" placeholder="Secret" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
</ul>
</div>
<div class="button button-fill" onclick="addManualOTP()">
Save
</div>
</div>
</div>
</div>
</div>
</div>
<div class="fab fab-right-bottom">
<a href="#">
<!-- First icon is visible when Speed Dial actions are closed -->
<i class="material-icons">add</i>
<!-- Second icon is visible when Speed Dial actions are opened -->
<i class="material-icons">close</i>
</a>
<!-- Speed Dial action buttons -->
<div class="fab-buttons fab-buttons-top">
<a href="#" data-popup="#addotppopup" class="fab-label-button popup-open">
<span><i class="fas fa-edit"></i></span>
<span class="fab-label">Manual</span>
</a>
<a href="#" onclick="scanOTPCode()" class="fab-label-button">
<span><i class="fas fa-qrcode"></i></span>
<span class="fab-label">QR code</span>
</a>
</div>
</div>
<div class="page-content">
{{#each otpcodes}}
<div class="card otpcard" data-secret="{{secret}}" data-index="{{index}}">
<div class="card-content card-content-padding">
<div>
<h1 class="otpcode">{{code}}</h1>
<a class="link float-right" onclick="deleteOTPCode('{{index}}')">
<i class="material-icons">delete</i>
</a>
</div>
<p class="otplabel">{{label}}</p>
</div>
</div>
{{/each}}
</div>
</div>

@ -38,7 +38,7 @@
<li class="item-content item-input">
<div class="item-inner">
<div class="item-input-wrap">
<input type="text" id="key" class="form-control" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
<input type="text" id="key" placeholder="Key" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
<span class="input-clear-button"></span>
</div>
</div>

@ -25,5 +25,10 @@ var routes = [
path: '/app',
url: './pages/app.html',
name: 'app'
}
},
{
path: '/otp',
templateUrl: './pages/otp.html',
name: 'otp'
},
];
Loading…
Cancel
Save