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 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.app import template_folder
|
||||||
from platypush.backend.http.utils import HttpUtils
|
from platypush.backend.http.utils import HttpUtils
|
||||||
|
@ -18,11 +19,18 @@ __routes__ = [
|
||||||
def login():
|
def login():
|
||||||
""" Login page """
|
""" Login page """
|
||||||
user_manager = UserManager()
|
user_manager = UserManager()
|
||||||
redirect_page = request.args.get('redirect', '/')
|
|
||||||
session_token = request.cookies.get('session_token')
|
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:
|
if session_token:
|
||||||
user = user_manager.authenticate_user_session(session_token)
|
user, session = user_manager.authenticate_user_session(session_token)
|
||||||
if user:
|
if user:
|
||||||
return redirect(redirect_page, 302)
|
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')
|
session_token = request.cookies.get('session_token')
|
||||||
|
|
||||||
if session_token:
|
if session_token:
|
||||||
user = user_manager.authenticate_user_session(session_token)
|
user, session = user_manager.authenticate_user_session(session_token)
|
||||||
if user:
|
if user:
|
||||||
return redirect(redirect_page, 302)
|
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')
|
user_session_token = request.cookies.get('session_token')
|
||||||
|
|
||||||
if user_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
|
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):
|
def decorator(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
|
@ -183,6 +204,13 @@ def authenticate(redirect_page='', skip_auth_methods=None):
|
||||||
|
|
||||||
return redirect('/login?redirect=' + redirect_page, 307)
|
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:
|
if n_users == 0 and 'session' not in skip_methods:
|
||||||
return redirect('/register?redirect=' + redirect_page, 307)
|
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 {
|
nav {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
height: var(--nav-height);
|
height: var(--nav-height);
|
||||||
|
background: $nav-bg;
|
||||||
margin-bottom: $nav-margin;
|
margin-bottom: $nav-margin;
|
||||||
|
border-bottom: $default-bottom;
|
||||||
|
box-shadow: 0 2.5px 4px 0 #bbb;
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
width: 75%;
|
||||||
|
display: inline-flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
background: $nav-bg;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: $default-bottom;
|
|
||||||
box-shadow: 0 2.5px 4px 0 #bbb;
|
|
||||||
|
|
||||||
li {
|
li {
|
||||||
padding: 1rem 1.5rem;
|
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 {
|
&:hover {
|
||||||
.decorator {
|
.decorator {
|
||||||
display: none;
|
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', {
|
Vue.component('plugin', {
|
||||||
template: '#tmpl-plugin',
|
template: '#tmpl-plugin',
|
||||||
props: ['config','tag'],
|
props: ['config','tag'],
|
||||||
|
@ -43,7 +27,7 @@ window.vm = new Vue({
|
||||||
const self = this;
|
const self = this;
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
self.now = new Date();
|
self.now = new Date();
|
||||||
}, 1000)
|
}, 1000);
|
||||||
|
|
||||||
initEvents();
|
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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% 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>
|
</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>
|
</nav>
|
||||||
|
|
||||||
{% endwith %}
|
{% 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()
|
self.user_manager = UserManager()
|
||||||
|
|
||||||
@action
|
@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
|
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.
|
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"
|
return None, "You need to authenticate in order to create another user"
|
||||||
|
|
||||||
if not self.user_manager.authenticate_user(executing_user, executing_user_password):
|
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:
|
try:
|
||||||
user = self.user_manager.create_user(username, password, **kwargs)
|
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)
|
return self.user_manager.update_password(username, old_password, new_password)
|
||||||
|
|
||||||
@action
|
@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
|
Delete a user
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.user_manager.authenticate_user(executing_user, executing_user_password):
|
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:
|
try:
|
||||||
return self.user_manager.delete_user(username)
|
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:
|
if not user:
|
||||||
return None, 'Invalid session token'
|
return None, 'Invalid session token'
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ from sqlalchemy.ext.declarative import declarative_base
|
||||||
from platypush.context import get_plugin
|
from platypush.context import get_plugin
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
Session = scoped_session(sessionmaker())
|
|
||||||
|
|
||||||
|
|
||||||
class UserManager:
|
class UserManager:
|
||||||
|
@ -41,6 +40,10 @@ class UserManager:
|
||||||
session = self._get_db_session()
|
session = self._get_db_session()
|
||||||
return session.query(User).count()
|
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):
|
def create_user(self, username, password, **kwargs):
|
||||||
session = self._get_db_session()
|
session = self._get_db_session()
|
||||||
if not username:
|
if not username:
|
||||||
|
@ -83,19 +86,23 @@ class UserManager:
|
||||||
|
|
||||||
if not user_session or (
|
if not user_session or (
|
||||||
user_session.expires_at and user_session.expires_at < datetime.datetime.utcnow()):
|
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()
|
user = session.query(User).filter_by(user_id=user_session.user_id).first()
|
||||||
|
|
||||||
# Hide password
|
# Hide password
|
||||||
user.password = None
|
user.password = None
|
||||||
return user
|
return user, session
|
||||||
|
|
||||||
def delete_user(self, username):
|
def delete_user(self, username):
|
||||||
session = self._get_db_session()
|
session = self._get_db_session()
|
||||||
user = self._get_user(session, username)
|
user = self._get_user(session, username)
|
||||||
if not user:
|
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.delete(user)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
@ -125,11 +132,11 @@ class UserManager:
|
||||||
|
|
||||||
user = self._get_user(session, username)
|
user = self._get_user(session, username)
|
||||||
user_session = UserSession(user_id=user.user_id, session_token=self._generate_token(),
|
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.add(user_session)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
return user_session
|
return user_session
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -151,9 +158,9 @@ class UserManager:
|
||||||
|
|
||||||
def _get_db_session(self):
|
def _get_db_session(self):
|
||||||
Base.metadata.create_all(self._engine)
|
Base.metadata.create_all(self._engine)
|
||||||
Session = scoped_session(sessionmaker())
|
session = scoped_session(sessionmaker())
|
||||||
Session.configure(bind=self._engine)
|
session.configure(bind=self._engine)
|
||||||
return Session()
|
return session()
|
||||||
|
|
||||||
def _authenticate_user(self, session, username, password):
|
def _authenticate_user(self, session, username, password):
|
||||||
user = self._get_user(session, username)
|
user = self._get_user(session, username)
|
||||||
|
@ -183,6 +190,7 @@ class UserSession(Base):
|
||||||
|
|
||||||
session_id = Column(Integer, primary_key=True)
|
session_id = Column(Integer, primary_key=True)
|
||||||
session_token = Column(String, unique=True, nullable=False)
|
session_token = Column(String, unique=True, nullable=False)
|
||||||
|
csrf_token = Column(String, unique=True)
|
||||||
user_id = Column(Integer, ForeignKey('user.user_id'), nullable=False)
|
user_id = Column(Integer, ForeignKey('user.user_id'), nullable=False)
|
||||||
created_at = Column(DateTime)
|
created_at = Column(DateTime)
|
||||||
expires_at = Column(DateTime)
|
expires_at = Column(DateTime)
|
||||||
|
|
Loading…
Reference in a new issue