Furhter work on user management

master
Gina Häußge 2013-04-12 23:08:14 +02:00
parent 93a73a0ad8
commit 3e5a6d3679
6 changed files with 556 additions and 94 deletions

View File

@ -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"])

View File

@ -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 */

View File

@ -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();

View File

@ -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>&nbsp;|&nbsp;<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>&nbsp;|&nbsp;<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>

View File

@ -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">&times;</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>&nbsp;|&nbsp;<a href="#" class="icon-key" title="Change password" data-bind="click: function() { $root.users.showChangePasswordDialog($data); }"></a>&nbsp;|&nbsp;<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">&times;</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">&times;</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">&times;</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>

View File

@ -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