diff --git a/octoprint/server.py b/octoprint/server.py index d59226e..c900f1f 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -138,11 +138,13 @@ def connect(): return jsonify(state="Connecting") @app.route(BASEURL + "control/disconnect", methods=["POST"]) +@login_required def disconnect(): printer.disconnect() return jsonify(state="Offline") @app.route(BASEURL + "control/command", methods=["POST"]) +@login_required def printerCommand(): if "application/json" in request.headers["Content-Type"]: data = request.json @@ -166,21 +168,25 @@ def printerCommand(): return jsonify(SUCCESS) @app.route(BASEURL + "control/print", methods=["POST"]) +@login_required def printGcode(): printer.startPrint() return jsonify(SUCCESS) @app.route(BASEURL + "control/pause", methods=["POST"]) +@login_required def pausePrint(): printer.togglePausePrint() return jsonify(SUCCESS) @app.route(BASEURL + "control/cancel", methods=["POST"]) +@login_required def cancelPrint(): printer.cancelPrint() return jsonify(SUCCESS) @app.route(BASEURL + "control/temperature", methods=["POST"]) +@login_required def setTargetTemperature(): if not printer.isOperational(): return jsonify(SUCCESS) @@ -198,6 +204,7 @@ def setTargetTemperature(): return jsonify(SUCCESS) @app.route(BASEURL + "control/jog", methods=["POST"]) +@login_required def jog(): if not printer.isOperational() or printer.isPrinting(): # do not jog when a print job is running or we don't have a connection @@ -234,6 +241,7 @@ def getSpeedValues(): return jsonify(feedrate=printer.feedrateState()) @app.route(BASEURL + "control/speed", methods=["POST"]) +@login_required def speed(): if not printer.isOperational(): return jsonify(SUCCESS) @@ -261,6 +269,7 @@ def readGcodeFile(filename): return send_from_directory(settings().getBaseFolder("uploads"), filename, as_attachment=True) @app.route(BASEURL + "gcodefiles/upload", methods=["POST"]) +@login_required def uploadGcodeFile(): filename = None if "gcode_file" in request.files.keys(): @@ -269,6 +278,7 @@ def uploadGcodeFile(): return jsonify(files=gcodeManager.getAllFileData(), filename=filename) @app.route(BASEURL + "gcodefiles/load", methods=["POST"]) +@login_required def loadGcodeFile(): if "filename" in request.values.keys(): printAfterLoading = False @@ -280,6 +290,7 @@ def loadGcodeFile(): return jsonify(SUCCESS) @app.route(BASEURL + "gcodefiles/delete", methods=["POST"]) +@login_required def deleteGcodeFile(): if "filename" in request.values.keys(): filename = request.values["filename"] @@ -318,6 +329,7 @@ def downloadTimelapse(filename): return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True) @app.route(BASEURL + "timelapse/", methods=["DELETE"]) +@login_required def deleteTimelapse(filename): if util.isAllowedFile(filename, set(["mpg"])): secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename)) @@ -326,6 +338,7 @@ def deleteTimelapse(filename): return getTimelapseData() @app.route(BASEURL + "timelapse/config", methods=["POST"]) +@login_required def setTimelapseConfig(): if request.values.has_key("type"): type = request.values["type"] @@ -389,6 +402,7 @@ def getSettings(): }) @app.route(BASEURL + "settings", methods=["POST"]) +@login_required def setSettings(): if "application/json" in request.headers["Content-Type"]: data = request.json @@ -434,6 +448,7 @@ def setSettings(): #~~ system control @app.route(BASEURL + "system", methods=["POST"]) +@login_required def performSystemAction(): logger = logging.getLogger(__name__) if request.values.has_key("action"): @@ -467,15 +482,14 @@ def login(): user = userManager.findUser(username) if user is not None: - passwordHash = users.UserManager.createPasswordHash(password) - if passwordHash == user.passwordHash: + if user.check_password(users.UserManager.createPasswordHash(password)): login_user(user, remember=remember) - return jsonify({"name": user.username, "roles": user.roles}) + return jsonify({"name": user.get_name(), "user": user.is_user(), "admin": user.is_admin()}) return app.make_response(("User unknown or password incorrect", 401, [])) elif "passive" in request.values.keys(): user = current_user if user is not None and not user.is_anonymous(): - return jsonify({"name": user.username, "roles": user.roles}) + return jsonify({"name": user.get_name(), "user": user.is_user(), "admin": user.is_admin()}) else: return jsonify(SUCCESS) @@ -488,8 +502,7 @@ def logout(): def load_user(id): if userManager is not None: return userManager.findUser(id) - else: - return users.DummyUser() + return None #~~ startup code class Server(): diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index 9cf01a3..6bef7b8 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -1,8 +1,80 @@ //~~ View models -function ConnectionViewModel() { +function LoginStateViewModel() { var self = this; + self.loggedIn = ko.observable(false); + self.username = ko.observable(undefined); + self.isAdmin = ko.observable(false); + self.isUser = ko.observable(false); + + self.userMenuText = ko.computed(function() { + if (self.loggedIn()) { + return "\"" + self.username() + "\""; + } else { + return "Login"; + } + }) + + self.requestData = function() { + $.ajax({ + url: AJAX_BASEURL + "login", + type: "POST", + data: {"passive": true}, + success: self.fromResponse + }) + } + + self.fromResponse = function(response) { + if (response && response.name) { + self.loggedIn(true); + self.username(response.name); + self.isUser(response.user); + self.isAdmin(response.admin); + } else { + self.loggedIn(false); + self.username(undefined); + self.isUser(false); + self.isAdmin(false); + } + } + + self.login = function() { + var username = $("#login_user").val(); + var password = $("#login_pass").val(); + var remember = $("#login_remember").is(":checked"); + + $.ajax({ + url: AJAX_BASEURL + "login", + type: "POST", + data: {"user": username, "pass": password, "remember": remember}, + success: function(response) { + $.pnotify({title: "Login successful", text: "You are now logged in", type: "success"}); + self.fromResponse(response); + }, + error: function(jqXHR, textStatus, errorThrown) { + $.pnotify({title: "Login failed", text: "User unknown or wrong password", type: "error"}); + } + }) + } + + self.logout = function() { + $.ajax({ + url: AJAX_BASEURL + "logout", + type: "POST", + success: function(response) { + $.pnotify({title: "Logout successful", text: "You are now logged out", type: "success"}); + self.fromResponse(response); + } + }) + } +} + +function ConnectionViewModel(loginStateViewModel) { + var self = this; + + self.loginState = loginStateViewModel; + self.portOptions = ko.observableArray(undefined); self.baudrateOptions = ko.observableArray(undefined); self.selectedPort = ko.observable(undefined); @@ -107,9 +179,11 @@ function ConnectionViewModel() { } } -function PrinterStateViewModel() { +function PrinterStateViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.stateString = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); @@ -238,9 +312,11 @@ function PrinterStateViewModel() { } } -function TemperatureViewModel(settingsViewModel) { +function TemperatureViewModel(loginStateViewModel, settingsViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.temp = ko.observable(undefined); self.bedTemp = ko.observable(undefined); self.targetTemp = ko.observable(undefined); @@ -412,9 +488,11 @@ function TemperatureViewModel(settingsViewModel) { } } -function ControlViewModel() { +function ControlViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); @@ -567,9 +645,11 @@ function ControlViewModel() { } -function SpeedViewModel() { +function SpeedViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.outerWall = ko.observable(undefined); self.innerWall = ko.observable(undefined); self.fill = ko.observable(undefined); @@ -625,9 +705,11 @@ function SpeedViewModel() { } } -function TerminalViewModel() { +function TerminalViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.log = []; self.isErrorOrClosed = ko.observable(undefined); @@ -654,6 +736,7 @@ function TerminalViewModel() { if (!self.log) self.log = [] self.log = self.log.concat(data) + self.log = self.log.slice(-300) self.updateOutput(); } @@ -690,9 +773,11 @@ function TerminalViewModel() { } } -function GcodeFilesViewModel() { +function GcodeFilesViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); @@ -824,9 +909,11 @@ function GcodeFilesViewModel() { } -function TimelapseViewModel() { +function TimelapseViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.timelapseType = ko.observable(undefined); self.timelapseTimedInterval = ko.observable(undefined); @@ -943,9 +1030,11 @@ function TimelapseViewModel() { } } -function GcodeViewModel() { +function GcodeViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.loadedFilename = undefined; self.status = 'idle'; self.enabled = false; @@ -1006,9 +1095,11 @@ function GcodeViewModel() { } -function SettingsViewModel() { +function SettingsViewModel(loginStateViewModel) { var self = this; + self.loginState = loginStateViewModel; + self.appearance_name = ko.observable(undefined); self.appearance_color = ko.observable(undefined); @@ -1135,9 +1226,10 @@ function SettingsViewModel() { } -function NavigationViewModel(appearanceViewModel, settingsViewModel) { +function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel) { var self = this; + self.loginState = loginStateViewModel; self.appearance = appearanceViewModel; self.systemActions = settingsViewModel.system_actions; @@ -1166,9 +1258,10 @@ function NavigationViewModel(appearanceViewModel, settingsViewModel) { } } -function DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, speedViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel) { +function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, speedViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel) { var self = this; + self.loginStateViewModel = loginStateViewModel; self.connectionViewModel = connectionViewModel; self.printerStateViewModel = printerStateViewModel; self.temperatureViewModel = temperatureViewModel; @@ -1185,6 +1278,7 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView $("#offline_overlay").hide(); self.timelapseViewModel.requestData(); $("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime()); + self.loginStateViewModel.requestData(); } }) self._socket.on("disconnect", function() { @@ -1480,20 +1574,22 @@ function AppearanceViewModel(settingsViewModel) { $(function() { //~~ View models - var connectionViewModel = new ConnectionViewModel(); - var printerStateViewModel = new PrinterStateViewModel(); - var settingsViewModel = new SettingsViewModel(); + var loginStateViewModel = new LoginStateViewModel(); + var connectionViewModel = new ConnectionViewModel(loginStateViewModel); + var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel); + var settingsViewModel = new SettingsViewModel(loginStateViewModel); var appearanceViewModel = new AppearanceViewModel(settingsViewModel); - var temperatureViewModel = new TemperatureViewModel(settingsViewModel); - var controlViewModel = new ControlViewModel(); - var speedViewModel = new SpeedViewModel(); - var terminalViewModel = new TerminalViewModel(); - var gcodeFilesViewModel = new GcodeFilesViewModel(); - var timelapseViewModel = new TimelapseViewModel(); - var gcodeViewModel = new GcodeViewModel(); - var navigationViewModel = new NavigationViewModel(appearanceViewModel, settingsViewModel); + var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel); + var controlViewModel = new ControlsViewModel(loginStateViewModel); + var speedViewModel = new SpeedViewModel(loginStateViewModel); + var terminalViewModel = new TerminalViewModel(loginStateViewModel); + var gcodeFilesViewModel = new GcodeFilesViewModel(loginStateViewModel); + var timelapseViewModel = new TimelapseViewModel(loginStateViewModel); + var gcodeViewModel = new GcodeViewModel(loginStateViewModel); + var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel); var dataUpdater = new DataUpdater( + loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, @@ -1505,7 +1601,8 @@ $(function() { gcodeViewModel ); - //work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at http://stackoverflow.com/questions/12506897/is-safari-on-ios-6-caching-ajax-results + // work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at + // http://stackoverflow.com/questions/12506897/is-safari-on-ios-6-caching-ajax-results $.ajaxSetup({ type: 'POST', headers: { "cache-control": "no-cache" } @@ -1547,6 +1644,7 @@ $(function() { }) $('#tabs a[data-toggle="tab"]').on('shown', function (e) { temperatureViewModel.updatePlot(); + terminalViewModel.updateOutput(); }); //~~ Speed controls @@ -1618,71 +1716,6 @@ $(function() { //~~ Offline overlay $("#offline_overlay_reconnect").click(function() {dataUpdater.reconnect()}); - //~~ Alert - - /* - function displayAlert(text, timeout, type) { - var placeholder = $("#alert_placeholder"); - - var alertType = ""; - if (type == "success" || type == "error" || type == "info") { - alertType = " alert-" + type; - } - - placeholder.append($("

