Lot of work and it works

This commit is contained in:
Andrea Mistrali 2024-07-01 17:42:32 +02:00
parent 4e84fc5697
commit c94f31d5ee
26 changed files with 1615 additions and 6 deletions

View File

@ -45,9 +45,25 @@ class Node(HSAPICall):
response = self.call('get')
return v1ListNodesResponse(**response.json())
def get(self, nodeId: str) -> v1NodeResponse:
response = self.call('get', call_path=nodeId)
return v1NodeResponse(**response.json())
def get(self, nodeId: str) -> v1Node:
# There is a bug in headscale API
# retrieving a specific node does not return the tags
# so we get the full list of nodes and extract the node with the
# ID we want
# response = self.call('get', call_path=nodeId)
nodelist = self.list()
node = [n for n in nodelist.nodes if n.id == nodeId]
if node:
return node[0] # type: ignore
else:
return v1Node()
def byUser(self, username: str) -> v1ListNodesResponse:
nodelist = self.list()
byUser = [n for n in nodelist.nodes if n.user.name == username]
return v1ListNodesResponse(nodes=byUser)
def delete(self, nodeId: str) -> None:
self.call('delete', call_path=nodeId)

2
hsapi/poetry.lock generated
View File

@ -327,4 +327,4 @@ zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "d6cbad8cd64ba63c62d1f3ab2dde75682d6a42c5b40808fd35615bccdf5fdd07"
content-hash = "28f675747b0ee9850925befb61509ac513e0acc429d7efc1abe2a0fea6d8f97d"

View File

@ -13,6 +13,9 @@ pydantic = "^2.7.4"
pydantic-settings = "^2.3.4"
[tool.poetry.group.dev.dependencies]
python-dotenv = "^1.0.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@ -1 +0,0 @@
headscale

70
hsman/app/__init__.py Normal file
View File

@ -0,0 +1,70 @@
from flask import Flask, render_template
from werkzeug.exceptions import HTTPException
from flask_mobility import Mobility
from flask_pyoidc import OIDCAuthentication
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata
from . import filters
import os
mobility = Mobility()
client_metadata = ClientMetadata(
client_id='***REMOVED***',
client_secret='***REMOVED***',
post_logout_redirect_uris=['https://example.com/logout'])
provider_config = ProviderConfiguration(issuer='***REMOVED***',
client_metadata=client_metadata,
auth_request_params={
'scope': ['openid',
'profile',
'groups',
'email']},
session_refresh_interval_seconds=1800)
auth = OIDCAuthentication({'default': provider_config})
def create_app(environment='development'):
from config import config
from .views import main_blueprint
# Instantiate app.
app = Flask(__name__)
# Set app config.
env = os.environ.get('FLASK_ENV', environment)
app.config.from_object(config[env])
app.config.from_prefixed_env(prefix="HSMAN")
config[env].configure(app)
app.config['APP_TZ'] = os.environ.get('TZ', 'UTC')
app.logger.info("middleware init: mobility")
mobility.init_app(app)
# Register blueprints.
app.logger.info("registering main blueprint")
app.register_blueprint(main_blueprint)
app.logger.info("jinja2 custom filters loaded")
filters.init_app(app)
app.config.update(
OIDC_REDIRECT_URI='http://localhost:5000/redirect',
SECRET_KEY="secreto"
)
auth.init_app(app)
# Error handlers.
@app.errorhandler(HTTPException)
def handle_http_error(exc):
return render_template('error.html', error=exc), exc.code
return app

85
hsman/app/filters.py Normal file
View File

@ -0,0 +1,85 @@
import humanize
import datetime
from flask import current_app
from zoneinfo import ZoneInfo
import logging
log = logging.getLogger(__name__)
def htime(ts):
if ts:
dt = datetime.datetime.fromtimestamp(ts)
return humanize.naturaltime(dt)
def htime_dt(dt):
if dt:
try:
return humanize.naturaltime(dt)
except ValueError:
return "Never"
def hdate(ts):
if ts:
dt = datetime.datetime.fromtimestamp(ts)
return humanize.naturaldate(dt)
def hdate_dt(dt):
if dt:
try:
return humanize.naturaldate(dt)
except ValueError:
return "Never"
def fmt_timestamp(ts):
with current_app.app_context():
tz = ZoneInfo(current_app.config['APP_TZ'])
if ts:
local_ts = datetime.datetime.fromtimestamp(ts, tz)
return "%s %s" % (local_ts.strftime('%Y-%m-%d %H:%M:%S'),
local_ts.tzname())
def fmt_datetime(dt):
with current_app.app_context():
tz = ZoneInfo(current_app.config['APP_TZ'])
if dt:
try:
local_ts = dt.fromtimestamp(dt.timestamp(), tz)
except OverflowError:
return "Never"
return "%s %s" % (local_ts.strftime('%Y-%m-%d %H:%M:%S'),
local_ts.tzname())
def fancyBool(bool_):
if bool_:
return '<span style="color: green">&#x2714;</span>'
else:
return '<span style="color: red">&#x2718;</span>'
def fancyOnline(online):
return ["offline", "online"][online]
# A list of all of the template_filter functions to add in `init_app`
FILTERS = [hdate,
hdate_dt,
htime,
htime_dt,
fmt_timestamp,
fmt_datetime,
fancyBool,
fancyOnline]
def init_app(app):
"""Register the template filters with an app instance"""
log.debug("filters init")
for func in FILTERS:
app.jinja_env.filters[func.__name__] = func

