Local assets and split routes

This commit is contained in:
Andrea Mistrali 2024-07-22 16:06:59 +02:00
parent bc637c25b4
commit a1159ac97e
31 changed files with 291 additions and 271 deletions

View File

@ -1,2 +1,6 @@
- improve configuration
- 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

View File

@ -31,7 +31,7 @@ auth = OIDCAuthentication({'default': provider_config})
def create_app(environment='development'):
from config import config
from .views import main_blueprint
from .views import main_blueprint, rest_blueprint
# BRUTTO BRUTTO
app_prefix = os.getenv('APP_PREFIX', '')
@ -56,6 +56,10 @@ def create_app(environment='development'):
main_blueprint.url_prefix}'")
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")
filters.init_app(app)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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"
}

View File

@ -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; }

View File

@ -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');
}

BIN
hsman/app/static/hsman.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -9,8 +9,8 @@ body.bootstrap,
body.bootstra-dark {
width: 100%;
height: 100%;
/* font-family: century-gothic, sans-serif; */
font-family: 'Quicksand', sans-serif;
font-family: century-gothic, sans-serif;
/* font-family: 'Quicksand', sans-serif; */
min-height: 100%;
/* margin: 0 0 60px; */
margin-bottom: 60px;

View File

@ -12,25 +12,25 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
{% block meta %}{% endblock %}
<!-- styles -->
<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">
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/toggle-bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/toggle-bootstrap-dark.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/toggle-bootstrap-print.min.css') }}">
<!-- 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 -->
<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">
<!-- Datatables -->
<link href="{{ url_for('static', filename='datatables/datatables.min.css') }}" rel="stylesheet">
{% 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">
<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="{{ url_for('static', filename='favicon/favicon-32x32.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="{{ url_for('static', filename='favicon/android-chrome-192x192.png') }}">
<link rel="icon" type="image/png" sizes="512x512" href="{{ url_for('static', filename='favicon/android-chrome-512x512.png') }}">
<link rel="manifest" href="{{ url_for('static', filename='favicon/site.webmanifest') }}">
</head>
<body class="bootstrap bootstrap-dark">
@ -40,7 +40,8 @@
<nav class="navbar navbar-expand-lg navbar-themed">
<!-- Navbar Brand -->
<a class="navbar-brand" href="{{ url_for('main.index') }}">
{{ config.APP_NAME }}
<img src="/static/hsman.png">
<!-- HSMAN -->
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
@ -61,7 +62,7 @@
</ul>
<ul class="navbar-nav">
<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-plug-circle-xmark"></i> -->
</a>
@ -86,6 +87,7 @@
<!-- Copyrights -->
<div class="col-lg-12 text-center">
<p class="text-muted mb-0 py-2">
<!-- <img src="/static/hsman.png" height="20px"> -->
Headscale Manager |
ver. {{ config.APP_VERSION }} ({{ config.APP_SHA }})
</p>
@ -93,15 +95,10 @@
</div>
</footer>
<!-- scripts -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
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='bootstrap/jquery-3.7.1.min.js') }}"></script>
<script src="{{ url_for('static', filename='bootstrap/popper-1.12.9.min.js') }}"></script>
<script src="{{ url_for('static', filename='bootstrap/bootstrap.min.js') }}"></script>
<script src="{{ url_for('static', filename='datatables/datatables.min.js') }}"></script>
<script src="{{ url_for('static', filename='main.js') }}" type="text/javascript"></script>
<script>
$(function () {

View File

@ -46,7 +46,7 @@
{{ node.expiry | htime_dt }}
</span>
{% 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">
<i class="fas fa-plug"></i>
</span>

View File

@ -1,8 +1,4 @@
{% 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 %}
<h3>nodes</h3>
@ -50,7 +46,7 @@
<td class="no-sort">
{% if node.expireDate and not node.expired %}
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect">
<a class="nodeco" href="{{ url_for('main.expireNodeList', nodeId=node.id) }}">
<a class="nodeco" href="{{ url_for('rest.expireNodeList', nodeId=node.id) }}">
<i class="fas fa-plug"></i>
</a>
</span>
@ -58,7 +54,7 @@
<i class="fas fa-plug disabled"></i>
{% endif %}
<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>
</a>
</span>
@ -71,7 +67,6 @@
{% 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', {

View File

@ -56,7 +56,7 @@
</a>
</div>
<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}}
</a>
</div>
@ -78,7 +78,7 @@
</a>
</div>
<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}}
</a>
</div>

View File

@ -1,9 +1,4 @@
{% 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 %}
<h3>{{ user.name }}</h3>
@ -169,7 +164,6 @@
{% 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 () {
$('.pak_copy').on('click', function() {

View File

@ -1,8 +1,4 @@
{% 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 %}
<h3>users</h3>
@ -48,7 +44,6 @@
{% 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', {

View File

@ -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))

View File

@ -0,0 +1,2 @@
from .main import *
from .rest import *

112
hsman/app/views/rest.py Normal file
View File

@ -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))