Implemented settings page and finalized multi-user support
This commit is contained in:
parent
cd9bdbb1c8
commit
f86e2eb5a7
18 changed files with 666 additions and 86 deletions
|
@ -1,6 +1,7 @@
|
|||
import datetime
|
||||
import re
|
||||
|
||||
from flask import Blueprint, request, redirect, render_template, make_response, url_for
|
||||
from flask import Blueprint, request, redirect, render_template, make_response
|
||||
|
||||
from platypush.backend.http.app import template_folder
|
||||
from platypush.backend.http.utils import HttpUtils
|
||||
|
@ -18,11 +19,18 @@ __routes__ = [
|
|||
def login():
|
||||
""" Login page """
|
||||
user_manager = UserManager()
|
||||
redirect_page = request.args.get('redirect', '/')
|
||||
session_token = request.cookies.get('session_token')
|
||||
|
||||
# redirect_page = request.args.get('redirect', request.headers.get('Referer', '/'))
|
||||
redirect_page = request.args.get('redirect')
|
||||
if not redirect_page:
|
||||
redirect_page = request.headers.get('Referer', '/')
|
||||
if re.search('(^https?://[^/]+)?/login[^?#]?', redirect_page):
|
||||
# Prevent redirect loop
|
||||
redirect_page = '/'
|
||||
|
||||
if session_token:
|
||||
user = user_manager.authenticate_user_session(session_token)
|
||||
user, session = user_manager.authenticate_user_session(session_token)
|
||||
if user:
|
||||
return redirect(redirect_page, 302)
|
||||
|
||||
|
|
34
platypush/backend/http/app/routes/logout.py
Normal file
34
platypush/backend/http/app/routes/logout.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from flask import Blueprint, request, redirect, make_response, abort
|
||||
|
||||
from platypush.backend.http.app import template_folder
|
||||
from platypush.user import UserManager
|
||||
|
||||
logout = Blueprint('logout', __name__, template_folder=template_folder)
|
||||
|
||||
# Declare routes list
|
||||
__routes__ = [
|
||||
logout,
|
||||
]
|
||||
|
||||
|
||||
@logout.route('/logout', methods=['GET', 'POST'])
|
||||
def logout():
|
||||
""" Logout page """
|
||||
user_manager = UserManager()
|
||||
redirect_page = request.args.get('redirect', request.headers.get('Referer', '/login'))
|
||||
session_token = request.cookies.get('session_token')
|
||||
|
||||
if not session_token:
|
||||
return abort(417, 'Not logged in')
|
||||
|
||||
user, session = user_manager.authenticate_user_session(session_token)
|
||||
if not user:
|
||||
return abort(403, 'Invalid session token')
|
||||
|
||||
redirect_target = redirect(redirect_page, 302)
|
||||
response = make_response(redirect_target)
|
||||
response.set_cookie('session_token', '', expires=0)
|
||||
return response
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
|
@ -22,7 +22,7 @@ def register():
|
|||
session_token = request.cookies.get('session_token')
|
||||
|
||||
if session_token:
|
||||
user = user_manager.authenticate_user_session(session_token)
|
||||
user, session = user_manager.authenticate_user_session(session_token)
|
||||
if user:
|
||||
return redirect(redirect_page, 302)
|
||||
|
||||
|
|
26
platypush/backend/http/app/routes/settings.py
Normal file
26
platypush/backend/http/app/routes/settings.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from flask import Blueprint, request, render_template
|
||||
|
||||
from platypush.backend.http.app import template_folder
|
||||
from platypush.backend.http.app.utils import authenticate
|
||||
from platypush.backend.http.utils import HttpUtils
|
||||
from platypush.config import Config
|
||||
from platypush.user import UserManager
|
||||
|
||||
settings = Blueprint('settings', __name__, template_folder=template_folder)
|
||||
|
||||
# Declare routes list
|
||||
__routes__ = [
|
||||
settings,
|
||||
]
|
||||
|
||||
|
||||
@settings.route('/settings', methods=['GET'])
|
||||
@authenticate()
|
||||
def settings():
|
||||
""" Settings page """
|
||||
user_manager = UserManager()
|
||||
users = user_manager.get_users()
|
||||
return render_template('settings/index.html', utils=HttpUtils, users=users, token=Config.get('token'))
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
|
@ -146,12 +146,33 @@ def _authenticate_session():
|
|||
user_session_token = request.cookies.get('session_token')
|
||||
|
||||
if user_session_token:
|
||||
user = user_manager.authenticate_user_session(user_session_token)
|
||||
user, session = user_manager.authenticate_user_session(user_session_token)
|
||||
|
||||
return user is not None
|
||||
|
||||
|
||||
def authenticate(redirect_page='', skip_auth_methods=None):
|
||||
def _authenticate_csrf_token():
|
||||
user_manager = UserManager()
|
||||
user_session_token = None
|
||||
user = None
|
||||
|
||||
if 'X-Session-Token' in request.headers:
|
||||
user_session_token = request.headers['X-Session-Token']
|
||||
elif 'session_token' in request.args:
|
||||
user_session_token = request.args.get('session_token')
|
||||
elif 'session_token' in request.cookies:
|
||||
user_session_token = request.cookies.get('session_token')
|
||||
|
||||
if user_session_token:
|
||||
user, session = user_manager.authenticate_user_session(user_session_token)
|
||||
|
||||
if user is None:
|
||||
return False
|
||||
|
||||
return session.csrf_token is None or request.form.get('csrf_token') == session.csrf_token
|
||||
|
||||
|
||||
def authenticate(redirect_page='', skip_auth_methods=None, check_csrf_token=False):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
|
@ -183,6 +204,13 @@ def authenticate(redirect_page='', skip_auth_methods=None):
|
|||
|
||||
return redirect('/login?redirect=' + redirect_page, 307)
|
||||
|
||||
# CSRF token check
|
||||
csrf_check_ok = True
|
||||
if check_csrf_token:
|
||||
csrf_check_ok = _authenticate_csrf_token()
|
||||
if not csrf_check_ok:
|
||||
return abort(403, 'Invalid or missing csrf_token')
|
||||
|
||||
if n_users == 0 and 'session' not in skip_methods:
|
||||
return redirect('/register?redirect=' + redirect_page, 307)
|
||||
|
||||
|
|
111
platypush/backend/http/static/css/source/settings/index.scss
Normal file
111
platypush/backend/http/static/css/source/settings/index.scss
Normal file
|
@ -0,0 +1,111 @@
|
|||
@import 'common/vars';
|
||||
|
||||
@import 'common/mixins';
|
||||
@import 'common/layout';
|
||||
@import 'common/elements';
|
||||
@import 'common/animations';
|
||||
@import 'common/modal';
|
||||
@import 'common/notifications';
|
||||
|
||||
@import 'settings/users';
|
||||
@import 'settings/token';
|
||||
|
||||
$nav-background: #f0f0f0;
|
||||
$section-header-background: #eee;
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
font-family: $default-font-family;
|
||||
font-size: $default-font-size;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
nav {
|
||||
background: $nav-background;
|
||||
color: $default-link-fg;
|
||||
border-right: $default-border-3;
|
||||
|
||||
ul {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-top: $default-border-3;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
|
||||
a:first-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
margin: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
height: inherit;
|
||||
|
||||
.section {
|
||||
overflow: auto;
|
||||
height: inherit;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: $default-border-2;
|
||||
background: $section-header-background;
|
||||
|
||||
h1 {
|
||||
margin: .45em .2em;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
&:hover {
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $default-link-fg;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
21
platypush/backend/http/static/css/source/settings/token.scss
Normal file
21
platypush/backend/http/static/css/source/settings/token.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
#token {
|
||||
.warning {
|
||||
background: $notification-error-bg;
|
||||
border: $notification-error-border;
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.token-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 2em;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
27
platypush/backend/http/static/css/source/settings/users.scss
Normal file
27
platypush/backend/http/static/css/source/settings/users.scss
Normal file
|
@ -0,0 +1,27 @@
|
|||
#users {
|
||||
ul {
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 1em .5em;
|
||||
border-bottom: $default-border-3;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 20em;
|
||||
|
||||
input {
|
||||
margin: 1em .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,21 +4,23 @@
|
|||
|
||||
nav {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
height: var(--nav-height);
|
||||
background: $nav-bg;
|
||||
margin-bottom: $nav-margin;
|
||||
border-bottom: $default-bottom;
|
||||
box-shadow: 0 2.5px 4px 0 #bbb;
|
||||
flex: 0 1 auto;
|
||||
z-index: 2;
|
||||
|
||||
ul {
|
||||
position: relative;
|
||||
width: 75%;
|
||||
display: inline-flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
background: $nav-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: $default-bottom;
|
||||
box-shadow: 0 2.5px 4px 0 #bbb;
|
||||
|
||||
li {
|
||||
padding: 1rem 1.5rem;
|
||||
|
@ -44,42 +46,6 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
.date-time {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
right: 0;
|
||||
margin-right: .7rem;
|
||||
font-size: 14pt;
|
||||
text-shadow: $nav-date-time-shadow;
|
||||
|
||||
.time {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.settings {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
padding: .75rem 1rem;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.decorator {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 20px solid transparent;
|
||||
border-bottom: 20px solid transparent;
|
||||
border-left: 17px solid $selected-bg;
|
||||
|
||||
&:hover {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.decorator {
|
||||
display: none;
|
||||
|
@ -90,5 +56,29 @@ nav {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-time {
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
display: inline-block;
|
||||
right: 0;
|
||||
margin-right: .7rem;
|
||||
font-size: 14pt;
|
||||
text-shadow: $nav-date-time-shadow;
|
||||
|
||||
.time {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.settings {
|
||||
display: inline-block;
|
||||
padding: .75rem 1rem;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,3 @@
|
|||
Vue.component('app-header', {
|
||||
template: '#tmpl-app-header',
|
||||
data: function() {
|
||||
return {
|
||||
now: new Date(),
|
||||
};
|
||||
},
|
||||
|
||||
created: function() {
|
||||
const self = this;
|
||||
setInterval(() => {
|
||||
self.now = new Date();
|
||||
}, 1000)
|
||||
},
|
||||
});
|
||||
|
||||
Vue.component('plugin', {
|
||||
template: '#tmpl-plugin',
|
||||
props: ['config','tag'],
|
||||
|
@ -43,7 +27,7 @@ window.vm = new Vue({
|
|||
const self = this;
|
||||
setInterval(() => {
|
||||
self.now = new Date();
|
||||
}, 1000)
|
||||
}, 1000);
|
||||
|
||||
initEvents();
|
||||
},
|
||||
|
|
184
platypush/backend/http/static/js/settings/index.js
Normal file
184
platypush/backend/http/static/js/settings/index.js
Normal file
|
@ -0,0 +1,184 @@
|
|||
window.vm = new Vue({
|
||||
el: '#app',
|
||||
data: function() {
|
||||
return {
|
||||
config: window.config,
|
||||
sessionToken: undefined,
|
||||
selectedTab: 'users',
|
||||
selectedUser: undefined,
|
||||
|
||||
modalVisible: {
|
||||
addUser: false,
|
||||
changePassword: false,
|
||||
},
|
||||
|
||||
formDisabled: {
|
||||
addUser: false,
|
||||
changePassword: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
userDropdownItems: function() {
|
||||
const self = this;
|
||||
return [
|
||||
{
|
||||
text: 'Change password',
|
||||
iconClass: 'fas fa-key',
|
||||
click: function() {
|
||||
self.modalVisible.changePassword = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Delete user',
|
||||
iconClass: 'fa fa-trash',
|
||||
click: async function() {
|
||||
if (!confirm('Are you sure that you want to remove the user ' + self.selectedUser + '?'))
|
||||
return;
|
||||
|
||||
await request('user.delete_user', {
|
||||
username: self.selectedUser,
|
||||
session_token: self.sessionToken,
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: 'User ' + self.selectedUser + ' removed',
|
||||
image: {
|
||||
iconClass: 'fas fa-check',
|
||||
},
|
||||
});
|
||||
|
||||
window.location.reload();
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
createUser: async function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
let form = [...this.$refs.createUserForm.querySelectorAll('input[name]')].reduce((map, input) => {
|
||||
map[input.name] = input.value;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
if (form.password != form.confirm_password) {
|
||||
createNotification({
|
||||
text: 'Please check that the passwords match',
|
||||
image: {
|
||||
iconClass: 'fas fa-times',
|
||||
},
|
||||
error: true,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.formDisabled.addUser = true;
|
||||
await request('user.create_user', {
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
session_token: this.sessionToken,
|
||||
});
|
||||
|
||||
this.formDisabled.addUser = false;
|
||||
createNotification({
|
||||
text: 'User ' + form.username + ' created',
|
||||
image: {
|
||||
iconClass: 'fas fa-check',
|
||||
},
|
||||
});
|
||||
|
||||
this.modalVisible.addUser = false;
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
onUserClick: function(username) {
|
||||
this.selectedUser = username;
|
||||
openDropdown(this.$refs.userDropdown.$el);
|
||||
},
|
||||
|
||||
onTokenFocus: function(event) {
|
||||
event.target.select();
|
||||
document.execCommand('copy');
|
||||
event.target.setAttribute('disabled', true);
|
||||
|
||||
createNotification({
|
||||
text: 'Token copied to clipboard',
|
||||
image: {
|
||||
iconClass: 'fas fa-copy',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onTokenBlur: function(event) {
|
||||
event.target.select();
|
||||
document.execCommand('copy');
|
||||
event.target.removeAttribute('disabled');
|
||||
|
||||
createNotification({
|
||||
text: 'Token copied to clipboard',
|
||||
image: {
|
||||
iconClass: 'fas fa-copy',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
changePassword: async function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
let form = [...this.$refs.changePasswordForm.querySelectorAll('input[name]')].reduce((map, input) => {
|
||||
map[input.name] = input.value;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
if (form.new_password !== form.confirm_new_password) {
|
||||
createNotification({
|
||||
text: 'Please check that the passwords match',
|
||||
image: {
|
||||
iconClass: 'fas fa-times',
|
||||
},
|
||||
error: true,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.formDisabled.changePassword = true;
|
||||
let success = await request('user.update_password', {
|
||||
username: form.username,
|
||||
old_password: form.password,
|
||||
new_password: form.new_password,
|
||||
});
|
||||
|
||||
this.formDisabled.changePassword = false;
|
||||
|
||||
if (success) {
|
||||
this.modalVisible.changePassword = false;
|
||||
createNotification({
|
||||
text: 'Password successfully updated',
|
||||
image: {
|
||||
iconClass: 'fas fa-check',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
text: 'Unable to update password: the current password is incorrect',
|
||||
image: {
|
||||
iconClass: 'fas fa-times',
|
||||
},
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created: function() {
|
||||
let cookies = Object.fromEntries(document.cookie.split('; ').map(x => x.split('=')));
|
||||
this.sessionToken = cookies.session_token;
|
||||
},
|
||||
});
|
||||
|
|
@ -27,15 +27,15 @@
|
|||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
<div class="date-time pull-right">
|
||||
<div class="settings">
|
||||
<i class="fas fa-cog"></i>
|
||||
</div>
|
||||
|
||||
<div class="time" v-text="now.toTimeString().substring(0,8)"></div>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<div class="date-time pull-right">
|
||||
<a href="/settings" class="settings">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
|
||||
<div class="time" v-text="now.toTimeString().substring(0,8)"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% endwith %}
|
||||
|
|
47
platypush/backend/http/templates/settings/index.html
Normal file
47
platypush/backend/http/templates/settings/index.html
Normal file
|
@ -0,0 +1,47 @@
|
|||
<!doctype html>
|
||||
<head>
|
||||
<title>Platypush Settings</title>
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/all.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/dist/settings.css') }}">
|
||||
|
||||
<script type="text/javascript">
|
||||
if (!window.config) {
|
||||
window.config = {};
|
||||
}
|
||||
|
||||
{% if token %}
|
||||
window.config.token = '{{ token }}';
|
||||
{% else %}
|
||||
window.config.token = undefined;
|
||||
{% endif %}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/vue.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/axios.min.js') }}"></script>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/api.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/notifications.js') }}"></script>
|
||||
|
||||
{% include 'elements.html' %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
{% include 'settings/nav.html' %}
|
||||
|
||||
<main class="col-s-8 col-m-9 col-l-10">
|
||||
{% include 'settings/sections/users.html' %}
|
||||
{% include 'settings/sections/token.html' %}
|
||||
</main>
|
||||
|
||||
{% include 'notifications.html' %}
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/settings/index.js') }}"></script>
|
||||
</body>
|
||||
|
47
platypush/backend/http/templates/settings/nav.html
Normal file
47
platypush/backend/http/templates/settings/nav.html
Normal file
|
@ -0,0 +1,47 @@
|
|||
<nav class="col-s-4 col-m-3 col-l-2">
|
||||
<ul>
|
||||
<li :class="{selected: selectedTab === 'back'}" @click="selectedTab = 'back'">
|
||||
<a :href="document.referrer" v-if="document.referrer">
|
||||
<div class="col-3 icon">
|
||||
<i class="fas fa-caret-left"></i>
|
||||
</div>
|
||||
<div class="col-9 title">
|
||||
Go Back
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/" v-else>
|
||||
<div class="col-3 icon">
|
||||
<i class="fas fa-home"></i>
|
||||
</div>
|
||||
<div class="col-9 title">
|
||||
Home
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li :class="{selected: selectedTab === 'users'}" @click="selectedTab = 'users'">
|
||||
<div class="col-3 icon">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div class="col-9 title">Users</div>
|
||||
</li>
|
||||
|
||||
<li :class="{selected: selectedTab === 'token'}" @click="selectedTab = 'token'">
|
||||
<div class="col-3 icon">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
<div class="col-9 title">Token</div>
|
||||
</li>
|
||||
|
||||
<li :class="{selected: selectedTab === 'logout'}" @click="selectedTab = 'logout'">
|
||||
<a href="/logout">
|
||||
<div class="col-3 icon">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</div>
|
||||
<div class="col-9 title">Logout</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<div class="section" :class="{hidden: selectedTab !== 'token'}" id="token">
|
||||
<header>
|
||||
<h1>Token</h1>
|
||||
</header>
|
||||
|
||||
<div class="warning" v-if="!config.token">
|
||||
No token configured for requests. It is strongly advised to generate a random token (e.g.
|
||||
<tt>dd if=/dev/urandom bs=18 count=1 | base64</tt>) and copy it in the <tt>token:</tt>
|
||||
section of your configuration file.
|
||||
</div>
|
||||
|
||||
<div class="token-container" v-else>
|
||||
<input type="text" :value="config.token"
|
||||
@focus="onTokenFocus" @blur="onTokenBlur">
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<div class="section" :class="{hidden: selectedTab !== 'users'}" id="users">
|
||||
<header>
|
||||
<h1 class="title col-8">Users</h1>
|
||||
<div class="col-4 pull-right">
|
||||
<button type="button">
|
||||
<i class="fa fa-plus" title="Add User" @click="modalVisible.addUser = true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<modal id="add-user" title="Add User" v-model="modalVisible.addUser">
|
||||
<form action="#" method="POST" ref="createUserForm" @submit="createUser">
|
||||
<input type="text" name="username" placeholder="Username" :disabled="formDisabled.addUser">
|
||||
<input type="password" name="password" placeholder="Password" :disabled="formDisabled.addUser">
|
||||
<input type="password" name="confirm_password" placeholder="Confirm password" :disabled="formDisabled.addUser">
|
||||
<input type="submit" value="Create User" :disabled="formDisabled.addUser">
|
||||
</form>
|
||||
</modal>
|
||||
|
||||
<modal id="change-password" title="Change password" v-model="modalVisible.changePassword" v-if="selectedUser">
|
||||
<form action="#" method="POST" ref="changePasswordForm" @submit="changePassword">
|
||||
<input type="text" name="username" placeholder="Username" :value="selectedUser" disabled="disabled">
|
||||
<input type="password" name="password" placeholder="Current password" :disabled="formDisabled.changePassword">
|
||||
<input type="password" name="new_password" placeholder="New password" :disabled="formDisabled.changePassword">
|
||||
<input type="password" name="confirm_new_password" placeholder="Confirm new password" :disabled="formDisabled.changePassword">
|
||||
<input type="submit" value="Change Password" :disabled="formDisabled.changePassword">
|
||||
</form>
|
||||
</modal>
|
||||
|
||||
<div class="body">
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li @click="onUserClick('{{ user.username }}')">{{ user.username }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<dropdown id="user-dropdown"
|
||||
ref="userDropdown"
|
||||
:items="userDropdownItems">
|
||||
</dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -12,7 +12,8 @@ class UserPlugin(Plugin):
|
|||
self.user_manager = UserManager()
|
||||
|
||||
@action
|
||||
def create_user(self, username, password, executing_user=None, executing_user_password=None, **kwargs):
|
||||
def create_user(self, username, password, executing_user=None, executing_user_password=None, session_token=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Create a user. This action needs to be executed by an already existing user, who needs to authenticate with
|
||||
their own credentials, unless this is the first user created on the system.
|
||||
|
@ -29,11 +30,13 @@ class UserPlugin(Plugin):
|
|||
|
||||
"""
|
||||
|
||||
if self.user_manager.get_user_count() > 0 and not executing_user:
|
||||
if self.user_manager.get_user_count() > 0 and not executing_user and not session_token:
|
||||
return None, "You need to authenticate in order to create another user"
|
||||
|
||||
if not self.user_manager.authenticate_user(executing_user, executing_user_password):
|
||||
return None, "Invalid credentials"
|
||||
user, session = self.user_manager.authenticate_user_session(session_token)
|
||||
if not user:
|
||||
return None, "Invalid credentials and/or session_token"
|
||||
|
||||
try:
|
||||
user = self.user_manager.create_user(username, password, **kwargs)
|
||||
|
@ -65,13 +68,15 @@ class UserPlugin(Plugin):
|
|||
return self.user_manager.update_password(username, old_password, new_password)
|
||||
|
||||
@action
|
||||
def delete_user(self, username, executing_user, executing_user_password):
|
||||
def delete_user(self, username, executing_user=None, executing_user_password=None, session_token=None):
|
||||
"""
|
||||
Delete a user
|
||||
"""
|
||||
|
||||
if not self.user_manager.authenticate_user(executing_user, executing_user_password):
|
||||
return None, "Invalid credentials"
|
||||
user, session = self.user_manager.authenticate_user_session(session_token)
|
||||
if not user:
|
||||
return None, "Invalid credentials and/or session_token"
|
||||
|
||||
try:
|
||||
return self.user_manager.delete_user(username)
|
||||
|
@ -123,7 +128,7 @@ class UserPlugin(Plugin):
|
|||
|
||||
"""
|
||||
|
||||
user = self.user_manager.authenticate_user_session(session_token=session_token)
|
||||
user, session = self.user_manager.authenticate_user_session(session_token=session_token)
|
||||
if not user:
|
||||
return None, 'Invalid session token'
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ from sqlalchemy.ext.declarative import declarative_base
|
|||
from platypush.context import get_plugin
|
||||
|
||||
Base = declarative_base()
|
||||
Session = scoped_session(sessionmaker())
|
||||
|
||||
|
||||
class UserManager:
|
||||
|
@ -41,6 +40,10 @@ class UserManager:
|
|||
session = self._get_db_session()
|
||||
return session.query(User).count()
|
||||
|
||||
def get_users(self):
|
||||
session = self._get_db_session()
|
||||
return session.query(User)
|
||||
|
||||
def create_user(self, username, password, **kwargs):
|
||||
session = self._get_db_session()
|
||||
if not username:
|
||||
|
@ -83,19 +86,23 @@ class UserManager:
|
|||
|
||||
if not user_session or (
|
||||
user_session.expires_at and user_session.expires_at < datetime.datetime.utcnow()):
|
||||
return None
|
||||
return None, None
|
||||
|
||||
user = session.query(User).filter_by(user_id=user_session.user_id).first()
|
||||
|
||||
# Hide password
|
||||
user.password = None
|
||||
return user
|
||||
return user, session
|
||||
|
||||
def delete_user(self, username):
|
||||
session = self._get_db_session()
|
||||
user = self._get_user(session, username)
|
||||
if not user:
|
||||
raise NameError('No such user: '.format(username))
|
||||
raise NameError('No such user: {}'.format(username))
|
||||
|
||||
user_sessions = session.query(UserSession).filter_by(user_id=user.user_id).all()
|
||||
for user_session in user_sessions:
|
||||
session.delete(user_session)
|
||||
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
|
@ -125,11 +132,11 @@ class UserManager:
|
|||
|
||||
user = self._get_user(session, username)
|
||||
user_session = UserSession(user_id=user.user_id, session_token=self._generate_token(),
|
||||
created_at=datetime.datetime.utcnow(), expires_at=expires_at)
|
||||
csrf_token=self._generate_token(), created_at=datetime.datetime.utcnow(),
|
||||
expires_at=expires_at)
|
||||
|
||||
session.add(user_session)
|
||||
session.commit()
|
||||
|
||||
return user_session
|
||||
|
||||
@staticmethod
|
||||
|
@ -151,9 +158,9 @@ class UserManager:
|
|||
|
||||
def _get_db_session(self):
|
||||
Base.metadata.create_all(self._engine)
|
||||
Session = scoped_session(sessionmaker())
|
||||
Session.configure(bind=self._engine)
|
||||
return Session()
|
||||
session = scoped_session(sessionmaker())
|
||||
session.configure(bind=self._engine)
|
||||
return session()
|
||||
|
||||
def _authenticate_user(self, session, username, password):
|
||||
user = self._get_user(session, username)
|
||||
|
@ -183,6 +190,7 @@ class UserSession(Base):
|
|||
|
||||
session_id = Column(Integer, primary_key=True)
|
||||
session_token = Column(String, unique=True, nullable=False)
|
||||
csrf_token = Column(String, unique=True)
|
||||
user_id = Column(Integer, ForeignKey('user.user_id'), nullable=False)
|
||||
created_at = Column(DateTime)
|
||||
expires_at = Column(DateTime)
|
||||
|
|
Loading…
Reference in a new issue