21
hsman/app/lib.py Normal file
View File

@ -0,0 +1,21 @@
from app import models
import os
from flask import request
import logging
log = logging.getLogger(__name__)
def remote_ip():
if 'HTTP_X_FORWARDED_FOR' in request.environ:
xff_parts = request.environ.get('HTTP_X_FORWARDED_FOR').split(',')
return xff_parts[0]
else:
return request.environ.get('REMOTE_ADDR')
def webMode():
is_gunicorn = "gunicorn" in os.environ.get('SERVER_SOFTWARE', '')
is_werkzeug = os.environ.get('WERKZEUG_RUN_MAIN', False) == "true"
return is_gunicorn or is_werkzeug

View File

@ -0,0 +1,95 @@
# Logging configuration
[loggers]
keys = root, wsgi, app, views, lib, model, sqlalchemy, scheduler, werkzeug, gunicorn
[handlers]
keys = console, accesslog
[formatters]
keys = generic, colored, accesslog
[logger_root]
level = DEBUG
handlers = console
[logger_gunicorn]
level = DEBUG
handlers = accesslog
qualname = gunicorn
propagate = 0
[logger_werkzeug]
level = INFO
handlers = accesslog
qualname = werkzeug
propagate = 0
[logger_wsgi]
level = DEBUG
handlers = console
qualname = wsgi
propagate = 0
[logger_app]
level = DEBUG
handlers = console
qualname = app
propagate = 0
[logger_views]
level = DEBUG
handlers = console
qualname = app.views
propagate = 0
[logger_lib]
level = DEBUG
handlers = console
qualname = app.lib
propagate = 0
[logger_model]
level = DEBUG
handlers = console
qualname = app.models
propagate = 0
[logger_sqlalchemy]
level = WARN
handlers = console
qualname = sqlalchemy.engine.Engine
propagate = 0
; qualname = sqlalchemy.engine.Engine
# "level = INFO" logs SQL queries.
# "level = DEBUG" logs SQL queries and results.
# "level = WARN" logs neither. (Recommended for production systems.)
[logger_scheduler]
level = WARN
handlers = console
qualname = app.scheduler, apscheduler
propagate = 0
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = colored
; formatter = generic
[handler_accesslog]
class = StreamHandler
args = (sys.stdout,)
level = NOTSET
formatter = accesslog
[formatter_generic]
format = %(asctime)s,%(msecs)03d %(levelname)-7.7s [%(name)s/%(filename)s:%(lineno)d]: %(message)s
datefmt = %Y-%m-%d %H:%M:%S
[formatter_colored]
format = %(asctime)s,%(msecs)03d %(levelname)-7.7s [%(name)s/%(filename)s:%(lineno)d]: %(message)s
datefmt = %H:%M:%S
[formatter_accesslog]
format = %(name)s %(message)s

View File

@ -0,0 +1,94 @@
# Logging configuration
[loggers]
keys = root, wsgi, app, views, lib, model, sqlalchemy, scheduler, werkzeug, gunicorn
[handlers]
keys = console, accesslog
[formatters]
keys = generic, colored, accesslog
[logger_root]
level = INFO
handlers = console
[logger_gunicorn]
level = INFO
handlers = accesslog
qualname = gunicorn
propagate = 0
[logger_werkzeug]
level = INFO
handlers = accesslog
qualname = werkzeug
propagate = 0
[logger_wsgi]
level = INFO
handlers = console
qualname = wsgi
propagate = 0
[logger_app]
level = INFO
handlers = console
qualname = app.
propagate = 0
[logger_views]
level = INFO
handlers = console
qualname = app.views
propagate = 0
[logger_lib]
level = INFO
handlers = console
qualname = app.lib
propagate = 0
[logger_model]
level = INFO
handlers = console
qualname = app.models
propagate = 0
[logger_sqlalchemy]
level = WARN
handlers = console
qualname = sqlalchemy.engine.Engine
propagate = 0
# "level = INFO" logs SQL queries.
# "level = DEBUG" logs SQL queries and results.
# "level = WARN" logs neither. (Recommended for production systems.)
[logger_scheduler]
level = INFO
handlers = console
qualname = app.scheduler
propagate = 0
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
; formatter = colored
formatter = generic
[handler_accesslog]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = accesslog
[formatter_generic]
format = %(asctime)s,%(msecs)03d %(levelname)-7.7s [%(name)s/%(filename)s:%(lineno)d]: %(message)s
datefmt = %Y-%m-%d %H:%M:%S
[formatter_colored]
format = %(asctime)s,%(msecs)03d %(levelname)-7.7s [%(name)s/%(filename)s:%(lineno)d]: %(message)s
datefmt = %H:%M:%S
[formatter_accesslog]
format = %(message)s

0
hsman/app/models.py Normal file
View File

77
hsman/app/static/main.css Normal file
View File