" + text + "

")); - placeholder.fadeIn(); - $("#activeAlert").delay(timeout).fadeOut("slow", function() {$(this).remove(); $("#alert_placeholder").hide();}); - } - */ - - //~~ Login/logout - - $("#login_button").click(function() { - var username = $("#login_user").val(); - var password = $("#login_pass").val(); - var remember = $("#login_remember").is(":checked"); - - $.ajax({ - url: AJAX_BASEURL + "login", - type: "POST", - data: {"user": username, "pass": password, "remember": remember}, - success: function(response) { - $.pnotify({title: "Login successful", text: "You are now logged in", type: "success"}); - $("#login_dropdown_text").text("\"" + response.name + "\""); - $("#login_dropdown_loggedout").removeClass("dropdown-menu").addClass("hide"); - $("#login_dropdown_loggedin").removeClass("hide").addClass("dropdown-menu"); - }, - error: function(jqXHR, textStatus, errorThrown) { - $.pnotify({title: "Login failed", text: "User unknown or wrong password", type: "error"}); - } - }) - }); - $("#logout_button").click(function(){ - $.ajax({ - url: AJAX_BASEURL + "logout", - type: "POST", - success: function(response) { - $.pnotify({title: "Logout successful", text: "You are now logged out", type: "success"}); - $("#login_dropdown_text").text("Login"); - $("#login_dropdown_loggedin").removeClass("dropdown-menu").addClass("hide"); - $("#login_dropdown_loggedout").removeClass("hide").addClass("dropdown-menu"); - } - }) - }) - - $.ajax({ - url: AJAX_BASEURL + "login", - type: "POST", - data: {"passive": true}, - success: function(response) { - if (response["name"]) { - $("#login_dropdown_text").text("\"" + response.name + "\""); - $("#login_dropdown_loggedout").removeClass("dropdown-menu").addClass("hide"); - $("#login_dropdown_loggedin").removeClass("hide").addClass("dropdown-menu"); - } - } - }) - //~~ knockout.js bindings ko.bindingHandlers.popover = { @@ -1717,14 +1750,15 @@ $(function() { var timelapseElement = document.getElementById("timelapse"); if (timelapseElement) { - ko.applyBindings(timelapseViewModel, document.getElementById("timelapse")); + ko.applyBindings(timelapseViewModel, timelapseElement); } var gCodeVisualizerElement = document.getElementById("gcode"); - if(gCodeVisualizerElement){ + if (gCodeVisualizerElement) { gcodeViewModel.initialize(); } //~~ startup commands + loginStateViewModel.requestData(); connectionViewModel.requestData(); controlViewModel.requestData(); gcodeFilesViewModel.requestData(); @@ -1745,7 +1779,7 @@ $(function() { $.pnotify.defaults.history = false; - // Fix input element click problem + // Fix input element click problem on login dialog $('.dropdown input, .dropdown label').click(function(e) { e.stopPropagation(); }); diff --git a/octoprint/templates/index.html b/octoprint/templates/index.html index 998e345..632ed60 100644 --- a/octoprint/templates/index.html +++ b/octoprint/templates/index.html @@ -33,9 +33,13 @@ OctoPrint @@ -113,9 +115,9 @@ @@ -209,12 +211,12 @@
- + °C
- - +

