Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c2bd5a1acc
|
|||
|
771a3e3260
|
|||
|
a881b94396
|
|||
|
33c0e603f8
|
|||
| 9fcae05d20 | |||
|
0409ac3d08
|
|||
|
425a1cd094
|
|||
|
31910dc034
|
|||
|
7fa17adfb1
|
|||
|
3301a36b5f
|
|||
|
da542f0bc2
|
|||
|
2cfbafbda5
|
|||
|
9b7b903c57
|
|||
|
379fef4b00
|
|||
|
bdba6db42d
|
|||
|
4fb45c41bd
|
|||
|
a1c66152ae
|
|||
|
2a38fb14dd
|
|||
|
d86b2b58c2
|
|||
|
b91e73f3a5
|
|||
|
71a3413cbe
|
|||
|
a1dadcd709
|
|||
|
b9fd722016
|
|||
|
c780b12b74
|
|||
|
2719ecadf0
|
|||
|
fe9b42e8a2
|
|||
| 14b09a0fcf | |||
|
c39c3a0ab6
|
|||
|
07ac2edb53
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -266,3 +266,4 @@ tags
|
|||||||
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
|
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
|
||||||
.flaskenv
|
.flaskenv
|
||||||
docker.env
|
docker.env
|
||||||
|
flask_session/
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ You can run the Flask application as any other Flask app, using `flask run` insi
|
|||||||
There are some settings that must/can be provided to the application:
|
There are some settings that must/can be provided to the application:
|
||||||
|
|
||||||
| Variable | Usage | Default |
|
| Variable | Usage | Default |
|
||||||
| -------------------------- | ---------------------------------------- | :-----: |
|
| -------------------------- | -------------------------------------------------------------- | :-----: |
|
||||||
| `APPLICATION_ROOT` | Base URI path for the app | `/` |
|
| `APPLICATION_ROOT` | Base URI path for the app | `/` |
|
||||||
| `HSMAN_SECRET_KEY` | Flask app secret key | |
|
| `HSMAN_SECRET_KEY` | Flask app secret key | |
|
||||||
| `HSMAN_ADMIN_GROUPS` | User groups that are considered admins | |
|
| `HSMAN_ADMIN_GROUPS` | Comma separated list of user groups that are considered admins | |
|
||||||
| `HSMAN_OIDC_CLIENT_ID` | OIDC client ID | |
|
| `HSMAN_OIDC_CLIENT_ID` | OIDC client ID | |
|
||||||
| `HSMAN_OIDC_CLIENT_SECRET` | OIDC clietn secret | |
|
| `HSMAN_OIDC_CLIENT_SECRET` | OIDC clietn secret | |
|
||||||
| `HSMAN_OIDC_URL` | OIDC server URL | |
|
| `HSMAN_OIDC_URL` | OIDC server URL | |
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from flask import Flask, render_template
|
from flask import Flask, render_template, g
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
from flask_mobility import Mobility
|
from flask_mobility import Mobility
|
||||||
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata
|
from flask_session import Session
|
||||||
|
|
||||||
|
|
||||||
from . import filters
|
from . import filters
|
||||||
from .lib import OIDCAuthentication
|
from .lib import OIDCAuthentication
|
||||||
@@ -12,6 +11,8 @@ import os
|
|||||||
mobility = Mobility()
|
mobility = Mobility()
|
||||||
|
|
||||||
auth = OIDCAuthentication()
|
auth = OIDCAuthentication()
|
||||||
|
# SESSION_TYPE = 'filesystem'
|
||||||
|
sess = Session()
|
||||||
|
|
||||||
|
|
||||||
def create_app(environment='development'):
|
def create_app(environment='development'):
|
||||||
@@ -29,6 +30,9 @@ def create_app(environment='development'):
|
|||||||
app.config.from_prefixed_env(prefix="HSMAN")
|
app.config.from_prefixed_env(prefix="HSMAN")
|
||||||
config[env].configure(app)
|
config[env].configure(app)
|
||||||
app.config['APP_TZ'] = os.environ.get('TZ', 'UTC')
|
app.config['APP_TZ'] = os.environ.get('TZ', 'UTC')
|
||||||
|
app.config['ADMIN_GROUPS'] = list(
|
||||||
|
map(str.strip, app.config.get('ADMIN_GROUPS', "").split(',')))
|
||||||
|
app.logger.debug(f"admin groups: {app.config['ADMIN_GROUPS']}")
|
||||||
|
|
||||||
app.logger.info("middleware init: mobility")
|
app.logger.info("middleware init: mobility")
|
||||||
mobility.init_app(app)
|
mobility.init_app(app)
|
||||||
@@ -38,21 +42,26 @@ def create_app(environment='development'):
|
|||||||
|
|
||||||
# Register blueprints.
|
# Register blueprints.
|
||||||
from .views import main_blueprint, rest_blueprint
|
from .views import main_blueprint, rest_blueprint
|
||||||
app.logger.info(f"registering main blueprint with prefix '{
|
app.logger.info(f"register blueprint: 'main' [prefix '{
|
||||||
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 '{
|
app.logger.info(f"register blueprint: 'rest' [prefix '{
|
||||||
rest_blueprint.url_prefix}'")
|
rest_blueprint.url_prefix}']")
|
||||||
app.register_blueprint(rest_blueprint)
|
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)
|
||||||
|
|
||||||
# Error handlers.
|
sess.init_app(app)
|
||||||
|
|
||||||
|
# Error handlers.
|
||||||
@app.errorhandler(HTTPException)
|
@app.errorhandler(HTTPException)
|
||||||
def handle_http_error(exc):
|
def handle_http_error(exc):
|
||||||
return render_template('error.html', error=exc), exc.code
|
return render_template('error.html', error=exc), exc.code
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_auth():
|
||||||
|
return dict(auth=auth)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
100
app/lib.py
100
app/lib.py
@@ -2,10 +2,10 @@ import os
|
|||||||
import functools
|
import functools
|
||||||
|
|
||||||
from flask import request, abort, current_app
|
from flask import request, abort, current_app
|
||||||
from flask import session as flask_session
|
from flask import session as flask_session, jsonify
|
||||||
from flask_pyoidc import OIDCAuthentication as _OIDCAuth
|
from flask_pyoidc import OIDCAuthentication as _OIDCAuth
|
||||||
from flask_pyoidc.user_session import UserSession
|
from flask_pyoidc.user_session import UserSession
|
||||||
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata
|
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata, ProviderMetadata
|
||||||
|
|
||||||
from typing import Callable, List
|
from typing import Callable, List
|
||||||
|
|
||||||
@@ -21,19 +21,6 @@ def remote_ip() -> str:
|
|||||||
return str(request.environ.get('REMOTE_ADDR'))
|
return str(request.environ.get('REMOTE_ADDR'))
|
||||||
|
|
||||||
|
|
||||||
def username() -> str:
|
|
||||||
userinfo = flask_session['userinfo']
|
|
||||||
return userinfo['email'].split('@')[0]
|
|
||||||
|
|
||||||
|
|
||||||
def login_name() -> str:
|
|
||||||
userinfo = flask_session['userinfo']
|
|
||||||
if 'preferred_username' in userinfo:
|
|
||||||
return userinfo['preferred_username']
|
|
||||||
else:
|
|
||||||
return username()
|
|
||||||
|
|
||||||
|
|
||||||
def webMode() -> bool:
|
def webMode() -> bool:
|
||||||
is_gunicorn = "gunicorn" in os.environ.get('SERVER_SOFTWARE', '')
|
is_gunicorn = "gunicorn" in os.environ.get('SERVER_SOFTWARE', '')
|
||||||
is_werkzeug = os.environ.get('WERKZEUG_RUN_MAIN', False) == "true"
|
is_werkzeug = os.environ.get('WERKZEUG_RUN_MAIN', False) == "true"
|
||||||
@@ -64,6 +51,69 @@ class OIDCAuthentication(_OIDCAuth):
|
|||||||
super().init_app(app)
|
super().init_app(app)
|
||||||
app.auth = self
|
app.auth = self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def userinfo(self) -> dict:
|
||||||
|
log.debug(flask_session.get('userinfo', {}))
|
||||||
|
return flask_session.get('userinfo', {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def username(self) -> str:
|
||||||
|
# This need to be changed after upgrading headscale version
|
||||||
|
# when hs will use the preferred_username field as username
|
||||||
|
return self.email.split('@')[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def email(self) -> str:
|
||||||
|
return self.userinfo.get('email', 'unknown')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def login_name(self) -> str:
|
||||||
|
return self.userinfo.get('preferred_username', self.username)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self) -> str:
|
||||||
|
return self.userinfo.get('name', self.username)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def groups(self) -> list:
|
||||||
|
return self.userinfo.get('groups', [])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def isAdmin(self) -> bool:
|
||||||
|
user_groups = self.userinfo.get('groups', [])
|
||||||
|
with current_app.app_context():
|
||||||
|
admin_groups = current_app.config.get('ADMIN_GROUPS', [])
|
||||||
|
admin_users = current_app.config.get('ADMIN_USERS', [])
|
||||||
|
|
||||||
|
authorized_groups = set(admin_groups).intersection(user_groups)
|
||||||
|
|
||||||
|
if len(authorized_groups):
|
||||||
|
log.debug(f"'{self.username}' is a member of {
|
||||||
|
authorized_groups}. isAdmin == True")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.username in admin_users:
|
||||||
|
log.debug(f"'{self.username}' is an admin user")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unathorized(self):
|
||||||
|
response = jsonify(
|
||||||
|
{'message': f"not authorized",
|
||||||
|
'comment': 'nice try, info logged',
|
||||||
|
'logged': f"'{self.username}@{remote_ip()}",
|
||||||
|
'result': 'GO AWAY!'})
|
||||||
|
log.warning(
|
||||||
|
f"user '{self.username}' attempted denied operation from {remote_ip()}")
|
||||||
|
return response, 403
|
||||||
|
|
||||||
|
def userOrAdmin(self, username: str):
|
||||||
|
"""
|
||||||
|
Check is the current user is an admin OR the username passed as argument
|
||||||
|
"""
|
||||||
|
return self.isAdmin or self.username == username
|
||||||
|
|
||||||
def authorize(self, provider_name: str, authz_fn: Callable, **kwargs):
|
def authorize(self, provider_name: str, authz_fn: Callable, **kwargs):
|
||||||
if provider_name not in self._provider_configurations:
|
if provider_name not in self._provider_configurations:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -76,7 +126,7 @@ class OIDCAuthentication(_OIDCAuth):
|
|||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
def oidc_decorator(view_func):
|
def oidc_decorator(view_func):
|
||||||
@ functools.wraps(view_func)
|
@functools.wraps(view_func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
# Retrieve session and client
|
# Retrieve session and client
|
||||||
session = UserSession(flask_session, provider_name)
|
session = UserSession(flask_session, provider_name)
|
||||||
@@ -165,23 +215,7 @@ class OIDCAuthentication(_OIDCAuth):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def _authz_fn(session) -> bool:
|
def _authz_fn(session) -> bool:
|
||||||
user_groups = session.userinfo.get('groups', [])
|
return self.isAdmin
|
||||||
username = session.userinfo.get('preferred_username', "")
|
|
||||||
with current_app.app_context():
|
|
||||||
admin_groups = current_app.config.get('ADMIN_GROUPS', [])
|
|
||||||
admin_users = current_app.config.get('ADMIN_USERS', [])
|
|
||||||
|
|
||||||
authorized_groups = set(admin_groups).intersection(user_groups)
|
|
||||||
|
|
||||||
if len(authorized_groups):
|
|
||||||
log.debug(f"'{username}' is a member of {
|
|
||||||
authorized_groups}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
if username in admin_users:
|
|
||||||
log.debug(f"'{username}' is an admin user")
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
return self.authorize(provider_name,
|
return self.authorize(provider_name,
|
||||||
authz_fn=_authz_fn)
|
authz_fn=_authz_fn)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ level = INFO
|
|||||||
handlers = console
|
handlers = console
|
||||||
|
|
||||||
[logger_access]
|
[logger_access]
|
||||||
level = INFO
|
level = ERROR
|
||||||
handlers = console
|
handlers = console
|
||||||
qualname = gunicorn.access
|
qualname = gunicorn.access
|
||||||
propagate = 0
|
propagate = 0
|
||||||
|
|||||||
7
app/static/bootstrap/bootstrap-grid.min.css
vendored
Normal file
7
app/static/bootstrap/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
8
app/static/bootstrap/bootstrap-reboot.min.css
vendored
Normal file
8
app/static/bootstrap/bootstrap-reboot.min.css
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Reboot v4.6.2 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2022 The Bootstrap Authors
|
||||||
|
* Copyright 2011-2022 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
|
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||||
|
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
||||||
7
app/static/bootstrap/bootstrap.bundle.min.js
vendored
Normal file
7
app/static/bootstrap/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9
app/static/bootstrap/bootstrap.min.css
vendored
9
app/static/bootstrap/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
8
app/static/bootstrap/bootstrap.min.js
vendored
8
app/static/bootstrap/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -20,6 +20,8 @@ body > .container,
|
|||||||
body > .container-fluid {
|
body > .container-fluid {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
|
margin-left: 200px;
|
||||||
|
margin-right: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
@@ -62,7 +64,7 @@ a:active {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.nodeco, a.nav-link, a.navbar-brand, a.routeToggle {
|
a.nodeco, a.nav-link, a.navbar-brand, a.routeToggle, a.plain {
|
||||||
text-decoration-line: none;
|
text-decoration-line: none;
|
||||||
text-decoration-style: unset;
|
text-decoration-style: unset;
|
||||||
}
|
}
|
||||||
@@ -118,3 +120,15 @@ i.disabled {
|
|||||||
span.expired {
|
span.expired {
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copy:hover {
|
||||||
|
transform: scale(1.5, 1.5);
|
||||||
|
-ms-transform: scale(1.5, 1.5)); /* IE 9 */
|
||||||
|
-webkit-transform: scale(1.5, 1.5);
|
||||||
|
}
|
||||||
|
.copy:hover::after {
|
||||||
|
content: "📄 click to copy";
|
||||||
|
font-size: 80%;
|
||||||
|
/* font-style:oblique; */
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,12 +14,10 @@ function renameNode(nodeId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createPKA(username) {
|
function createPKA(username) {
|
||||||
console.log(username);
|
|
||||||
var url = `${username}/pakcreate`;
|
var url = `${username}/pakcreate`;
|
||||||
var ephemereal = $("#ephemereal").is(":checked");
|
var ephemereal = $("#ephemereal").is(":checked");
|
||||||
var reusable = $("#reusable").is(":checked");
|
var reusable = $("#reusable").is(":checked");
|
||||||
var expiration = $("#expiration").val();
|
var expiration = $("#expiration").val();
|
||||||
console.log(expiration);
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -65,3 +63,65 @@ function toggleExpired(obj) {
|
|||||||
$(".pka-expired").addClass("pka-hide");
|
$(".pka-expired").addClass("pka-hide");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function backfillips(obj) {
|
||||||
|
var url = "backfillips";
|
||||||
|
var button = $(obj);
|
||||||
|
var original = button.html();
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
method: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
xhrFields: {
|
||||||
|
withCredentials: true,
|
||||||
|
},
|
||||||
|
data: {},
|
||||||
|
success: function (data) {
|
||||||
|
if (data.length) {
|
||||||
|
button.html("Updated");
|
||||||
|
} else {
|
||||||
|
button.html("Done");
|
||||||
|
}
|
||||||
|
setTimeout(function () {
|
||||||
|
button.html(original);
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadACL(obj) {
|
||||||
|
var fd = new FormData();
|
||||||
|
var files = $("#upload")[0].files[0];
|
||||||
|
fd.append("file", files);
|
||||||
|
|
||||||
|
// When we close the modal, we reload the page
|
||||||
|
$("#uploadACL").on("hidden.bs.modal", function (event) {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "policy/upload",
|
||||||
|
type: "POST",
|
||||||
|
xhrFields: {
|
||||||
|
withCredentials: true,
|
||||||
|
},
|
||||||
|
data: fd,
|
||||||
|
contentType: false,
|
||||||
|
processData: false,
|
||||||
|
success: function (response) {
|
||||||
|
if (response != 0) {
|
||||||
|
$("#output").html("acl updated");
|
||||||
|
} else {
|
||||||
|
$("#output").html("acl not updated");
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
$("#uploadACL").modal("hide");
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
error: function (response) {
|
||||||
|
console.log(response.responseJSON.message);
|
||||||
|
$("#output").html(response.responseJSON.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,15 +50,20 @@
|
|||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav mr-auto">
|
<ul class="navbar-nav mr-auto">
|
||||||
|
{% if auth.isAdmin %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('main.nodes') }}">nodes</a>
|
<a class="nav-link" href="{{ url_for('main.nodes') }}"><i class="fas fa-desktop"></i> nodes</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('main.users') }}">users</a>
|
<a class="nav-link" href="{{ url_for('main.users') }}"><i class="fas fa-address-card"></i> users</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('main.routes') }}">routes</a>
|
<a class="nav-link" href="{{ url_for('main.routes') }}"><i class="fas fa-satellite-dish"></i> routes</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('main.policy') }}"><i class="fas fa-user-shield"></i> policy</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item me-right">
|
<li class="nav-item me-right">
|
||||||
@@ -74,8 +79,9 @@
|
|||||||
{% if g.is_mobile %}
|
{% if g.is_mobile %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="container">
|
<div class="container-lg">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<p></p>
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="jumbotron my-4">
|
<div class="jumbotron jumbotron-fluid my-4">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1>{{ '%s - %s' % (error.code, error.name) }}</h1>
|
<h1>Oops, something went wrong</h1>
|
||||||
|
<h1>{{ '%s - %s' % (error.code, error.name) }}</h2>
|
||||||
<p>{{ error.description }}.</p>
|
<p>{{ error.description }}.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>Welcome, {{ session.userinfo.name }}</h3>
|
<h3>
|
||||||
|
<!-- Welcome, {{ auth.full_name }} -->
|
||||||
|
authentication info
|
||||||
|
</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<h4>authentication info</h4>
|
<!-- <h4>authentication info</h4> -->
|
||||||
<div class="row data">
|
<div class="row data">
|
||||||
<div class="col col-2">
|
<div class="col col-2">
|
||||||
<strong>email</strong>
|
<strong>full name</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-6">
|
<div class="col col-6">
|
||||||
{{ session.userinfo.email }}
|
<span data-toggle="tooltip" data-placement="right" title="OIDC username: {{ auth.login_name }}">
|
||||||
<!-- {{ session.userinfo.email_verified | fancyBool | safe }} -->
|
{{ auth.full_name }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row data">
|
<div class="row data">
|
||||||
@@ -18,7 +22,17 @@
|
|||||||
<strong>username</strong>
|
<strong>username</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-6">
|
<div class="col col-6">
|
||||||
{{ session.userinfo.preferred_username }}
|
<span data-toggle="tooltip" data-placement="right" title="OIDC username: {{ auth.login_name }}">
|
||||||
|
{{ auth.username }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row data">
|
||||||
|
<div class="col col-2">
|
||||||
|
<strong>email</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col col-6">
|
||||||
|
{{ auth.email }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row data">
|
<div class="row data">
|
||||||
@@ -27,19 +41,44 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-6">
|
<div class="col col-6">
|
||||||
<i class="fas fa-angle-right"></i>
|
<i class="fas fa-angle-right"></i>
|
||||||
{{ session.userinfo.groups[0]}}
|
{% if not auth.groups[0] or auth.groups[0] in config['ADMIN_GROUPS'] %}
|
||||||
|
<span class="badge badge-pill badge-warning">
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-pill badge-dark">
|
||||||
|
{% endif %}
|
||||||
|
{{ auth.groups[0] | default('no group')}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% for group in session.userinfo.groups[1:] |sort %}
|
{% for group in auth.groups[1:] | default([]) |sort %}
|
||||||
<div class="row data">
|
<div class="row data">
|
||||||
<div class="col col-2">
|
<div class="col col-2">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-6">
|
<div class="col col-6">
|
||||||
<i class="fas fa-angle-right"></i> {{ group }}
|
<i class="fas fa-angle-right"></i>
|
||||||
|
{% if group in config['ADMIN_GROUPS'] %}
|
||||||
|
<span class="badge badge-pill badge-warning">
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-pill badge-dark">
|
||||||
|
{% endif %}
|
||||||
|
{{ group }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<div class="row data">
|
||||||
|
<div class="col col-2">
|
||||||
|
<strong>access level</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col col-6">
|
||||||
|
{% if auth.isAdmin %}
|
||||||
|
<span class="badge badge-pill badge-danger">ADMIN</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-pill badge-info">USER</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<h4>your devices</h4>
|
<h4>your devices</h4>
|
||||||
<div class="row strong">
|
<div class="row strong">
|
||||||
@@ -52,7 +91,7 @@
|
|||||||
{% for node in userNodeList %}
|
{% for node in userNodeList %}
|
||||||
<div class="row data">
|
<div class="row data">
|
||||||
<div class="col col-2">
|
<div class="col col-2">
|
||||||
{{ node.givenName}}
|
<a href="{{url_for('main.node', nodeId=node.id) }}">{{ node.givenName}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-2">
|
<div class="col col-2">
|
||||||
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
|
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
|
||||||
@@ -69,7 +108,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-2">
|
<div class="col col-2">
|
||||||
<span data-toggle="tooltip" data-placement="right" title="delete">
|
<span data-toggle="tooltip" data-placement="right" title="delete">
|
||||||
<a class="nodeco" href="{{ url_for('rest.deleteOwnNode', 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>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<p></p>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-3 float-left">
|
<div class="col col-3 float-left">
|
||||||
<strong>status</strong>
|
<strong>status</strong>
|
||||||
@@ -36,6 +35,9 @@
|
|||||||
<div class="col col-8 float-left">
|
<div class="col col-8 float-left">
|
||||||
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
|
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
|
||||||
{{ node.createdAt | htime_dt }}
|
{{ node.createdAt | htime_dt }}
|
||||||
|
<span class="badge badge-pill badge-warning">
|
||||||
|
{{ node.registerMethod.name }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,8 +75,11 @@
|
|||||||
<h5>addresses</h5>
|
<h5>addresses</h5>
|
||||||
{% for ip in node.ipAddresses %}
|
{% for ip in node.ipAddresses %}
|
||||||
<div class="row data">
|
<div class="row data">
|
||||||
<div class="col col-3">
|
<div class="col col-6">
|
||||||
|
<span class="address copy"
|
||||||
|
value="{{ ip }}">
|
||||||
{{ ip }}
|
{{ ip }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -126,7 +131,9 @@
|
|||||||
<strong>machineKey</strong>
|
<strong>machineKey</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-8 float-left">
|
<div class="col col-8 float-left">
|
||||||
|
<span class="copy" value="{{ node.machineKey }}">
|
||||||
<code>{{ node.machineKey }}</code>
|
<code>{{ node.machineKey }}</code>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,7 +142,9 @@
|
|||||||
<strong>nodeKey</strong>
|
<strong>nodeKey</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-8 float-left">
|
<div class="col col-8 float-left">
|
||||||
|
<span class="copy" value="{{ node.nodeKey }}">
|
||||||
<code>{{ node.nodeKey }}</code>
|
<code>{{ node.nodeKey }}</code>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -144,7 +153,9 @@
|
|||||||
<strong>discoKey</strong>
|
<strong>discoKey</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-8 float-left">
|
<div class="col col-8 float-left">
|
||||||
|
<span class="copy" value="{{ node.discoKey }}">
|
||||||
<code>{{ node.discoKey }}</code>
|
<code>{{ node.discoKey }}</code>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p></p>
|
<p></p>
|
||||||
@@ -210,3 +221,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
$('.copy').on('click', function() {
|
||||||
|
copyToClipboard(this)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>nodes</h3>
|
<div class="row data justify-content-between">
|
||||||
|
<div class="col col-4">
|
||||||
|
<h3>nodes</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col col-4">
|
||||||
|
<div class="float-right">
|
||||||
|
<span data-toggle="tooltip" data-placement="right" title="Recheck all IP addresses of all nodes">
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm" onClick="backfillips(this);">Backfill IP addresses</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<p></p>
|
|
||||||
<table id="nodes" class="display" style="width:100%">
|
<table id="nodes" class="display" style="width:100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -52,7 +62,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('rest.expireNodeList', nodeId=node.id) }}">
|
<a class="nodeco" href="{{ url_for('rest.expireNode', nodeId=node.id) }}">
|
||||||
<i class="fas fa-plug"></i>
|
<i class="fas fa-plug"></i>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
124
app/templates/policy.html
Normal file
124
app/templates/policy.html
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row data justify-content-between">
|
||||||
|
<div class="col col-4">
|
||||||
|
<h3>policy</h3>
|
||||||
|
<footer class="blockquote-footer">
|
||||||
|
for more info see <a href="https://tailscale.com/kb/1337/acl-syntax" target="_blank">tailscale docs</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<div class="col col-4">
|
||||||
|
<div class="float-right">
|
||||||
|
last update:
|
||||||
|
<span data-toggle="tooltip"
|
||||||
|
data-placement="right"
|
||||||
|
title="{{ policy.updatedAt | fmt_datetime }}">
|
||||||
|
{{policy.updatedAt | htime_dt }}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm plain"><a class="plain" href="{{ url_for('main.policy', action='view') }}" target="_blank">View</a></button>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm plain"><a class="plain" href="{{ url_for('main.policy', action='download') }}">Download</a></button>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm plain" data-toggle="modal" data-target="#uploadACL">Upload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-6">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-4"><h5>source</h5></div>
|
||||||
|
<div class="col col-8"><h5>destination</h5></div>
|
||||||
|
</div>
|
||||||
|
{% for acl in policy.policy['acls'] %}
|
||||||
|
<div class="row data">
|
||||||
|
<div class="col col-4">
|
||||||
|
{{ ','.join(acl['src']) }}</div>
|
||||||
|
<div class="col col-8">{{ ',<br />'.join(acl['dst']) | safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col col-6">
|
||||||
|
<!-- groups -->
|
||||||
|
<div id="groups">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" id="groupsHeading">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<button class="btn btn-link" data-toggle="collapse" data-target="#groupsContent" aria-expanded="true" aria-controls="groupsContent">
|
||||||
|
groups
|
||||||
|
</button>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div id="groupsContent" class="collapse show" aria-labelledby="groupsHeading" data-parent="#groups">
|
||||||
|
<div class="card-body">
|
||||||
|
{% for group,users in policy.policy['groups'].items() %}
|
||||||
|
<div class="row data">
|
||||||
|
<div class="col col-6">
|
||||||
|
{{ group }}
|
||||||
|
</div>
|
||||||
|
<div class="col col-6">
|
||||||
|
{{ ", ".join(users) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- hosts -->
|
||||||
|
<div class="hosts">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" id="hostsHeading">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<button class="btn btn-link" data-toggle="collapse" data-target="#hostsContent" aria-expanded="true" aria-controls="hostsContent">
|
||||||
|
hosts
|
||||||
|
</button>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div id="hostsContent" class="collapse" aria-labelledby="hostsHeading" data-parent="#hosts">
|
||||||
|
<div class="card-body">
|
||||||
|
{% for host, value in policy.policy['hosts'].items() %}
|
||||||
|
<div class="row data">
|
||||||
|
<div class="col col3">
|
||||||
|
{{ host }}
|
||||||
|
</div>
|
||||||
|
<div class="col col3">
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- upload acl modal -->
|
||||||
|
<div class="modal fade" id="uploadACL" tabindex="-1" role="dialog" aria-labelledby="uploadACL" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="renameModalLabel">upload ACL</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="file" name="upload" id="upload">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span id="output"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" onClick="uploadACL(this);">Upload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h5>
|
<h3>
|
||||||
Routing table
|
routing table
|
||||||
<small class="text-muted">
|
<span class="text-muted" style="font-size: 60%;">
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<footer class="blockquote-footer">
|
||||||
click on the icon in <em>enabled</em> column to toggle route status
|
click on the icon in <em>enabled</em> column to toggle route status
|
||||||
</small>
|
</footer>
|
||||||
</h5>
|
|
||||||
<hr>
|
<hr>
|
||||||
<p></p>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-12">
|
<div class="col col-12">
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
<h3>{{ user.name }}</h3>
|
<h3>{{ user.name }}</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<p></p>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-3">
|
<div class="col col-3">
|
||||||
<strong>registered</strong>
|
<strong>registered</strong>
|
||||||
@@ -49,7 +48,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('rest.expireNodeUser', nodeId=node.id) }}">
|
<a class="nodeco" href="{{ url_for('rest.expireNode', nodeId=node.id) }}">
|
||||||
<i class="fas fa-plug"></i>
|
<i class="fas fa-plug"></i>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
@@ -57,7 +56,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('rest.deleteNodeUser', 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>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>users</h3>
|
<h3>users</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<p></p>
|
|
||||||
<table id="users" class="display" style="width:100%">
|
<table id="users" class="display" style="width:100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -17,7 +16,7 @@
|
|||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a class="plain" href="{{ url_for('main.user', userName=user.name)}}">
|
<a class="plain" href="{{ url_for('main.user', userName=user.name) }}">
|
||||||
{{user.name}}
|
{{user.name}}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -31,7 +30,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="no-sort">
|
<td class="no-sort">
|
||||||
<span data-toggle="tooltip" data-placement="right" title="delete">
|
<span data-toggle="tooltip" data-placement="right" title="delete">
|
||||||
<a class="nodeco" href="/user/{{user.name}}/delete">
|
<a class="nodeco" href="{{ url_for('rest.deleteUser', userName=user.name) }}">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2,16 +2,14 @@ import logging
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import render_template, Blueprint, request
|
from flask import render_template, Blueprint
|
||||||
from flask import redirect, session, url_for
|
from flask import redirect, session, url_for
|
||||||
from app import auth
|
from app import auth
|
||||||
|
|
||||||
# from ..lib import username
|
from flask import jsonify, make_response
|
||||||
|
|
||||||
from flask import jsonify
|
|
||||||
from flask_pyoidc.user_session import UserSession
|
from flask_pyoidc.user_session import UserSession
|
||||||
|
|
||||||
from hsapi_client import Node, User, Route, PreAuthKey
|
from hsapi_client import Node, User, Route, PreAuthKey, Policy
|
||||||
from hsapi_client.preauthkeys import v1ListPreAuthKeyRequest
|
from hsapi_client.preauthkeys import v1ListPreAuthKeyRequest
|
||||||
|
|
||||||
|
|
||||||
@@ -26,30 +24,29 @@ def health():
|
|||||||
return jsonify(dict(status="OK", version=current_app.config['APP_VERSION']))
|
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'])
|
@main_blueprint.route('/token', methods=['GET', 'POST'])
|
||||||
@auth.authorize_admins('default')
|
@auth.authorize_admins('default')
|
||||||
def token():
|
def token():
|
||||||
user_session = UserSession(session)
|
user_session = UserSession(session)
|
||||||
# return jsonify(user_session.userinfo)
|
|
||||||
return jsonify(access_token=user_session.access_token,
|
return jsonify(access_token=user_session.access_token,
|
||||||
id_token=user_session.id_token,
|
id_token=user_session.id_token,
|
||||||
userinfo=user_session.userinfo)
|
userinfo=user_session.userinfo)
|
||||||
|
|
||||||
|
|
||||||
|
@main_blueprint.route('/', methods=['GET', 'POST'])
|
||||||
|
@auth.access_control('default')
|
||||||
|
def index():
|
||||||
|
hs_user = auth.username
|
||||||
|
userNodeList = [n for n in Node().list().nodes if n.user.name == hs_user]
|
||||||
|
return render_template('index.html',
|
||||||
|
userNodeList=userNodeList)
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/logout')
|
@main_blueprint.route('/logout')
|
||||||
@auth.oidc_logout
|
@auth.oidc_logout
|
||||||
def logout():
|
def logout():
|
||||||
|
# UserSession(session).clear()
|
||||||
|
session.clear()
|
||||||
return redirect(url_for('main.index'))
|
return redirect(url_for('main.index'))
|
||||||
|
|
||||||
|
|
||||||
@@ -62,12 +59,14 @@ def nodes():
|
|||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/node/<int:nodeId>', methods=['GET'])
|
@main_blueprint.route('/node/<int:nodeId>', methods=['GET'])
|
||||||
@auth.authorize_admins('default')
|
@auth.access_control('default')
|
||||||
def node(nodeId):
|
def node(nodeId):
|
||||||
# There is a bug in HS api with retrieving a single node
|
# There is a bug in HS api with retrieving a single node
|
||||||
# and we added a workaround to hsapi, so node.get() returns a
|
# and we added a workaround to hsapi, so node.get() returns a
|
||||||
# v1Node object instead of v1NodeResponse, so we access directly
|
# v1Node object instead of v1NodeResponse, so we access directly
|
||||||
# `node`, instead of `node.node`
|
# `node`, instead of `node.node`
|
||||||
|
if not auth.userOrAdmin(auth.username):
|
||||||
|
return auth.unathorized
|
||||||
node = Node().get(nodeId)
|
node = Node().get(nodeId)
|
||||||
routes = Node().routes(nodeId)
|
routes = Node().routes(nodeId)
|
||||||
isExitNode = any(
|
isExitNode = any(
|
||||||
@@ -131,3 +130,25 @@ def routes():
|
|||||||
return render_template("routes.html",
|
return render_template("routes.html",
|
||||||
exitNodes=exitNodes,
|
exitNodes=exitNodes,
|
||||||
routes=final)
|
routes=final)
|
||||||
|
|
||||||
|
|
||||||
|
@main_blueprint.route('/policy', defaults={'action': None}, methods=['GET'])
|
||||||
|
@main_blueprint.route('/policy/<action>', methods=['GET'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def policy(action):
|
||||||
|
policy = Policy().get()
|
||||||
|
if action == "view":
|
||||||
|
return policy.json
|
||||||
|
|
||||||
|
elif action == "download":
|
||||||
|
updateStr = policy.updatedAt.strftime(format='%Y%m%d-%H%M')
|
||||||
|
log.debug(updateStr)
|
||||||
|
filename = f"acl-{updateStr}.json"
|
||||||
|
response = make_response(policy.json)
|
||||||
|
response.headers['Content-Disposition'] = f'attachment; filename={
|
||||||
|
filename}'
|
||||||
|
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
return render_template("policy.html",
|
||||||
|
policy=policy)
|
||||||
|
|||||||
@@ -4,18 +4,21 @@ from flask import Blueprint, request
|
|||||||
from flask import redirect, url_for
|
from flask import redirect, url_for
|
||||||
from app import auth
|
from app import auth
|
||||||
|
|
||||||
from ..lib import login_name, username
|
from flask import jsonify, make_response
|
||||||
|
|
||||||
from flask import jsonify
|
from hsapi_client import Node, User, Route, PreAuthKey, Policy
|
||||||
|
|
||||||
from hsapi_client import Node, User, Route, PreAuthKey
|
|
||||||
from hsapi_client.preauthkeys import (v1CreatePreAuthKeyRequest,
|
from hsapi_client.preauthkeys import (v1CreatePreAuthKeyRequest,
|
||||||
v1ExpirePreAuthKeyRequest)
|
v1ExpirePreAuthKeyRequest)
|
||||||
|
from hsapi_client.policies import v1Policy
|
||||||
|
|
||||||
|
from hsapi_client.config import HTTPException
|
||||||
|
|
||||||
|
from app.lib import remote_ip
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
# REST calls
|
|
||||||
|
|
||||||
|
# REST calls
|
||||||
rest_blueprint = Blueprint(
|
rest_blueprint = Blueprint(
|
||||||
'rest', __name__, url_prefix=os.getenv('APPLICATION_ROOT', '/'))
|
'rest', __name__, url_prefix=os.getenv('APPLICATION_ROOT', '/'))
|
||||||
|
|
||||||
@@ -23,98 +26,58 @@ rest_blueprint = Blueprint(
|
|||||||
@rest_blueprint.route('/routeToggle/<int:routeId>', methods=['GET'])
|
@rest_blueprint.route('/routeToggle/<int:routeId>', methods=['GET'])
|
||||||
@auth.authorize_admins('default')
|
@auth.authorize_admins('default')
|
||||||
def routeToggle(routeId: int):
|
def routeToggle(routeId: int):
|
||||||
routes = Route().list()
|
route = Route().get(routeId)
|
||||||
route = [r for r in routes.routes if r.id == routeId]
|
|
||||||
if route:
|
if route:
|
||||||
route = route[0]
|
|
||||||
if route.enabled:
|
if route.enabled:
|
||||||
action = 'disabled'
|
action = 'disabled'
|
||||||
Route().disable(routeId)
|
|
||||||
else:
|
else:
|
||||||
Route().enable(routeId)
|
|
||||||
action = 'enabled'
|
action = 'enabled'
|
||||||
log.info(
|
log.info(
|
||||||
f"route '{route.prefix}' via '{route.node.givenName}'"
|
f"route '{route.prefix}' via '{route.node.givenName}' "
|
||||||
f"{action} by '{username()}'")
|
f"{action} by '{auth.username}@{remote_ip()}'")
|
||||||
return redirect(url_for("main.routes"))
|
Route().toggle(routeId)
|
||||||
|
return redirect(request.referrer)
|
||||||
|
|
||||||
|
|
||||||
@rest_blueprint.route('/node/<int:nodeId>/expire', methods=['GET'])
|
@rest_blueprint.route('/node/<int:nodeId>/expire', methods=['GET'])
|
||||||
@auth.authorize_admins('default')
|
@auth.access_control('default')
|
||||||
def expireNode(nodeId: int):
|
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>/user-expire', methods=['GET'])
|
|
||||||
@auth.authorize_admins('default')
|
|
||||||
def expireNodeUser(nodeId: int):
|
|
||||||
"""
|
"""
|
||||||
This expires a node from the node page.
|
This expires a node from the node page.
|
||||||
The difference from above is that it returns to the /node/nodeId page
|
The difference from above is that it returns to the /node/nodeId page
|
||||||
"""
|
"""
|
||||||
node = Node().get(nodeId)
|
node = Node().get(nodeId)
|
||||||
userName = node.user.name
|
if not auth.userOrAdmin(node.user.name):
|
||||||
|
return auth.unathorized
|
||||||
Node().expire(nodeId)
|
Node().expire(nodeId)
|
||||||
log.info(f"node '{nodeId}' expired by '{username()}'")
|
log.info(f"node '{nodeId}' expired by '{auth.username}@{remote_ip()}'")
|
||||||
return redirect(url_for("main.user", userName=userName))
|
return redirect(request.referrer)
|
||||||
|
|
||||||
|
|
||||||
@rest_blueprint.route('/node/<int:nodeId>/list-expire', methods=['GET'])
|
@rest_blueprint.route('/node/<int:nodeId>/delete', methods=['GET'])
|
||||||
@auth.authorize_admins('default')
|
@auth.access_control('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):
|
def deleteNode(nodeId: int):
|
||||||
Node().delete(nodeId)
|
|
||||||
log.info(f"node '{nodeId}' deleted by '{username()}'")
|
|
||||||
return redirect(url_for("main.nodes"))
|
|
||||||
|
|
||||||
|
|
||||||
@rest_blueprint.route('/node/<int:nodeId>/delete-own', methods=['GET'])
|
|
||||||
@auth.access_control('default')
|
|
||||||
def deleteOwnNode(nodeId: int):
|
|
||||||
node = Node().get(nodeId)
|
node = Node().get(nodeId)
|
||||||
if node.user.name != username():
|
if not auth.userOrAdmin(node.user.name):
|
||||||
response = jsonify({'message': 'not authorized'})
|
return auth.unathorized
|
||||||
return response, 401
|
Node().expire(nodeId)
|
||||||
Node().delete(nodeId)
|
Node().delete(nodeId)
|
||||||
log.info(f"'{username()}' delete their own node '{nodeId}'")
|
log.info(f"node '{nodeId}' deleted by '{auth.username}@{remote_ip()}'")
|
||||||
return redirect(url_for("main.index"))
|
return redirect(request.referrer)
|
||||||
|
|
||||||
|
|
||||||
@rest_blueprint.route('/node/<int:nodeId>/delete-user', methods=['GET'])
|
@rest_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET'])
|
||||||
@auth.access_control('default')
|
@auth.access_control('default')
|
||||||
def deleteNodeUser(nodeId: int):
|
|
||||||
node = Node().get(nodeId)
|
|
||||||
Node().delete(nodeId)
|
|
||||||
log.info(f"'{username()}' delete their own node '{nodeId}'")
|
|
||||||
return redirect(url_for("main.user", userName=node.user.name))
|
|
||||||
|
|
||||||
|
|
||||||
@ rest_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET'])
|
|
||||||
@ auth.authorize_admins('default')
|
|
||||||
def renameNode(nodeId: int, newName: str):
|
def renameNode(nodeId: int, newName: str):
|
||||||
|
node = Node().get(nodeId)
|
||||||
|
if not auth.userOrAdmin(node.user.name):
|
||||||
|
return auth.unathorized
|
||||||
Node().rename(nodeId, newName)
|
Node().rename(nodeId, newName)
|
||||||
return jsonify(dict(newName=newName))
|
return jsonify(dict(newName=newName))
|
||||||
|
|
||||||
|
|
||||||
@ rest_blueprint.route('/user/<userName>/delete', methods=['GET'])
|
@rest_blueprint.route('/user/<userName>/delete', methods=['GET'])
|
||||||
@ auth.authorize_admins('default')
|
@auth.authorize_admins('default')
|
||||||
def deleteUser(userName: str):
|
def deleteUser(userName: str):
|
||||||
nodes = Node().byUser(userName)
|
nodes = Node().byUser(userName)
|
||||||
for node in nodes.nodes:
|
for node in nodes.nodes:
|
||||||
@@ -124,11 +87,10 @@ def deleteUser(userName: str):
|
|||||||
return redirect(url_for("main.users"))
|
return redirect(url_for("main.users"))
|
||||||
|
|
||||||
|
|
||||||
@ rest_blueprint.route('/user/<userName>/pakcreate', methods=['POST'])
|
@rest_blueprint.route('/user/<userName>/pakcreate', methods=['POST'])
|
||||||
@ auth.authorize_admins('default')
|
@auth.authorize_admins('default')
|
||||||
def createPKA(userName: str):
|
def createPKA(userName: str):
|
||||||
data = request.json
|
data = request.json
|
||||||
log.debug(data)
|
|
||||||
expiration = f"{data['expiration']}:00Z"
|
expiration = f"{data['expiration']}:00Z"
|
||||||
req = v1CreatePreAuthKeyRequest(user=userName,
|
req = v1CreatePreAuthKeyRequest(user=userName,
|
||||||
reusable=data['reusable'],
|
reusable=data['reusable'],
|
||||||
@@ -138,11 +100,35 @@ def createPKA(userName: str):
|
|||||||
return jsonify(dict(key=pak.preAuthKey.key))
|
return jsonify(dict(key=pak.preAuthKey.key))
|
||||||
|
|
||||||
|
|
||||||
@ rest_blueprint.route('/user/<userName>/expire/<key>', methods=['GET'])
|
@rest_blueprint.route('/user/<userName>/expire/<key>', methods=['GET'])
|
||||||
@ auth.authorize_admins('default')
|
@auth.authorize_admins('default')
|
||||||
def expirePKA(userName: str, key: str):
|
def expirePKA(userName: str, key: str):
|
||||||
log.debug(key)
|
|
||||||
req = v1ExpirePreAuthKeyRequest(user=userName, key=key)
|
req = v1ExpirePreAuthKeyRequest(user=userName, key=key)
|
||||||
|
|
||||||
PreAuthKey().expire(req)
|
PreAuthKey().expire(req)
|
||||||
return redirect(url_for('main.user', userName=userName))
|
return redirect(url_for('main.user', userName=userName))
|
||||||
|
|
||||||
|
|
||||||
|
@rest_blueprint.route('/backfillips', methods=['POST'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def backfillips():
|
||||||
|
response = Node().backfillips()
|
||||||
|
return jsonify(response.changes)
|
||||||
|
|
||||||
|
|
||||||
|
@rest_blueprint.route('/policy/upload', methods=['POST'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def policyUpload():
|
||||||
|
file = request.files['file']
|
||||||
|
try:
|
||||||
|
acl = ''.join(map(bytes.decode, file.readlines()))
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
log.debug(e)
|
||||||
|
return jsonify(message=str(e)), 422
|
||||||
|
|
||||||
|
policy = Policy()
|
||||||
|
try:
|
||||||
|
policy.put(acl)
|
||||||
|
except HTTPException as e:
|
||||||
|
return jsonify(message=str(e.message)), 422
|
||||||
|
return jsonify(message="acl updated"), 200
|
||||||
|
|||||||
11
config.py
11
config.py
@@ -13,15 +13,21 @@ class BaseConfig(object):
|
|||||||
APP_PREFIX = os.getenv('APP_PREFIX', '')
|
APP_PREFIX = os.getenv('APP_PREFIX', '')
|
||||||
DEBUG_TB_ENABLED = False
|
DEBUG_TB_ENABLED = False
|
||||||
WTF_CSRF_ENABLED = False
|
WTF_CSRF_ENABLED = False
|
||||||
|
# Session
|
||||||
|
# We store sessions in filesystem, max 100 files, expire in 2 hours
|
||||||
|
SESSION_TYPE = 'filesystem'
|
||||||
|
SESSION_FILE_THRESHOLD = 100
|
||||||
|
PERMANENT_SESSION_LIFETIME = 7200
|
||||||
|
|
||||||
# All the followinf vars can be overriden
|
# All the following vars can be overriden
|
||||||
# in the environment, using `HSMAN_` prefix
|
# in the environment, using `HSMAN_` prefix
|
||||||
SECRET_KEY = "secreto"
|
SECRET_KEY = "secreto"
|
||||||
ADMIN_GROUPS = ["adminGroup"]
|
ADMIN_GROUPS = "adminGroup"
|
||||||
OIDC_CLIENT_ID = 'client-id'
|
OIDC_CLIENT_ID = 'client-id'
|
||||||
OIDC_CLIENT_SECRET = 'client-secreto'
|
OIDC_CLIENT_SECRET = 'client-secreto'
|
||||||
OIDC_URL = "https://myidp.example.com/auth"
|
OIDC_URL = "https://myidp.example.com/auth"
|
||||||
OIDC_REDIRECT_URI = 'http://localhost:5000/auth'
|
OIDC_REDIRECT_URI = 'http://localhost:5000/auth'
|
||||||
|
OIDC_CLOCK_SKEW = 30
|
||||||
|
|
||||||
# These are required by hsapi, should not be defined here
|
# These are required by hsapi, should not be defined here
|
||||||
# HSAPI_SERVER = "https://headscale.example.com"
|
# HSAPI_SERVER = "https://headscale.example.com"
|
||||||
@@ -38,6 +44,7 @@ class DevelopmentConfig(BaseConfig):
|
|||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
ENVIRONMENT = "develop"
|
ENVIRONMENT = "develop"
|
||||||
|
TEMPLATES_AUTO_RELOAD = True
|
||||||
|
|
||||||
|
|
||||||
class TestingConfig(BaseConfig):
|
class TestingConfig(BaseConfig):
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ RUN apk --update --no-cache add \
|
|||||||
libffi-dev \
|
libffi-dev \
|
||||||
curl && \
|
curl && \
|
||||||
chmod g+w /run && \
|
chmod g+w /run && \
|
||||||
pip install poetry gunicorn
|
pip install poetry gunicorn poetry-plugin-export
|
||||||
|
|
||||||
COPY . /hsman
|
COPY . /hsman
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ preload_app = True
|
|||||||
|
|
||||||
# logconfig = "app/logging/production.ini"
|
# logconfig = "app/logging/production.ini"
|
||||||
logconfig = "app/logging/production.ini"
|
logconfig = "app/logging/production.ini"
|
||||||
# access_log_format = "%(h)s %(l)s %(t)s %(r)s %(s)s %(b)s %(f)s %(a)s"
|
access_log_format = "%(h)s %({x-forwarded-for}i)s %(r)s %(s)s %(b)s %(L)s"
|
||||||
# Log to stdout.
|
# Log to stdout.
|
||||||
accesslog = "-"
|
accesslog = "-"
|
||||||
errorlog = "-"
|
errorlog = "-"
|
||||||
|
|||||||
1293
poetry.lock
generated
1293
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "hsman"
|
name = "hsman"
|
||||||
version = "0.9.6"
|
version = "0.9.24"
|
||||||
description = "Flask Admin webui for Headscale"
|
description = "Flask Admin webui for Headscale"
|
||||||
authors = ["Andrea Mistrali <andrea@mistrali.pw>"]
|
authors = ["Andrea Mistrali <andrea@mistrali.pw>"]
|
||||||
license = "BSD"
|
license = "BSD"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.11,<4.0"
|
python = ">=3.11,<4.0"
|
||||||
@@ -15,7 +16,9 @@ flask-mobility = "^2.0.1"
|
|||||||
humanize = "^4.9.0"
|
humanize = "^4.9.0"
|
||||||
flask-pydantic = "^0.12.0"
|
flask-pydantic = "^0.12.0"
|
||||||
uvicorn = "^0.30.1"
|
uvicorn = "^0.30.1"
|
||||||
hsapi-client = "^0.9.2"
|
hsapi-client = "^0.9.9"
|
||||||
|
# hsapi_client = { path = "../hsapi-client", develop = true }
|
||||||
|
flask-session = "^0.8.0"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|||||||
3
wsgi.py
3
wsgi.py
@@ -1,6 +1,5 @@
|
|||||||
from app import create_app
|
from app import create_app
|
||||||
from app import lib
|
from app import lib
|
||||||
from app import models
|
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
import os
|
import os
|
||||||
@@ -20,7 +19,7 @@ log.debug(f"Running in web mode: {lib.webMode()}")
|
|||||||
def get_context():
|
def get_context():
|
||||||
# flask cli context setup
|
# flask cli context setup
|
||||||
"""Objects exposed here will be automatically available from the shell."""
|
"""Objects exposed here will be automatically available from the shell."""
|
||||||
return dict(app=app, models=models)
|
return dict(app=app)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in New Issue
Block a user