@ -0,0 +1,77 @@
/* custom css */
html {
position: relative;
min-height: 100%;
font-size: 90%;
}
body.bootstrap,
body.bootstra-dark {
width: 100%;
height: 100%;
/* font-family: century-gothic, sans-serif; */
font-family: 'Quicksand', sans-serif;
min-height: 100%;
/* margin: 0 0 60px; */
margin-bottom: 60px;
}
body > .container,
body > .container-fluid {
margin-top: 30px;
margin-bottom: 30px;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
/* Set the fixed height of the footer here */
height: 30px;
margin-top: 0 auto;
/* Vertically center the text there */
line-height: 10px;
padding-left: 1rem;
padding-right: 1rem;
}
.theme-icon {
width: 1.3em;
height: 1.3em;
margin-right: 10px;
}
a.plain:link {
color: unset;
text-decoration-line: underline;
text-decoration-style: dotted;
}
a.plain:visited {
color: unset;
text-decoration-line: underline;
text-decoration-style: dotted;
}
a.plain:hover {
color: unset;
text-decoration: underline;
}
a.plain:active {
color: unset;
text-decoration: none;
}
table.dataTable tbody tr:hover {
background-color: #444;
}
table.dataTable tbody tr:hover > .sorting_1 {
background-color: #444;
}
.data:hover{
/* background-color: rgba(63, 140, 211, 0.808); */
background-color: #444;
}

55
hsman/app/static/main.js Normal file
View File

@ -0,0 +1,55 @@
// custom javascript
var sun_icon = `
<div class="theme-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="theme-icon" fill="none" viewBox="0 0 24 24" stroke="yellow" stroke-opacity="0.6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</div>
`;
var moon_icon = `
<div class="theme-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="theme-icon" fill="none" viewBox="0 0 18 24" stroke="grey" stroke-opacity="0.6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</div>
`;
function getCookie(cookie) {
let name = cookie + "=";
let ca = document.cookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == " ") {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function setCookie(cname, cvalue) {
const d = new Date();
d.setTime(d.getTime() + 3650 * 24 * 60 * 60 * 1000); // 10 years cookie
let expires = "expires=" + d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}
function toggleTheme(obj) {
console.log(getCookie("theme"));
console.log(document.cookie);
if (getCookie("theme") == "light") {
setCookie("theme", "dark");
obj.html(sun_icon);
$("body").addClass("bootstrap-dark");
} else {
// switch to light mode
setCookie("theme", "light");
obj.html(moon_icon);
$("body").removeClass("bootstrap-dark");
}
}

View File

@ -0,0 +1,24 @@
<!-- Put your reusable template code into macros here -->
{% macro theme_icon(theme) %}
<div class="theme-icon">
{% if theme == "dark" %}
<svg xmlns="http://www.w3.org/2000/svg" class="theme-icon" fill="none" viewBox="0 0 24 24" stroke="yellow"
stroke-opacity="0.6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
{% else %}
<svg xmlns="http://www.w3.org/2000/svg" class="theme-icon" fill="none" viewBox="0 0 18 24" stroke="grey"
stroke-opacity="0.6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
{% endif %}
</div>
{% endmacro %}
{% macro body_theme(theme) -%}
{% if theme == "dark" %}
bootstrap-dark
{% endif %}
{%- endmacro -%}

View File

@ -0,0 +1,118 @@
{% import '_macros.html.j2' as mc -%}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ config.APP_NAME }}</title>
<!-- meta -->
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
{% block meta %}{% endblock %}
<!-- styles -->
<!-- <link rel="stylesheet" href="{{ url_for('static', filename='bootstrap.min.css') }}" -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@forevolve/bootstrap-dark@1.0.0/dist/css/toggle-bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@forevolve/bootstrap-dark@1.0.0/dist/css/toggle-bootstrap-dark.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@forevolve/bootstrap-dark@1.0.0/dist/css/toggle-bootstrap-print.min.css" />
<!-- Quicksand font -->
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Century gothic font -->
<link rel="stylesheet" href="https://use.typekit.net/oov2wcw.css">
<link href="{{ url_for('static', filename='main.css') }}" rel="stylesheet" media="screen">
{% block links %}{% endblock %}
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="192x192" href="/static/favicon/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/static/favicon/android-chrome-512x512.png">
<link rel="manifest" href="/static/favicon/site.webmanifest">
</head>
<!-- <body class="bootstrap {{ mc.body_theme(g.theme) }}"> -->
<body class="bootstrap bootstrap-dark">
<!-- Header -->
<header>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-themed">
<!-- Navbar Brand -->
<a class="navbar-brand" href="{{ url_for('main.index') }}">
{{ config.APP_NAME }}
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/nodes">nodes</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/users">users</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/routes">routes</a>
</li>
</ul>
<!-- <ul class="navbar-nav">
<li class="nav-item me-right">
<a href="#" id="themeSwitch">
<div class="theme-icon">{{ mc.theme_icon(theme)}}</div>
</a>
</li>
</ul> -->
</div>
</nav>
</header>
{% if g.is_mobile %}
<div class="container-fluid">
{% else %}
<div class="container">
{% endif %}
<!-- Main Content -->
{% block content %}{% endblock %}
</div>
<!-- Footer-->
<footer class="footer text-center">
<div class="container">
<div class="row">
<!-- Copyrights -->
<div class="col-lg-12 text-center">
<p class="text-muted mb-0 py-2">
Headscale Manager |
ver. {{ config.APP_VERSION }} ({{ config.APP_SHA }})
</p>
</div>
</div>
</footer>
<!-- scripts -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
<script src="{{ url_for('static', filename='main.js') }}" type="text/javascript"></script>
<script>
$(function () {
// themeSwitch = $('#themeSwitch');
// themeSwitch.click(function(event) {
// event.preventDefault();
// toggleTheme(themeSwitch);
// })
$('[data-toggle="tooltip"]').tooltip();
})
</script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<div class="jumbotron my-4">
<div class="text-center">
<h1>{{ '%s - %s' % (error.code, error.name) }}</h1>
<p>{{ error.description }}.</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block content %}
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Egestas tellus rutrum tellus pellentesque eu tincidunt. Aenean et tortor at risus viverra adipiscing. Et malesuada fames ac turpis egestas sed tempus. Amet commodo nulla facilisi nullam vehicula ipsum a arcu. Quam viverra orci sagittis eu volutpat odio. Elit ut aliquam purus sit amet luctus. Vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare massa. Ultrices gravida dictum fusce ut placerat orci nulla. Sed faucibus turpis in eu mi bibendum. Vel facilisis volutpat est velit egestas dui id ornare arcu. Scelerisque eu ultrices vitae auctor. Sem nulla pharetra diam sit amet. Lectus proin nibh nisl condimentum id venenatis a condimentum vitae.
</p>
<p></p>
<hr>
<p>
Auctor elit sed vulputate mi sit amet mauris. Tincidunt eget nullam non nisi est. Leo vel orci porta non pulvinar neque laoreet suspendisse interdum. Volutpat commodo sed egestas egestas fringilla phasellus faucibus scelerisque. Ipsum dolor sit amet consectetur adipiscing elit pellentesque habitant morbi. Ultricies mi quis hendrerit dolor magna. Porta nibh venenatis cras sed felis eget. Quam vulputate dignissim suspendisse in. Pellentesque dignissim enim sit amet venenatis urna cursus eget nunc. Nunc lobortis mattis aliquam faucibus purus in.
</p>
{% endblock %}

View File

@ -0,0 +1,168 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col col-2 float-left">
<strong>name</strong>
</div>
<div class="col col-8 float-left">
{{ node.givenName }}
</div>
</div>
<div class="row">
<div class="col col-2 float-left">
<strong>registered</strong>
</div>
<div class="col col-8 float-left">
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
{{ node.createdAt | htime_dt }}
</span>
</div>
</div>
<div class="row">
<div class="col col-2 float-left">
<strong>expiry</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>
</div>
</div>
<div class="row">
<div class="col col-2 float-left">
<strong>user</strong>
</div>
<div class="col col-8 float-left">
<a href='{{ url_for("main.user", userName=node.user.name) }}' class="plain">{{ node.user.name }}</a>
</div>
</div>
<p></p>
<div class="row">
<div class="col col-3 float-left">
<h5>addresses</h5>
</div>
</div>
{% for ip in node.ipAddresses %}
<div class="row data">
<div class="col col-3 float-left">
{{ ip }}
</div>
</div>
{% endfor %}
<p></p>
<div class="row">
<div class="col col-3 float-left">
<h5>tags</h5>
</div>
</div>
<div class="row">
<div class="col col-2 float-left">
<strong>
announced
</strong>
</div>
<div class="col col-6 float-left">
{% if node.validTags %}
{% for tag in node.validTags %}
<span class="badge badge-pill badge-success">
{{ tag }}
</span>
{% endfor %}
{% else %}
None
{% endif %}
</div>
</div>
<div class="row">
<div class="col col-2 float-left">
<strong>forced</strong>
</div>
<div class="col col-6 float-left">
{% if node.forced %}
{% for tag in node.forcedTags %}
<h3><span class="badge badge-pill badge-info">
{{ tag }}
</span></h3>
{% endfor %}
{% else %}
None
{% endif %}
</div>
</div>
<p></p>
<div class="row">
<div class="col col-3 float-left">
<h5>keys</h5>
</div>
</div>
<div class="row">
<div class="col col-2 float-left">
<strong>machineKey</strong>
</div>
<div class="col col-8 float-left">
<code>{{ node.machineKey }}</code>
</div>
</div>
<div class="row">
<div class="col col-2 float-left">
<strong>nodeKey</strong>
</div>
<div class="col col-8 float-left">
<code>{{ node.nodeKey }}</code>
</div>
</div>
<div class="row">
<div class="col col-2 float-left">
<strong>discoKey</strong>
</div>
<div class="col col-8 float-left">
<code>{{ node.discoKey }}</code>
</div>
</div>
<p></p>
<div class="row">
<div class="col col-3 float-left">
<h5>
routes
{% if isExitNode %}
<span class="small badge-pill badge-success">Exit Node</span>
{% endif %}
</h5>
</div>
</div>
<div class="row">
<div class="col col-3 float-left">
<strong>prefix</strong>
</div>
<div class="col col-3 float-left">
<strong>enabled</strong>
</div>
<div class="col col-3 float-left">
<strong>primary</strong>
</div>
</div>
{% for route in routes | sort(attribute='prefix') %}
<div class="row data">
<div class="col col-3 float-left">
{{ route.prefix }}
</div>
<div class="col col-3 float-left">
{{ route.enabled | fancyBool | safe }}
</div>
<div class="col col-3 float-left">
{{ route.isPrimary | fancyBool | safe }}
</div>
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block links %}
<!-- Datatables -->
<link href="https://cdn.datatables.net/v/bs4/dt-2.0.8/b-3.0.2/b-colvis-3.0.2/b-html5-3.0.2/cr-2.0.3/sr-1.4.1/datatables.min.css" rel="stylesheet">
{% endblock %}
{% block content %}
<table id="nodes" class="display" style="width:100%">
<thead>
<tr>
<th>name</th>
<th>user</th>
<th>registered on</th>
<th>last connect</th>
<th>online</th>
</tr>
</thead>
<tbody>
{% for node in nodes %}
<tr>
<td>
<a class="plain" href="{{ url_for('main.node', nodeId=node.id)}}">
{{node.givenName}}
</a>
</td>
<td>{{node.user.name}}</td>
<td data-order="{{ node.createdAt }}">
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
{{node.createdAt | htime_dt }}
</span>
</td>
<td data-order="{{ node.lastSeen | fmt_datetime }}">
<span data-toggle="tooltip" data-placement="right" title="{{ node.lastSeen | fmt_datetime }}">
{{node.lastSeen | htime_dt }}
</span>
</td>
<td data-filter="{{ node.online | fancyOnline }}">
{{node.online | fancyBool | safe}}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block scripts %}
<script src="https://cdn.datatables.net/v/bs4/dt-2.0.8/b-3.0.2/b-colvis-3.0.2/b-html5-3.0.2/cr-2.0.3/sr-1.4.1/datatables.min.js"></script>
<script>
$(function () {
new DataTable('#nodes', {
paging: true,
lengthMenu: [15, 30, 50, 100, { label: 'All', value: -1 }],
pageLength: 30,
fixedHeader: false,
select: false,
keys: false,
});
})
</script>
{% endblock %}

