menu, admin login, anonymized portal , closes #273

This commit is contained in:
Disassembler 2018-08-17 14:10:29 +02:00
parent 0926190a70
commit 4d4903e7ec
No known key found for this signature in database
GPG Key ID: 524BD33A0EE29499
17 changed files with 673 additions and 272 deletions

View File

@ -5,7 +5,7 @@ SOURCE_DIR=$(realpath $(dirname "${0}"))/basic
# Install packages
apk --no-cache add --virtual .useful git file htop openssh-server openssh-sftp-server
apk --no-cache add curl docker gettext kbd-misc libressl python3 py3-dnspython py3-jinja2 py3-requests py3-werkzeug nginx
apk --no-cache add curl docker gettext kbd-misc libressl python3 py3-bcrypt py3-cffi py3-dnspython py3-jinja2 py3-requests py3-six py3-werkzeug nginx
# Copy profile files and settings
mkdir -p /root/.config/htop /root/.ssh

View File

@ -297,6 +297,13 @@ class AppMgr:
if tools.is_service_started(app):
tools.restart_service(app)
def update_password(self, oldpassword, newpassword):
# Update LUKS password and adminpwd for WSGI application
tools.update_luks_password(oldpassword, newpassword)
self.conf['host']['adminpwd'] = tools.adminpwd_hash(newpassword)
# Save config to file
self.save_conf()
def request_cert(self):
# Remove all possible conflicting certificates requested in the past
certs = [i for i in os.listdir('/etc/acme.sh.d') if i not in ('account.conf', 'ca', 'http.header')]

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import bcrypt
import dns.exception
import dns.resolver
import os
@ -100,3 +101,19 @@ def get_cert_info():
data['subject'] = dict(data['subject'][i][0] for i in range(len(data['subject'])))
data['issuer'] = dict(data['issuer'][i][0] for i in range(len(data['issuer'])))
return data
def adminpwd_hash(password):
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def adminpwd_verify(password, hash):
return bcrypt.checkpw(password.encode(), hash.encode())
def update_luks_password(oldpassword, newpassword):
input = '{}\n{}'.format(oldpassword, newpassword).encode()
subprocess.run(['cryptsetup', 'luksChangeKey', '/dev/sda2'], input=input, check=True)
def shutdown_vm():
subprocess.run(['/sbin/poweroff'])
def reboot_vm():
subprocess.run(['/sbin/reboot'])

View File