General

- - - + + +
@@ -341,7 +343,7 @@ @@ -360,30 +362,30 @@
- + % - +
- + % - +
- + % - +
- + % - +
@@ -482,7 +484,7 @@
- +
{% if enableTimelapse %} @@ -490,7 +492,7 @@

Timelapse Configuration

- @@ -505,7 +507,7 @@
- +

Finished Timelapses

diff --git a/octoprint/users.py b/octoprint/users.py index d502da7..f8268f1 100644 --- a/octoprint/users.py +++ b/octoprint/users.py @@ -150,22 +150,34 @@ class UnknownRole(Exception): class User(UserMixin): def __init__(self, username, passwordHash, active, roles): - self.username = username - self.passwordHash = passwordHash - self.active = active - self.roles = roles + self._username = username + self._passwordHash = passwordHash + self._active = active + self._roles = roles + + def check_password(self, passwordHash): + return self._passwordHash == passwordHash def get_id(self): - return self.username + return self._username + + def get_name(self): + return self._username def is_active(self): - return self.active + return self._active + + def is_user(self): + return "user" in self._roles + + def is_admin(self): + return "admin" in self._roles ##~~ DummyUser object to use when accessControl is disabled -class DummyUser(UserMixin): +class DummyUser(User): def __init__(self): - self.roles = UserManager.valid_roles + User.__init__(self, "dummy", "", True, UserManager.valid_roles) - def get_id(self): - return "dummy" \ No newline at end of file + def check_password(self, passwordHash): + return True \ No newline at end of file