View File

@ -0,0 +1,96 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col col-8">
<h5>Exit nodes</h5>
</div>
</div>
{% for exitNode in exitNodes %}
<div class="row data">
<div class="col col-2">
<span data-toggle="tooltip" data-placement="right" title="{{ exitNode.ipAddresses | join('\n') }}">
&nbsp; {{ exitNode.givenName }}
</span>
</div>
</div>
{% endfor %}
<p></p>
{% for prefix, rts in routes.items() %}
<div class="row">
<div class="col col-2 float-left">
<strong>prefix</strong>
</div>
<div class="col col-2 float-left">
<strong>gateway</strong>
</div>
<div class="col col-2 float-left">
<strong>enabled</strong>
</div>
<div class="col col-2 float-left">
<strong>primary</strong>
</div>
</div>
<div class="row data">
<div class="col col-2 float-left">
{{ prefix}}
</div>
<div class="col col-2 float-left">
<span>
<strong>
<a class="plain" href="{{ url_for('main.node', nodeId=rts[0].node.id) }}">
<span data-toggle="tooltip" data-placement="right"
title="{{ rts[0].node.ipAddresses | join('\n') }}">
{{ rts[0].node.givenName}}
</span>
</a>
</strong>
</span>
</div>
<div class="col col-2 float-left">
{{ rts[0].isPrimary | fancyBool | safe}}
</div>
<div class="col col-2 float-left">
{{ rts[0].enabled | fancyBool | safe}}
</div>
</div>
{% for rt in rts[1:] %}
<div class="row data">
<div class="col col-2 float-left" style="border: 1px red;">
<span>&nbsp;</span>
</div>
<div class="col col-2">
{% if not rt.enabled %}
<s>
<a class="plain" href="{{ url_for('main.node', nodeId=rt.node.id) }}">
<span data-toggle="tooltip" data-placement="right"
title="{{ rt.node.ipAddresses | join('\n') }}">
{{ rt.node.givenName}}
</span>
</a>
</s>
{% else %}
<em>
<a class="plain" href="{{ url_for('main.node', nodeId=rt.node.id) }}">
<span data-toggle="tooltip" data-placement="right"
title="{{ rt.node.ipAddresses | join('\n') }}">
{{ rt.node.givenName}}
</a>
</em>
{% endif %}
</div>
<div class="col col-2 float-left">
{{ rt.enabled | fancyBool | safe}}
</div>
<div class="col col-2 float-left">
{{ rt.isPrimary | fancyBool | safe}}
</div>
</div>
{% endfor %}
<p></p>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col col-2 float-left">
<strong>name</strong>
</div>
<div class="col col-8 float-left">
{{ user.name }}
</div>
</div>
<div class="row">
<div class="col col-2 float-left">
<strong>registered</strong>
</div>
<div class="col col-8 float-left">
<span data-toggle="tooltip" data-placement="right" title="{{ user.createdAt | fmt_datetime }}">
{{ user.createdAt | htime_dt }}
</span>
</div>
</div>
<p></p>
<div class="row">
<div class="col col-3 float-left">
<h5>nodes</h5>
</div>
</div>
<div class="row">
<div class="col col-3 float-left">
<strong>name</strong>
</div>
<div class="col col-3">
<strong>last connect</strong>
</div>
<div class="col col-3">
<strong>online</strong>
</div>
</div>
{% for node in userNodeList %}
<div class="row data">
<div class="col col-3 float-left">
<a href="{{ url_for('main.node', nodeId=node.id) }}" class="plain">
{{ node.givenName }}
</a>
</div>
<div class="col col-3">
<span data-toggle="tooltip" data-placement="right" title="{{ node.lastSeen | fmt_datetime }}">
{{node.lastSeen | htime_dt }}
</span>
</div>
<div class="col col-3">
{{node.online | fancyBool | safe}}
</div>
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block links %}
<!-- Datatables -->
<link href="https://cdn.datatables.net/v/bs4/dt-2.0.8/b-3.0.2/b-colvis-3.0.2/b-html5-3.0.2/cr-2.0.3/sr-1.4.1/datatables.min.css" rel="stylesheet">
{% endblock %}
{% block content %}
<table id="users" class="display" style="width:100%">
<thead>
<tr>
<th>name</th>
<th>registered on</th>
<th>online</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>
<a class="plain" href="{{ url_for('main.user', userName=user.name)}}">
{{user.name}}
</a>
</td>
<td data-order="{{ user.createdAt }}">
<span data-toggle="tooltip" data-placement="right" title="{{ user.createdAt | fmt_datetime }}">
{{user.createdAt | htime_dt }}
</span>
</td>
<td data-filter="{{ online[user.name] | fancyOnline }}">
{{online[user.name] | fancyBool | safe }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block scripts %}
<script src="https://cdn.datatables.net/v/bs4/dt-2.0.8/b-3.0.2/b-colvis-3.0.2/b-html5-3.0.2/cr-2.0.3/sr-1.4.1/datatables.min.js"></script>
<script>
$(function () {
new DataTable('#users', {
paging: true,
lengthMenu: [15, 30, 50, 100, { label: 'All', value: -1 }],
pageLength: 30,
fixedHeader: false,
select: false,
keys: false,
});
})
</script>
{% endblock %}

