Local assets and split routes
|
@ -1,2 +1,6 @@
|
||||||
- improve configuration
|
- improve configuration
|
||||||
- improve APP_PREFIX
|
- improve APP_PREFIX
|
||||||
|
- edit bootstrap CSS to fix fonts and colors
|
||||||
|
- try to use a datatable for routes, with grouping
|
||||||
|
- more tooltips, for hosts, showing IP addresses (?)
|
||||||
|
- move to github and set up pipeline
|
||||||
|
|
|
@ -31,7 +31,7 @@ auth = OIDCAuthentication({'default': provider_config})
|
||||||
def create_app(environment='development'):
|
def create_app(environment='development'):
|
||||||
|
|
||||||
from config import config
|
from config import config
|
||||||
from .views import main_blueprint
|
from .views import main_blueprint, rest_blueprint
|
||||||
|
|
||||||
# BRUTTO BRUTTO
|
# BRUTTO BRUTTO
|
||||||
app_prefix = os.getenv('APP_PREFIX', '')
|
app_prefix = os.getenv('APP_PREFIX', '')
|
||||||
|
@ -56,6 +56,10 @@ def create_app(environment='development'):
|
||||||
main_blueprint.url_prefix}'")
|
main_blueprint.url_prefix}'")
|
||||||
app.register_blueprint(main_blueprint)
|
app.register_blueprint(main_blueprint)
|
||||||
|
|
||||||
|
app.logger.info(f"registering rest blueprint with prefix '{
|
||||||
|
rest_blueprint.url_prefix}'")
|
||||||
|
app.register_blueprint(rest_blueprint)
|
||||||
|
|
||||||
app.logger.info("jinja2 custom filters loaded")
|
app.logger.info("jinja2 custom filters loaded")
|
||||||
filters.init_app(app)
|
filters.init_app(app)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}
|
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 470 B |
After Width: | Height: | Size: 871 B |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name":"Headscale Manager",
|
||||||
|
"short_name":"hsman",
|
||||||
|
"icons":[
|
||||||
|
{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},
|
||||||
|
{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],
|
||||||
|
"theme_color":"#000000",
|
||||||
|
"background_color":"#000000",
|
||||||
|
"display":"standalone"
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* The Typekit service used to deliver this font or fonts for use on websites
|
||||||
|
* is provided by Adobe and is subject to these Terms of Use
|
||||||
|
* http://www.adobe.com/products/eulas/tou_typekit. For font license
|
||||||
|
* information, see the list below.
|
||||||
|
*
|
||||||
|
* century-gothic:
|
||||||
|
* - http://typekit.com/eulas/00000000000000003b9b1f23
|
||||||
|
*
|
||||||
|
* © 2009-2024 Adobe Systems Incorporated. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
/*{"last_published":"2021-09-06 05:08:57 UTC"}*/
|
||||||
|
|
||||||
|
@import url("https://p.typekit.net/p.css?s=1&k=oov2wcw&ht=tk&f=39203&a=85994746&app=typekit&e=css");
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family:"century-gothic";
|
||||||
|
src:url("https://use.typekit.net/af/afc5c6/00000000000000003b9b1f23/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3") format("woff2"),url("https://use.typekit.net/af/afc5c6/00000000000000003b9b1f23/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3") format("woff"),url("https://use.typekit.net/af/afc5c6/00000000000000003b9b1f23/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3") format("opentype");
|
||||||
|
font-display:auto;font-style:normal;font-weight:400;font-stretch:normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tk-century-gothic { font-family: "century-gothic",sans-serif; }
|
|
@ -0,0 +1,21 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Quicksand';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/quicksand/v31/6xK-dSZaM9iE8KbpRA_LJ3z8mH9BOJvgkP8o18E.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Quicksand';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/quicksand/v31/6xK-dSZaM9iE8KbpRA_LJ3z8mH9BOJvgkM0o18E.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Quicksand';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/quicksand/v31/6xK-dSZaM9iE8KbpRA_LJ3z8mH9BOJvgkCEv18E.ttf) format('truetype');
|
||||||
|
}
|
After Width: | Height: | Size: 4.9 KiB |
|
@ -9,8 +9,8 @@ body.bootstrap,
|
||||||
body.bootstra-dark {
|
body.bootstra-dark {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
/* font-family: century-gothic, sans-serif; */
|
font-family: century-gothic, sans-serif;
|
||||||
font-family: 'Quicksand', sans-serif;
|
/* font-family: 'Quicksand', sans-serif; */
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
/* margin: 0 0 60px; */
|
/* margin: 0 0 60px; */
|
||||||
margin-bottom: 60px;
|
margin-bottom: 60px;
|
||||||
|
|
|
@ -12,25 +12,25 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
{% block meta %}{% endblock %}
|
{% block meta %}{% endblock %}
|
||||||
<!-- styles -->
|
<!-- styles -->
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"
|
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/bootstrap.min.css') }}">
|
||||||
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
|
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/toggle-bootstrap.min.css') }}">
|
||||||
<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="{{ url_for('static', filename='bootstrap/toggle-bootstrap-dark.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="{{ url_for('static', filename='bootstrap/toggle-bootstrap-print.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 -->
|
<!-- Century gothic font -->
|
||||||
<link rel="stylesheet" href="https://use.typekit.net/oov2wcw.css">
|
<link rel="stylesheet" href="{{ url_for('static', filename='fonts/century-gothic.css') }}">
|
||||||
<!-- fontawesome -->
|
<!-- fontawesome -->
|
||||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css" integrity="sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ" crossorigin="anonymous">
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css" integrity="sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ" crossorigin="anonymous">
|
||||||
|
|
||||||
<link href="{{ url_for('static', filename='main.css') }}" rel="stylesheet" media="screen">
|
<link href="{{ url_for('static', filename='main.css') }}" rel="stylesheet" media="screen">
|
||||||
|
<!-- Datatables -->
|
||||||
|
<link href="{{ url_for('static', filename='datatables/datatables.min.css') }}" rel="stylesheet">
|
||||||
{% block links %}{% endblock %}
|
{% block links %}{% endblock %}
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png">
|
<link rel="icon" type="image/png" sizes="180x180" href="{{ url_for('static', filename='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="32x32" href="{{ url_for('static', filename='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="16x16" href="{{ url_for('static', filename='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="192x192" href="{{ url_for('static', filename='favicon/android-chrome-192x192.png') }}">
|
||||||
<link rel="icon" type="image/png" sizes="512x512" href="/static/favicon/android-chrome-512x512.png">
|
<link rel="icon" type="image/png" sizes="512x512" href="{{ url_for('static', filename='favicon/android-chrome-512x512.png') }}">
|
||||||
<link rel="manifest" href="/static/favicon/site.webmanifest">
|
<link rel="manifest" href="{{ url_for('static', filename='favicon/site.webmanifest') }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bootstrap bootstrap-dark">
|
<body class="bootstrap bootstrap-dark">
|
||||||
|
@ -40,7 +40,8 @@
|
||||||
<nav class="navbar navbar-expand-lg navbar-themed">
|
<nav class="navbar navbar-expand-lg navbar-themed">
|
||||||
<!-- Navbar Brand -->
|
<!-- Navbar Brand -->
|
||||||
<a class="navbar-brand" href="{{ url_for('main.index') }}">
|
<a class="navbar-brand" href="{{ url_for('main.index') }}">
|
||||||
{{ config.APP_NAME }}
|
<img src="/static/hsman.png">
|
||||||
|
<!-- HSMAN -->
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
|
||||||
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
@ -61,7 +62,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item me-right">
|
<li class="nav-item me-right">
|
||||||
<a href="/logout" id="themeSwitch">
|
<a href="{{url_for('main.logout') }}" id="themeSwitch">
|
||||||
<i class="fas fa-sign-out-alt"></i>
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
<!-- <i class="fas fa-plug-circle-xmark"></i> -->
|
<!-- <i class="fas fa-plug-circle-xmark"></i> -->
|
||||||
</a>
|
</a>
|
||||||
|
@ -86,6 +87,7 @@
|
||||||
<!-- Copyrights -->
|
<!-- Copyrights -->
|
||||||
<div class="col-lg-12 text-center">
|
<div class="col-lg-12 text-center">
|
||||||
<p class="text-muted mb-0 py-2">
|
<p class="text-muted mb-0 py-2">
|
||||||
|
<!-- <img src="/static/hsman.png" height="20px"> -->
|
||||||
Headscale Manager |
|
Headscale Manager |
|
||||||
ver. {{ config.APP_VERSION }} ({{ config.APP_SHA }})
|
ver. {{ config.APP_VERSION }} ({{ config.APP_SHA }})
|
||||||
</p>
|
</p>
|
||||||
|
@ -93,15 +95,10 @@
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<!-- scripts -->
|
<!-- scripts -->
|
||||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
|
<script src="{{ url_for('static', filename='bootstrap/jquery-3.7.1.min.js') }}"></script>
|
||||||
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
|
<script src="{{ url_for('static', filename='bootstrap/popper-1.12.9.min.js') }}"></script>
|
||||||
crossorigin="anonymous"></script>
|
<script src="{{ url_for('static', filename='bootstrap/bootstrap.min.js') }}"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
|
<script src="{{ url_for('static', filename='datatables/datatables.min.js') }}"></script>
|
||||||
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 src="{{ url_for('static', filename='main.js') }}" type="text/javascript"></script>
|
||||||
<script>
|
<script>
|
||||||
$(function () {
|
$(function () {
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
{{ node.expiry | htime_dt }}
|
{{ node.expiry | htime_dt }}
|
||||||
</span>
|
</span>
|
||||||
{% if node.expireDate and not node.expired %}
|
{% if node.expireDate and not node.expired %}
|
||||||
<a href="{{ url_for('main.expireNode', nodeId=node.id) }}">
|
<a href="{{ url_for('rest.expireNode', nodeId=node.id) }}">
|
||||||
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect node">
|
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect node">
|
||||||
<i class="fas fa-plug"></i>
|
<i class="fas fa-plug"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block content %}
|
||||||
<h3>nodes</h3>
|
<h3>nodes</h3>
|
||||||
|
@ -50,7 +46,7 @@
|
||||||
<td class="no-sort">
|
<td class="no-sort">
|
||||||
{% if node.expireDate and not node.expired %}
|
{% if node.expireDate and not node.expired %}
|
||||||
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect">
|
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect">
|
||||||
<a class="nodeco" href="{{ url_for('main.expireNodeList', nodeId=node.id) }}">
|
<a class="nodeco" href="{{ url_for('rest.expireNodeList', nodeId=node.id) }}">
|
||||||
<i class="fas fa-plug"></i>
|
<i class="fas fa-plug"></i>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
@ -58,7 +54,7 @@
|
||||||
<i class="fas fa-plug disabled"></i>
|
<i class="fas fa-plug disabled"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span data-toggle="tooltip" data-placement="right" title="delete">
|
<span data-toggle="tooltip" data-placement="right" title="delete">
|
||||||
<a class="nodeco" href="{{ url_for('main.deleteNode', nodeId=node.id) }}">
|
<a class="nodeco" href="{{ url_for('rest.deleteNode', nodeId=node.id) }}">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
@ -71,7 +67,6 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% 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>
|
<script>
|
||||||
$(function () {
|
$(function () {
|
||||||
new DataTable('#nodes', {
|
new DataTable('#nodes', {
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-2 float-left">
|
<div class="col col-2 float-left">
|
||||||
<a class="routeToggle" href="{{ url_for('main.routeToggle', routeId=rts[0].id) }}">
|
<a class="routeToggle" href="{{ url_for('rest.routeToggle', routeId=rts[0].id) }}">
|
||||||
{{ rts[0].enabled | fancyBool | safe}}
|
{{ rts[0].enabled | fancyBool | safe}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-2 float-left">
|
<div class="col col-2 float-left">
|
||||||
<a class="routeToggle" href="{{ url_for('main.routeToggle', routeId=rt.id) }}">
|
<a class="routeToggle" href="{{ url_for('rest.routeToggle', routeId=rt.id) }}">
|
||||||
{{ rt.enabled | fancyBool | safe}}
|
{{ rt.enabled | fancyBool | safe}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,4 @@
|
||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block content %}
|
||||||
|
|
||||||
<h3>{{ user.name }}</h3>
|
<h3>{{ user.name }}</h3>
|
||||||
|
@ -169,7 +164,6 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% 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>
|
<script>
|
||||||
$(function () {
|
$(function () {
|
||||||
$('.pak_copy').on('click', function() {
|
$('.pak_copy').on('click', function() {
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block content %}
|
||||||
<h3>users</h3>
|
<h3>users</h3>
|
||||||
|
@ -48,7 +44,6 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% 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>
|
<script>
|
||||||
$(function () {
|
$(function () {
|
||||||
new DataTable('#users', {
|
new DataTable('#users', {
|
||||||
|
|
|
@ -1,222 +0,0 @@
|
||||||
import logging
|
|
||||||
import datetime
|
|
||||||
import os
|
|
||||||
from flask import current_app
|
|
||||||
from flask import render_template, Blueprint, request
|
|
||||||
from flask import redirect, session, url_for
|
|
||||||
from app import auth
|
|
||||||
|
|
||||||
from .lib import username
|
|
||||||
|
|
||||||
from flask import jsonify
|
|
||||||
from flask_pyoidc.user_session import UserSession
|
|
||||||
|
|
||||||
from hsapi_client import Node, User, Route, PreAuthKey
|
|
||||||
from hsapi_client.preauthkeys import (v1ListPreAuthKeyRequest,
|
|
||||||
v1CreatePreAuthKeyRequest,
|
|
||||||
v1ExpirePreAuthKeyRequest)
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger()
|
|
||||||
|
|
||||||
main_blueprint = Blueprint(
|
|
||||||
'main', __name__, url_prefix=os.getenv('APP_PREFIX', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/health', methods=['GET'])
|
|
||||||
def health():
|
|
||||||
return jsonify(dict(status="OK", version=current_app.config['APP_VERSION']))
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/', methods=['GET', 'POST'])
|
|
||||||
@auth.access_control('default')
|
|
||||||
def index():
|
|
||||||
user_session = UserSession(session)
|
|
||||||
hs_user = user_session.userinfo['email'].split('@')[0]
|
|
||||||
userNodeList = [n for n in Node().list().nodes if n.user.name == hs_user]
|
|
||||||
return render_template('index.html',
|
|
||||||
userNodeList=userNodeList,
|
|
||||||
session=user_session)
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/token', methods=['GET', 'POST'])
|
|
||||||
@auth.access_control('default')
|
|
||||||
def token():
|
|
||||||
user_session = UserSession(session)
|
|
||||||
return jsonify(user_session.userinfo)
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/logout')
|
|
||||||
@auth.oidc_logout
|
|
||||||
def logout():
|
|
||||||
return redirect('/')
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/nodes', methods=['GET'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def nodes():
|
|
||||||
nodelist = Node().list()
|
|
||||||
return render_template('nodes.html',
|
|
||||||
nodes=nodelist.nodes)
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/node/<int:nodeId>', methods=['GET'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
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')))
|
|
||||||
return render_template("node.html",
|
|
||||||
routes=routes.routes,
|
|
||||||
isExitNode=isExitNode,
|
|
||||||
node=node)
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/users', methods=['GET'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def users():
|
|
||||||
userList = User().list()
|
|
||||||
# Get online status of devices of the user
|
|
||||||
online = {}
|
|
||||||
nodeList = Node().list()
|
|
||||||
for user in userList.users:
|
|
||||||
userNodeList = [n for n in nodeList.nodes if n.user.name == user.name]
|
|
||||||
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'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def user(userName):
|
|
||||||
user = User().get(userName)
|
|
||||||
userNodeList = [n for n in Node().list().nodes if n.user.name == userName]
|
|
||||||
|
|
||||||
preauthkeyreq = v1ListPreAuthKeyRequest(user=userName)
|
|
||||||
preauthKeys = PreAuthKey().list(preauthkeyreq)
|
|
||||||
|
|
||||||
defaultExpiry = datetime.datetime.now() + datetime.timedelta(days=7)
|
|
||||||
expStr = defaultExpiry.strftime('%Y-%m-%dT%H:%M')
|
|
||||||
|
|
||||||
return render_template("user.html",
|
|
||||||
user=user.user,
|
|
||||||
defaultExpiry=expStr,
|
|
||||||
preauthKeys=preauthKeys.preAuthKeys,
|
|
||||||
userNodeList=userNodeList)
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/routes', methods=['GET'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def routes():
|
|
||||||
routes = Route().list()
|
|
||||||
|
|
||||||
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)
|
|
||||||
return render_template("routes.html",
|
|
||||||
exitNodes=exitNodes,
|
|
||||||
routes=final)
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/routeToggle/<int:routeId>', methods=['GET'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def routeToggle(routeId: int):
|
|
||||||
routes = Route().list()
|
|
||||||
route = [r for r in routes.routes if r.id == routeId]
|
|
||||||
if route:
|
|
||||||
route = route[0]
|
|
||||||
if route.enabled:
|
|
||||||
action = 'disabled'
|
|
||||||
Route().disable(routeId)
|
|
||||||
else:
|
|
||||||
Route().enable(routeId)
|
|
||||||
action = 'enabled'
|
|
||||||
log.info(
|
|
||||||
f"route '{route.prefix}' via '{route.node.givenName}'"
|
|
||||||
f"{action} by '{username()}'")
|
|
||||||
return redirect(url_for("main.routes"))
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/node/<int:nodeId>/expire', methods=['GET'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def expireNode(nodeId: int):
|
|
||||||
"""
|
|
||||||
This expires a node from the node page.
|
|
||||||
The difference from above is that it returns to the /node/nodeId page
|
|
||||||
"""
|
|
||||||
Node().expire(nodeId)
|
|
||||||
log.info(f"node '{nodeId}' expired by '{username()}'")
|
|
||||||
return redirect(url_for("main.node", nodeId=nodeId))
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/node/<int:nodeId>/list-expire', methods=['GET'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def expireNodeList(nodeId: int):
|
|
||||||
"""
|
|
||||||
This expires a node from the node list.
|
|
||||||
The difference from above is that it returns to the /nodes page
|
|
||||||
"""
|
|
||||||
Node().expire(nodeId)
|
|
||||||
log.info(f"node '{nodeId}' expired by '{username()}'")
|
|
||||||
return redirect(url_for("main.nodes"))
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/node/<int:nodeId>/delete', methods=['GET'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def deleteNode(nodeId: int):
|
|
||||||
Node().delete(nodeId)
|
|
||||||
return redirect(url_for("main.nodes"))
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def renameNode(nodeId: str, newName: str):
|
|
||||||
Node().rename(nodeId, newName)
|
|
||||||
return jsonify(dict(newName=newName))
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/user/<userName>/delete', methods=['GET'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def deleteUser(userName: str):
|
|
||||||
nodes = Node().byUser(userName)
|
|
||||||
for node in nodes.nodes:
|
|
||||||
Node().expire(node.id)
|
|
||||||
Node().delete(node.id)
|
|
||||||
User().delete(userName)
|
|
||||||
return redirect(url_for("main.users"))
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/user/<userName>/pakcreate', methods=['POST'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def createPKA(userName: str):
|
|
||||||
data = request.json
|
|
||||||
log.debug(data)
|
|
||||||
expiration = f"{data['expiration']}:00Z"
|
|
||||||
req = v1CreatePreAuthKeyRequest(user=userName,
|
|
||||||
reusable=data['reusable'],
|
|
||||||
ephemeral=data['ephemeral'],
|
|
||||||
expiration=expiration)
|
|
||||||
pak = PreAuthKey().create((req))
|
|
||||||
return jsonify(dict(key=pak.preAuthKey.key))
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/user/<userName>/expire/<key>', methods=['GET'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def expirePKA(userName: str, key: str):
|
|
||||||
log.debug(key)
|
|
||||||
req = v1ExpirePreAuthKeyRequest(user=userName, key=key)
|
|
||||||
|
|
||||||
PreAuthKey().expire(req)
|
|
||||||
return redirect(url_for('main.user', userName=userName))
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .main import *
|
||||||
|
from .rest import *
|
|
@ -0,0 +1,112 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from flask import Blueprint, request
|
||||||
|
from flask import redirect, url_for
|
||||||
|
from app import auth
|
||||||
|
|
||||||
|
from ..lib import username
|
||||||
|
|
||||||
|
from flask import jsonify
|
||||||
|
|
||||||
|
from hsapi_client import Node, User, Route, PreAuthKey
|
||||||
|
from hsapi_client.preauthkeys import (v1CreatePreAuthKeyRequest,
|
||||||
|
v1ExpirePreAuthKeyRequest)
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger()
|
||||||
|
# REST calls
|
||||||
|
|
||||||
|
rest_blueprint = Blueprint(
|
||||||
|
'rest', __name__, url_prefix=os.getenv('APP_PREFIX', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
@rest_blueprint.route('/routeToggle/<int:routeId>', methods=['GET'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def routeToggle(routeId: int):
|
||||||
|
routes = Route().list()
|
||||||
|
route = [r for r in routes.routes if r.id == routeId]
|
||||||
|
if route:
|
||||||
|
route = route[0]
|
||||||
|
if route.enabled:
|
||||||
|
action = 'disabled'
|
||||||
|
Route().disable(routeId)
|
||||||
|
else:
|
||||||
|
Route().enable(routeId)
|
||||||
|
action = 'enabled'
|
||||||
|
log.info(
|
||||||
|
f"route '{route.prefix}' via '{route.node.givenName}'"
|
||||||
|
f"{action} by '{username()}'")
|
||||||
|
return redirect(url_for("main.routes"))
|
||||||
|
|
||||||
|
|
||||||
|
@rest_blueprint.route('/node/<int:nodeId>/expire', methods=['GET'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def expireNode(nodeId: int):
|
||||||
|
"""
|
||||||
|
This expires a node from the node page.
|
||||||
|
The difference from above is that it returns to the /node/nodeId page
|
||||||
|
"""
|
||||||
|
Node().expire(nodeId)
|
||||||
|
log.info(f"node '{nodeId}' expired by '{username()}'")
|
||||||
|
return redirect(url_for("main.node", nodeId=nodeId))
|
||||||
|
|
||||||
|
|
||||||
|
@rest_blueprint.route('/node/<int:nodeId>/list-expire', methods=['GET'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def expireNodeList(nodeId: int):
|
||||||
|
"""
|
||||||
|
This expires a node from the node list.
|
||||||
|
The difference from above is that it returns to the /nodes page
|
||||||
|
"""
|
||||||
|
Node().expire(nodeId)
|
||||||
|
log.info(f"node '{nodeId}' expired by '{username()}'")
|
||||||
|
return redirect(url_for("main.nodes"))
|
||||||
|
|
||||||
|
|
||||||
|
@rest_blueprint.route('/node/<int:nodeId>/delete', methods=['GET'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def deleteNode(nodeId: int):
|
||||||
|
Node().delete(nodeId)
|
||||||
|
return redirect(url_for("main.nodes"))
|
||||||
|
|
||||||
|
|
||||||
|
@rest_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def renameNode(nodeId: int, newName: str):
|
||||||
|
Node().rename(nodeId, newName)
|
||||||
|
return jsonify(dict(newName=newName))
|
||||||
|
|
||||||
|
|
||||||
|
@rest_blueprint.route('/user/<userName>/delete', methods=['GET'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def deleteUser(userName: str):
|
||||||
|
nodes = Node().byUser(userName)
|
||||||
|
for node in nodes.nodes:
|
||||||
|
Node().expire(node.id)
|
||||||
|
Node().delete(node.id)
|
||||||
|
User().delete(userName)
|
||||||
|
return redirect(url_for("main.users"))
|
||||||
|
|
||||||
|
|
||||||
|
@rest_blueprint.route('/user/<userName>/pakcreate', methods=['POST'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def createPKA(userName: str):
|
||||||
|
data = request.json
|
||||||
|
log.debug(data)
|
||||||
|
expiration = f"{data['expiration']}:00Z"
|
||||||
|
req = v1CreatePreAuthKeyRequest(user=userName,
|
||||||
|
reusable=data['reusable'],
|
||||||
|
ephemeral=data['ephemeral'],
|
||||||
|
expiration=expiration)
|
||||||
|
pak = PreAuthKey().create((req))
|
||||||
|
return jsonify(dict(key=pak.preAuthKey.key))
|
||||||
|
|
||||||
|
|
||||||
|
@rest_blueprint.route('/user/<userName>/expire/<key>', methods=['GET'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def expirePKA(userName: str, key: str):
|
||||||
|
log.debug(key)
|
||||||
|
req = v1ExpirePreAuthKeyRequest(user=userName, key=key)
|
||||||
|
|
||||||
|
PreAuthKey().expire(req)
|
||||||
|
return redirect(url_for('main.user', userName=userName))
|