forked from platypush/platypush
Support for multi-users and authentication for HTTP pages
This commit is contained in:
parent
674c164fc1
commit
1c1ecc18df
21 changed files with 669 additions and 175 deletions
|
@ -209,6 +209,7 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
||||||
'cv2',
|
'cv2',
|
||||||
'nfc',
|
'nfc',
|
||||||
'ndef',
|
'ndef',
|
||||||
|
'bcrypt',
|
||||||
]
|
]
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
from flask import Blueprint, request, render_template
|
from flask import Blueprint, request, render_template
|
||||||
|
|
||||||
from platypush.backend.http.app import template_folder, static_folder
|
from platypush.backend.http.app import template_folder, static_folder
|
||||||
from platypush.backend.http.app.utils import authenticate, authentication_ok, \
|
from platypush.backend.http.app.utils import authenticate, get_websocket_port
|
||||||
get_websocket_port
|
|
||||||
|
|
||||||
from platypush.backend.http.utils import HttpUtils
|
from platypush.backend.http.utils import HttpUtils
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
|
@ -16,11 +15,9 @@ __routes__ = [
|
||||||
|
|
||||||
|
|
||||||
@dashboard.route('/dashboard', methods=['GET'])
|
@dashboard.route('/dashboard', methods=['GET'])
|
||||||
|
@authenticate()
|
||||||
def dashboard():
|
def dashboard():
|
||||||
""" Route for the fullscreen dashboard """
|
""" Route for the fullscreen dashboard """
|
||||||
if not authentication_ok(request):
|
|
||||||
return authenticate()
|
|
||||||
|
|
||||||
http_conf = Config.get('backend.http')
|
http_conf = Config.get('backend.http')
|
||||||
dashboard_conf = http_conf.get('dashboard', {})
|
dashboard_conf = http_conf.get('dashboard', {})
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,7 @@ import json
|
||||||
from flask import Blueprint, abort, request, Response
|
from flask import Blueprint, abort, request, Response
|
||||||
|
|
||||||
from platypush.backend.http.app import template_folder
|
from platypush.backend.http.app import template_folder
|
||||||
from platypush.backend.http.app.utils import authenticate, authentication_ok, \
|
from platypush.backend.http.app.utils import authenticate, logger, send_message
|
||||||
logger, send_message
|
|
||||||
|
|
||||||
from platypush.backend.http.utils import HttpUtils
|
|
||||||
|
|
||||||
|
|
||||||
execute = Blueprint('execute', __name__, template_folder=template_folder)
|
execute = Blueprint('execute', __name__, template_folder=template_folder)
|
||||||
|
|
||||||
|
@ -16,17 +12,16 @@ __routes__ = [
|
||||||
execute,
|
execute,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@execute.route('/execute', methods=['POST'])
|
@execute.route('/execute', methods=['POST'])
|
||||||
|
@authenticate(skip_auth_methods=['session'])
|
||||||
def execute():
|
def execute():
|
||||||
""" Endpoint to execute commands """
|
""" Endpoint to execute commands """
|
||||||
if not authentication_ok(request): return authenticate()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg = json.loads(request.data.decode('utf-8'))
|
msg = json.loads(request.data.decode('utf-8'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger().error('Unable to parse JSON from request {}: {}'.format(
|
logger().error('Unable to parse JSON from request {}: {}'.format(request.data, str(e)))
|
||||||
request.data, str(e)))
|
return abort(400, str(e))
|
||||||
abort(400, str(e))
|
|
||||||
|
|
||||||
logger().info('Received message on the HTTP backend: {}'.format(msg))
|
logger().info('Received message on the HTTP backend: {}'.format(msg))
|
||||||
|
|
||||||
|
@ -34,9 +29,8 @@ def execute():
|
||||||
response = send_message(msg)
|
response = send_message(msg)
|
||||||
return Response(str(response or {}), mimetype='application/json')
|
return Response(str(response or {}), mimetype='application/json')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger().error('Error while running HTTP action: {}. Request: {}'.
|
logger().error('Error while running HTTP action: {}. Request: {}'.format(str(e), msg))
|
||||||
format(str(e), msg))
|
return abort(500, str(e))
|
||||||
abort(500, str(e))
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -3,8 +3,7 @@ import json
|
||||||
from flask import Blueprint, abort, request, Response
|
from flask import Blueprint, abort, request, Response
|
||||||
|
|
||||||
from platypush.backend.http.app import template_folder
|
from platypush.backend.http.app import template_folder
|
||||||
from platypush.backend.http.app.utils import authenticate, authentication_ok, \
|
from platypush.backend.http.app.utils import authenticate, logger, send_message
|
||||||
logger, send_message
|
|
||||||
|
|
||||||
from platypush.message.event.http.hook import WebhookEvent
|
from platypush.message.event.http.hook import WebhookEvent
|
||||||
|
|
||||||
|
@ -16,10 +15,11 @@ __routes__ = [
|
||||||
hook,
|
hook,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@hook.route('/hook/<hook_name>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
|
@hook.route('/hook/<hook_name>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
|
||||||
|
@authenticate(skip_auth_methods=['session'])
|
||||||
def hook(hook_name):
|
def hook(hook_name):
|
||||||
""" Endpoint for custom webhooks """
|
""" Endpoint for custom webhooks """
|
||||||
if not authentication_ok(request): return authenticate()
|
|
||||||
|
|
||||||
event_args = {
|
event_args = {
|
||||||
'hook': hook_name,
|
'hook': hook_name,
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from flask import Blueprint, request, render_template
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
from platypush.backend.http.app import template_folder, static_folder
|
from platypush.backend.http.app import template_folder, static_folder
|
||||||
from platypush.backend.http.app.utils import authenticate, authentication_ok, \
|
from platypush.backend.http.app.utils import authenticate, get_websocket_port
|
||||||
get_websocket_port
|
|
||||||
|
|
||||||
from platypush.backend.http.utils import HttpUtils
|
from platypush.backend.http.utils import HttpUtils
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
|
@ -19,11 +18,9 @@ __routes__ = [
|
||||||
|
|
||||||
|
|
||||||
@index.route('/')
|
@index.route('/')
|
||||||
|
@authenticate()
|
||||||
def index():
|
def index():
|
||||||
""" Route to the main web panel """
|
""" Route to the main web panel """
|
||||||
if not authentication_ok(request):
|
|
||||||
return authenticate()
|
|
||||||
|
|
||||||
configured_plugins = Config.get_plugins()
|
configured_plugins = Config.get_plugins()
|
||||||
enabled_templates = {}
|
enabled_templates = {}
|
||||||
enabled_scripts = {}
|
enabled_scripts = {}
|
||||||
|
|
46
platypush/backend/http/app/routes/login.py
Normal file
46
platypush/backend/http/app/routes/login.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from flask import Blueprint, request, redirect, render_template, make_response, url_for
|
||||||
|
|
||||||
|
from platypush.backend.http.app import template_folder
|
||||||
|
from platypush.backend.http.utils import HttpUtils
|
||||||
|
from platypush.user import UserManager
|
||||||
|
|
||||||
|
login = Blueprint('login', __name__, template_folder=template_folder)
|
||||||
|
|
||||||
|
# Declare routes list
|
||||||
|
__routes__ = [
|
||||||
|
login,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@login.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
""" Login page """
|
||||||
|
user_manager = UserManager()
|
||||||
|
redirect_page = request.args.get('redirect', '/')
|
||||||
|
session_token = request.cookies.get('session_token')
|
||||||
|
|
||||||
|
if session_token:
|
||||||
|
user = user_manager.authenticate_user_session(session_token)
|
||||||
|
if user:
|
||||||
|
return redirect(redirect_page, 302)
|
||||||
|
|
||||||
|
if request.form:
|
||||||
|
username = request.form.get('username')
|
||||||
|
password = request.form.get('password')
|
||||||
|
remember = request.form.get('remember')
|
||||||
|
session = user_manager.create_user_session(username=username, password=password,
|
||||||
|
expires_at=datetime.datetime.utcnow() + datetime.timedelta(days=1)
|
||||||
|
if not remember else None)
|
||||||
|
|
||||||
|
if session:
|
||||||
|
redirect_target = redirect(redirect_page, 302)
|
||||||
|
response = make_response(redirect_target)
|
||||||
|
response.set_cookie('session_token', session.session_token)
|
||||||
|
return response
|
||||||
|
|
||||||
|
return render_template('login.html', utils=HttpUtils)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -1,15 +1,12 @@
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from flask import abort, jsonify, request, Response, Blueprint, \
|
from flask import Response, Blueprint, send_from_directory
|
||||||
send_from_directory
|
|
||||||
|
|
||||||
from platypush.backend.http.app import template_folder
|
from platypush.backend.http.app import template_folder
|
||||||
from platypush.backend.http.app.utils import logger, get_remote_base_url, \
|
from platypush.backend.http.app.utils import authenticate, send_request
|
||||||
authenticate_user, send_request
|
|
||||||
|
|
||||||
camera = Blueprint('camera', __name__, template_folder=template_folder)
|
camera = Blueprint('camera', __name__, template_folder=template_folder)
|
||||||
|
|
||||||
|
@ -18,6 +15,7 @@ __routes__ = [
|
||||||
camera,
|
camera,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_device_id(device_id=None):
|
def get_device_id(device_id=None):
|
||||||
if device_id is None:
|
if device_id is None:
|
||||||
device_id = str(send_request(action='camera.get_default_device_id').output)
|
device_id = str(send_request(action='camera.get_default_device_id').output)
|
||||||
|
@ -32,8 +30,8 @@ def get_frame_file(device_id=None):
|
||||||
|
|
||||||
if device_id not in status:
|
if device_id not in status:
|
||||||
was_recording = False
|
was_recording = False
|
||||||
response = send_request(action='camera.start_recording',
|
send_request(action='camera.start_recording',
|
||||||
device_id=device_id)
|
device_id=device_id)
|
||||||
|
|
||||||
while not frame_file:
|
while not frame_file:
|
||||||
frame_file = send_request(action='camera.status', device_id=device_id). \
|
frame_file = send_request(action='camera.status', device_id=device_id). \
|
||||||
|
@ -43,12 +41,10 @@ def get_frame_file(device_id=None):
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
if not was_recording:
|
if not was_recording:
|
||||||
# stop_recording will delete the temporary frames. Copy the image file
|
|
||||||
# to a temporary file before stopping recording
|
|
||||||
tmp_file = None
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(prefix='camera_capture_', suffix='.jpg',
|
with tempfile.NamedTemporaryFile(prefix='camera_capture_', suffix='.jpg',
|
||||||
delete=False) as f:
|
delete=False) as f:
|
||||||
|
# stop_recording will delete the temporary frames. Copy the image file
|
||||||
|
# to a temporary file before stopping recording
|
||||||
tmp_file = f.name
|
tmp_file = f.name
|
||||||
|
|
||||||
shutil.copyfile(frame_file, tmp_file)
|
shutil.copyfile(frame_file, tmp_file)
|
||||||
|
@ -62,7 +58,6 @@ def video_feed(device_id=None):
|
||||||
device_id = get_device_id(device_id)
|
device_id = get_device_id(device_id)
|
||||||
send_request(action='camera.start_recording', device_id=device_id)
|
send_request(action='camera.start_recording', device_id=device_id)
|
||||||
last_frame_file = None
|
last_frame_file = None
|
||||||
last_frame = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
@ -77,33 +72,35 @@ def video_feed(device_id=None):
|
||||||
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
|
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
|
||||||
|
|
||||||
last_frame_file = frame_file
|
last_frame_file = frame_file
|
||||||
last_frame = frame
|
|
||||||
finally:
|
finally:
|
||||||
send_request(action='camera.stop_recording', device_id=device_id)
|
send_request(action='camera.stop_recording', device_id=device_id)
|
||||||
|
|
||||||
|
|
||||||
@camera.route('/camera/<device_id>/frame', methods=['GET'])
|
@camera.route('/camera/<device_id>/frame', methods=['GET'])
|
||||||
@authenticate_user
|
@authenticate()
|
||||||
def get_camera_frame(device_id):
|
def get_camera_frame(device_id):
|
||||||
frame_file = get_frame_file(device_id)
|
frame_file = get_frame_file(device_id)
|
||||||
return send_from_directory(os.path.dirname(frame_file),
|
return send_from_directory(os.path.dirname(frame_file),
|
||||||
os.path.basename(frame_file))
|
os.path.basename(frame_file))
|
||||||
|
|
||||||
|
|
||||||
@camera.route('/camera/frame', methods=['GET'])
|
@camera.route('/camera/frame', methods=['GET'])
|
||||||
@authenticate_user
|
@authenticate()
|
||||||
def get_default_camera_frame():
|
def get_default_camera_frame():
|
||||||
frame_file = get_frame_file()
|
frame_file = get_frame_file()
|
||||||
return send_from_directory(os.path.dirname(frame_file),
|
return send_from_directory(os.path.dirname(frame_file),
|
||||||
os.path.basename(frame_file))
|
os.path.basename(frame_file))
|
||||||
|
|
||||||
|
|
||||||
@camera.route('/camera/stream', methods=['GET'])
|
@camera.route('/camera/stream', methods=['GET'])
|
||||||
@authenticate_user
|
@authenticate()
|
||||||
def get_default_stream_feed():
|
def get_default_stream_feed():
|
||||||
return Response(video_feed(),
|
return Response(video_feed(),
|
||||||
mimetype='multipart/x-mixed-replace; boundary=frame')
|
mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||||
|
|
||||||
|
|
||||||
@camera.route('/camera/<device_id>/stream', methods=['GET'])
|
@camera.route('/camera/<device_id>/stream', methods=['GET'])
|
||||||
@authenticate_user
|
@authenticate()
|
||||||
def get_stream_feed(device_id):
|
def get_stream_feed(device_id):
|
||||||
return Response(video_feed(device_id),
|
return Response(video_feed(device_id),
|
||||||
mimetype='multipart/x-mixed-replace; boundary=frame')
|
mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||||
|
|
53
platypush/backend/http/app/routes/register.py
Normal file
53
platypush/backend/http/app/routes/register.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from flask import Blueprint, request, redirect, render_template, make_response, url_for
|
||||||
|
|
||||||
|
from platypush.backend.http.app import template_folder
|
||||||
|
from platypush.backend.http.utils import HttpUtils
|
||||||
|
from platypush.user import UserManager
|
||||||
|
|
||||||
|
register = Blueprint('register', __name__, template_folder=template_folder)
|
||||||
|
|
||||||
|
# Declare routes list
|
||||||
|
__routes__ = [
|
||||||
|
register,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@register.route('/register', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
""" Registration page """
|
||||||
|
user_manager = UserManager()
|
||||||
|
redirect_page = request.args.get('redirect', '/')
|
||||||
|
session_token = request.cookies.get('session_token')
|
||||||
|
|
||||||
|
if session_token:
|
||||||
|
user = user_manager.authenticate_user_session(session_token)
|
||||||
|
if user:
|
||||||
|
return redirect(redirect_page, 302)
|
||||||
|
|
||||||
|
if request.form:
|
||||||
|
username = request.form.get('username')
|
||||||
|
password = request.form.get('password')
|
||||||
|
confirm_password = request.form.get('confirm_password')
|
||||||
|
remember = request.form.get('remember')
|
||||||
|
|
||||||
|
if password == confirm_password:
|
||||||
|
user_manager.create_user(username=username, password=password)
|
||||||
|
session = user_manager.create_user_session(username=username, password=password,
|
||||||
|
expires_at=datetime.datetime.utcnow() + datetime.timedelta(days=1)
|
||||||
|
if not remember else None)
|
||||||
|
|
||||||
|
if session:
|
||||||
|
redirect_target = redirect(redirect_page, 302)
|
||||||
|
response = make_response(redirect_target)
|
||||||
|
response.set_cookie('session_token', session.session_token)
|
||||||
|
return response
|
||||||
|
|
||||||
|
if user_manager.get_user_count() > 0:
|
||||||
|
return redirect('/login?redirect=' + redirect_page, 302)
|
||||||
|
|
||||||
|
return render_template('register.html', utils=HttpUtils)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -5,8 +5,6 @@ from flask import Blueprint, abort, send_from_directory
|
||||||
|
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.backend.http.app import template_folder, static_folder
|
from platypush.backend.http.app import template_folder, static_folder
|
||||||
from platypush.backend.http.app.utils import authenticate, authentication_ok, \
|
|
||||||
send_message
|
|
||||||
|
|
||||||
|
|
||||||
img_folder = os.path.join(static_folder, 'resources', 'img')
|
img_folder = os.path.join(static_folder, 'resources', 'img')
|
||||||
|
@ -21,6 +19,7 @@ __routes__ = [
|
||||||
img,
|
img,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@resources.route('/resources/<path:path>', methods=['GET'])
|
@resources.route('/resources/<path:path>', methods=['GET'])
|
||||||
def resources_path(path):
|
def resources_path(path):
|
||||||
""" Custom static resources """
|
""" Custom static resources """
|
||||||
|
|
|
@ -2,10 +2,9 @@ import importlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from flask import request, Response
|
from flask import request, redirect, Response
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
|
|
||||||
# NOTE: The HTTP service will *only* work on top of a Redis bus. The default
|
# NOTE: The HTTP service will *only* work on top of a Redis bus. The default
|
||||||
|
@ -15,6 +14,7 @@ from platypush.bus.redis import RedisBus
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.message import Message
|
from platypush.message import Message
|
||||||
from platypush.message.request import Request
|
from platypush.message.request import Request
|
||||||
|
from platypush.user import UserManager
|
||||||
from platypush.utils import get_redis_queue_name_by_message, get_ip_or_hostname
|
from platypush.utils import get_redis_queue_name_by_message, get_ip_or_hostname
|
||||||
|
|
||||||
_bus = None
|
_bus = None
|
||||||
|
@ -27,6 +27,7 @@ def bus():
|
||||||
_bus = RedisBus()
|
_bus = RedisBus()
|
||||||
return _bus
|
return _bus
|
||||||
|
|
||||||
|
|
||||||
def logger():
|
def logger():
|
||||||
global _logger
|
global _logger
|
||||||
if not _logger:
|
if not _logger:
|
||||||
|
@ -50,6 +51,7 @@ def logger():
|
||||||
|
|
||||||
return _logger
|
return _logger
|
||||||
|
|
||||||
|
|
||||||
def get_message_response(msg):
|
def get_message_response(msg):
|
||||||
redis = Redis(**bus().redis_args)
|
redis = Redis(**bus().redis_args)
|
||||||
response = redis.blpop(get_redis_queue_name_by_message(msg), timeout=60)
|
response = redis.blpop(get_redis_queue_name_by_message(msg), timeout=60)
|
||||||
|
@ -60,16 +62,21 @@ def get_message_response(msg):
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyProtectedMember
|
||||||
def get_http_port():
|
def get_http_port():
|
||||||
from platypush.backend.http import HttpBackend
|
from platypush.backend.http import HttpBackend
|
||||||
http_conf = Config.get('backend.http')
|
http_conf = Config.get('backend.http')
|
||||||
return http_conf.get('port', HttpBackend._DEFAULT_HTTP_PORT)
|
return http_conf.get('port', HttpBackend._DEFAULT_HTTP_PORT)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyProtectedMember
|
||||||
def get_websocket_port():
|
def get_websocket_port():
|
||||||
from platypush.backend.http import HttpBackend
|
from platypush.backend.http import HttpBackend
|
||||||
http_conf = Config.get('backend.http')
|
http_conf = Config.get('backend.http')
|
||||||
return http_conf.get('websocket_port', HttpBackend._DEFAULT_WEBSOCKET_PORT)
|
return http_conf.get('websocket_port', HttpBackend._DEFAULT_WEBSOCKET_PORT)
|
||||||
|
|
||||||
|
|
||||||
def send_message(msg):
|
def send_message(msg):
|
||||||
msg = Message.build(msg)
|
msg = Message.build(msg)
|
||||||
|
|
||||||
|
@ -88,6 +95,7 @@ def send_message(msg):
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def send_request(action, **kwargs):
|
def send_request(action, **kwargs):
|
||||||
msg = {
|
msg = {
|
||||||
'type': 'request',
|
'type': 'request',
|
||||||
|
@ -99,36 +107,97 @@ def send_request(action, **kwargs):
|
||||||
|
|
||||||
return send_message(msg)
|
return send_message(msg)
|
||||||
|
|
||||||
def authenticate():
|
|
||||||
return Response('Authentication required', 401,
|
|
||||||
{'WWW-Authenticate': 'Basic realm="Login required"'})
|
|
||||||
|
|
||||||
def authentication_ok(req):
|
def _authenticate_token():
|
||||||
token = Config.get('token')
|
token = Config.get('token')
|
||||||
if not token:
|
|
||||||
return True
|
|
||||||
|
|
||||||
user_token = None
|
user_token = None
|
||||||
|
|
||||||
# Check if
|
if 'X-Token' in request.headers:
|
||||||
if 'X-Token' in req.headers:
|
user_token = request.headers['X-Token']
|
||||||
user_token = req.headers['X-Token']
|
elif 'token' in request.args:
|
||||||
elif req.authorization:
|
user_token = request.args.get('token')
|
||||||
# TODO support for user check
|
|
||||||
user_token = req.authorization.password
|
|
||||||
elif 'token' in req.args:
|
|
||||||
user_token = req.args.get('token')
|
|
||||||
else:
|
else:
|
||||||
try:
|
return False
|
||||||
args = json.loads(req.data.decode('utf-8'))
|
|
||||||
user_token = args.get('token')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if user_token == token:
|
return token and user_token == token
|
||||||
return True
|
|
||||||
|
|
||||||
|
def _authenticate_http():
|
||||||
|
user_manager = UserManager()
|
||||||
|
|
||||||
|
if not request.authorization:
|
||||||
|
return False
|
||||||
|
|
||||||
|
username = request.authorization.username
|
||||||
|
password = request.authorization.password
|
||||||
|
return user_manager.authenticate_user(username, password)
|
||||||
|
|
||||||
|
|
||||||
|
def _authenticate_session():
|
||||||
|
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 = user_manager.authenticate_user_session(user_session_token)
|
||||||
|
|
||||||
|
return user is not None
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate(redirect_page='', skip_auth_methods=None):
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
user_manager = UserManager()
|
||||||
|
n_users = user_manager.get_user_count()
|
||||||
|
token = Config.get('token')
|
||||||
|
skip_methods = skip_auth_methods or []
|
||||||
|
|
||||||
|
# User/pass HTTP authentication
|
||||||
|
http_auth_ok = True
|
||||||
|
if n_users > 0 and 'http' not in skip_methods:
|
||||||
|
http_auth_ok = _authenticate_http()
|
||||||
|
if http_auth_ok:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
# Token-based authentication
|
||||||
|
token_auth_ok = True
|
||||||
|
if token and 'token' not in skip_methods:
|
||||||
|
token_auth_ok = _authenticate_token()
|
||||||
|
if token_auth_ok:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
# Session token based authentication
|
||||||
|
session_auth_ok = True
|
||||||
|
if n_users > 0 and 'session' not in skip_methods:
|
||||||
|
session_auth_ok = _authenticate_session()
|
||||||
|
if session_auth_ok:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return redirect('/login?redirect=' + redirect_page, 307)
|
||||||
|
|
||||||
|
if n_users == 0 and 'session' not in skip_methods:
|
||||||
|
return redirect('/register?redirect=' + redirect_page, 307)
|
||||||
|
|
||||||
|
if ('http' not in skip_methods and http_auth_ok) or \
|
||||||
|
('token' not in skip_methods and token_auth_ok) or \
|
||||||
|
('session' not in skip_methods and session_auth_ok):
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return Response('Authentication required', 401,
|
||||||
|
{'WWW-Authenticate': 'Basic realm="Login required"'})
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_routes():
|
def get_routes():
|
||||||
routes_dir = os.path.join(
|
routes_dir = os.path.join(
|
||||||
|
@ -141,8 +210,7 @@ def get_routes():
|
||||||
if f.endswith('.py'):
|
if f.endswith('.py'):
|
||||||
mod_name = '.'.join(
|
mod_name = '.'.join(
|
||||||
(base_module + '.' + os.path.join(path, f).replace(
|
(base_module + '.' + os.path.join(path, f).replace(
|
||||||
os.path.dirname(__file__), '')[1:] \
|
os.path.dirname(__file__), '')[1:].replace(os.sep, '.')).split('.')
|
||||||
.replace(os.sep, '.')).split('.') \
|
|
||||||
[:(-2 if f == '__init__.py' else -1)])
|
[:(-2 if f == '__init__.py' else -1)])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -162,6 +230,7 @@ def get_local_base_url():
|
||||||
proto=('https' if http_conf.get('ssl_cert') else 'http'),
|
proto=('https' if http_conf.get('ssl_cert') else 'http'),
|
||||||
port=get_http_port())
|
port=get_http_port())
|
||||||
|
|
||||||
|
|
||||||
def get_remote_base_url():
|
def get_remote_base_url():
|
||||||
http_conf = Config.get('backend.http') or {}
|
http_conf = Config.get('backend.http') or {}
|
||||||
return '{proto}://{host}:{port}'.format(
|
return '{proto}://{host}:{port}'.format(
|
||||||
|
@ -169,12 +238,4 @@ def get_remote_base_url():
|
||||||
host=get_ip_or_hostname(), port=get_http_port())
|
host=get_ip_or_hostname(), port=get_http_port())
|
||||||
|
|
||||||
|
|
||||||
def authenticate_user(route):
|
|
||||||
@wraps(route)
|
|
||||||
def authenticated_route(*args, **kwargs):
|
|
||||||
if not authentication_ok(request): return authenticate()
|
|
||||||
return route(*args, **kwargs)
|
|
||||||
return authenticated_route
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
color: $text-icon-color;
|
color: $text-icon-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=text] {
|
input[type=text],
|
||||||
|
input[type=password] {
|
||||||
border-radius: 5rem;
|
border-radius: 5rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
46
platypush/backend/http/static/css/source/login/index.scss
Normal file
46
platypush/backend/http/static/css/source/login/index.scss
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
@import 'common/vars';
|
||||||
|
|
||||||
|
@import 'common/mixins';
|
||||||
|
@import 'common/layout';
|
||||||
|
@import 'common/elements';
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
font-family: $default-font-family;
|
||||||
|
font-size: $default-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
form {
|
||||||
|
border: $default-border-3;
|
||||||
|
border-radius: 3em;
|
||||||
|
padding: 4em;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text],
|
||||||
|
input[type=password] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit] {
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $default-link-fg;
|
||||||
|
}
|
||||||
|
|
1
platypush/backend/http/static/css/source/register
Symbolic link
1
platypush/backend/http/static/css/source/register
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
login
|
|
@ -1,46 +0,0 @@
|
||||||
$(document).ready(function() {
|
|
||||||
var onEvent = function(event) {
|
|
||||||
if (event.args.type == 'platypush.message.event.web.widget.WidgetUpdateEvent') {
|
|
||||||
var $widget = $('#' + event.args.widget);
|
|
||||||
delete event.args.widget;
|
|
||||||
|
|
||||||
for (var key of Object.keys(event.args)) {
|
|
||||||
$widget.find('[data-bind=' + key + ']').text(event.args[key]);
|
|
||||||
}
|
|
||||||
} else if (event.args.type == 'platypush.message.event.web.DashboardIframeUpdateEvent') {
|
|
||||||
var url = event.args.url;
|
|
||||||
var $modal = $('#iframe-modal');
|
|
||||||
var $iframe = $modal.find('iframe');
|
|
||||||
$iframe.attr('src', url);
|
|
||||||
$iframe.prop('width', event.args.width || '100%');
|
|
||||||
$iframe.prop('height', event.args.height || '600');
|
|
||||||
|
|
||||||
if ('timeout' in event.args) {
|
|
||||||
setTimeout(function() {
|
|
||||||
$iframe.removeAttr('src');
|
|
||||||
$modal.fadeOut();
|
|
||||||
}, parseFloat(event.args.timeout) * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
$modal.fadeIn();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var initDashboard = function() {
|
|
||||||
if ('background_image' in window.config) {
|
|
||||||
$('body').css('background-image', 'url(' + window.config.background_image + ')');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var initEvents = function() {
|
|
||||||
window.registerEventListener(onEvent);
|
|
||||||
};
|
|
||||||
|
|
||||||
var init = function() {
|
|
||||||
initDashboard();
|
|
||||||
initEvents();
|
|
||||||
};
|
|
||||||
|
|
||||||
init();
|
|
||||||
});
|
|
||||||
|
|
29
platypush/backend/http/templates/login.html
Normal file
29
platypush/backend/http/templates/login.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<!doctype html>
|
||||||
|
<head>
|
||||||
|
<title>Platypush login page</title>
|
||||||
|
|
||||||
|
<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/login.css') }}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<form class="login" method="POST">
|
||||||
|
<div class="row">
|
||||||
|
<input type="text" name="username" placeholder="Username">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<input type="password" name="password" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
<div class="row pull-right">
|
||||||
|
<input type="submit" class="btn btn-primary" value="Login">
|
||||||
|
</div>
|
||||||
|
<div class="row pull-right">
|
||||||
|
Keep me logged in on this device <input type="checkbox" name="remember">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<head>
|
|
||||||
<title>Platypush map service</title>
|
|
||||||
|
|
||||||
<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/font-awesome.min.css') }}">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/application.css') }}">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/toggles.css') }}">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/map.css') }}">
|
|
||||||
<link href='//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700' rel='stylesheet' type='text/css'>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery-3.3.1.min.js') }}"></script>
|
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/application.js') }}"></script>
|
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/map.js') }}"></script>
|
|
||||||
|
|
||||||
<script async defer src="https://maps.googleapis.com/maps/api/js?key={{ api_key }}&v=3.exp&callback=initMap"></script>
|
|
||||||
<script type="text/javascript">
|
|
||||||
window.db_conf = JSON.parse('{{ utils.to_json(config["db"]) | safe }}');
|
|
||||||
window.websocket_port = {{ websocket_port }};
|
|
||||||
window.has_ssl = {% print('true' if has_ssl else 'false') %};
|
|
||||||
window.map_start = "{{ start }}";
|
|
||||||
window.map_end = "{{ end }}";
|
|
||||||
|
|
||||||
{% if zoom %}
|
|
||||||
window.zoom = {{ zoom }};
|
|
||||||
{% else %}
|
|
||||||
window.zoom = undefined;
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if token %}
|
|
||||||
window.token = '{{ token }}'
|
|
||||||
{% else %}
|
|
||||||
window.token = undefined
|
|
||||||
{% endif %}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<div id="map-container" width="100" height="100">
|
|
||||||
<div id="map"></div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<body>
|
|
||||||
|
|
32
platypush/backend/http/templates/register.html
Normal file
32
platypush/backend/http/templates/register.html
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<!doctype html>
|
||||||
|
<head>
|
||||||
|
<title>Platypush registration page</title>
|
||||||
|
|
||||||
|
<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/login.css') }}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<form class="register" method="POST">
|
||||||
|
<div class="row">
|
||||||
|
<input type="text" name="username" placeholder="New username">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<input type="password" name="password" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<input type="password" name="confirm_password" placeholder="Confirm password">
|
||||||
|
</div>
|
||||||
|
<div class="row pull-right">
|
||||||
|
<input type="submit" class="btn btn-primary" value="Register">
|
||||||
|
</div>
|
||||||
|
<div class="row pull-right">
|
||||||
|
Keep me logged in on this device <input type="checkbox" name="remember">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
|
|
@ -43,7 +43,7 @@ class DbPlugin(Plugin):
|
||||||
elif not isinstance(value, int) and not isinstance(value, float):
|
elif not isinstance(value, int) and not isinstance(value, float):
|
||||||
value = "'{}'".format(str(value))
|
value = "'{}'".format(str(value))
|
||||||
|
|
||||||
return eval('{}.{}=={}'.format(table, column, value))
|
return eval('table.c.{}=={}'.format(column, value))
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def execute(self, statement, engine=None, *args, **kwargs):
|
def execute(self, statement, engine=None, *args, **kwargs):
|
||||||
|
|
138
platypush/plugins/user.py
Normal file
138
platypush/plugins/user.py
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
from platypush.plugins import Plugin, action
|
||||||
|
from platypush.user import UserManager
|
||||||
|
|
||||||
|
|
||||||
|
class UserPlugin(Plugin):
|
||||||
|
"""
|
||||||
|
Plugin to programmatically create and manage users and user sessions
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.user_manager = UserManager()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def create_user(self, username, password, executing_user=None, executing_user_password=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.
|
||||||
|
|
||||||
|
:return: dict::
|
||||||
|
|
||||||
|
{
|
||||||
|
"user_id": int,
|
||||||
|
"username": str,
|
||||||
|
"created_at": str (in ISO format)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.user_manager.get_user_count() > 0 and not executing_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):
|
||||||
|
return None, "Invalid credentials"
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = self.user_manager.create_user(username, password, **kwargs)
|
||||||
|
except (NameError, ValueError) as e:
|
||||||
|
return None, str(e)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'user_id': user.user_id,
|
||||||
|
'username': user.username,
|
||||||
|
'created_at': user.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
def authenticate_user(self, username, password):
|
||||||
|
"""
|
||||||
|
Authenticate a user
|
||||||
|
:return: True if the provided username and password are correct, False otherwise
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.user_manager.authenticate_user(username, password)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def update_password(self, username, old_password, new_password):
|
||||||
|
"""
|
||||||
|
Update the password of a user
|
||||||
|
:return: True if the password was successfully updated, false otherwise
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.user_manager.update_password(username, old_password, new_password)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def delete_user(self, username, executing_user, executing_user_password):
|
||||||
|
"""
|
||||||
|
Delete a user
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.user_manager.authenticate_user(executing_user, executing_user_password):
|
||||||
|
return None, "Invalid credentials"
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.user_manager.delete_user(username)
|
||||||
|
except NameError:
|
||||||
|
return None, "No such user: {}".format(username)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def create_session(self, username, password, expires_at=None):
|
||||||
|
"""
|
||||||
|
Create a user session
|
||||||
|
:return: dict::
|
||||||
|
|
||||||
|
{
|
||||||
|
"session_token": str,
|
||||||
|
"user_id": int,
|
||||||
|
"created_at": str (in ISO format),
|
||||||
|
"expires_at": str (in ISO format),
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = self.user_manager.create_user_session(username=username,
|
||||||
|
password=password,
|
||||||
|
expires_at=expires_at)
|
||||||
|
|
||||||
|
if not session:
|
||||||
|
return None, "Invalid credentials"
|
||||||
|
|
||||||
|
return {
|
||||||
|
'session_token': session.session_token,
|
||||||
|
'user_id': session.user_id,
|
||||||
|
'created_at': session.created_at.isoformat(),
|
||||||
|
'expires_at': session.expires_at.isoformat() if session.expires_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
def authenticate_session(self, session_token):
|
||||||
|
"""
|
||||||
|
Authenticate a session by token and return the associated user
|
||||||
|
:return: dict::
|
||||||
|
|
||||||
|
{
|
||||||
|
"user_id": int,
|
||||||
|
"username": str,
|
||||||
|
"created_at": str (in ISO format)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = self.user_manager.authenticate_user_session(session_token=session_token)
|
||||||
|
if not user:
|
||||||
|
return None, 'Invalid session token'
|
||||||
|
|
||||||
|
return {
|
||||||
|
'user_id': user.user_id,
|
||||||
|
'username': user.username,
|
||||||
|
'created_at': user.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
def delete_session(self, session_token):
|
||||||
|
"""
|
||||||
|
Delete a user session
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.user_manager.delete_user_session(session_token)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
191
platypush/user/__init__.py
Normal file
191
platypush/user/__init__.py
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||||
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
from platypush.context import get_plugin
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
Session = scoped_session(sessionmaker())
|
||||||
|
|
||||||
|
|
||||||
|
class UserManager:
|
||||||
|
"""
|
||||||
|
Main class for managing platform users
|
||||||
|
"""
|
||||||
|
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
def __init__(self):
|
||||||
|
db_plugin = get_plugin('db')
|
||||||
|
if not db_plugin:
|
||||||
|
raise ModuleNotFoundError('Please enable/configure the db plugin for multi-user support')
|
||||||
|
|
||||||
|
self._engine = db_plugin._get_engine()
|
||||||
|
|
||||||
|
def get_user(self, username):
|
||||||
|
session = self._get_db_session()
|
||||||
|
user = self._get_user(session, username)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Hide password
|
||||||
|
user.password = None
|
||||||
|
return user
|
||||||
|
|
||||||
|
def get_user_count(self):
|
||||||
|
session = self._get_db_session()
|
||||||
|
return session.query(User).count()
|
||||||
|
|
||||||
|
def create_user(self, username, password, **kwargs):
|
||||||
|
session = self._get_db_session()
|
||||||
|
if not username:
|
||||||
|
raise ValueError('Invalid or empty username')
|
||||||
|
if not password:
|
||||||
|
raise ValueError('Please provide a password for the user')
|
||||||
|
|
||||||
|
user = self._get_user(session, username)
|
||||||
|
if user:
|
||||||
|
raise NameError('The user {} already exists'.format(username))
|
||||||
|
|
||||||
|
record = User(username=username, password=self._encrypt_password(password),
|
||||||
|
created_at=datetime.datetime.utcnow(), **kwargs)
|
||||||
|
|
||||||
|
session.add(record)
|
||||||
|
session.commit()
|
||||||
|
user = self._get_user(session, username)
|
||||||
|
|
||||||
|
# Hide password
|
||||||
|
user.password = None
|
||||||
|
return user
|
||||||
|
|
||||||
|
def update_password(self, username, old_password, new_password):
|
||||||
|
session = self._get_db_session()
|
||||||
|
if not self._authenticate_user(session, username, old_password):
|
||||||
|
return False
|
||||||
|
|
||||||
|
user = self._get_user(session, username)
|
||||||
|
user.password = self._encrypt_password(new_password)
|
||||||
|
session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def authenticate_user(self, username, password):
|
||||||
|
session = self._get_db_session()
|
||||||
|
return self._authenticate_user(session, username, password)
|
||||||
|
|
||||||
|
def authenticate_user_session(self, session_token):
|
||||||
|
session = self._get_db_session()
|
||||||
|
user_session = session.query(UserSession).filter_by(session_token=session_token).first()
|
||||||
|
|
||||||
|
if not user_session or (
|
||||||
|
user_session.expires_at and user_session.expires_at < datetime.datetime.utcnow()):
|
||||||
|
return None
|
||||||
|
|
||||||
|
user = session.query(User).filter_by(user_id=user_session.user_id).first()
|
||||||
|
|
||||||
|
# Hide password
|
||||||
|
user.password = None
|
||||||
|
return user
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
session.delete(user)
|
||||||
|
session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def delete_user_session(self, session_token):
|
||||||
|
session = self._get_db_session()
|
||||||
|
user_session = session.query(UserSession).filter_by(session_token=session_token).first()
|
||||||
|
|
||||||
|
if not user_session:
|
||||||
|
return False
|
||||||
|
|
||||||
|
session.delete(user_session)
|
||||||
|
session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_user_session(self, username, password, expires_at=None):
|
||||||
|
session = self._get_db_session()
|
||||||
|
if not self._authenticate_user(session, username, password):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if expires_at:
|
||||||
|
if isinstance(expires_at, int) or isinstance(expires_at, float):
|
||||||
|
expires_at = datetime.datetime.fromtimestamp(expires_at)
|
||||||
|
elif isinstance(expires_at, str):
|
||||||
|
expires_at = datetime.datetime.fromisoformat(expires_at)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
session.add(user_session)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return user_session
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_user(session, username):
|
||||||
|
return session.query(User).filter_by(username=username).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _encrypt_password(pwd):
|
||||||
|
return bcrypt.hashpw(pwd.encode(), bcrypt.gensalt(12))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _check_password(pwd, hashed_pwd):
|
||||||
|
return bcrypt.checkpw(pwd.encode(), hashed_pwd)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_token():
|
||||||
|
rand = bytes(random.randint(0, 255) for _ in range(0, 255))
|
||||||
|
return hashlib.sha256(rand).hexdigest()
|
||||||
|
|
||||||
|
def _get_db_session(self):
|
||||||
|
Base.metadata.create_all(self._engine)
|
||||||
|
Session = scoped_session(sessionmaker())
|
||||||
|
Session.configure(bind=self._engine)
|
||||||
|
return Session()
|
||||||
|
|
||||||
|
def _authenticate_user(self, session, username, password):
|
||||||
|
user = self._get_user(session, username)
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self._check_password(password, user.password)
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
""" Models the User table """
|
||||||
|
|
||||||
|
__tablename__ = 'user'
|
||||||
|
__table_args__ = ({'sqlite_autoincrement': True})
|
||||||
|
|
||||||
|
user_id = Column(Integer, primary_key=True)
|
||||||
|
username = Column(String, unique=True, nullable=False)
|
||||||
|
password = Column(String)
|
||||||
|
created_at = Column(DateTime)
|
||||||
|
|
||||||
|
|
||||||
|
class UserSession(Base):
|
||||||
|
""" Models the UserSession table """
|
||||||
|
|
||||||
|
__tablename__ = 'user_session'
|
||||||
|
__table_args__ = ({'sqlite_autoincrement': True})
|
||||||
|
|
||||||
|
session_id = Column(Integer, primary_key=True)
|
||||||
|
session_token = Column(String, unique=True, nullable=False)
|
||||||
|
user_id = Column(Integer, ForeignKey('user.user_id'), nullable=False)
|
||||||
|
created_at = Column(DateTime)
|
||||||
|
expires_at = Column(DateTime)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -32,6 +32,9 @@ requests
|
||||||
# Database plugin support
|
# Database plugin support
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
|
|
||||||
|
# Support for multi-users and password authentication
|
||||||
|
bcrypt
|
||||||
|
|
||||||
# RSS feeds support
|
# RSS feeds support
|
||||||
# feedparser
|
# feedparser
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue