Minor bug fixes
This commit is contained in:
parent
42e2b884a1
commit
fe5979aa5b
|
@ -7,49 +7,51 @@ import logging
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def htime(ts):
|
||||
if ts:
|
||||
dt = datetime.datetime.fromtimestamp(ts)
|
||||
def htime(timestamp):
|
||||
if timestamp:
|
||||
dt = datetime.datetime.fromtimestamp(timestamp)
|
||||
return humanize.naturaltime(dt)
|
||||
|
||||
|
||||
def htime_dt(dt):
|
||||
if dt:
|
||||
def htime_dt(datetime):
|
||||
if datetime:
|
||||
try:
|
||||
return humanize.naturaltime(dt)
|
||||
return humanize.naturaltime(datetime)
|
||||
except ValueError:
|
||||
return "Never"
|
||||
return "Never"
|
||||
|
||||
|
||||
def hdate(ts):
|
||||
if ts:
|
||||
dt = datetime.datetime.fromtimestamp(ts)
|
||||
def hdate(timestamp):
|
||||
if timestamp:
|
||||
dt = datetime.datetime.fromtimestamp(timestamp)
|
||||
return humanize.naturaldate(dt)
|
||||
|
||||
|
||||
def hdate_dt(dt):
|
||||
if dt:
|
||||
def hdate_dt(datetime):
|
||||
if datetime:
|
||||
try:
|
||||
return humanize.naturaldate(dt)
|
||||
return humanize.naturaldate(datetime)
|
||||
except ValueError:
|
||||
return "Never"
|
||||
return "Never"
|
||||
|
||||
|
||||
def fmt_timestamp(ts):
|
||||
def fmt_timestamp(timestamp):
|
||||
with current_app.app_context():
|
||||
tz = ZoneInfo(current_app.config['APP_TZ'])
|
||||
if ts:
|
||||
local_ts = datetime.datetime.fromtimestamp(ts, tz)
|
||||
if timestamp:
|
||||
local_ts = datetime.datetime.fromtimestamp(timestamp, tz)
|
||||
return "%s %s" % (local_ts.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
local_ts.tzname())
|
||||
|
||||
|
||||
def fmt_datetime(dt):
|
||||
def fmt_datetime(datetime):
|
||||
with current_app.app_context():
|
||||
tz = ZoneInfo(current_app.config['APP_TZ'])
|
||||
if dt:
|
||||
if datetime:
|
||||
try:
|
||||
local_ts = dt.fromtimestamp(dt.timestamp(), tz)
|
||||
local_ts = datetime.fromtimestamp(datetime.timestamp(), tz)
|
||||
except OverflowError:
|
||||
return "Never"
|
||||
return "%s %s" % (local_ts.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
|
|
|
@ -20,6 +20,13 @@ def remote_ip() -> str:
|
|||
return str(request.environ.get('REMOTE_ADDR'))
|
||||
|
||||
|
||||
def username() -> str:
|
||||
userinfo = flask_session['userinfo']
|
||||
if 'preferred_username' in userinfo:
|
||||
return userinfo['preferred_username']
|
||||
return userinfo['email']
|
||||
|
||||
|
||||
def webMode() -> bool:
|
||||
is_gunicorn = "gunicorn" in os.environ.get('SERVER_SOFTWARE', '')
|
||||
is_werkzeug = os.environ.get('WERKZEUG_RUN_MAIN', False) == "true"
|
||||
|
|
|
@ -103,3 +103,10 @@ div.dt-container div.dt-scroll-body {
|
|||
tr.pka-hide {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
i.disabled {
|
||||
color: #888;
|
||||
}
|
||||
span.expired {
|
||||
color: #888;
|
||||
}
|
||||
|
|
|
@ -16,9 +16,3 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro body_theme(theme) -%}
|
||||
{% if theme == "dark" %}
|
||||
bootstrap-dark
|
||||
{% endif %}
|
||||
{%- endmacro -%}
|
||||
|
|
|
@ -34,7 +34,6 @@
|
|||
<link rel="manifest" href="/static/favicon/site.webmanifest">
|
||||
</head>
|
||||
|
||||
<!-- <body class="bootstrap {{ mc.body_theme(g.theme) }}"> -->
|
||||
<body class="bootstrap bootstrap-dark">
|
||||
<!-- Header -->
|
||||
<header>
|
||||
|
|
|
@ -14,6 +14,18 @@
|
|||
</h3>
|
||||
<hr>
|
||||
<p></p>
|
||||
<div class="row">
|
||||
<div class="col col-3 float-left">
|
||||
<strong>status</strong>
|
||||
</div>
|
||||
<div class="col col-8 float-left">
|
||||
{% if node.online %}
|
||||
<span class="badge badge-pill badge-success">online</span>
|
||||
{% else %}
|
||||
<span class="badge badge-pill badge-danger">offline</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-3 float-left">
|
||||
<strong>registered</strong>
|
||||
|
@ -27,20 +39,23 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col col-3 float-left">
|
||||
<strong>expiry</strong>
|
||||
<strong>expire</strong>
|
||||
</div>
|
||||
<div class="col col-8 float-left">
|
||||
<span data-toggle="tooltip" data-placement="right" title="{{ node.expiry | fmt_datetime }}">
|
||||
{{ node.expiry | htime_dt }}
|
||||
</span>
|
||||
{% if node.expireDate and not node.expired %}
|
||||
<a href="/node/{{ node.id }}/expire">
|
||||
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect node">
|
||||
<i class="fas fa-plug"></i>
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-3 float-left">
|
||||
<strong>owner</strong>
|
||||
|
@ -73,7 +88,7 @@
|
|||
<div class="col col-6 float-left">
|
||||
{% if node.validTags %}
|
||||
{% for tag in node.validTags %}
|
||||
<span class="badge badge-pill badge-info">
|
||||
<span class="badge badge-pill badge-warning">
|
||||
{{ tag }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
|
@ -89,7 +104,7 @@
|
|||
<div class="col col-6 float-left">
|
||||
{% if node.forced %}
|
||||
{% for tag in node.forcedTags %}
|
||||
<h3><span class="badge badge-pill badge-info">
|
||||
<h3><span class="badge badge-pill badge-primary">
|
||||
{{ tag }}
|
||||
</span></h3>
|
||||
{% endfor %}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<tr>
|
||||
<th>name</th>
|
||||
<th>user</th>
|
||||
<th>registered</th>
|
||||
<th>expire</th>
|
||||
<th>last event</th>
|
||||
<th>online</th>
|
||||
<th> </th>
|
||||
|
@ -32,9 +32,11 @@
|
|||
{{node.user.name}}
|
||||
</a>
|
||||
</td>
|
||||
<td data-order="{{ node.createdAt }}">
|
||||
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
|
||||
{{node.createdAt | htime_dt }}
|
||||
<td>
|
||||
<span data-toggle="tooltip" data-placement="right"
|
||||
title="{{ node.expiry | fmt_datetime }}"
|
||||
class="{% if node.expired %}expired{% endif %}">
|
||||
{{node.expireDate | htime_dt | safe}}
|
||||
</span>
|
||||
</td>
|
||||
<td data-order="{{ node.lastSeen | fmt_datetime }}">
|
||||
|
@ -46,11 +48,15 @@
|
|||
{{node.online | fancyBool | safe}}
|
||||
</td>
|
||||
<td class="no-sort">
|
||||
{% if node.expireDate and not node.expired %}
|
||||
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect">
|
||||
<a class="nodeco" href="/node/{{node.id}}/list-expire">
|
||||
<a class="nodeco" href="/node/{{node.id}}/list-expire" disabled>
|
||||
<i class="fas fa-plug"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% else %}
|
||||
<i class="fas fa-plug disabled"></i>
|
||||
{% endif %}
|
||||
<span data-toggle="tooltip" data-placement="right" title="delete">
|
||||
<a class="nodeco" href="/node/{{node.id}}/delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
|
|
|
@ -5,6 +5,8 @@ from flask import render_template, Blueprint, request
|
|||
from flask import redirect, session, url_for
|
||||
from app import auth
|
||||
|
||||
from .lib import username
|
||||
|
||||
from flask import jsonify
|
||||
from flask_pyoidc.user_session import UserSession
|
||||
|
||||
|
@ -19,13 +21,13 @@ log = logging.getLogger()
|
|||
main_blueprint = Blueprint('main', __name__)
|
||||
|
||||
|
||||
@ main_blueprint.route('/health', methods=['GET'])
|
||||
@main_blueprint.route('/health', methods=['GET'])
|
||||
def health():
|
||||
return jsonify(dict(status="OK", version=current_app.config['APP_VERSION']))
|
||||
|
||||
|
||||
@ main_blueprint.route('/', methods=['GET', 'POST'])
|
||||
@ auth.access_control('default')
|
||||
@main_blueprint.route('/', methods=['GET', 'POST'])
|
||||
@auth.access_control('default')
|
||||
def index():
|
||||
user_session = UserSession(session)
|
||||
hs_user = user_session.userinfo['email'].split('@')[0]
|
||||
|
@ -35,29 +37,29 @@ def index():
|
|||
session=user_session)
|
||||
|
||||
|
||||
@ main_blueprint.route('/token', methods=['GET', 'POST'])
|
||||
@ auth.access_control('default')
|
||||
@main_blueprint.route('/token', methods=['GET', 'POST'])
|
||||
@auth.access_control('default')
|
||||
def token():
|
||||
user_session = UserSession(session)
|
||||
return jsonify(user_session.userinfo)
|
||||
|
||||
|
||||
@ main_blueprint.route('/logout')
|
||||
@ auth.oidc_logout
|
||||
@main_blueprint.route('/logout')
|
||||
@auth.oidc_logout
|
||||
def logout():
|
||||
return redirect('/')
|
||||
|
||||
|
||||
@ main_blueprint.route('/nodes', methods=['GET'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/nodes', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def nodes():
|
||||
nodelist = Node().list()
|
||||
return render_template('nodes.html',
|
||||
nodes=nodelist.nodes)
|
||||
|
||||
|
||||
@ main_blueprint.route('/node/<int:nodeId>', methods=['GET'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/node/<int:nodeId>', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def node(nodeId):
|
||||
# There is a bug in HS api with retrieving a single node
|
||||
# and we added a workaround to hsapi, so node.get() returns a
|
||||
|
@ -72,8 +74,8 @@ def node(nodeId):
|
|||
node=node)
|
||||
|
||||
|
||||
@ main_blueprint.route('/users', methods=['GET'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/users', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def users():
|
||||
userList = User().list()
|
||||
# Get online status of devices of the user
|
||||
|
@ -88,8 +90,8 @@ def users():
|
|||
online=online)
|
||||
|
||||
|
||||
@ main_blueprint.route('/user/<userName>', methods=['GET'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/user/<userName>', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def user(userName):
|
||||
user = User().get(userName)
|
||||
userNodeList = [n for n in Node().list().nodes if n.user.name == userName]
|
||||
|
@ -107,8 +109,8 @@ def user(userName):
|
|||
userNodeList=userNodeList)
|
||||
|
||||
|
||||
@ main_blueprint.route('/routes', methods=['GET'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/routes', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def routes():
|
||||
routes = Route().list()
|
||||
|
||||
|
@ -126,52 +128,65 @@ def routes():
|
|||
routes=final)
|
||||
|
||||
|
||||
@ main_blueprint.route('/routeToggle/<int:routeId>', methods=['GET'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/routeToggle/<int:routeId>', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def routeToggle(routeId: int):
|
||||
routes = Route().list()
|
||||
route = [r for r in routes.routes if r.id == routeId]
|
||||
if route:
|
||||
route = route[0]
|
||||
if route.enabled:
|
||||
action = 'disabled'
|
||||
Route().disable(routeId)
|
||||
log.info(f"route {route.prefix}/{route.node.givenName} disabled")
|
||||
else:
|
||||
Route().enable(routeId)
|
||||
log.info(f"route {route.prefix}/{route.node.givenName} enabled")
|
||||
action = 'enabled'
|
||||
log.info(
|
||||
f"route '{route.prefix}' via '{route.node.givenName}'"
|
||||
f"{action} by '{username()}'")
|
||||
return redirect(url_for("main.routes"))
|
||||
|
||||
|
||||
@ main_blueprint.route('/node/<int:nodeId>/expire', methods=['GET'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/node/<int:nodeId>/expire', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def expireNode(nodeId: int):
|
||||
"""
|
||||
This expires a node from the node page.
|
||||
The difference from above is that it returns to the /node/nodeId page
|
||||
"""
|
||||
Node().expire(nodeId)
|
||||
log.info(f"node '{nodeId}' expired by '{username()}'")
|
||||
return redirect(url_for("main.node", nodeId=nodeId))
|
||||
|
||||
|
||||
@ main_blueprint.route('/node/<int:nodeId>/list-expire', methods=['GET'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/node/<int:nodeId>/list-expire', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def expireNodeList(nodeId: int):
|
||||
"""
|
||||
This expires a node from the node list.
|
||||
The difference from above is that it returns to the /nodes page
|
||||
"""
|
||||
Node().expire(nodeId)
|
||||
log.info(f"node '{nodeId}' expired by '{username()}'")
|
||||
return redirect(url_for("main.nodes"))
|
||||
|
||||
|
||||
@ main_blueprint.route('/node/<int:nodeId>/delete', methods=['GET'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/node/<int:nodeId>/delete', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def deleteNode(nodeId: int):
|
||||
Node().delete(nodeId)
|
||||
return redirect(url_for("main.nodes"))
|
||||
|
||||
|
||||
@ main_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def renameNode(nodeId: int, newName: str):
|
||||
Node().rename(nodeId, newName)
|
||||
return jsonify(dict(newName=newName))
|
||||
|
||||
|
||||
@ main_blueprint.route('/user/<userName>/delete', methods=['GET'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/user/<userName>/delete', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def deleteUser(userName: str):
|
||||
nodes = Node().byUser(userName)
|
||||
for node in nodes.nodes:
|
||||
|
@ -181,8 +196,8 @@ def deleteUser(userName: str):
|
|||
return redirect(url_for("main.users"))
|
||||
|
||||
|
||||
@ main_blueprint.route('/user/<userName>/pakcreate', methods=['POST'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/user/<userName>/pakcreate', methods=['POST'])
|
||||
@auth.authorize_admins('default')
|
||||
def createPKA(userName: str):
|
||||
data = request.json
|
||||
log.debug(data)
|
||||
|
@ -195,8 +210,8 @@ def createPKA(userName: str):
|
|||
return jsonify(dict(key=pak.preAuthKey.key))
|
||||
|
||||
|
||||
@ main_blueprint.route('/user/<userName>/expire/<key>', methods=['GET'])
|
||||
@ auth.authorize_admins('default')
|
||||
@main_blueprint.route('/user/<userName>/expire/<key>', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def expirePKA(userName: str, key: str):
|
||||
log.debug(key)
|
||||
req = v1ExpirePreAuthKeyRequest(user=userName, key=key)
|
||||
|
|
|
@ -8,7 +8,8 @@ ARG BUILD_DATE
|
|||
ENV APP_VERSION=${APP_VERSION:-alpha0}
|
||||
ENV APP_SHA=${APP_SHA:-000000}
|
||||
# useful in case we want to run in debug mode
|
||||
ENV FLASK_APP /hsmon/wsgi.py
|
||||
ENV FLASK_APP /hsman/wsgi.py
|
||||
ENV FLASK_ENV production
|
||||
|
||||
RUN apk --update --no-cache add \
|
||||
bash \
|
||||
|
@ -18,16 +19,18 @@ RUN apk --update --no-cache add \
|
|||
chmod g+w /run && \
|
||||
pip install poetry gunicorn
|
||||
|
||||
COPY . /hsmon
|
||||
COPY . /hsman
|
||||
|
||||
RUN cd hsmon && \
|
||||
RUN cd hsman && \
|
||||
poetry install && \
|
||||
poetry export | pip install -r /dev/stdin
|
||||
|
||||
|
||||
WORKDIR /hsman
|
||||
|
||||
HEALTHCHECK --interval=20s --timeout=3s CMD curl -I -s -o /dev/null localhost:5000/health || exit 1
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
# exectute start up script
|
||||
ENTRYPOINT ["/hsmon/docker/entrypoint.sh"]
|
||||
ENTRYPOINT ["/hsman/docker/entrypoint.sh"]
|
||||
|
|
|
@ -5,11 +5,12 @@ if [ $# -gt 0 ]; then
|
|||
fi
|
||||
|
||||
echo Starting ${APP_VERSION}
|
||||
if [ -e /debug ]; then
|
||||
echo "Running app in debug mode!"
|
||||
export PYTHONPATH=/app/site-packages
|
||||
/hsmon/site-packages/bin/flask run --host 0.0.0.0
|
||||
|
||||
if [ $FLASK_ENV == "development" ]; then
|
||||
echo "Running app in debug mode"
|
||||
export FLASK_DEBUG=1
|
||||
flask run --host 0.0.0.0
|
||||
else
|
||||
echo "Running app in production mode!"
|
||||
gunicorn --chdir hsmon wsgi:app -w 1 --threads 2 -b 0.0.0.0:5000
|
||||
echo "Running app in production mode"
|
||||
gunicorn wsgi:app --config gunicorn.conf.py
|
||||
fi
|
||||
|
|
|
@ -4,13 +4,15 @@ proc_name = "hsmon"
|
|||
default_proc_name = "hsmon"
|
||||
|
||||
bind = "0.0.0.0:5000"
|
||||
workers = 1
|
||||
workers = 2
|
||||
threads = 4
|
||||
preload_app = True
|
||||
worker_class = "uvicorn.workers.UvicornWorker"
|
||||
# worker_class = "uvicorn.workers.UvicornWorker"
|
||||
|
||||
chdir = "/hsman"
|
||||
|
||||
# logconfig = "app/logging/production.ini"
|
||||
logconfig = "/hsmon/app/logging/production.ini"
|
||||
logconfig = "app/logging/production.ini"
|
||||
# access_log_format = "%(h)s %(l)s %(t)s %(r)s %(s)s %(b)s %(f)s %(a)s"
|
||||
# Log to stdout.
|
||||
accesslog = "-"
|
||||
|
|
|
@ -457,13 +457,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "hsapi-client"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
description = "Headscale API client"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.11"
|
||||
files = [
|
||||
{file = "hsapi_client-0.9.1-py3-none-any.whl", hash = "sha256:1b6531526ed476daaefa16eeb3a16e768372f4bd5d879da7bf4ed75a4bd36a5b"},
|
||||
{file = "hsapi_client-0.9.1.tar.gz", hash = "sha256:b047a21e642ae4ba108a17f6b68d71af8ba851270421261df92c97acdcfdc907"},
|
||||
{file = "hsapi_client-0.9.2-py3-none-any.whl", hash = "sha256:bbda176e10b7ba0c1376eac6e8fcbd9027a41fab21585edad275cfbd21cce66d"},
|
||||
{file = "hsapi_client-0.9.2.tar.gz", hash = "sha256:902256e6240e47b3eecacde930f9d67e12a0d46d34d7dbcc86e95b2976013099"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1186,4 +1186,4 @@ watchdog = ["watchdog (>=2.3)"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.11,<4.0"
|
||||
content-hash = "462d0b3a2b307ddc04cb74309f7a1a85679a3a3bf05155a8022d0047d14d2241"
|
||||
content-hash = "b69a53708d986528f101f49ccb7e61fb1377d244511d9cd335b0ec200510b37b"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "hsman"
|
||||
version = "0.1.0"
|
||||
version = "0.9.0"
|
||||
description = "Flask Admin webui for Headscale"
|
||||
authors = ["Andrea Mistrali <andrea@mistrali.pw>"]
|
||||
license = "BSD"
|
||||
|
@ -11,11 +11,11 @@ python = ">=3.11,<4.0"
|
|||
Flask = "^3.0.3"
|
||||
Flask-pyoidc = "^3.14.3"
|
||||
gunicorn = "^22.0.0"
|
||||
hsapi-client = "^0.9.1"
|
||||
flask-mobility = "^2.0.1"
|
||||
humanize = "^4.9.0"
|
||||
flask-pydantic = "^0.12.0"
|
||||
uvicorn = "^0.30.1"
|
||||
hsapi-client = "^0.9.2"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
|
Loading…
Reference in New Issue