Furhter work on user management
parent
93a73a0ad8
commit
3e5a6d3679
|
@ -122,31 +122,29 @@ def index():
|
|||
|
||||
#~~ Printer control
|
||||
|
||||
@app.route(BASEURL + "control/connectionOptions", methods=["GET"])
|
||||
@app.route(BASEURL + "control/connection/options", methods=["GET"])
|
||||
def connectionOptions():
|
||||
return jsonify(getConnectionOptions())
|
||||
|
||||
@app.route(BASEURL + "control/connect", methods=["POST"])
|
||||
@app.route(BASEURL + "control/connection", methods=["POST"])
|
||||
@login_required
|
||||
def connect():
|
||||
port = None
|
||||
baudrate = None
|
||||
if "port" in request.values.keys():
|
||||
port = request.values["port"]
|
||||
if "baudrate" in request.values.keys():
|
||||
baudrate = request.values["baudrate"]
|
||||
if "save" in request.values.keys():
|
||||
settings().set(["serial", "port"], port)
|
||||
settings().setInt(["serial", "baudrate"], baudrate)
|
||||
settings().save()
|
||||
printer.connect(port=port, baudrate=baudrate)
|
||||
return jsonify(state="Connecting")
|
||||
if "command" in request.values.keys() and request.values["command"] == "connect":
|
||||
port = None
|
||||
baudrate = None
|
||||
if "port" in request.values.keys():
|
||||
port = request.values["port"]
|
||||
if "baudrate" in request.values.keys():
|
||||
baudrate = request.values["baudrate"]
|
||||
if "save" in request.values.keys():
|
||||
settings().set(["serial", "port"], port)
|
||||
settings().setInt(["serial", "baudrate"], baudrate)
|
||||
settings().save()
|
||||
printer.connect(port=port, baudrate=baudrate)
|
||||
elif "command" in request.values.keys() and request.values["command"] == "disconnect":
|
||||
printer.disconnect()
|
||||
|
||||
@app.route(BASEURL + "control/disconnect", methods=["POST"])
|
||||
@login_required
|
||||
def disconnect():
|
||||
printer.disconnect()
|
||||
return jsonify(state="Offline")
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@app.route(BASEURL + "control/command", methods=["POST"])
|
||||
@login_required
|
||||
|
@ -172,36 +170,27 @@ def printerCommand():
|
|||
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@app.route(BASEURL + "control/print", methods=["POST"])
|
||||
@app.route(BASEURL + "control/job", 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()
|
||||
def printJobControl():
|
||||
if "command" in request.values.keys():
|
||||
if request.values["command"] == "start":
|
||||
printer.startPrint()
|
||||
elif request.values["command"] == "pause":
|
||||
printer.togglePausePrint()
|
||||
elif request.values["command"] == "cancel":
|
||||
printer.cancelPrint()
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@app.route(BASEURL + "control/temperature", methods=["POST"])
|
||||
@login_required
|
||||
def setTargetTemperature():
|
||||
if not printer.isOperational():
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
elif request.values.has_key("temp"):
|
||||
# set target temperature
|
||||
if "temp" in request.values.keys():
|
||||
# set target temperature
|
||||
temp = request.values["temp"]
|
||||
printer.command("M104 S" + temp)
|
||||
|
||||
elif request.values.has_key("bedTemp"):
|
||||
if "bedTemp" in request.values.keys():
|
||||
# set target bed temperature
|
||||
bedTemp = request.values["bedTemp"]
|
||||
printer.command("M140 S" + bedTemp)
|
||||
|
@ -342,7 +331,7 @@ def deleteTimelapse(filename):
|
|||
os.remove(secure)
|
||||
return getTimelapseData()
|
||||
|
||||
@app.route(BASEURL + "timelapse/config", methods=["POST"])
|
||||
@app.route(BASEURL + "timelapse", methods=["POST"])
|
||||
@login_required
|
||||
def setTimelapseConfig():
|
||||
if request.values.has_key("type"):
|
||||
|
@ -451,6 +440,90 @@ def setSettings():
|
|||
|
||||
return getSettings()
|
||||
|
||||
#~~ user settings
|
||||
|
||||
@app.route(BASEURL + "users", methods=["GET"])
|
||||
@login_required
|
||||
@admin_permission.require()
|
||||
def getUsers():
|
||||
return jsonify({"users": userManager.getAllUsers()})
|
||||
|
||||
@app.route(BASEURL + "users", methods=["POST"])
|
||||
@login_required
|
||||
@admin_permission.require()
|
||||
def addUser():
|
||||
if "application/json" in request.headers["Content-Type"]:
|
||||
data = request.json
|
||||
|
||||
name = data["name"]
|
||||
password = data["password"]
|
||||
active = data["active"]
|
||||
|
||||
roles = ["user"]
|
||||
if "admin" in data.keys() and data["admin"]:
|
||||
roles.append("admin")
|
||||
|
||||
try:
|
||||
userManager.addUser(name, password, active, roles)
|
||||
except users.UserAlreadyExists:
|
||||
return app.make_response(("User already exists: " % name, 409, []))
|
||||
return getUsers()
|
||||
|
||||
@app.route(BASEURL + "users/<username>", methods=["GET"])
|
||||
@login_required
|
||||
@admin_permission.require()
|
||||
def getUser(username):
|
||||
user = userManager.findUser(username)
|
||||
if user is not None:
|
||||
return jsonify(user.asDict())
|
||||
else:
|
||||
return app.make_response(("Unknown user: " % username, 404, []))
|
||||
|
||||
@app.route(BASEURL + "users/<username>", methods=["PUT"])
|
||||
@login_required
|
||||
@admin_permission.require()
|
||||
def updateUser(username):
|
||||
user = userManager.findUser(username)
|
||||
if user is not None:
|
||||
if "application/json" in request.headers["Content-Type"]:
|
||||
data = request.json
|
||||
|
||||
# change roles
|
||||
roles = ["user"]
|
||||
if "admin" in data.keys() and data["admin"]:
|
||||
roles.append("admin")
|
||||
userManager.changeUserRoles(username, roles)
|
||||
|
||||
# change activation
|
||||
if "active" in data.keys():
|
||||
userManager.changeUserActivation(username, data["active"])
|
||||
return getUsers()
|
||||
else:
|
||||
return app.make_response(("Unknown user: " % username, 404, []))
|
||||
|
||||
@app.route(BASEURL + "users/<username>", methods=["DELETE"])
|
||||
@login_required
|
||||
@admin_permission.require()
|
||||
def removeUser(username):
|
||||
try:
|
||||
userManager.removeUser(username)
|
||||
return getUsers()
|
||||
except users.UnknownUser:
|
||||
return app.make_response(("Unknown user: " % username, 404, []))
|
||||
|
||||
@app.route(BASEURL + "users/<username>/password", methods=["PUT"])
|
||||
@login_required
|
||||
@admin_permission.require()
|
||||
def changePasswordForUser(username):
|
||||
if "application/json" in request.headers["Content-Type"]:
|
||||
data = request.json
|
||||
if "password" in data.keys() and data["password"]:
|
||||
try:
|
||||
userManager.changeUserPassword(username, data["password"])
|
||||
except users.UnknownUser:
|
||||
return app.make_response(("Unknown user: " % username, 404, []))
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
#~~ system control
|
||||
|
||||
@app.route(BASEURL + "system", methods=["POST"])
|
||||
|
|
|
@ -112,7 +112,7 @@ body {
|
|||
.octoprint-container {
|
||||
.accordion-heading {
|
||||
.settings-trigger {
|
||||
float: right;
|
||||
//float: right;
|
||||
padding: 0px 15px;
|
||||
}
|
||||
|
||||
|
@ -192,6 +192,32 @@ table {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// user settings
|
||||
&.settings_users_name {
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.settings_users_active, &.settings_users_admin {
|
||||
text-align: center;
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
&.settings_users_actions {
|
||||
text-align: center;
|
||||
width: 60px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
|
||||
&.disabled {
|
||||
color: #ccc;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -334,7 +360,6 @@ ul.dropdown-menu li a {
|
|||
|
||||
/** Settings dialog */
|
||||
#settings_dialog {
|
||||
width: 650px;
|
||||
}
|
||||
|
||||
/** Footer */
|
||||
|
|
|
@ -100,7 +100,7 @@ function ConnectionViewModel(loginStateViewModel) {
|
|||
|
||||
self.requestData = function() {
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "control/connectionOptions",
|
||||
url: AJAX_BASEURL + "control/connection/options",
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
success: function(response) {
|
||||
|
@ -155,6 +155,7 @@ function ConnectionViewModel(loginStateViewModel) {
|
|||
self.connect = function() {
|
||||
if (self.isErrorOrClosed()) {
|
||||
var data = {
|
||||
"command": "connect",
|
||||
"port": self.selectedPort(),
|
||||
"baudrate": self.selectedBaudrate()
|
||||
};
|
||||
|
@ -163,7 +164,7 @@ function ConnectionViewModel(loginStateViewModel) {
|
|||
data["save"] = true;
|
||||
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "control/connect",
|
||||
url: AJAX_BASEURL + "control/connection",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: data
|
||||
|
@ -171,9 +172,10 @@ function ConnectionViewModel(loginStateViewModel) {
|
|||
} else {
|
||||
self.requestData();
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "control/disconnect",
|
||||
url: AJAX_BASEURL + "control/connection",
|
||||
type: "POST",
|
||||
dataType: "json"
|
||||
dataType: "json",
|
||||
data: {"command": "disconnect"}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1095,11 +1097,181 @@ function GcodeViewModel(loginStateViewModel) {
|
|||
|
||||
}
|
||||
|
||||
function SettingsViewModel(loginStateViewModel) {
|
||||
function UsersViewModel(loginStateViewModel) {
|
||||
var self = this;
|
||||
|
||||
self.loginState = loginStateViewModel;
|
||||
|
||||
// initialize list helper
|
||||
self.listHelper = new ItemListHelper(
|
||||
"users",
|
||||
{
|
||||
"name": function(a, b) {
|
||||
// sorts ascending
|
||||
if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
|
||||
if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
{},
|
||||
"name",
|
||||
[],
|
||||
CONFIG_USERSPERPAGE
|
||||
)
|
||||
|
||||
self.emptyUser = {name: "", admin: false, active: false};
|
||||
|
||||
self.currentUser = ko.observable(self.emptyUser);
|
||||
|
||||
self.editorUsername = ko.observable(undefined);
|
||||
self.editorPassword = ko.observable(undefined);
|
||||
self.editorRepeatedPassword = ko.observable(undefined);
|
||||
self.editorAdmin = ko.observable(undefined);
|
||||
self.editorActive = ko.observable(undefined);
|
||||
|
||||
self.currentUser.subscribe(function(newValue) {
|
||||
if (newValue === undefined) {
|
||||
self.editorUsername(undefined);
|
||||
self.editorAdmin(undefined);
|
||||
self.editorActive(undefined);
|
||||
} else {
|
||||
self.editorUsername(newValue.name);
|
||||
self.editorAdmin(newValue.admin);
|
||||
self.editorActive(newValue.active);
|
||||
}
|
||||
self.editorPassword(undefined);
|
||||
self.editorRepeatedPassword(undefined);
|
||||
});
|
||||
|
||||
self.editorPasswordMismatch = ko.computed(function() {
|
||||
return self.editorPassword() != self.editorRepeatedPassword();
|
||||
});
|
||||
|
||||
self.requestData = function() {
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "users",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: self.fromResponse
|
||||
});
|
||||
}
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
self.listHelper.updateItems(response.users);
|
||||
}
|
||||
|
||||
self.showAddUserDialog = function() {
|
||||
self.currentUser(undefined);
|
||||
$("#settings-usersDialogAddUser").modal("show");
|
||||
}
|
||||
|
||||
self.confirmAddUser = function() {
|
||||
var user = {name: self.editorUsername(), password: self.editorPassword(), admin: self.editorAdmin(), active: self.editorActive()};
|
||||
self.addUser(user, function() {
|
||||
// close dialog
|
||||
self.currentUser(undefined);
|
||||
$("#settings-usersDialogAddUser").modal("hide");
|
||||
});
|
||||
}
|
||||
|
||||
self.showEditUserDialog = function(user) {
|
||||
self.currentUser(user);
|
||||
$("#settings-usersDialogEditUser").modal("show");
|
||||
}
|
||||
|
||||
self.confirmEditUser = function() {
|
||||
var user = self.currentUser();
|
||||
user.active = self.editorActive();
|
||||
user.admin = self.editorAdmin();
|
||||
|
||||
// make AJAX call
|
||||
self.updateUser(user, function() {
|
||||
// close dialog
|
||||
self.currentUser(undefined);
|
||||
$("#settings-usersDialogEditUser").modal("hide");
|
||||
});
|
||||
}
|
||||
|
||||
self.showChangePasswordDialog = function(user) {
|
||||
self.currentUser(user);
|
||||
$("#settings-usersDialogChangePassword").modal("show");
|
||||
}
|
||||
|
||||
self.confirmChangePassword = function() {
|
||||
self.updatePassword(self.currentUser().name, self.editorPassword(), function() {
|
||||
// close dialog
|
||||
self.currentUser(undefined);
|
||||
$("#settings-usersDialogChangePassword").modal("hide");
|
||||
});
|
||||
}
|
||||
|
||||
//~~ AJAX calls
|
||||
|
||||
self.addUser = function(user, callback) {
|
||||
if (user === undefined) return;
|
||||
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "users",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
data: JSON.stringify(user),
|
||||
success: function(response) {
|
||||
self.fromResponse(response);
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.removeUser = function(user, callback) {
|
||||
if (user === undefined) return;
|
||||
if (user.name == loginStateViewModel.username()) {
|
||||
// we do not allow to delete ourself
|
||||
$.pnotify({title: "Not possible", text: "You may not delete your own account.", type: "error"});
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "users/" + user.name,
|
||||
type: "DELETE",
|
||||
success: function(response) {
|
||||
self.fromResponse(response);
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.updateUser = function(user, callback) {
|
||||
if (user === undefined) return;
|
||||
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "users/" + user.name,
|
||||
type: "PUT",
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
data: JSON.stringify(user),
|
||||
success: function(response) {
|
||||
self.fromResponse(response);
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.updatePassword = function(username, password, callback) {
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "users/" + username + "/password",
|
||||
type: "PUT",
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
data: JSON.stringify({password: password}),
|
||||
success: callback
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function SettingsViewModel(loginStateViewModel, usersViewModel) {
|
||||
var self = this;
|
||||
|
||||
self.loginState = loginStateViewModel;
|
||||
self.users = usersViewModel;
|
||||
|
||||
self.appearance_name = ko.observable(undefined);
|
||||
self.appearance_color = ko.observable(undefined);
|
||||
|
||||
|
@ -1143,7 +1315,8 @@ function SettingsViewModel(loginStateViewModel) {
|
|||
type: "GET",
|
||||
dataType: "json",
|
||||
success: self.fromResponse
|
||||
})
|
||||
});
|
||||
self.users.requestData();
|
||||
}
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
|
@ -1574,10 +1747,11 @@ function AppearanceViewModel(settingsViewModel) {
|
|||
$(function() {
|
||||
|
||||
//~~ View models
|
||||
var loginStateViewModel = new LoginStateViewModel();
|
||||
var loginStateViewModel = new LoginStateViewModel(loginStateViewModel);
|
||||
var usersViewModel = new UsersViewModel(loginStateViewModel);
|
||||
var connectionViewModel = new ConnectionViewModel(loginStateViewModel);
|
||||
var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel);
|
||||
var settingsViewModel = new SettingsViewModel(loginStateViewModel);
|
||||
var settingsViewModel = new SettingsViewModel(loginStateViewModel, usersViewModel);
|
||||
var appearanceViewModel = new AppearanceViewModel(settingsViewModel);
|
||||
var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel);
|
||||
var controlViewModel = new ControlsViewModel(loginStateViewModel);
|
||||
|
@ -1618,7 +1792,7 @@ $(function() {
|
|||
return false;
|
||||
})
|
||||
|
||||
//~~ Print job control
|
||||
//~~ Print job control (should move to PrinterStateViewModel)
|
||||
|
||||
//~~ Temperature control (should really move to knockout click binding)
|
||||
|
||||
|
@ -1779,6 +1953,11 @@ $(function() {
|
|||
|
||||
$.pnotify.defaults.history = false;
|
||||
|
||||
$.fn.modal.defaults.maxHeight = function(){
|
||||
// subtract the height of the modal header and footer
|
||||
return $(window).height() - 165;
|
||||
}
|
||||
|
||||
// Fix input element click problem on login dialog
|
||||
$('.dropdown input, .dropdown label').click(function(e) {
|
||||
e.stopPropagation();
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<link rel="apple-touch-icon" sizes="144x144" href="{{ url_for('static', filename='img/apple-touch-icon-144x144.png') }}">
|
||||
|
||||
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet" media="screen">
|
||||
<link href="{{ url_for('static', filename='css/bootstrap-modal.css') }}" rel="stylesheet" media="screen">
|
||||
<link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet" media="screen">
|
||||
<link href="{{ url_for('static', filename='css/jquery.fileupload-ui.css') }}" rel="stylesheet" media="screen">
|
||||
<link href="{{ url_for('static', filename='css/jquery.pnotify.default.css') }}" rel="stylesheet" media="screen">
|
||||
|
@ -19,6 +20,7 @@
|
|||
var AJAX_BASEURL = "/ajax/";
|
||||
var CONFIG_GCODEFILESPERPAGE = 5;
|
||||
var CONFIG_TIMELAPSEFILESPERPAGE = 10;
|
||||
var CONFIG_USERSPERPAGE = 10;
|
||||
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
|
||||
|
||||
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/WebSocketMain.swf') }}";
|
||||
|
@ -128,7 +130,7 @@
|
|||
|
||||
<div class="settings-trigger btn-group">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="icon-wrench"></i>
|
||||
<i class="icon-list">sdfsdf</i>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> Sort by name (ascending)</a></li>
|
||||
|
@ -517,18 +519,18 @@
|
|||
</div>
|
||||
<table class="table table-striped table-hover table-condensed table-hover" id="timelapse_files">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="timelapse_files_name">Name</th>
|
||||
<th class="timelapse_files_size">Size</th>
|
||||
<th class="timelapse_files_action">Action</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="timelapse_files_name">Name</th>
|
||||
<th class="timelapse_files_size">Size</th>
|
||||
<th class="timelapse_files_action">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: listHelper.paginatedItems">
|
||||
<tr data-bind="attr: {title: name}">
|
||||
<td class="timelapse_files_name" data-bind="text: name"></td>
|
||||
<td class="timelapse_files_size" data-bind="text: size"></td>
|
||||
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: $parent.removeFile"></a> | <a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
|
||||
</tr>
|
||||
<tr data-bind="attr: {title: name}">
|
||||
<td class="timelapse_files_name" data-bind="text: name"></td>
|
||||
<td class="timelapse_files_size" data-bind="text: size"></td>
|
||||
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: $parent.removeFile"></a> | <a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination pagination-mini pagination-centered">
|
||||
|
@ -565,6 +567,8 @@
|
|||
<script type="text/javascript" src="{{ url_for('static', filename='js/underscore-min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/knockout-2.2.1.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap-modalmanager.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap-modal.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.ui.core.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.ui.widget.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.ui.mouse.js') }}"></script>
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
<div id="settings_dialog" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="settings_dialog_label" aria-hidden="true">
|
||||
<div id="settings_dialog" class="modal hide fade container" tabindex="-1" role="dialog" aria-labelledby="settings_dialog_label" aria-hidden="true">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3 id="settings_dialog_label">OctoPrint Settings</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="tabbable">
|
||||
<ul class="nav nav-pills" id="settingsTabs">
|
||||
<ul class="nav nav-list span4" id="settingsTabs">
|
||||
<li class="active"><a href="#settings_printerParameters" data-toggle="tab">Printer Parameters</a></li>
|
||||
<li><a href="#settings_webcam" data-toggle="tab">Webcam</a></li>
|
||||
<li><a href="#settings_features" data-toggle="tab">Features</a></li>
|
||||
<li><a href="#settings_folder" data-toggle="tab">Folder</a></li>
|
||||
<li><a href="#settings_temperature" data-toggle="tab">Temperature</a></li>
|
||||
<li><a href="#settings_appearance" data-toggle="tab">Appearance</a></li>
|
||||
<li><a href="#settings_users" data-toggle="tab">Users</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-content span8">
|
||||
<div class="tab-pane active" id="settings_printerParameters">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
|
@ -184,6 +185,148 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="settings_users">
|
||||
<table class="table table-condensed table-hover" id="system_users">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="settings_users_name">Name</th>
|
||||
<th class="settings_users_active">Active</th>
|
||||
<th class="settings_users_admin">Admin</th>
|
||||
<th class="settings_users_actions">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: users.listHelper.paginatedItems">
|
||||
<tr>
|
||||
<td class="settings_users_name" data-bind="text: name"></td>
|
||||
<td class="settings_users_active"><i data-bind="css: { 'icon-check': active, 'icon-check-empty': !active }"></i></td>
|
||||
<td class="settings_users_admin"><i data-bind="css: { 'icon-check': admin, 'icon-check-empty': !admin }"></i></td>
|
||||
<td class="settings_users_actions" class="system_users_action">
|
||||
<a href="#" class="icon-pencil" title="Update User" data-bind="click: function() { $root.users.showEditUserDialog($data); }"></a> | <a href="#" class="icon-key" title="Change password" data-bind="click: function() { $root.users.showChangePasswordDialog($data); }"></a> | <a href="#" class="icon-trash" title="Delete user" data-bind="click: function() { $root.users.removeUser($data); }"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination pagination-mini pagination-centered">
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: users.listHelper.currentPage() === 0}"><a href="#" data-bind="click: users.listHelper.prevPage">«</a></li>
|
||||
</ul>
|
||||
<ul data-bind="foreach: users.listHelper.pages">
|
||||
<li data-bind="css: { active: $data.number === $root.users.listHelper.currentPage(), disabled: $data.number === -1 }"><a href="#" data-bind="text: $data.text, click: function() { $root.users.listHelper.changePage($data.number); }"></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: users.listHelper.currentPage() === users.listHelper.lastPage()}"><a href="#" data-bind="click: users.listHelper.nextPage">»</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button title="Add user" class="btn" data-bind="click: $root.users.showAddUserDialog"><i class="icon-plus"></i> Create new user</button>
|
||||
|
||||
<!-- Modals for user management -->
|
||||
|
||||
<div id="settings-usersDialogAddUser" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3>Create new user</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-usersDialogAddUserName">Username</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" id="settings-usersDialogAddUserName" data-bind="value: $root.users.editorUsername" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-usersDialogAddUserPassword1">Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-block-level" id="settings-usersDialogAddUserPassword1" data-bind="value: $root.users.editorPassword" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="css: {error: $root.users.editorPasswordMismatch()}">
|
||||
<label class="control-label" for="settings-usersDialogAddUserPassword2">Repeat Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-block-level" id="settings-usersDialogAddUserPassword2" data-bind="value: $root.users.editorRepeatedPassword, valueUpdate: 'afterkeydown'" required>
|
||||
<span class="help-inline" data-bind="visible: $root.users.editorPasswordMismatch()">Passwords do not match</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="settings-usersDialogAddUserActive" data-bind="checked: $root.users.editorActive"> Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="settings-usersDialogAddUserAdmin" data-bind="checked: $root.users.editorAdmin"> Admin
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Abort</button>
|
||||
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmAddUser(); }, enable: !$root.users.editorPasswordMismatch()">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-usersDialogEditUser" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3>Edit user "<span data-bind="text: $root.users.editorUsername"></span>"</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="settings-usersDialogEditUserActive" data-bind="checked: $root.users.editorActive"> Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="settings-usersDialogEditUserAdmin" data-bind="checked: $root.users.editorAdmin"> Admin
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Abort</button>
|
||||
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmEditUser(); }">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-usersDialogChangePassword" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3>Change password for user "<span data-bind="text: $root.users.editorUsername"></span>"</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-usersDialogChangePasswordPassword1">New Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-block-level" id="settings-usersDialogChangePasswordPassword1" data-bind="value: $root.users.editorPassword" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="css: {error: $root.users.editorPasswordMismatch()}">
|
||||
<label class="control-label" for="settings-usersDialogChangePasswordPassword2">Repeat Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-block-level" id="settings-usersDialogChangePasswordPassword2" data-bind="value: $root.users.editorRepeatedPassword, valueUpdate: 'afterkeydown'" required>
|
||||
<span class="help-inline" data-bind="visible: $root.users.editorPasswordMismatch()">Passwords do not match</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Abort</button>
|
||||
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmChangePassword(); }, enable: !$root.users.editorPasswordMismatch()">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,16 +16,22 @@ class UserManager(object):
|
|||
def createPasswordHash(password):
|
||||
return hashlib.sha512(password + "mvBUTvwzBzD3yPwvnJ4E4tXNf3CGJvvW").hexdigest()
|
||||
|
||||
def addUser(self, username, password):
|
||||
def addUser(self, username, password, active, roles):
|
||||
pass
|
||||
|
||||
def addRoleToUser(self, username, role):
|
||||
def changeUserActivation(self, username, active):
|
||||
pass
|
||||
|
||||
def removeRoleFromUser(self, username, role):
|
||||
def changeUserRoles(self, username, roles):
|
||||
pass
|
||||
|
||||
def updateUser(self, username, password):
|
||||
def addRolesToUser(self, username, roles):
|
||||
pass
|
||||
|
||||
def removeRolesFromUser(self, username, roles):
|
||||
pass
|
||||
|
||||
def changeUserPassword(self, username, password):
|
||||
pass
|
||||
|
||||
def removeUser(self, username):
|
||||
|
@ -34,6 +40,9 @@ class UserManager(object):
|
|||
def findUser(self, username=None):
|
||||
return None
|
||||
|
||||
def getAllUsers(self):
|
||||
return []
|
||||
|
||||
##~~ FilebasedUserManager, takes available users from users.yaml file
|
||||
|
||||
class FilebasedUserManager(UserManager):
|
||||
|
@ -44,22 +53,18 @@ class FilebasedUserManager(UserManager):
|
|||
if userfile is None:
|
||||
userfile = os.path.join(settings().settings_dir, "users.yaml")
|
||||
self._userfile = userfile
|
||||
self._users = None
|
||||
self._users = {}
|
||||
self._dirty = False
|
||||
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
self._users = {
|
||||
"admin": User("admin", "7557160613d5258f883014a7c3c0428de53040fc152b1791f1cc04a62b428c0c2a9c46ed330cdce9689353ab7a5352ba2b2ceb459b96e9c8ed7d0cb0b2c0c076", True, ["user", "admin"]),
|
||||
"user": User("user", "ced28770ae4457f420e322a5c7b8abc5f31432aef2552871909d6f4f372d1e0d6e0e7be14114656971eeba88e6462d5ea596b656d521c847047a496fecc431a5", True, ["user"])
|
||||
}
|
||||
if os.path.exists(self._userfile) and os.path.isfile(self._userfile):
|
||||
with open(self._userfile, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
for name in data.keys():
|
||||
attributes = data[name]
|
||||
self._users[name] = User(name, attributes.password, attributes.active, attributes.roles)
|
||||
self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"])
|
||||
|
||||
def _save(self, force=False):
|
||||
if not self._dirty and not force:
|
||||
|
@ -69,9 +74,9 @@ class FilebasedUserManager(UserManager):
|
|||
for name in self._users.keys():
|
||||
user = self._users[name]
|
||||
data[name] = {
|
||||
"password": user.passwordHash,
|
||||
"active": user.active,
|
||||
"roles": user.roles
|
||||
"password": user._passwordHash,
|
||||
"active": user._active,
|
||||
"roles": user._roles
|
||||
}
|
||||
|
||||
with open(self._userfile, "wb") as f:
|
||||
|
@ -79,42 +84,65 @@ class FilebasedUserManager(UserManager):
|
|||
self._dirty = False
|
||||
self._load()
|
||||
|
||||
def addUser(self, username, password):
|
||||
def addUser(self, username, password, active=False, roles=["user"]):
|
||||
if username in self._users.keys():
|
||||
raise UserAlreadyExists(username)
|
||||
|
||||
self._users[username] = User(username, UserManager.createPasswordHash(password), False, ["user"])
|
||||
self._users[username] = User(username, UserManager.createPasswordHash(password), active, roles)
|
||||
self._dirty = True
|
||||
self._save()
|
||||
|
||||
def addRoleToUser(self, username, role):
|
||||
def changeUserActivation(self, username, active):
|
||||
if not username in self._users.keys():
|
||||
raise UnknownUser(username)
|
||||
|
||||
if self._users[username]._active != active:
|
||||
self._users[username]._active = active
|
||||
self._dirty = True
|
||||
self._save()
|
||||
|
||||
def changeUserRoles(self, username, roles):
|
||||
if not username in self._users.keys():
|
||||
raise UnknownUser(username)
|
||||
|
||||
user = self._users[username]
|
||||
if not role in user.roles:
|
||||
user.roles.append(role)
|
||||
self._dirty = True
|
||||
self._save()
|
||||
|
||||
def removeRoleFromUser(self, username, role):
|
||||
removedRoles = set(user._roles) - set(roles)
|
||||
self.removeRolesFromUser(username, removedRoles)
|
||||
|
||||
addedRoles = set(roles) - set(user._roles)
|
||||
self.addRolesToUser(username, addedRoles)
|
||||
|
||||
def addRolesToUser(self, username, roles):
|
||||
if not username in self._users.keys():
|
||||
raise UnknownUser(username)
|
||||
|
||||
user = self._users[username]
|
||||
if role in user.roles:
|
||||
user.roles.remove(role)
|
||||
self._dirty = True
|
||||
self._save()
|
||||
for role in roles:
|
||||
if not role in user._roles:
|
||||
user._roles.append(role)
|
||||
self._dirty = True
|
||||
self._save()
|
||||
|
||||
def updateUser(self, username, password):
|
||||
def removeRolesFromUser(self, username, roles):
|
||||
if not username in self._users.keys():
|
||||
raise UnknownUser(username)
|
||||
|
||||
user = self._users[username]
|
||||
for role in roles:
|
||||
if role in user._roles:
|
||||
user._roles.remove(role)
|
||||
self._dirty = True
|
||||
self._save()
|
||||
|
||||
def changeUserPassword(self, username, password):
|
||||
if not username in self._users.keys():
|
||||
raise UnknownUser(username)
|
||||
|
||||
passwordHash = UserManager.createPasswordHash(password)
|
||||
user = self._users[username]
|
||||
if user.passwordHash != passwordHash:
|
||||
user.passwordHash = passwordHash
|
||||
if user._passwordHash != passwordHash:
|
||||
user._passwordHash = passwordHash
|
||||
self._dirty = True
|
||||
self._save()
|
||||
|
||||
|
@ -135,6 +163,9 @@ class FilebasedUserManager(UserManager):
|
|||
|
||||
return self._users[username]
|
||||
|
||||
def getAllUsers(self):
|
||||
return map(lambda x: x.asDict(), self._users.values())
|
||||
|
||||
##~~ Exceptions
|
||||
|
||||
class UserAlreadyExists(Exception):
|
||||
|
@ -158,6 +189,13 @@ class User(UserMixin):
|
|||
self._active = active
|
||||
self._roles = roles
|
||||
|
||||
def asDict(self):
|
||||
return {
|
||||
"name": self._username,
|
||||
"active": self.is_active(),
|
||||
"admin": self.is_admin()
|
||||
}
|
||||
|
||||
def check_password(self, passwordHash):
|
||||
return self._passwordHash == passwordHash
|
||||
|
||||
|
|
Loading…
Reference in New Issue