Lot of work and it works
This commit is contained in:
parent
4e84fc5697
commit
c94f31d5ee
|
@ -45,9 +45,25 @@ class Node(HSAPICall):
|
||||||
response = self.call('get')
|
response = self.call('get')
|
||||||
return v1ListNodesResponse(**response.json())
|
return v1ListNodesResponse(**response.json())
|
||||||
|
|
||||||
def get(self, nodeId: str) -> v1NodeResponse:
|
def get(self, nodeId: str) -> v1Node:
|
||||||
response = self.call('get', call_path=nodeId)
|
# There is a bug in headscale API
|
||||||
return v1NodeResponse(**response.json())
|
# 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:
|
def delete(self, nodeId: str) -> None:
|
||||||
self.call('delete', call_path=nodeId)
|
self.call('delete', call_path=nodeId)
|
||||||
|
|
|
@ -327,4 +327,4 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "d6cbad8cd64ba63c62d1f3ab2dde75682d6a42c5b40808fd35615bccdf5fdd07"
|
content-hash = "28f675747b0ee9850925befb61509ac513e0acc429d7efc1abe2a0fea6d8f97d"
|
||||||
|
|
|
@ -13,6 +13,9 @@ pydantic = "^2.7.4"
|
||||||
pydantic-settings = "^2.3.4"
|
pydantic-settings = "^2.3.4"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
python-dotenv = "^1.0.1"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
headscale
|
|
|
@ -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
|
|
@ -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">✔</span>'
|
||||||
|
else:
|
||||||
|
return '<span style="color: red">✘</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
|
|
@ -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
|
|
@ -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 = [0;1m%(asctime)s,%(msecs)03d [1;31m%(levelname)-7.7s [1;34m[%(name)s/%(filename)s:%(lineno)d]: [0m%(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
|
|
||||||
|
[formatter_accesslog]
|
||||||
|
format = %(name)s %(message)s
|
|
@ -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 = [0;1m%(asctime)s,%(msecs)03d [1;31m%(levelname)-7.7s [1;34m[%(name)s/%(filename)s:%(lineno)d]: [0m%(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
|
|
||||||
|
[formatter_accesslog]
|
||||||
|
format = %(message)s
|
|
@ -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;
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 -%}
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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') }}">
|
||||||
|
{{ 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> </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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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)
|
|
@ -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)
|
|
@ -11,6 +11,24 @@ files = [
|
||||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
{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]]
|
[[package]]
|
||||||
name = "blinker"
|
name = "blinker"
|
||||||
version = "1.8.2"
|
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 = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||||
test-randomorder = ["pytest-randomly"]
|
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]]
|
[[package]]
|
||||||
name = "defusedxml"
|
name = "defusedxml"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -286,6 +315,20 @@ files = [
|
||||||
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
|
{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]]
|
[[package]]
|
||||||
name = "flask"
|
name = "flask"
|
||||||
version = "3.0.3"
|
version = "3.0.3"
|
||||||
|
@ -308,6 +351,35 @@ Werkzeug = ">=3.0.0"
|
||||||
async = ["asgiref (>=3.2)"]
|
async = ["asgiref (>=3.2)"]
|
||||||
dotenv = ["python-dotenv"]
|
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]]
|
[[package]]
|
||||||
name = "flask-pyoidc"
|
name = "flask-pyoidc"
|
||||||
version = "3.14.3"
|
version = "3.14.3"
|
||||||
|
@ -325,6 +397,21 @@ importlib-resources = "*"
|
||||||
oic = "1.6.1"
|
oic = "1.6.1"
|
||||||
requests = "*"
|
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]]
|
[[package]]
|
||||||
name = "future"
|
name = "future"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -376,6 +463,20 @@ requests = ">=2.32.3,<3.0.0"
|
||||||
type = "file"
|
type = "file"
|
||||||
url = "../hsapi/dist/hsapi-0.9.0-py3-none-any.whl"
|
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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.7"
|
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"]
|
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)"]
|
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]]
|
[[package]]
|
||||||
name = "itsdangerous"
|
name = "itsdangerous"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
@ -413,6 +551,25 @@ files = [
|
||||||
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
|
{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]]
|
[[package]]
|
||||||
name = "jinja2"
|
name = "jinja2"
|
||||||
version = "3.1.4"
|
version = "3.1.4"
|
||||||
|
@ -518,6 +675,20 @@ files = [
|
||||||
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
|
{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]]
|
[[package]]
|
||||||
name = "oic"
|
name = "oic"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
|
@ -558,6 +729,74 @@ files = [
|
||||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
{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]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "2.22"
|
version = "2.22"
|
||||||
|
@ -739,6 +978,20 @@ python-dotenv = ">=0.21.0"
|
||||||
toml = ["tomli (>=2.0.1)"]
|
toml = ["tomli (>=2.0.1)"]
|
||||||
yaml = ["pyyaml (>=6.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]]
|
[[package]]
|
||||||
name = "pyjwkest"
|
name = "pyjwkest"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
|
@ -801,6 +1054,40 @@ files = [
|
||||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
{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]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.12.2"
|
version = "4.12.2"
|
||||||
|
@ -829,6 +1116,17 @@ h2 = ["h2 (>=4,<5)"]
|
||||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
zstd = ["zstandard (>=0.18.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]]
|
[[package]]
|
||||||
name = "werkzeug"
|
name = "werkzeug"
|
||||||
version = "3.0.3"
|
version = "3.0.3"
|
||||||
|
@ -849,4 +1147,4 @@ watchdog = ["watchdog (>=2.3)"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.11,<4.0"
|
python-versions = ">=3.11,<4.0"
|
||||||
content-hash = "be026a1506b86db9bcdd3d965e4383396fcd074370a026dd80bc89f186e0ecdc"
|
content-hash = "57399db8700fb5bbcdee00cb67fb178f1e7d5bd0b0ef700225ef192bea3a58b6"
|
||||||
|
|
|
@ -12,8 +12,15 @@ Flask = "^3.0.3"
|
||||||
Flask-pyoidc = "^3.14.3"
|
Flask-pyoidc = "^3.14.3"
|
||||||
gunicorn = "^22.0.0"
|
gunicorn = "^22.0.0"
|
||||||
hsapi = {path = "../hsapi/dist/hsapi-0.9.0-py3-none-any.whl"}
|
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]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue