mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-08-21 14:37:16 +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