111
hsman/app/views.py Normal file
View File

@ -0,0 +1,111 @@
from flask import render_template, Blueprint, current_app, g
from flask import request, after_this_request, redirect, session
from app import auth
from flask import jsonify
from flask_pyoidc.user_session import UserSession
from hsapi import Node, User, Route
import logging
log = logging.getLogger()
main_blueprint = Blueprint('main', __name__)
# @main_blueprint.before_request
# def set_theme():
# g.theme = request.cookies.get('theme', 'light')
@main_blueprint.route('/', methods=['GET', 'POST'])
@auth.oidc_auth('default')
def index():
user_session = UserSession(session)
return jsonify(access_token=user_session.access_token,
id_token=user_session.id_token,
userinfo=user_session.userinfo)
@main_blueprint.route('/logout')
@auth.oidc_logout
def logout():
return redirect('/')
@main_blueprint.route('/nodes', methods=['GET'])
def nodes():
nodelist = Node().list()
log.debug(nodelist)
return render_template('nodes.html',
nodes=nodelist.nodes)
@main_blueprint.route('/node/<int:nodeId>', methods=['GET'])
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
# v1Node object instead of v1NodeResponse, so we access directly
# `node`, instead of `node.node`
node = Node().get(nodeId)
routes = Node().routes(nodeId)
isExitNode = any((r for r in routes.routes if r.prefix.endswith('/0')))
log.debug(node)
return render_template("node.html",
routes=routes.routes,
isExitNode=isExitNode,
node=node.node)
@main_blueprint.route('/users', methods=['GET'])
def users():
userList = User().list()
# Get online status of devices of the user
online = {}
nodeList = Node().list()
for user in userList.users:
log.debug(user)
userNodeList = [n for n in nodeList.nodes if n.user.name == user.name]
log.debug(userNodeList)
online[user.name] = any(map(lambda x: x.online, userNodeList))
return render_template('users.html',
users=userList.users,
online=online)
@main_blueprint.route('/user/<userName>', methods=['GET'])
def user(userName):
# There is a bug in HS api with retrieving a single node
# and we added a workaround to hsapi, so node.get() returns a
# v1Node object instead of v1NodeResponse, so we access directly
# `node`, instead of `node.node`
user = User().get(userName)
log.debug(user)
userNodeList = [n for n in Node().list().nodes if n.user.name == userName]
return render_template("user.html",
user=user.user,
userNodeList=userNodeList)
@main_blueprint.route('/routes', methods=['GET'])
def routes():
routes = Route().list()
log.debug(routes)
prefixes = set(
(r.prefix for r in routes.routes if not r.prefix.endswith('/0')))
exitNodes = [r.node for r in routes.routes if r.prefix.endswith(('0/0'))]
final = {}
for prefix in prefixes:
rrp = [x for x in routes.routes if x.prefix == prefix]
final[prefix] = sorted(rrp, key=lambda x: x.isPrimary, reverse=True)
log.debug(final)
return render_template("routes.html",
exitNodes=exitNodes,
routes=final)

47
hsman/config.py Normal file
View File

