mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-11-20 09:56:07 -05:00
preliminary web support for auth (#26)
Some caveats:
* it doesn't record the peer IP yet, which makes it harder to verify
sessions are valid. This is a little annoying to do in hyper now
(see hyperium/hyper#1410). The direct peer might not be what we want
right now anyway because there's no TLS support yet (see #27). In
the meantime, the sane way to expose Moonfire NVR to the Internet is
via a proxy server, and recording the proxy's IP is not useful.
Maybe better to interpret a RFC 7239 Forwarded header (and/or
the older X-Forwarded-{For,Proto} headers).
* it doesn't ever use Secure (https-only) cookies, for a similar reason.
It's not safe to use even with a tls proxy until this is fixed.
* there's no "moonfire-nvr config" support for inspecting/invalidating
sessions yet.
* in debug builds, logging in is crazy slow. See libpasta/libpasta#9.
Some notes:
* I removed the Javascript "no-use-before-defined" lint, as some of
the functions form a cycle.
* Fixed #20 along the way. I needed to add support for properly
returning non-OK HTTP statuses to signal unauthorized and such.
* I removed the Access-Control-Allow-Origin header support, which was
at odds with the "SameSite=lax" in the cookie header. The "yarn
start" method for running a local proxy server accomplishes the same
thing as the Access-Control-Allow-Origin support in a more secure
manner.
This commit is contained in:
@@ -65,6 +65,7 @@ import MoonfireAPI from './lib/MoonfireAPI';
|
||||
const api = new MoonfireAPI();
|
||||
let streamViews = null; // StreamView objects
|
||||
let calendarView = null; // CalendarView object
|
||||
let loginDialog = null;
|
||||
|
||||
/**
|
||||
* Currently selected time format specification.
|
||||
@@ -206,7 +207,34 @@ function fetch(selectedRange, videoLength) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the page after receiving camera data.
|
||||
* Updates the session bar at the top of the page.
|
||||
*
|
||||
* @param {Object} session the "session" key of the main API request's JSON,
|
||||
* or null.
|
||||
*/
|
||||
function updateSession(session) {
|
||||
let sessionBar = $('#session');
|
||||
sessionBar.empty();
|
||||
if (session === null) {
|
||||
sessionBar.hide();
|
||||
return;
|
||||
}
|
||||
sessionBar.append($('<span id="session-username" />').text(session.username));
|
||||
let logout = $('<a>logout</a>');
|
||||
logout.click(() => {
|
||||
api
|
||||
.logout(session.csrf)
|
||||
.done(() => {
|
||||
onReceivedTopLevel(null);
|
||||
loginDialog.dialog('open');
|
||||
});
|
||||
});
|
||||
sessionBar.append(' | ', logout);
|
||||
sessionBar.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the page after receiving top-level data.
|
||||
*
|
||||
* Sets the following globals:
|
||||
* zone - timezone from data received
|
||||
@@ -215,9 +243,16 @@ function fetch(selectedRange, videoLength) {
|
||||
* Builds the dom for the left side controllers
|
||||
*
|
||||
* @param {Object} data JSON resulting from the main API request /api/?days=
|
||||
* or null if the request failed.
|
||||
*/
|
||||
function onReceivedCameras(data) {
|
||||
newTimeZone(data.timeZoneName);
|
||||
function onReceivedTopLevel(data) {
|
||||
if (data === null) {
|
||||
data = {cameras: [], session: null};
|
||||
} else {
|
||||
newTimeZone(data.timeZoneName);
|
||||
}
|
||||
|
||||
updateSession(data.session);
|
||||
|
||||
// Set up controls and values
|
||||
const nvrSettingsView = new NVRSettingsView();
|
||||
@@ -236,6 +271,9 @@ function onReceivedCameras(data) {
|
||||
const streamsParent = $('#streams');
|
||||
const videos = $('#videos');
|
||||
|
||||
streamsParent.empty();
|
||||
videos.empty();
|
||||
|
||||
streamViews = [];
|
||||
let streamSelectorCameras = [];
|
||||
for (const cameraJson of data.cameras) {
|
||||
@@ -276,6 +314,57 @@ function onReceivedCameras(data) {
|
||||
console.log('Loaded: ' + streamViews.length + ' stream views');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submit action on the login form.
|
||||
*/
|
||||
function sendLoginRequest() {
|
||||
if (loginDialog.pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
let username = $('#login-username').val();
|
||||
let password = $('#login-password').val();
|
||||
let submit = $('#login-submit');
|
||||
let error = $('#login-error');
|
||||
|
||||
error.empty();
|
||||
error.removeClass('ui-state-highlight');
|
||||
submit.button('option', 'disabled', true);
|
||||
loginDialog.pending = true;
|
||||
console.info('logging in as', username);
|
||||
api
|
||||
.login(username, password)
|
||||
.done(() => {
|
||||
console.info('login successful');
|
||||
loginDialog.dialog('close');
|
||||
sendTopLevelRequest();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.info('login failed:', e);
|
||||
error.show();
|
||||
error.addClass('ui-state-highlight');
|
||||
error.text(e.responseText);
|
||||
})
|
||||
.always(() => {
|
||||
submit.button('option', 'disabled', false);
|
||||
loginDialog.pending = false;
|
||||
});
|
||||
}
|
||||
|
||||
/** Sends the top-level api request. */
|
||||
function sendTopLevelRequest() {
|
||||
api
|
||||
.request(api.nvrUrl(true))
|
||||
.done((data) => onReceivedTopLevel(data))
|
||||
.catch((e) => {
|
||||
console.error('NVR load exception: ', e);
|
||||
onReceivedTopLevel(null);
|
||||
if (e.status == 401) {
|
||||
loginDialog.dialog('open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing the entire application.
|
||||
*/
|
||||
@@ -289,16 +378,22 @@ export default class NVRApplication {
|
||||
* Start the application.
|
||||
*/
|
||||
start() {
|
||||
api
|
||||
.request(api.nvrUrl(true))
|
||||
.done((data) => onReceivedCameras(data))
|
||||
.fail((req, status, err) => {
|
||||
console.error('NVR load error: ', status, err);
|
||||
onReceivedCameras({cameras: []});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('NVR load exception: ', e);
|
||||
onReceivedCameras({cameras: []});
|
||||
});
|
||||
loginDialog = $('#login').dialog({
|
||||
autoOpen: false,
|
||||
modal: true,
|
||||
buttons: [
|
||||
{
|
||||
id: 'login-submit',
|
||||
text: 'Login',
|
||||
click: sendLoginRequest,
|
||||
},
|
||||
],
|
||||
});
|
||||
loginDialog.pending = false;
|
||||
loginDialog.find('form').on('submit', function(event) {
|
||||
event.preventDefault();
|
||||
sendLoginRequest();
|
||||
});
|
||||
sendTopLevelRequest();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ body {
|
||||
#nav {
|
||||
float: left;
|
||||
}
|
||||
#session {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#datetime .ui-datepicker {
|
||||
width: 100%;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<title>Moonfire NVR</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="session">
|
||||
</div>
|
||||
<div id="nav">
|
||||
<form action="#">
|
||||
<fieldset>
|
||||
@@ -72,6 +74,27 @@
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<table id="videos"></table>
|
||||
</body>
|
||||
<table id="videos"></table>
|
||||
<div id="login">
|
||||
<form>
|
||||
<fieldset>
|
||||
<table>
|
||||
<tr>
|
||||
<td><label for="login-username">Username:</label></td>
|
||||
<td><input type="text" id="login-username" name="username"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="login-password">Password:</label></td>
|
||||
<td><input type="password" id="login-password" name="password"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><input type="submit" tabindex="-1" style="position:absolute; top:-1000px"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<p id="login-error"></p>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -158,4 +158,37 @@ export default class MoonfireAPI {
|
||||
cache: cacheOk,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new AJAX request to log in.
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} password
|
||||
* @return {Request}
|
||||
*/
|
||||
login(username, password) {
|
||||
return $.ajax(this._builder.makeUrl('login'), {
|
||||
data: {
|
||||
username: username,
|
||||
password: password,
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new AJAX request to log out.
|
||||
*
|
||||
* @param {String} csrf: the csrf request token as returned in
|
||||
* <tt>/api/</tt> response JSON.
|
||||
* @return {Request}
|
||||
*/
|
||||
logout(csrf) {
|
||||
return $.ajax(this._builder.makeUrl('logout'), {
|
||||
data: {
|
||||
csrf: csrf,
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user