mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-07-06 17:14:07 +00:00
Init
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
config.yaml
|
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
10
.idea/OutlineFleet.iml
generated
Normal file
10
.idea/OutlineFleet.iml
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (OutlineFleet)" project-jdk-type="Python SDK" />
|
||||
<component name="PyCharmProfessionalAdvertiser">
|
||||
<option name="shown" value="true" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/OutlineFleet.iml" filepath="$PROJECT_DIR$/.idea/OutlineFleet.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
70
lib.py
Normal file
70
lib.py
Normal file
@ -0,0 +1,70 @@
|
||||
import logging
|
||||
from typing import TypedDict, List
|
||||
from outline_vpn.outline_vpn import OutlineKey, OutlineVPN
|
||||
import yaml
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%d-%m-%Y %H:%M:%S')
|
||||
log = logging.getLogger('OutlineFleet.lib')
|
||||
|
||||
|
||||
class ServerDict(TypedDict):
|
||||
name: str
|
||||
url: str
|
||||
cert: str
|
||||
comment: str
|
||||
server_id: str
|
||||
metrics_enabled: str
|
||||
created_timestamp_ms: int
|
||||
version: str
|
||||
port_for_new_access_keys: int
|
||||
hostname_for_access_keys: str
|
||||
keys: List[OutlineKey]
|
||||
|
||||
|
||||
class Server:
|
||||
def __init__(self,
|
||||
url: str,
|
||||
cert: str,
|
||||
comment: str,
|
||||
):
|
||||
self.client = OutlineVPN(api_url=url, cert_sha256=cert)
|
||||
self.data: ServerDict = {
|
||||
'name': self.client.get_server_information()["name"],
|
||||
'url': url,
|
||||
'cert': cert,
|
||||
'comment': comment,
|
||||
'server_id': self.client.get_server_information()["serverId"],
|
||||
'metrics_enabled': self.client.get_server_information()["metricsEnabled"],
|
||||
'created_timestamp_ms': self.client.get_server_information()["createdTimestampMs"],
|
||||
'version': self.client.get_server_information()["version"],
|
||||
'port_for_new_access_keys': self.client.get_server_information()["portForNewAccessKeys"],
|
||||
'hostname_for_access_keys': self.client.get_server_information()["hostnameForAccessKeys"],
|
||||
'keys': self.client.get_keys()
|
||||
}
|
||||
|
||||
def info(self) -> ServerDict:
|
||||
return self.data
|
||||
|
||||
def apply_config(self, config):
|
||||
if config.get("name"):
|
||||
self.client.set_server_name(config.get("name"))
|
||||
log.info("Changed %s name to '%s'", self.data["server_id"], config.get("name"))
|
||||
if config.get("metrics"):
|
||||
self.client.set_metrics_status(True if config.get("metrics") == 'True' else False)
|
||||
log.info("Changed %s metrics status to '%s'", self.data["server_id"], config.get("metrics"))
|
||||
if config.get("port_for_new_access_keys"):
|
||||
self.client.set_port_new_for_access_keys(int(config.get("port_for_new_access_keys")))
|
||||
log.info("Changed %s port_for_new_access_keys to '%s'", self.data["server_id"], config.get("port_for_new_access_keys"))
|
||||
if config.get("hostname_for_access_keys"):
|
||||
self.client.set_hostname(config.get("hostname_for_access_keys"))
|
||||
log.info("Changed %s hostname_for_access_keys to '%s'", self.data["server_id"], config.get("hostname_for_access_keys"))
|
||||
if config.get("comment"):
|
||||
with open("config.yaml", "r") as file:
|
||||
config_file = yaml.safe_load(file) or {}
|
||||
config_file["servers"][self.data['server_id']]['comment'] = config.get("comment")
|
||||
with open("config.yaml", "w") as file:
|
||||
yaml.safe_dump(config_file, file)
|
||||
log.info("Changed %s comment to '%s'", self.data["server_id"], config.get("comment"))
|
91
main.py
Normal file
91
main.py
Normal file
@ -0,0 +1,91 @@
|
||||
from outline_vpn.outline_vpn import OutlineVPN
|
||||
import yaml
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Flask, render_template, request, url_for, redirect
|
||||
|
||||
from lib import Server
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%d-%m-%Y %H:%M:%S')
|
||||
log = logging.getLogger('OutlineFleet')
|
||||
|
||||
SERVERS = list()
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def format_timestamp(ts):
|
||||
return datetime.fromtimestamp(ts//1000).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
|
||||
def update_state():
|
||||
global SERVERS
|
||||
SERVERS = list()
|
||||
config = dict()
|
||||
try:
|
||||
with open("config.yaml", "r") as file:
|
||||
config = yaml.safe_load(file)
|
||||
except:
|
||||
with open("config.yaml", "w"):
|
||||
pass
|
||||
|
||||
if config:
|
||||
servers = config.get('servers', None)
|
||||
for server_id, config in servers.items():
|
||||
server = Server(url=config["url"], cert=config["cert"], comment=config["comment"])
|
||||
SERVERS.append(server)
|
||||
log.info("Server found: %s", server.info()["name"])
|
||||
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
if request.method == 'GET':
|
||||
return render_template(
|
||||
'index.html',
|
||||
SERVERS=SERVERS,
|
||||
nt=request.args.get('nt'),
|
||||
nl=request.args.get('nl'),
|
||||
selected_server=request.args.get('selected_server'),
|
||||
format_timestamp=format_timestamp)
|
||||
else:
|
||||
server = request.form['server_id']
|
||||
server = next((item for item in SERVERS if item.info()["server_id"] == server), None)
|
||||
server.apply_config(request.form)
|
||||
update_state()
|
||||
return redirect(url_for('index', nt="Updated Outline VPN Server", selected_server=request.args.get('selected_server')))
|
||||
|
||||
|
||||
@app.route('/add_server', methods=['GET', 'POST'])
|
||||
def add_server():
|
||||
if request.method == 'GET':
|
||||
return render_template('add_server.html')
|
||||
else:
|
||||
with open("config.yaml", "r") as file:
|
||||
config = yaml.safe_load(file) or {}
|
||||
|
||||
servers = config.get('servers', dict())
|
||||
|
||||
try:
|
||||
new_server = Server(url=request.form['url'], cert=request.form['cert'], comment=request.form['comment'])
|
||||
except:
|
||||
return redirect(url_for('index', nt="Couldn't access Outline VPN Server", nl="error"))
|
||||
|
||||
servers[new_server.data["server_id"]] = {
|
||||
'name': new_server.data["name"],
|
||||
'url': new_server.data["url"],
|
||||
'comment': new_server.data["comment"],
|
||||
'cert': request.form['cert']
|
||||
}
|
||||
config["servers"] = servers
|
||||
with open("config.yaml", "w") as file:
|
||||
yaml.safe_dump(config, file)
|
||||
update_state()
|
||||
return redirect(url_for('index', nt="Added Outline VPN Server"))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
update_state()
|
||||
app.run()
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
outline-vpn-api
|
||||
PyYAML
|
||||
Flask
|
355
static/layout.css
Normal file
355
static/layout.css
Normal file
@ -0,0 +1,355 @@
|
||||
/*
|
||||
* -- BASE STYLES --
|
||||
* Most of these are inherited from Base, but I want to change a few.
|
||||
*/
|
||||
body {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #1b98f8;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* -- HELPER STYLES --
|
||||
* Over-riding some of the .pure-button styles to make my buttons look unique
|
||||
*/
|
||||
.primary-button,
|
||||
.secondary-button {
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.primary-button {
|
||||
color: #fff;
|
||||
background: #1b98f8;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.secondary-button {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
color: #666;
|
||||
padding: 0.5em 2em;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
* -- LAYOUT STYLES --
|
||||
* This layout consists of three main elements, `#nav` (navigation bar), `#list` (server list), and `#main` (server content). All 3 elements are within `#layout`
|
||||
*/
|
||||
#layout, #nav, #list, #main {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Make the navigation 100% width on phones */
|
||||
#nav {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
background: rgb(37, 42, 58);
|
||||
text-align: center;
|
||||
}
|
||||
/* Show the "Menu" button on phones */
|
||||
#nav .nav-menu-button {
|
||||
display: block;
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* When "Menu" is clicked, the navbar should be 80% height */
|
||||
#nav.active {
|
||||
height: 80%;
|
||||
}
|
||||
/* Don't show the navigation items... */
|
||||
.nav-inner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ...until the "Menu" button is clicked */
|
||||
#nav.active .nav-inner {
|
||||
display: block;
|
||||
padding: 2em 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* -- NAV BAR STYLES --
|
||||
* Styling the default .pure-menu to look a little more unique.
|
||||
*/
|
||||
#nav .pure-menu {
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
#nav .pure-menu-link:hover,
|
||||
#nav .pure-menu-link:focus {
|
||||
background: rgb(55, 60, 90);
|
||||
}
|
||||
#nav .pure-menu-link {
|
||||
color: #fff;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
#nav .pure-menu-heading {
|
||||
border-bottom: none;
|
||||
font-size:110%;
|
||||
color: rgb(75, 113, 151);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* -- server STYLES --
|
||||
* Styles relevant to the server messages, labels, counts, and more.
|
||||
*/
|
||||
.server-count {
|
||||
color: rgb(75, 113, 151);
|
||||
}
|
||||
|
||||
.server-label-personal,
|
||||
.server-label-work,
|
||||
.server-label-travel {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.server-label-personal {
|
||||
background: #ffc94c;
|
||||
}
|
||||
.server-label-work {
|
||||
background: #41ccb4;
|
||||
}
|
||||
.server-label-travel {
|
||||
background: #40c365;
|
||||
}
|
||||
|
||||
|
||||
/* server Item Styles */
|
||||
.server-item {
|
||||
padding: 0.9em 1em;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-left: 6px solid transparent;
|
||||
}
|
||||
.server-name {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.server-name,
|
||||
.server-info {
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
}
|
||||
.server-info {
|
||||
color: #999;
|
||||
font-size: 80%;
|
||||
}
|
||||
.server-comment {
|
||||
font-size: 90%;
|
||||
margin: 0.4em 0;
|
||||
}
|
||||
.server-add {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 150%;
|
||||
color: #999;
|
||||
}
|
||||
.server-add:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.server-item-selected {
|
||||
background: #eee;
|
||||
}
|
||||
.server-item-unread {
|
||||
border-left: 6px solid #1b98f8;
|
||||
}
|
||||
|
||||
/* server Content Styles */
|
||||
.server-content-header, .server-content-body, .server-content-footer {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
.server-content-header {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.server-content-title {
|
||||
margin: 0.5em 0 0;
|
||||
}
|
||||
.server-content-subtitle {
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
.server-content-subtitle span {
|
||||
color: #999;
|
||||
}
|
||||
.server-content-controls {
|
||||
margin-top: 2em;
|
||||
text-align: right;
|
||||
}
|
||||
.server-content-controls .secondary-button {
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.server-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* -- TABLET (AND UP) MEDIA QUERIES --
|
||||
* On tablets and other medium-sized devices, we want to customize some
|
||||
* of the mobile styles.
|
||||
*/
|
||||
@media (min-width: 40em) {
|
||||
|
||||
/* Move the layout over so we can fit the nav + list in on the left */
|
||||
#layout {
|
||||
padding-left:500px; /* "left col (nav + list)" width */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* These are position:fixed; elements that will be in the left 500px of the screen */
|
||||
#nav, #list {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
#nav {
|
||||
margin-left:-500px; /* "left col (nav + list)" width */
|
||||
width:150px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Show the menu items on the larger screen */
|
||||
.nav-inner {
|
||||
display: block;
|
||||
padding: 2em 0;
|
||||
}
|
||||
|
||||
/* Hide the "Menu" button on larger screens */
|
||||
#nav .nav-menu-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#list {
|
||||
margin-left: -350px;
|
||||
width: 100%;
|
||||
height: 33%;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#main {
|
||||
position: fixed;
|
||||
top: 33%;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 150px;
|
||||
overflow: auto;
|
||||
width: auto; /* so that it's not 100% */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* -- DESKTOP (AND UP) MEDIA QUERIES --
|
||||
* On desktops and other large-sized devices, we want to customize some
|
||||
* of the mobile styles.
|
||||
*/
|
||||
@media (min-width: 60em) {
|
||||
|
||||
/* This will take up the entire height, and be a little thinner */
|
||||
#list {
|
||||
margin-left: -350px;
|
||||
width:350px;
|
||||
height: 100%;
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* This will now take up it's own column, so don't need position: fixed; */
|
||||
#main {
|
||||
position: static;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.alert {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
line-height: 1.8;
|
||||
border-radius: 5px;
|
||||
cursor: hand;
|
||||
cursor: pointer;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.alertCheckbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:checked + .alert {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alertText {
|
||||
display: table;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.alertClose {
|
||||
float: right;
|
||||
padding-top: 0px;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #DDD;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: #EFE;
|
||||
border: 1px solid #DED;
|
||||
color: #9A9;
|
||||
}
|
||||
|
||||
.notice {
|
||||
background-color: #EFF;
|
||||
border: 1px solid #DEE;
|
||||
color: #9AA;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: #FDF7DF;
|
||||
border: 1px solid #FEEC6F;
|
||||
color: #C9971C;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #FEE;
|
||||
border: 1px solid #EDD;
|
||||
color: #A66;
|
||||
}
|
11
static/pure.css
Normal file
11
static/pure.css
Normal file
File diff suppressed because one or more lines are too long
20
templates/add_server.html
Normal file
20
templates/add_server.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Add new server{% endblock %}
|
||||
{% block content %}
|
||||
<form class="pure-form pure-form-stacked" method="POST">
|
||||
<fieldset>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<input type="text" class="pure-u-23-24" name="url" placeholder="Server management URL"/>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<input type="text"class="pure-u-23-24" name="cert" placeholder="Certificate"/>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<input type="text" class="pure-u-23-24" name="comment" placeholder="Comment"/>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="pure-button pure-input-1 pure-button-primary">Add</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endblock %}
|
90
templates/base.html
Normal file
90
templates/base.html
Normal file
@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='pure.css') }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='layout.css') }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="layout" class="content pure-g">
|
||||
<div id="nav" class="pure-u-1-3">
|
||||
<a href="#" id="menuLink" class="nav-menu-button">Menu</a>
|
||||
|
||||
<div class="nav-inner">
|
||||
<button onclick="location.href='/';" style="cursor:pointer;" class="primary-button pure-button">OutlineFleet</button>
|
||||
|
||||
<div class="pure-menu custom-restricted-width">
|
||||
<ul class="pure-menu-list">
|
||||
<li class="pure-menu-item"><a href="/" class="pure-menu-link">Servers</a></li>
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link">Clients</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<!-- Script to make the Menu link work -->
|
||||
<!-- Just stripped down version of the js/ui.js script for the side-menu layout -->
|
||||
<script>
|
||||
function getElements() {
|
||||
return {
|
||||
menu: document.getElementById('nav'),
|
||||
menuLink: document.getElementById('menuLink')
|
||||
};
|
||||
}
|
||||
|
||||
function toggleClass(element, className) {
|
||||
var classes = element.className.split(/\s+/);
|
||||
var length = classes.length;
|
||||
var i = 0;
|
||||
|
||||
for (; i < length; i++) {
|
||||
if (classes[i] === className) {
|
||||
classes.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The className is not found
|
||||
if (length === classes.length) {
|
||||
classes.push(className);
|
||||
}
|
||||
|
||||
element.className = classes.join(' ');
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
var active = 'active';
|
||||
var elements = getElements();
|
||||
|
||||
toggleClass(elements.menu, active);
|
||||
}
|
||||
|
||||
function handleEvent(e) {
|
||||
var elements = getElements();
|
||||
|
||||
if (e.target.id === elements.menuLink.id) {
|
||||
toggleMenu();
|
||||
e.preventDefault();
|
||||
} else if (elements.menu.className.indexOf('active') !== -1) {
|
||||
toggleMenu();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.addEventListener('click', handleEvent);
|
||||
});
|
||||
</script>
|
||||
{% if nt %}
|
||||
<label>
|
||||
<input type="checkbox" class="alertCheckbox" autocomplete="off" />
|
||||
<div class="alert {% if nl == 'error' %}error{% else %}success{% endif %}">
|
||||
<span class="alertText">{{nt}}
|
||||
<br class="clear"/></span>
|
||||
</div>
|
||||
</label>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
105
templates/index.html
Normal file
105
templates/index.html
Normal file
@ -0,0 +1,105 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="list" class="pure-u-1-3" xmlns="http://www.w3.org/1999/html">
|
||||
<div class="server-item pure-g">
|
||||
<h1 class="server-content-title">Servers</h1>
|
||||
</div>
|
||||
{% for server in SERVERS %}
|
||||
<div class="server-item server-item-{% if loop.index0 == selected_server|int %}unread{% else %}selected{% endif %} pure-g">
|
||||
<div class="pure-u-3-4" onclick="location.href='/?selected_server={{loop.index0}}';">
|
||||
<h5 class="server-name">{{ server.info()["name"] }}</h5>
|
||||
<h4 class="server-info">{{ '/'.join(server.info()["url"].split('/')[0:-1]) }}</h4>
|
||||
<h4 class="server-info">Port {{ server.info()["port_for_new_access_keys"] }}</h4>
|
||||
<h4 class="server-info">Hostname {{ server.info()["hostname_for_access_keys"] }}</h4>
|
||||
<h4 class="server-info">v.{{ server.info()["version"] }}</h4>
|
||||
<p class="server-comment">
|
||||
{{ server.info()["comment"] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div onclick="location.href='/add_server';" class="server-item server-add pure-g">
|
||||
<div class="pure-u-1">
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if SERVERS %}
|
||||
|
||||
{% if server is none %}
|
||||
{% set server = SERVERS[0] %}
|
||||
{% else %}
|
||||
{% set server = SERVERS[selected_server|int] %}
|
||||
{% endif %}
|
||||
<div id="main" class="pure-u-1-3">
|
||||
<div class="server-content">
|
||||
<div class="server-content-header pure-g">
|
||||
<div class="pure-u-1-2">
|
||||
<h1 class="server-content-title">{{server.info()["name"]}}</h1>
|
||||
<p class="server-content-subtitle">
|
||||
<span>v.{{server.info()["version"]}} {{server.info()["server_id"]}}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- <div class="server-content-controls pure-u-1-2">-->
|
||||
<!-- <button class="secondary-button pure-button">Reply</button>-->
|
||||
<!-- <button class="secondary-button pure-button">Forward</button>-->
|
||||
<!-- <button class="secondary-button pure-button">Move to</button>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
|
||||
<div class="server-content-body">
|
||||
<h3>Clients: {{ server.info()['keys']|length }}</h3>
|
||||
<form class="pure-form pure-form-stacked" method="POST">
|
||||
<fieldset>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="name">Server Name</br> Note that this will not be reflected on the devices of the users that you invited to connect to it.</label>
|
||||
<input type="text" id="name" class="pure-u-23-24" name="name" value="{{server.info()['name']}}"/>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="url">Server URL</label>
|
||||
<input type="text" readonly id="url" class="pure-u-23-24" name="url" value="{{server.info()['url']}}"/>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="comment">Comment</label>
|
||||
<input type="text" id="comment" class="pure-u-23-24" name="comment" value="{{server.info()['comment']}}"/>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="port_for_new_access_keys">Port For New Access Keys</label>
|
||||
<input type="text" id="port_for_new_access_keys" class="pure-u-23-24" name="port_for_new_access_keys" value="{{server.info()['port_for_new_access_keys']}}"/>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="hostname_for_access_keys">Hostname For Access Keys</label>
|
||||
<input type="text" id="hostname_for_access_keys" class="pure-u-23-24" name="hostname_for_access_keys" value="{{server.info()['hostname_for_access_keys']}}"/>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="cert">Server Access Certificate</label>
|
||||
<input type="text" readonly id="cert" class="pure-u-23-24" name="cert" value="{{server.info()['cert']}}"/>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="created_timestamp_ms">Created</label>
|
||||
<input type="text" readonly id="created_timestamp_ms" class="pure-u-23-24" name="created_timestamp_ms" value="{{format_timestamp(server.info()['created_timestamp_ms']) }}"/>
|
||||
</div>
|
||||
<input type="hidden" readonly id="server_id" class="pure-u-23-24" name="server_id" value="{{server.info()['server_id']}}"/>
|
||||
</div>
|
||||
<p>Share anonymous metrics</p>
|
||||
<label for="metrics_enabled" class="pure-radio">
|
||||
<input type="radio" id="metrics_enabled" name="metrics" value="True" {% if server.info()['metrics_enabled'] == True %}checked{% endif %} /> Enable
|
||||
</label>
|
||||
<label for="metrics_disabled" class="pure-radio">
|
||||
<input type="radio" id="metrics_disabled" name="metrics" value="False" {% if server.info()['metrics_enabled'] == False %}checked{% endif %} /> Disable
|
||||
</label>
|
||||
|
||||
<button type="submit" class="pure-button pure-button-primary">Submit</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
Reference in New Issue
Block a user