@ -0,0 +1,47 @@
import os
import importlib.metadata
base_dir = os.path.dirname(os.path.abspath(__file__))
class BaseConfig(object):
"""Base configuration."""
APP_NAME = "HSMAN"
APP_VERSION = os.getenv('APP_VERSION', "0.0alpha1")
APP_SHA = os.getenv('APP_SHA', "000000")
DEBUG_TB_ENABLED = False
WTF_CSRF_ENABLED = False
@staticmethod
def configure(app):
# Implement this method to do further configuration on your app.
pass
class DevelopmentConfig(BaseConfig):
"""Development configuration."""
DEBUG = True
ENVIRONMENT = "develop"
class TestingConfig(BaseConfig):
"""Testing configuration."""
TESTING = True
PRESERVE_CONTEXT_ON_EXCEPTION = False
ENVIRONMENT = "test"
class ProductionConfig(BaseConfig):
"""Production configuration."""
ENVIRONMENT = "production"
WTF_CSRF_ENABLED = True
config = dict(
development=DevelopmentConfig,
testing=TestingConfig,
production=ProductionConfig)

300
hsman/poetry.lock generated
View File

@ -11,6 +11,24 @@ files = [
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "asttokens"
version = "2.4.1"
description = "Annotate AST trees with source code positions"
optional = false
python-versions = "*"
files = [
{file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"},
{file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"},
]
[package.dependencies]
six = ">=1.12.0"
[package.extras]
astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"]
test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"]
[[package]]
name = "blinker"
version = "1.8.2"
@ -275,6 +293,17 @@ ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
[[package]]
name = "decorator"
version = "5.1.1"
description = "Decorators for Humans"
optional = false
python-versions = ">=3.5"
files = [
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
]
[[package]]
name = "defusedxml"
version = "0.7.1"
@ -286,6 +315,20 @@ files = [
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
[[package]]
name = "executing"
version = "2.0.1"
description = "Get the currently executing AST node of a frame, and other information"
optional = false
python-versions = ">=3.5"
files = [
{file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"},
{file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"},
]
[package.extras]
tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
[[package]]
name = "flask"
version = "3.0.3"
@ -308,6 +351,35 @@ Werkzeug = ">=3.0.0"
async = ["asgiref (>=3.2)"]
dotenv = ["python-dotenv"]
[[package]]
name = "flask-mobility"
version = "2.0.1"
description = "A Flask extension to simplify building mobile-friendly sites."
optional = false
python-versions = "*"
files = [
{file = "Flask-Mobility-2.0.1.tar.gz", hash = "sha256:d05835e2e92f467ce2ddaed993bda21cf4ca54f73f39c574fad900f825715574"},
{file = "Flask_Mobility-2.0.1-py3-none-any.whl", hash = "sha256:e2154f2830eb8c1b3d069b67f65ebb7a0216506f08a52f58cde80fedc7763175"},
]
[package.dependencies]
Flask = "*"
[[package]]
name = "flask-pydantic"
version = "0.12.0"
description = "Flask extension for integration with Pydantic library"
optional = false
python-versions = ">=3.7"
files = [
{file = "Flask-Pydantic-0.12.0.tar.gz", hash = "sha256:b80f18cc7efe332b47eafbae141541e9875777132ba30d7c95f9fef9bf2a6977"},
{file = "Flask_Pydantic-0.12.0-py3-none-any.whl", hash = "sha256:c80825d74eefa137d8d7f2396f6b376ee128329196d79eff1442bcfd816a6228"},
]
[package.dependencies]
Flask = "*"
pydantic = ">=2.0"
[[package]]
name = "flask-pyoidc"
version = "3.14.3"
@ -325,6 +397,21 @@ importlib-resources = "*"
oic = "1.6.1"
requests = "*"
[[package]]
name = "flask-shell-ipython"
version = "0.5.1"
description = "Replace default `flask shell` command by similar command running IPython."
optional = false
python-versions = ">=3.6, <4"
files = [
{file = "flask_shell_ipython-0.5.1-py2.py3-none-any.whl", hash = "sha256:8c948c0721bcc5b8eb274e1831c8a428dfc693099609c33d2f330091782cce10"},
]
[package.dependencies]
click = "*"
Flask = ">=1.0"
IPython = ">=5.0.0"
[[package]]
name = "future"
version = "1.0.0"
@ -376,6 +463,20 @@ requests = ">=2.32.3,<3.0.0"
type = "file"
url = "../hsapi/dist/hsapi-0.9.0-py3-none-any.whl"
[[package]]
name = "humanize"
version = "4.9.0"
description = "Python humanize utilities"
optional = false
python-versions = ">=3.8"
files = [
{file = "humanize-4.9.0-py3-none-any.whl", hash = "sha256:ce284a76d5b1377fd8836733b983bfb0b76f1aa1c090de2566fcf008d7f6ab16"},
{file = "humanize-4.9.0.tar.gz", hash = "sha256:582a265c931c683a7e9b8ed9559089dea7edcf6cc95be39a3cbc2c5d5ac2bcfa"},
]
[package.extras]
tests = ["freezegun", "pytest", "pytest-cov"]
[[package]]
name = "idna"
version = "3.7"
@ -402,6 +503,43 @@ files = [
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"]
[[package]]
name = "ipython"
version = "8.26.0"
description = "IPython: Productive Interactive Computing"
optional = false
python-versions = ">=3.10"
files = [
{file = "ipython-8.26.0-py3-none-any.whl", hash = "sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff"},
{file = "ipython-8.26.0.tar.gz", hash = "sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
decorator = "*"
jedi = ">=0.16"
matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""}
prompt-toolkit = ">=3.0.41,<3.1.0"
pygments = ">=2.4.0"
stack-data = "*"
traitlets = ">=5.13.0"
typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""}
[package.extras]
all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"]
black = ["black"]
doc = ["docrepr", "exceptiongroup", "intersphinx-registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing-extensions"]
kernel = ["ipykernel"]
matplotlib = ["matplotlib"]
nbconvert = ["nbconvert"]
nbformat = ["nbformat"]
notebook = ["ipywidgets", "notebook"]
parallel = ["ipyparallel"]
qtconsole = ["qtconsole"]
test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"]
test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"]
[[package]]
name = "itsdangerous"
version = "2.2.0"
@ -413,6 +551,25 @@ files = [
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
]
[[package]]
name = "jedi"
version = "0.19.1"
description = "An autocompletion tool for Python that can be used for text editors."
optional = false
python-versions = ">=3.6"
files = [
{file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"},
{file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"},
]
[package.dependencies]
parso = ">=0.8.3,<0.9.0"
[package.extras]
docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
[[package]]
name = "jinja2"
version = "3.1.4"
@ -518,6 +675,20 @@ files = [
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
]
[[package]]
name = "matplotlib-inline"
version = "0.1.7"
description = "Inline Matplotlib backend for Jupyter"
optional = false
python-versions = ">=3.8"
files = [
{file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"},
{file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
]
[package.dependencies]
traitlets = "*"
[[package]]
name = "oic"
version = "1.6.1"
@ -558,6 +729,74 @@ files = [
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "parso"
version = "0.8.4"
description = "A Python Parser"
optional = false
python-versions = ">=3.6"
files = [
{file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"},
{file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"},
]
[package.extras]
qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
testing = ["docopt", "pytest"]
[[package]]
name = "pexpect"
version = "4.9.0"
description = "Pexpect allows easy control of interactive console applications."
optional = false
python-versions = "*"
files = [
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
{file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
]
[package.dependencies]
ptyprocess = ">=0.5"
[[package]]
name = "prompt-toolkit"
version = "3.0.47"
description = "Library for building powerful interactive command lines in Python"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"},
{file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"},
]
[package.dependencies]
wcwidth = "*"
[[package]]
name = "ptyprocess"
version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
optional = false
python-versions = "*"
files = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
]
[[package]]
name = "pure-eval"
version = "0.2.2"
description = "Safely evaluate AST nodes without side effects"
optional = false
python-versions = "*"
files = [
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
{file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
]
[package.extras]
tests = ["pytest"]
[[package]]
name = "pycparser"
version = "2.22"
@ -739,6 +978,20 @@ python-dotenv = ">=0.21.0"
toml = ["tomli (>=2.0.1)"]
yaml = ["pyyaml (>=6.0.1)"]
[[package]]
name = "pygments"
version = "2.18.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
files = [
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyjwkest"
version = "1.4.2"
@ -801,6 +1054,40 @@ files = [
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "stack-data"
version = "0.6.3"
description = "Extract data from python stack frames and tracebacks for informative displays"
optional = false
python-versions = "*"
files = [
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
]
[package.dependencies]
asttokens = ">=2.1.0"
executing = ">=1.2.0"
pure-eval = "*"
[package.extras]
tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
[[package]]
name = "traitlets"
version = "5.14.3"
description = "Traitlets Python configuration system"
optional = false
python-versions = ">=3.8"
files = [
{file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"},
{file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
]
[package.extras]
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
[[package]]
name = "typing-extensions"
version = "4.12.2"
@ -829,6 +1116,17 @@ h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "wcwidth"
version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[[package]]
name = "werkzeug"
version = "3.0.3"
@ -849,4 +1147,4 @@ watchdog = ["watchdog (>=2.3)"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.11,<4.0"
content-hash = "be026a1506b86db9bcdd3d965e4383396fcd074370a026dd80bc89f186e0ecdc"
content-hash = "57399db8700fb5bbcdee00cb67fb178f1e7d5bd0b0ef700225ef192bea3a58b6"

View File

@ -12,8 +12,15 @@ Flask = "^3.0.3"
Flask-pyoidc = "^3.14.3"
gunicorn = "^22.0.0"
hsapi = {path = "../hsapi/dist/hsapi-0.9.0-py3-none-any.whl"}
flask-mobility = "^2.0.1"
humanize = "^4.9.0"
flask-pydantic = "^0.12.0"
[tool.poetry.group.dev.dependencies]
flask-shell-ipython = "^0.5.1"
python-dotenv = "^1.0.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

29
hsman/wsgi.py Normal file
View File

@ -0,0 +1,29 @@
from app import create_app
from app import lib
from app import models
import logging
import logging.config
import os
logconffile = os.path.join('app/logging', '%s.ini' %
os.environ.get('FLASK_ENV', 'development'))
logging.config.fileConfig(logconffile, disable_existing_loggers=True)
log = logging.getLogger(__name__)
app = create_app()
log.debug(f"Running in web mode: {lib.webMode()}")
@app.shell_context_processor
def get_context():
# flask cli context setup
"""Objects exposed here will be automatically available from the shell."""
return dict(app=app, models=models)
if __name__ == '__main__':
log.info("direct run")
app.run()