@ -13,38 +13,13 @@ from jinja2 import Environment, FileSystemLoader
from . import AppMgr
from . import tools
from .validator import InvalidValueException
from .wsgilang import WSGILang
from .wsgisession import WSGISession
class Lang:
lang = {
'malformed_request': 'Byl zaslán chybný požadavek. Obnovte stránku a zkuste akci zopakovat.',
'invalid_domain': 'Zadaný doménový název "{}" není platný.',
'invalid_port': 'Zadaný port "{}" není platný.',
'host_updated': 'Nastavení hostitele bylo úspěšně změněno. Přejděte na URL <a href="{}">{}</a> a pokračujte následujícími kroky.',
'dns_record_does_not_exist': 'DNS záznam pro název "{}" neexistuje.',
'dns_record_mismatch': 'DNS záznam pro název "{}" směřuje na IP {} místo očekávané {}.',
'dns_timeout': 'Nepodařilo se kontaktovat DNS server. Zkontrolujte, zda má virtuální stroj přístup k internetu.',
'dns_records_ok': 'DNS záznamy jsou nastaveny správně.',
'http_host_not_reachable': 'Adresa {} není dostupná z internetu. Zkontrolujte nastavení síťových komponent.',
'http_timeout': 'Nepodařilo se kontaktovat ping server. Zkontrolujte, zda má virtuální stroj přístup k internetu.',
'http_hosts_ok': 'Síť je nastavena správně. Všechny aplikace na portu {} jsou z internetu dostupné.',
'cert_file_missing': 'Nebyl vybrán soubor s certifikátem.',
'key_file_missing': 'Nebyl vybrán soubor se soukromým klíčem.',
'cert_request_error': 'Došlo k chybě při žádosti o certifikát. Zkontrolujte, zda je virtuální stroj dostupný z internetu na portu 80.',
'cert_installed': 'Certifikát byl úspěšně nainstalován. Obnovte stránku nebo restartujte webový prohlížeč pro jeho načtení.',
'common_updated': 'Nastavení aplikací bylo úspěšně změněno.',
'app_started': '<span class="info">Spuštěna</span> (<a href="#" class="app-stop">zastavit</a>)',
'app_stopped': '<span class="error">Zastavena</span> (<a href="#" class="app-start">spustit</a>)',
'stop_start_error': 'Došlo k chybě při spouštění/zastavování. Zkuste akci opakovat nebo restartuje virtuální stroj.',
}
def __getattr__(self, key):
def function(*args):
return self.lang[key].format(*args)
return function
SESSION_KEY = os.urandom(26)
class WSGIApp(object):
def __init__(self):
self.lang = Lang()
self.jinja_env = Environment(loader=FileSystemLoader('/srv/spotter/templates'), autoescape=True, lstrip_blocks=True, trim_blocks=True)
self.jinja_env.globals.update(is_service_autostarted=tools.is_service_autostarted)
self.jinja_env.globals.update(is_service_started=tools.is_service_started)
@ -54,16 +29,38 @@ class WSGIApp(object):
def wsgi_app(self, environ, start_response):
request = Request(environ)
# Enhance request
request.mgr = AppMgr()
request.session = WSGISession(request.cookies, SESSION_KEY)
request.session.lang = WSGILang()
# Dispatch request
response = self.dispatch_request(request)
response = response(environ, start_response)
# Defer nginx restart for /update-host request
if request.path == '/update-host':
return ClosingIterator(response, tools.restart_nginx)
return response
# Save session if changed
request.session.save(response)
return response(environ, start_response)
def dispatch_request(self, request):
map = Map([
adapter = self.get_url_map(request.session).bind_to_environ(request.environ)
try:
endpoint, values = adapter.match()
return getattr(self, endpoint)(request, **values)
except NotFound as e:
# Return custom 404 page
response = self.render_template('404.html', request)
response.status_code = 404
return response
except HTTPException as e:
return e
def get_url_map(self, session):
rules = [
Rule('/', endpoint='portal_view'),
Rule('/login', methods=['GET'], endpoint='login_view'),
Rule('/login', methods=['POST'], endpoint='login_action'),
Rule('/logout', endpoint='logout_action')
]
if session['admin']:
rules += [
Rule('/setup-host', endpoint='setup_host_view'),
Rule('/setup-apps', endpoint='setup_apps_view'),
Rule('/update-host', endpoint='update_host_action'),
@ -76,31 +73,46 @@ class WSGIApp(object):
Rule('/update-app-autostart', endpoint='update_app_autostart_action'),
Rule('/start-app', endpoint='start_app_action'),
Rule('/stop-app', endpoint='stop_app_action'),
])
adapter = map.bind_to_environ(request.environ)
try:
endpoint, values = adapter.match()
return getattr(self, endpoint)(request, **values)
except NotFound as e:
response = self.render_template('404.html')
response.status_code = 404
return response
except HTTPException as e:
return e
Rule('/update-password', endpoint='update_password_action'),
Rule('/shutdown-vm', endpoint='shutdown_vm_action'),
Rule('/reboot-vm', endpoint='reboot_vm_action'),
]
return Map(rules)
def render_template(self, template_name, **context):
def render_template(self, template_name, request, **context):
# Enhance context
context['conf'] = request.mgr.conf
context['session'] = request.session
# Render template
t = self.jinja_env.get_template(template_name)
return Response(t.render(context), mimetype='text/html')
def render_json(self, data):
return Response(json.dumps(data), mimetype='application/json')
def login_view(self, request):
return self.render_template('login.html', request)
def login_action(self, request):
password = request.form['password']
if tools.adminpwd_verify(password, request.mgr.conf['host']['adminpwd']):
request.session['admin'] = True
return redirect('/')
else:
return self.render_template('login.html', request, message=request.session.lang.bad_password())
def logout_action(self, request):
request.session.reset()
return redirect('/')
def portal_view(self, request):
# Default view. If domain is set to the default dummy domain, redirects to first-run setup instead.
mgr = AppMgr()
if mgr.domain == 'spotter.vm':
if request.mgr.domain == 'spotter.vm':
request.session['admin'] = True
return redirect('/setup-host')
return self.render_template('portal.html', conf=mgr.conf)
if request.session['admin']:
return self.render_template('portal-admin.html', request)
return self.render_template('portal-user.html', request)
def setup_host_view(self, request):
# First-run setup view.
@ -110,35 +122,34 @@ class WSGIApp(object):
in_ipv6 = tools.get_local_ipv6()
is_letsencrypt = os.path.exists('/etc/periodic/daily/acme-sh')
cert_info = tools.get_cert_info()
mgr = AppMgr()
return self.render_template('setup-host.html', conf=mgr.conf, ex_ipv4=ex_ipv4, ex_ipv6=ex_ipv6, in_ipv4=in_ipv4, in_ipv6=in_ipv6, is_letsencrypt=is_letsencrypt, cert_info=cert_info)
return self.render_template('setup-host.html', request, ex_ipv4=ex_ipv4, ex_ipv6=ex_ipv6, in_ipv4=in_ipv4, in_ipv6=in_ipv6, is_letsencrypt=is_letsencrypt, cert_info=cert_info)
def setup_apps_view(self, request):
# Application manager view.
mgr = AppMgr()
return self.render_template('setup-apps.html', conf=mgr.conf)
return self.render_template('setup-apps.html', request)
def update_host_action(self, request):
# Update domain and port, then restart nginx (done via ClosingIterator in self.wsgi_app())
try:
domain = request.form['domain']
port = request.form['port']
mgr = AppMgr()
mgr.update_host(domain, port, False)
request.mgr.update_host(domain, port, False)
server_name = request.environ['HTTP_X_FORWARDED_SERVER_NAME']
url = 'https://{}/setup-host'.format('{}:{}'.format(server_name, port) if port != '443' else server_name)
return self.render_json({'ok': self.lang.host_updated(url, url)})
response = self.render_json({'ok': request.session.lang.host_updated(url, url)})
response.call_on_close(tools.restart_nginx)
return response
except BadRequest:
return self.render_json({'error': self.lang.malformed_request()})
return self.render_json({'error': request.session.lang.malformed_request()})
except InvalidValueException as e:
if e.args[0] == 'domain':
return self.render_json({'error': self.lang.invalid_domain(domain)})
return self.render_json({'error': request.session.lang.invalid_domain(domain)})
if e.args[0] == 'port':
return self.render_json({'error': self.lang.invalid_port(port)})
return self.render_json({'error': request.session.lang.invalid_port(port)})
def verify_dns_action(self, request):
# Check if all FQDNs for all applications are resolvable and point to current external IP
mgr = AppMgr()
mgr = request.mgr
domains = [mgr.domain]+['{}.{}'.format(mgr.conf['apps'][app]['host'], mgr.domain) for app in mgr.conf['apps']]
ipv4 = tools.get_external_ipv4()
ipv6 = tools.get_external_ipv6()
@ -147,103 +158,127 @@ class WSGIApp(object):
a = tools.resolve_ip(domain, 'A')
aaaa = tools.resolve_ip(domain, 'AAAA')
if not a and not aaaa:
return self.render_json({'error': self.lang.dns_record_does_not_exist(domain)})
return self.render_json({'error': request.session.lang.dns_record_does_not_exist(domain)})
if a and a != ipv4:
return self.render_json({'error': self.lang.dns_record_mismatch(domain, a, ipv4)})
return self.render_json({'error': request.session.lang.dns_record_mismatch(domain, a, ipv4)})
if aaaa and aaaa != ipv6:
return self.render_json({'error': self.lang.dns_record_mismatch(domain, aaaa, ipv6)})
return self.render_json({'error': request.session.lang.dns_record_mismatch(domain, aaaa, ipv6)})
except:
return self.render_json({'error': self.lang.dns_timeout()})
return self.render_json({'ok': self.lang.dns_records_ok()})
return self.render_json({'error': request.session.lang.dns_timeout()})
return self.render_json({'ok': request.session.lang.dns_records_ok()})
def verify_http_action(self, request, **kwargs):
# Check if all applications are accessible from the internet using 3rd party ping service
proto = kwargs['proto']
mgr = AppMgr()
mgr = request.mgr
domains = [mgr.domain]+['{}.{}'.format(mgr.conf['apps'][app]['host'], mgr.domain) for app in mgr.conf['apps']]
for domain in domains:
host = '{}:{}'.format(domain, mgr.port) if proto == 'https' and mgr.port != '443' else domain
url = '{}://{}/'.format(proto, host)
try:
if not tools.ping_url(url):
return self.render_json({'error': self.lang.http_host_not_reachable(url)})
return self.render_json({'error': request.session.lang.http_host_not_reachable(url)})
except:
return self.render_json({'error': self.lang.http_timeout()})
return self.render_json({'ok': self.lang.http_hosts_ok(mgr.port if proto == 'https' else '80')})
return self.render_json({'error': request.session.lang.http_timeout()})
return self.render_json({'ok': request.session.lang.http_hosts_ok(mgr.port if proto == 'https' else '80')})
def update_cert_action(self, request):
# Update certificate - either request via Let's Encrypt or manually upload files
try:
mgr = AppMgr()
if request.form['method'] not in ['auto', 'manual']:
raise BadRequest()
if request.form['method'] == 'manual':
if not request.files['public']:
return self.render_json({'error': self.lang.cert_file_missing()})
return self.render_json({'error': request.session.lang.cert_file_missing()})
if not request.files['private']:
return self.render_json({'error': self.lang.key_file_missing()})
return self.render_json({'error': request.session.lang.key_file_missing()})
request.files['public'].save('/tmp/public.pem')
request.files['private'].save('/tmp/private.pem')
mgr.install_cert('/tmp/public.pem', '/tmp/private.pem')
request.mgr.install_cert('/tmp/public.pem', '/tmp/private.pem')
os.unlink('/tmp/public.pem')
os.unlink('/tmp/private.pem')
else:
mgr.request_cert()
request.mgr.request_cert()
except BadRequest:
return self.render_json({'error': self.lang.malformed_request()})
return self.render_json({'error': request.session.lang.malformed_request()})
except:
return self.render_json({'error': self.lang.cert_request_error()})
return self.render_json({'ok': self.lang.cert_installed()})
return self.render_json({'error': request.session.lang.cert_request_error()})
return self.render_json({'ok': request.session.lang.cert_installed()})
def update_common_action(self, request):
# Update common settings shared between apps - admin e-mail address, Google Maps API key
try:
mgr = AppMgr()
mgr.update_common(request.form['email'], request.form['gmaps-api-key'])
request.mgr.update_common(request.form['email'], request.form['gmaps-api-key'])
except BadRequest:
return self.render_json({'error': self.lang.malformed_request()})
return self.render_json({'ok': self.lang.common_updated()})
return self.render_json({'error': request.session.lang.malformed_request()})
return self.render_json({'ok': request.session.lang.common_updated()})
def update_app_visibility_action(self, request):
# Update application visibility on portal page
try:
mgr = AppMgr()
if request.form['value'] == 'true':
mgr.show_tiles(request.form['app'])
request.mgr.show_tiles(request.form['app'])
else:
mgr.hide_tiles(request.form['app'])
request.mgr.hide_tiles(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': self.lang.malformed_request()})
return self.render_json({'error': request.session.lang.malformed_request()})
return self.render_json({'ok': 'ok'})
def update_app_autostart_action(self, request):
# Update value determining if the app should be automatically started after VM boot
try:
mgr = AppMgr()
if request.form['value'] == 'true':
mgr.enable_autostart(request.form['app'])
request.mgr.enable_autostart(request.form['app'])
else:
mgr.disable_autostart(request.form['app'])
request.mgr.disable_autostart(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': self.lang.malformed_request()})
return self.render_json({'error': request.session.lang.malformed_request()})
return self.render_json({'ok': 'ok'})
def start_app_action(self, request):
# Starts application along with its dependencies
try:
mgr = AppMgr()
mgr.start_app(request.form['app'])
request.mgr.start_app(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': self.lang.malformed_request()})
return self.render_json({'error': request.session.lang.malformed_request()})
except:
return self.render_json({'error': self.lang.stop_start_error()})
return self.render_json({'ok': self.lang.app_started()})
return self.render_json({'error': request.session.lang.stop_start_error()})
return self.render_json({'ok': request.session.lang.app_started()})
def stop_app_action(self, request):
# Stops application along with its dependencies
try:
mgr = AppMgr()
mgr.stop_app(request.form['app'])
request.mgr.stop_app(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': self.lang.malformed_request()})
return self.render_json({'error': request.session.lang.malformed_request()})
except:
return self.render_json({'error': self.lang.stop_start_error()})
return self.render_json({'ok': self.lang.app_stopped()})
return self.render_json({'error': request.session.lang.stop_start_error()})
return self.render_json({'ok': request.session.lang.app_stopped()})
def update_password_action(self, request):
# Updates password for both HDD encryption (LUKS-on-LVM) and admin account to spotter-appmgr
try:
if request.form['newpassword'] != request.form['newpassword2']:
return self.render_json({'error': request.session.lang.password_mismatch()})
if request.form['newpassword'] == '':
return self.render_json({'error': request.session.lang.password_empty()})
# No need to explicitly validate old password, update_luks_password will raise exception if it's wrong
request.mgr.update_password(request.form['oldpassword'], request.form['newpassword'])
except:
return self.render_json({'error': request.session.lang.bad_password()})
return self.render_json({'ok': request.session.lang.password_changed()})
def reboot_vm_action(self, request):
# Reboots VM
response = self.render_json({'ok': request.session.lang.reboot_initiated()})
response.call_on_close(tools.reboot_vm)
return response
def shutdown_vm_action(self, request):
# Shuts down VM
response = self.render_json({'ok': request.session.lang.shutdown_initiated()})
response.call_on_close(tools.shutdown_vm)
return response
class InvalidRecordException(Exception):
pass

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
class WSGILang:
lang = {
'malformed_request': 'Byl zaslán chybný požadavek. Obnovte stránku a zkuste akci zopakovat.',
'invalid_domain': 'Zadaný doménový název "{}" není platný.',
'invalid_port': 'Zadaný port "{}" není platný.',
'host_updated': 'Nastavení hostitele bylo úspěšně změněno. Přejděte na URL <a href="{}">{}</a> a pokračujte následujícími kroky.',
'dns_record_does_not_exist': 'DNS záznam pro název "{}" neexistuje.',
'dns_record_mismatch': 'DNS záznam pro název "{}" směřuje na IP {} místo očekávané {}.',
'dns_timeout': 'Nepodařilo se kontaktovat DNS server. Zkontrolujte, zda má virtuální stroj přístup k internetu.',
'dns_records_ok': 'DNS záznamy jsou nastaveny správně.',
'http_host_not_reachable': 'Adresa {} není dostupná z internetu. Zkontrolujte nastavení síťových komponent.',
'http_timeout': 'Nepodařilo se kontaktovat ping server. Zkontrolujte, zda má virtuální stroj přístup k internetu.',
'http_hosts_ok': 'Síť je nastavena správně. Všechny aplikace na portu {} jsou z internetu dostupné.',
'cert_file_missing': 'Nebyl vybrán soubor s certifikátem.',
'key_file_missing': 'Nebyl vybrán soubor se soukromým klíčem.',
'cert_request_error': 'Došlo k chybě při žádosti o certifikát. Zkontrolujte, zda je virtuální stroj dostupný z internetu na portu 80.',
'cert_installed': 'Certifikát byl úspěšně nainstalován. Obnovte stránku nebo restartujte webový prohlížeč pro jeho načtení.',
'common_updated': 'Nastavení aplikací bylo úspěšně změněno.',
'app_started': '<span class="info">Spuštěna</span> (<a href="#" class="app-stop">zastavit</a>)',
'app_stopped': '<span class="error">Zastavena</span> (<a href="#" class="app-start">spustit</a>)',
'stop_start_error': 'Došlo k chybě při spouštění/zastavování. Zkuste akci opakovat nebo restartuje virtuální stroj.',
'bad_password': 'Nesprávné heslo',
'password_mismatch': 'Zadaná hesla se neshodují',
'password_empty': 'Nové heslo nesmí být prázdné',
'password_changed': 'Heslo úspěšně změněno',
'reboot_initiated': 'Příkaz odeslán. Vyčkejte na restartování virtuálního stroje.',
'shutdown_initiated': 'Příkaz odeslán. Vyčkejte na vypnutí virtuálního stroje.',
}
def __getattr__(self, key):
def function(*args):
return self.lang[key].format(*args)
return function

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from werkzeug.contrib.securecookie import SecureCookie
class WSGISession:
def __init__(self, cookies, secret_key):
self.secret_key = secret_key
data = cookies.get('session')
if data:
self.sc = SecureCookie.unserialize(data, secret_key)
else:
self.reset()
if 'admin' not in self.sc:
self.reset()
def __getitem__(self, key):
return self.sc.__getitem__(key)
def __setitem__(self, key, value):
return self.sc.__setitem__(key, value)
def __delitem__(self, key):
return self.sc.__delitem__(key)
def __contains__(self, key):
return self.sc.__contains__(key)
def reset(self):
self.sc = SecureCookie(secret_key=self.secret_key)
self.sc['admin'] = False
def save(self, response):
if self.sc.should_save:
data = self.sc.serialize()
response.set_cookie('session', data, httponly=True)

View File

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
import argparse
import getpass
import sys
sys.path.append('/srv/spotter')
@ -58,6 +59,9 @@ parser_update_common.set_defaults(action='update-common')
parser_update_common.add_argument('--email', help='Administrative e-mail address')
parser_update_common.add_argument('--gmaps-api-key', help='Google Maps API key')
parser_update_password = subparsers.add_parser('update-password', help='Updates password for HDD encryption and WSGI administration interface')
parser_update_password.set_defaults(action='update-password')
parser_request_cert = subparsers.add_parser('request-cert', help='Requests and installs Let\'s Encrypt certificate for currently set domain')
parser_request_cert.set_defaults(action='request-cert')
@ -90,6 +94,10 @@ elif args.action == 'update-host':
mgr.update_host(args.domain, args.port)
elif args.action == 'update-common':
mgr.update_common(args.email, args.gmaps_api_key)
elif args.action == 'update-password':
oldpassword = getpass.getpass('Old password: ')
newpassword = getpass.getpass('New password: ')
mgr.update_password(oldpassword, newpassword)
elif args.action == 'request-cert':
mgr.request_cert()
elif args.action == 'install-cert':

View File

@ -132,6 +132,7 @@
"gmaps-api-key": ""
},
"host": {
"adminpwd": "$2b$12$nLrIefUoWN.pK6j90gsfkO0/tg4EGXDmdjN8HOGB0U.9BcHTFxzWS",
"domain": "spotter.vm",
"port": "443"
}

View File

@ -17,12 +17,35 @@ img {
nav {
float: right;
margin-right: 30px;
}
nav #menu-button {
float: right;
cursor: pointer;
}
nav #menu-button div {
width: 24px;
height: 4px;
background-color: #fff;
border: 1px solid #000;
margin: 2px 0px;
}
nav ul {
display: none;
list-style: none;
border: 1px solid #000;
margin: 26px 0px 0px 0px;
position: absolute;
background-color: #fff;
padding: 10px;
right: 30px;
z-index: 1;
}
nav a {
display: block;
color: #00c;
}
h1, h2 {
@ -44,13 +67,13 @@ header p,
.setup-box {
background-color: #fff;
margin-top: 13px;
margin-right: 13px;
border: solid 1px #000;
padding: 10px;
}
.portal-box {
position: relative;
margin-right: 13px;
width: 365px;
float: left;
height: 175px;
@ -98,6 +121,7 @@ header p,
}
.setup-box input[type="text"],
.setup-box input[type="password"],
.setup-box input[type="submit"],
.setup-box input[type="button"],
.setup-box input[type="file"],

View File

@ -0,0 +1,200 @@
$(function() {
$('#update-host').on('submit', update_host);
$('#verify-dns').on('click', verify_dns);
$('#verify-https').on('click', verify_https);
$('#verify-http').on('click', verify_http);
$('#cert-method').on('change', toggle_cert_method);
$('#update-cert').on('submit', update_cert);
$('#update-common').on('submit', update_common);
$('.app-visible').on('click', update_app_visibility);
$('.app-autostart').on('click', update_app_autostart);
$('tr[data-app]').on('click', '.app-start', start_app).on('click', '.app-stop', stop_app);
$('#update-password').on('submit', update_password);
$('#reboot-vm').on('click', reboot_vm);
$('#shutdown-vm').on('click', shutdown_vm);
});
function update_host() {
$('#host-submit').hide();
$('#host-message').hide();
$('#host-wait').show();
$.post('/update-host', {'domain': $('#domain').val(), 'port': $('#port').val()}, function(data) {
$('#host-wait').hide();
if (data.error) {
$('#host-message').attr('class','error').html(data.error).show();
$('#host-submit').show();
} else {
$('#host-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function verify_dns() {
$('#verify-dns').hide();
$('#dns-message').hide();
$('#dns-wait').show();
$.get('/verify-dns', function(data) {
$('#dns-wait').hide();
if (data.error) {
$('#dns-message').attr('class','error').html(data.error).show();
$('#verify-dns').show();
} else {
$('#dns-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function _verify_http(proto) {
$('#verify-'+proto).hide();
$('#'+proto+'-message').hide();
$('#'+proto+'-wait').show();
$.get('/verify-' + proto, function(data) {
$('#'+proto+'-wait').hide();
if (data.error) {
$('#'+proto+'-message').attr('class','error').html(data.error).show();
$('#verify-'+proto).show();
} else {
$('#'+proto+'-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function verify_http() {
return _verify_http('http');
}
function verify_https() {
return _verify_http('https');
}
function toggle_cert_method() {
if ($('#cert-method').val() == 'manual') {
$('.cert-upload').show();
} else {
$('.cert-upload').hide();
}
}
function update_cert() {
$('#cert-submit').hide();
$('#cert-message').hide();
$('#cert-wait').show();
$.ajax({url: '/update-cert', type: 'POST', data: new FormData($('#update-cert')[0]), cache: false, contentType: false, processData: false, success: function(data) {
$('#cert-wait').hide();
if (data.error) {
$('#cert-message').attr('class','error').text(data.error).show();
$('#cert-submit').show();
} else {
$('#cert-message').attr('class','info').text(data.ok).show();
}
}});
return false;
}
function update_common() {
$('#common-submit').hide();
$('#common-message').hide();
$('#common-wait').show();
$.post('/update-common', {'email': $('#email').val(), 'gmaps-api-key': $('#gmaps-api-key').val()}, function(data) {
$('#common-wait').hide();
if (data.error) {
$('#common-message').attr('class','error').html(data.error).show();
$('#common-submit').show();
} else {
$('#common-message').attr('class','info').html(data.ok).show();
$('#common-submit').show();
}
});
return false;
}
function update_app_visibility(ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var value = el.is(':checked') ? 'true' : '';
$.post('/update-app-visibility', {'app': app, 'value': value}, function(data) {
if (data.error) {
el.prop('checked', !value);
alert(data.error);
}
});
}
function update_app_autostart(ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var value = el.is(':checked') ? 'true' : '';
$.post('/update-app-autostart', {'app': app, 'value': value}, function(data) {
if (data.error) {
el.prop('checked', !value);
alert(data.error);
}
});
}
function start_app(ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var td = el.closest('td');
td.html('<div class="loader"></div>');
$.post('/start-app', {'app': app}, function(data) {
if (data.error) {
td.attr('class','error').html(data.error);
} else {
td.removeAttr('class').html(data.ok);
}
});
return false;
}
function stop_app(ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var td = el.closest('td');
td.html('<div class="loader"></div>');
$.post('/stop-app', {'app': app}, function(data) {
if (data.error) {
td.attr('class','error').html(data.error);
} else {
td.removeAttr('class').html(data.ok);
}
});
return false;
}
function update_password() {
$('#password-submit').hide();
$('#password-message').hide();
$('#password-wait').show();
$.post('/update-password', {'oldpassword': $('#oldpassword').val(), 'newpassword': $('#newpassword').val(), 'newpassword2': $('#newpassword2').val()}, function(data) {
$('#password-wait').hide();
if (data.error) {
$('#password-message').attr('class','error').html(data.error).show();
$('#password-submit').show();
} else {
$('#password-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function reboot_vm() {
if (confirm('Do you really want to reboot VM?')) {
$.get('/reboot-vm', function(data) {
$('#vm-message').attr('class','info').html(data.ok).show();
});
}
return false;
}
function shutdown_vm() {
if (confirm('Do you really want to shutdown VM?')) {
$.get('/shutdown-vm', function(data) {
$('#vm-message').attr('class','info').html(data.ok).show();
});
}
return false;
}

View File

@ -1,163 +1,7 @@
$(function() {
$('#update-host').on('submit', update_host);
$('#verify-dns').on('click', verify_dns);
$('#verify-https').on('click', verify_https);
$('#verify-http').on('click', verify_http);
$('#cert-method').on('change', toggle_cert_method);
$('#update-cert').on('submit', update_cert);
$('#update-common').on('submit', update_common);
$('.app-visible').on('click', update_app_visibility);
$('.app-autostart').on('click', update_app_autostart);
$('tr[data-app]').on('click', '.app-start', start_app).on('click', '.app-stop', stop_app);
$('#menu-button').on('click', toggle_menu);
});
function update_host() {
$('#host-submit').hide();
$('#host-message').hide();
$('#host-wait').show();
$.post('/update-host', {'domain': $('#domain').val(), 'port': $('#port').val()}, function(data) {
$('#host-wait').hide();
if (data.error) {
$('#host-message').attr('class','error').html(data.error).show();
$('#host-submit').show();
} else {
$('#host-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function verify_dns() {
$('#verify-dns').hide();
$('#dns-message').hide();
$('#dns-wait').show();
$.get('/verify-dns', function(data) {
$('#dns-wait').hide();
if (data.error) {
$('#dns-message').attr('class','error').html(data.error).show();
$('#verify-dns').show();
} else {
$('#dns-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function _verify_http(proto) {
$('#verify-'+proto).hide();
$('#'+proto+'-message').hide();
$('#'+proto+'-wait').show();
$.get('/verify-' + proto, function(data) {
$('#'+proto+'-wait').hide();
if (data.error) {
$('#'+proto+'-message').attr('class','error').html(data.error).show();
$('#verify-'+proto).show();
} else {
$('#'+proto+'-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function verify_http() {
return _verify_http('http');
}
function verify_https() {
return _verify_http('https');
}
function toggle_cert_method() {
if ($('#cert-method').val() == 'manual') {
$('.cert-upload').show();
} else {
$('.cert-upload').hide();
}
}
function update_cert() {
$('#cert-submit').hide();
$('#cert-message').hide();
$('#cert-wait').show();
$.ajax({url: '/update-cert', type: 'POST', data: new FormData($('#update-cert')[0]), cache: false, contentType: false, processData: false, success: function(data) {
$('#cert-wait').hide();
if (data.error) {
$('#cert-message').attr('class','error').text(data.error).show();
$('#cert-submit').show();
} else {
$('#cert-message').attr('class','info').text(data.ok).show();
}
}});
return false;
}
function update_common() {
$('#common-submit').hide();
$('#common-message').hide();
$('#common-wait').show();
$.post('/update-common', {'email': $('#email').val(), 'gmaps-api-key': $('#gmaps-api-key').val()}, function(data) {
$('#common-wait').hide();
if (data.error) {
$('#common-message').attr('class','error').html(data.error).show();
$('#common-submit').show();
} else {
$('#common-message').attr('class','info').html(data.ok).show();
$('#common-submit').show();
}
});
return false;
}
function update_app_visibility(ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var value = el.is(':checked') ? 'true' : '';
$.post('/update-app-visibility', {'app': app, 'value': value}, function(data) {
if (data.error) {
el.prop('checked', !value);
alert(data.error);
}
});
}
function update_app_autostart(ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var value = el.is(':checked') ? 'true' : '';
$.post('/update-app-autostart', {'app': app, 'value': value}, function(data) {
if (data.error) {
el.prop('checked', !value);
alert(data.error);
}
});
}
function start_app(ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var td = el.closest('td');
td.html('<div class="loader"></div>');
$.post('/start-app', {'app': app}, function(data) {
if (data.error) {
td.attr('class','error').html(data.error);
} else {
td.removeAttr('class').html(data.ok);
}
});
return false;
}
function stop_app(ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var td = el.closest('td');
td.html('<div class="loader"></div>');
$.post('/stop-app', {'app': app}, function(data) {
if (data.error) {
td.attr('class','error').html(data.error);
} else {
td.removeAttr('class').html(data.ok);
}
});
return false;
function toggle_menu() {
$('#menu').toggle();
}

View File

@ -11,13 +11,27 @@
<link rel="stylesheet" href="static/css/style.css" type="text/css" media="screen">
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/script.js"></script>
{% if session.admin %}
<script src="static/js/admin.js"></script>
{% endif %}
</head>
<body>
<nav>
{% set template = self._TemplateReference__context.name %}
{% if template != 'portal.html' %}<a href="/">Portál</a>{% endif %}
{% if template != 'setup-host.html' %}<a href="/setup-host">Nastavení hostitele</a>{% endif %}
{% if template != 'setup-apps.html' %}<a href="/setup-apps">Nastavení aplikací</a>{% endif %}
<div id="menu-button">
<div></div>
<div></div>
<div></div>
</div>
<ul id="menu">
<li><a href="/">Portál</a></li>
{% if session.admin %}
<li><a href="/setup-host">Nastavení hostitele</a></li>
<li><a href="/setup-apps">Nastavení aplikací</a></li>
<li><a href="/logout">Odhlášení</a></li>
{% else %}
<li><a href="/login">Přihlášení</a></li>
{% endif %}
</ul>
</nav>
<header>
<h1>CLUSTER NGO</h1>

View File

@ -0,0 +1,28 @@
{% extends 'layout.html' %}
{% block title %}Přihlášení{% endblock %}
{% block body %}
<div class="setup-box">
<h2>Přihlášení</h2>
<form action="/login" method="post">
<table>
<tr>
<td>Jméno:</td>
<td>admin</td>
</tr>
<tr>
<td>Heslo</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<input type="submit" value="Přihlásit">
</td>
</tr>
</table>
{% if message is defined %}
<p class="error">{{ message }}</p>
{% endif %}
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,123 @@
{% extends 'layout.html' %}
{% block title %}Cluster NGO{% endblock %}
{% block body %}
{% set host = '{}:{}'.format(conf['host']['domain'], conf['host']['port']) if conf['host']['port'] != '443' else conf['host']['domain'] %}
{% if conf['apps']['sahana-demo']['visible'] %}
<div class="portal-box">
<h2><a href="https://sahana-demo.{{ host }}/eden/">Řízení humanítární činnosti</a></h2>
<p>Přístup určený k bezpečnému vyzkoušení aplikace. Zde můžete přidávat i mazat testovací data.</p>
</div>
{% endif %}
{% if conf['apps']['sambro']['visible'] %}
<div class="portal-box">
<h2><a href="https://sambro.{{ host }}/eden/">Centrum hlášení a výstrah</a></h2>
<p>Samostatná instance s šablonou pro centrum hlášení a výstrah.</p>
</div>
{% endif %}
{% if conf['apps']['crisiscleanup']['visible'] %}
<div class="portal-box">
<h2><a href="https://cc.{{ host }}">Mapování následků katastrof</a></h2>
<p><strong>Mapování krizové pomoci</strong> při odstraňování následků katastrof a koordinaci práce. Jde o majetek, ne o lidi.</p>
</div>
{% endif %}
{% if conf['apps']['ckan']['visible'] %}
<div class="portal-box">
<h2><a href="https://ckan.{{ host }}">Datový sklad</a></h2>
<p><strong>Repository</strong> management a datová analýza pro vytváření otevřených dat.</p>
</div>
{% endif %}
{% if conf['apps']['opendatakit-build']['visible'] %}
<div class="portal-box">
<h2><a href="https://odkbuild.{{ host }}">Sběr formulářových dat</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>Aplikace pro návrh formulářů</p>
</div>
{% endif %}
{% if conf['apps']['opendatakit']['visible'] %}
<div class="portal-box">
<h2><a href="https://odk.{{ host }}/">Sběr formulářových dat</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.</p>
</div>
{% endif %}
{% if conf['apps']['openmapkit']['visible'] %}
<div class="portal-box">
<h2><a href="https://omk.{{ host }}">Sběr mapových dat</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>
</div>
{% endif %}
{% if conf['apps']['frontlinesms']['visible'] %}
<div class="portal-box">
<h2><a href="https://sms.{{ host }}">Hromadné odesílání zpráv</a></h2>
<p><strong>SMS messaging</strong> přes veřejné datové brány</p>
</div>
{% endif %}
{% if conf['apps']['seeddms']['visible'] %}
<div class="portal-box">
<h2><a href="https://dms.{{ host }}">Archiv dokumentace</a></h2>
<p><strong>Dokument management</strong> na dokumentaci a projektovou dokumentaci</p>
</div>
{% endif %}
{% if conf['apps']['pandora']['visible'] %}
<div class="portal-box">
<h2><a href="https://pandora.{{ host }}">Archiv medií</a></h2>
<p><strong>Media management</strong> na foto a video z krizové události. Tvorba metadat, komentářů, lokalizace v čase a na mapě.</p>
</div>
{% endif %}
{% if conf['apps']['ushahidi']['visible'] %}
<div class="portal-box">
<h2><a href="https://ush.{{ host }}">Skupinová reakce na události</a></h2>
<p>Reakce na krizovou událost. Shromažďujte zprávy od obětí a pracovníků v terénu prostřednictvím SMS, e-mailu, webu, Twitteru.</p>
</div>
{% endif %}
{% if conf['apps']['kanboard']['visible'] %}
<div class="portal-box">
<h2><a href="https://kb.{{ host }}">Kanban řízení projektů</a></h2>
<p>Usnadňuje tvorbu a řízení projektů s pomocí Kanban metodiky.</p>
</div>
{% endif %}
{% if conf['apps']['gnuhealth']['visible'] %}
<div class="portal-box">
<h2><a href="https://gh.{{ host }}/index.html">Lékařské záznamy pacientů</a></h2>
<p>Zdravotní a nemocniční informační systém.</p>
</div>
{% endif %}
{% if conf['apps']['sigmah']['visible'] %}
<div class="portal-box">
<h2><a href="https://sigmah.{{ host }}/sigmah/">Finanční řízení sbírek</a></h2>
<p>Rozpočtování získávání finančních prostředků.</p>
</div>
{% endif %}
{% if conf['apps']['motech']['visible'] %}
<div class="portal-box">
<h2><a href="https://motech.{{ host }}/">Automatizace komunikace</a></h2>
<p>Integrace zdravotnických a komunikačních služeb.</p>
</div>
{% endif %}
{% if conf['apps']['mifosx']['visible'] %}
<div class="portal-box">
<h2><a href="https://mifosx.{{ host }}/">Mikrofinancování rozvojových projektů</a></h2>
<p>Nástroj na rozvojovou, humanitární pomoc a mikrofinancování.</p>
</div>
{% endif %}
<div class="portal-box">
<h2><a href="http://spotter.ngo"><img src="static/img/Cluster_Spotter.png" alt="Cluster Spotter" title="Cluster Spotter">Cluster Spotter</a></h2>
<p>Info o Misi a Vizi projektu, včetně kontaktu. Zachovejte data bezpečná a neposkytujte je nepovolaným osobám.<br>
<small>CC 4.0 CZ by <a href="http://trendspotter.cz">TS</a>. Content is based on PD, CC, GNU/GPL. Brand names, trademarks belong to their respective holders.</small>
</p>
</div>
{% endblock %}

View File

@ -55,4 +55,40 @@
</tbody>
</table>
</div>
<div class="setup-box">
<h2>Správce virtuálního stroje</h2>
<p>Změna hesla k šifrovanému diskovému oddílu a administračnímu rozhraní.</p>
<form id="update-password" action="/update-password" method="post">
<table>
<tr>
<td>Stávající heslo:</td>
<td><input type="password" name="oldpassword" id="oldpassword"></td>
</tr>
<tr>
<td>Nové heslo:</td>
<td><input type="password" name="newpassword" id="newpassword"></td>
</tr>
<tr>
<td>Kontrola nového hesla:</td>
<td><input type="password" name="newpassword2" id="newpassword2"></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2">
<input type="submit" id="password-submit" value="Změnit heslo">
<div id="password-message"></div>
<div id="password-wait" class="loader-wrap">
<div class="loader"></div>
<span>Provádí se změna hesla, prosím čekejte...</span>
</div>
</td>
</tr>
</table>
</form>
<p>Restartování nebo vypnutí virtuálního stroje.</p>
<input type="button" id="reboot-vm" value="Restartovat VM">
<input type="button" id="shutdown-vm" value="Vypnout VM">
<div id="vm-message"></div>
</div>
{% endblock %}

View File

@ -10,10 +10,7 @@ application = WSGIApp()
if __name__ == '__main__':
import os
from werkzeug.contrib.fixers import ProxyFix
from werkzeug.serving import run_simple
from werkzeug.wsgi import SharedDataMiddleware
application = SharedDataMiddleware(application, {
'/static': os.path.join(os.path.dirname(__file__), 'static')
})
run_simple('127.0.0.1', 8080, application, use_reloader=True)
run_simple('127.0.0.1', 8080, ProxyFix(application))