menu, admin login, anonymized portal , closes #273
This commit is contained in:
parent
0926190a70
commit
4d4903e7ec
2
basic.sh
2
basic.sh
@ -5,7 +5,7 @@ SOURCE_DIR=$(realpath $(dirname "${0}"))/basic
|
|||||||
|
|
||||||
# Install packages
|
# Install packages
|
||||||
apk --no-cache add --virtual .useful git file htop openssh-server openssh-sftp-server
|
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
|
# Copy profile files and settings
|
||||||
mkdir -p /root/.config/htop /root/.ssh
|
mkdir -p /root/.config/htop /root/.ssh
|
||||||
|
@ -297,6 +297,13 @@ class AppMgr:
|
|||||||
if tools.is_service_started(app):
|
if tools.is_service_started(app):
|
||||||
tools.restart_service(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):
|
def request_cert(self):
|
||||||
# Remove all possible conflicting certificates requested in the past
|
# 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')]
|
certs = [i for i in os.listdir('/etc/acme.sh.d') if i not in ('account.conf', 'ca', 'http.header')]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
import dns.exception
|
import dns.exception
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
import os
|
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['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'])))
|
data['issuer'] = dict(data['issuer'][i][0] for i in range(len(data['issuer'])))
|
||||||
return data
|
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'])
|
||||||
|
@ -13,38 +13,13 @@ from jinja2 import Environment, FileSystemLoader
|
|||||||
from . import AppMgr
|
from . import AppMgr
|
||||||
from . import tools
|
from . import tools
|
||||||
from .validator import InvalidValueException
|
from .validator import InvalidValueException
|
||||||
|
from .wsgilang import WSGILang
|
||||||
|
from .wsgisession import WSGISession
|
||||||
|
|
||||||
class Lang:
|
SESSION_KEY = os.urandom(26)
|
||||||
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
|
|
||||||
|
|
||||||
class WSGIApp(object):
|
class WSGIApp(object):
|
||||||
def __init__(self):
|
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 = 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_autostarted=tools.is_service_autostarted)
|
||||||
self.jinja_env.globals.update(is_service_started=tools.is_service_started)
|
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):
|
def wsgi_app(self, environ, start_response):
|
||||||
request = Request(environ)
|
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 = self.dispatch_request(request)
|
||||||
response = response(environ, start_response)
|
# Save session if changed
|
||||||
# Defer nginx restart for /update-host request
|
request.session.save(response)
|
||||||
if request.path == '/update-host':
|
return response(environ, start_response)
|
||||||
return ClosingIterator(response, tools.restart_nginx)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def dispatch_request(self, request):
|
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('/', 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-host', endpoint='setup_host_view'),
|
||||||
Rule('/setup-apps', endpoint='setup_apps_view'),
|
Rule('/setup-apps', endpoint='setup_apps_view'),
|
||||||
Rule('/update-host', endpoint='update_host_action'),
|
Rule('/update-host', endpoint='update_host_action'),
|
||||||
@ -76,31 +73,46 @@ class WSGIApp(object):
|
|||||||
Rule('/update-app-autostart', endpoint='update_app_autostart_action'),
|
Rule('/update-app-autostart', endpoint='update_app_autostart_action'),
|
||||||
Rule('/start-app', endpoint='start_app_action'),
|
Rule('/start-app', endpoint='start_app_action'),
|
||||||
Rule('/stop-app', endpoint='stop_app_action'),
|
Rule('/stop-app', endpoint='stop_app_action'),
|
||||||
])
|
Rule('/update-password', endpoint='update_password_action'),
|
||||||
adapter = map.bind_to_environ(request.environ)
|
Rule('/shutdown-vm', endpoint='shutdown_vm_action'),
|
||||||
try:
|
Rule('/reboot-vm', endpoint='reboot_vm_action'),
|
||||||
endpoint, values = adapter.match()
|
]
|
||||||
return getattr(self, endpoint)(request, **values)
|
return Map(rules)
|
||||||
except NotFound as e:
|
|
||||||
response = self.render_template('404.html')
|
|
||||||
response.status_code = 404
|
|
||||||
return response
|
|
||||||
except HTTPException as e:
|
|
||||||
return e
|
|
||||||
|
|
||||||
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)
|
t = self.jinja_env.get_template(template_name)
|
||||||
return Response(t.render(context), mimetype='text/html')
|
return Response(t.render(context), mimetype='text/html')
|
||||||
|
|
||||||
def render_json(self, data):
|
def render_json(self, data):
|
||||||
return Response(json.dumps(data), mimetype='application/json')
|
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):
|
def portal_view(self, request):
|
||||||
# Default view. If domain is set to the default dummy domain, redirects to first-run setup instead.
|
# Default view. If domain is set to the default dummy domain, redirects to first-run setup instead.
|
||||||
mgr = AppMgr()
|
if request.mgr.domain == 'spotter.vm':
|
||||||
if mgr.domain == 'spotter.vm':
|
request.session['admin'] = True
|
||||||
return redirect('/setup-host')
|
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):
|
def setup_host_view(self, request):
|
||||||
# First-run setup view.
|
# First-run setup view.
|
||||||
@ -110,35 +122,34 @@ class WSGIApp(object):
|
|||||||
in_ipv6 = tools.get_local_ipv6()
|
in_ipv6 = tools.get_local_ipv6()
|
||||||
is_letsencrypt = os.path.exists('/etc/periodic/daily/acme-sh')
|
is_letsencrypt = os.path.exists('/etc/periodic/daily/acme-sh')
|
||||||
cert_info = tools.get_cert_info()
|
cert_info = tools.get_cert_info()
|
||||||
mgr = AppMgr()
|
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)
|
||||||
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)
|
|
||||||
|
|
||||||
def setup_apps_view(self, request):
|
def setup_apps_view(self, request):
|
||||||
# Application manager view.
|
# Application manager view.
|
||||||
mgr = AppMgr()
|
return self.render_template('setup-apps.html', request)
|
||||||
return self.render_template('setup-apps.html', conf=mgr.conf)
|
|
||||||
|
|
||||||
def update_host_action(self, request):
|
def update_host_action(self, request):
|
||||||
# Update domain and port, then restart nginx (done via ClosingIterator in self.wsgi_app())
|
# Update domain and port, then restart nginx (done via ClosingIterator in self.wsgi_app())
|
||||||
try:
|
try:
|
||||||
domain = request.form['domain']
|
domain = request.form['domain']
|
||||||
port = request.form['port']
|
port = request.form['port']
|
||||||
mgr = AppMgr()
|
request.mgr.update_host(domain, port, False)
|
||||||
mgr.update_host(domain, port, False)
|
|
||||||
server_name = request.environ['HTTP_X_FORWARDED_SERVER_NAME']
|
server_name = request.environ['HTTP_X_FORWARDED_SERVER_NAME']
|
||||||
url = 'https://{}/setup-host'.format('{}:{}'.format(server_name, port) if port != '443' else 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:
|
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:
|
except InvalidValueException as e:
|
||||||
if e.args[0] == 'domain':
|
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':
|
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):
|
def verify_dns_action(self, request):
|
||||||
# Check if all FQDNs for all applications are resolvable and point to current external IP
|
# 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']]
|
domains = [mgr.domain]+['{}.{}'.format(mgr.conf['apps'][app]['host'], mgr.domain) for app in mgr.conf['apps']]
|
||||||
ipv4 = tools.get_external_ipv4()
|
ipv4 = tools.get_external_ipv4()
|
||||||
ipv6 = tools.get_external_ipv6()
|
ipv6 = tools.get_external_ipv6()
|
||||||
@ -147,103 +158,127 @@ class WSGIApp(object):
|
|||||||
a = tools.resolve_ip(domain, 'A')
|
a = tools.resolve_ip(domain, 'A')
|
||||||
aaaa = tools.resolve_ip(domain, 'AAAA')
|
aaaa = tools.resolve_ip(domain, 'AAAA')
|
||||||
if not a and not 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:
|
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:
|
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:
|
except:
|
||||||
return self.render_json({'error': self.lang.dns_timeout()})
|
return self.render_json({'error': request.session.lang.dns_timeout()})
|
||||||
return self.render_json({'ok': self.lang.dns_records_ok()})
|
return self.render_json({'ok': request.session.lang.dns_records_ok()})
|
||||||
|
|
||||||
def verify_http_action(self, request, **kwargs):
|
def verify_http_action(self, request, **kwargs):
|
||||||
# Check if all applications are accessible from the internet using 3rd party ping service
|
# Check if all applications are accessible from the internet using 3rd party ping service
|
||||||
proto = kwargs['proto']
|
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']]
|
domains = [mgr.domain]+['{}.{}'.format(mgr.conf['apps'][app]['host'], mgr.domain) for app in mgr.conf['apps']]
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
host = '{}:{}'.format(domain, mgr.port) if proto == 'https' and mgr.port != '443' else domain
|
host = '{}:{}'.format(domain, mgr.port) if proto == 'https' and mgr.port != '443' else domain
|
||||||
url = '{}://{}/'.format(proto, host)
|
url = '{}://{}/'.format(proto, host)
|
||||||
try:
|
try:
|
||||||
if not tools.ping_url(url):
|
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:
|
except:
|
||||||
return self.render_json({'error': self.lang.http_timeout()})
|
return self.render_json({'error': request.session.lang.http_timeout()})
|
||||||
return self.render_json({'ok': self.lang.http_hosts_ok(mgr.port if proto == 'https' else '80')})
|
return self.render_json({'ok': request.session.lang.http_hosts_ok(mgr.port if proto == 'https' else '80')})
|
||||||
|
|
||||||
def update_cert_action(self, request):
|
def update_cert_action(self, request):
|
||||||
# Update certificate - either request via Let's Encrypt or manually upload files
|
# Update certificate - either request via Let's Encrypt or manually upload files
|
||||||
try:
|
try:
|
||||||
mgr = AppMgr()
|
|
||||||
if request.form['method'] not in ['auto', 'manual']:
|
if request.form['method'] not in ['auto', 'manual']:
|
||||||
raise BadRequest()
|
raise BadRequest()
|
||||||
if request.form['method'] == 'manual':
|
if request.form['method'] == 'manual':
|
||||||
if not request.files['public']:
|
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']:
|
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['public'].save('/tmp/public.pem')
|
||||||
request.files['private'].save('/tmp/private.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/public.pem')
|
||||||
os.unlink('/tmp/private.pem')
|
os.unlink('/tmp/private.pem')
|
||||||
else:
|
else:
|
||||||
mgr.request_cert()
|
request.mgr.request_cert()
|
||||||
except BadRequest:
|
except BadRequest:
|
||||||
return self.render_json({'error': self.lang.malformed_request()})
|
return self.render_json({'error': request.session.lang.malformed_request()})
|
||||||
except:
|
except:
|
||||||
return self.render_json({'error': self.lang.cert_request_error()})
|
return self.render_json({'error': request.session.lang.cert_request_error()})
|
||||||
return self.render_json({'ok': self.lang.cert_installed()})
|
return self.render_json({'ok': request.session.lang.cert_installed()})
|
||||||
|
|
||||||
def update_common_action(self, request):
|
def update_common_action(self, request):
|
||||||
|
# Update common settings shared between apps - admin e-mail address, Google Maps API key
|
||||||
try:
|
try:
|
||||||
mgr = AppMgr()
|
request.mgr.update_common(request.form['email'], request.form['gmaps-api-key'])
|
||||||
mgr.update_common(request.form['email'], request.form['gmaps-api-key'])
|
|
||||||
except BadRequest:
|
except BadRequest:
|
||||||
return self.render_json({'error': self.lang.malformed_request()})
|
return self.render_json({'error': request.session.lang.malformed_request()})
|
||||||
return self.render_json({'ok': self.lang.common_updated()})
|
return self.render_json({'ok': request.session.lang.common_updated()})
|
||||||
|
|
||||||
def update_app_visibility_action(self, request):
|
def update_app_visibility_action(self, request):
|
||||||
|
# Update application visibility on portal page
|
||||||
try:
|
try:
|
||||||
mgr = AppMgr()
|
|
||||||
if request.form['value'] == 'true':
|
if request.form['value'] == 'true':
|
||||||
mgr.show_tiles(request.form['app'])
|
request.mgr.show_tiles(request.form['app'])
|
||||||
else:
|
else:
|
||||||
mgr.hide_tiles(request.form['app'])
|
request.mgr.hide_tiles(request.form['app'])
|
||||||
except (BadRequest, InvalidValueException):
|
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'})
|
return self.render_json({'ok': 'ok'})
|
||||||
|
|
||||||
def update_app_autostart_action(self, request):
|
def update_app_autostart_action(self, request):
|
||||||
|
# Update value determining if the app should be automatically started after VM boot
|
||||||
try:
|
try:
|
||||||
mgr = AppMgr()
|
|
||||||
if request.form['value'] == 'true':
|
if request.form['value'] == 'true':
|
||||||
mgr.enable_autostart(request.form['app'])
|
request.mgr.enable_autostart(request.form['app'])
|
||||||
else:
|
else:
|
||||||
mgr.disable_autostart(request.form['app'])
|
request.mgr.disable_autostart(request.form['app'])
|
||||||
except (BadRequest, InvalidValueException):
|
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'})
|
return self.render_json({'ok': 'ok'})
|
||||||
|
|
||||||
def start_app_action(self, request):
|
def start_app_action(self, request):
|
||||||
|
# Starts application along with its dependencies
|
||||||
try:
|
try:
|
||||||
mgr = AppMgr()
|
request.mgr.start_app(request.form['app'])
|
||||||
mgr.start_app(request.form['app'])
|
|
||||||
except (BadRequest, InvalidValueException):
|
except (BadRequest, InvalidValueException):
|
||||||
return self.render_json({'error': self.lang.malformed_request()})
|
return self.render_json({'error': request.session.lang.malformed_request()})
|
||||||
except:
|
except:
|
||||||
return self.render_json({'error': self.lang.stop_start_error()})
|
return self.render_json({'error': request.session.lang.stop_start_error()})
|
||||||
return self.render_json({'ok': self.lang.app_started()})
|
return self.render_json({'ok': request.session.lang.app_started()})
|
||||||
|
|
||||||
def stop_app_action(self, request):
|
def stop_app_action(self, request):
|
||||||
|
# Stops application along with its dependencies
|
||||||
try:
|
try:
|
||||||
mgr = AppMgr()
|
request.mgr.stop_app(request.form['app'])
|
||||||
mgr.stop_app(request.form['app'])
|
|
||||||
except (BadRequest, InvalidValueException):
|
except (BadRequest, InvalidValueException):
|
||||||
return self.render_json({'error': self.lang.malformed_request()})
|
return self.render_json({'error': request.session.lang.malformed_request()})
|
||||||
except:
|
except:
|
||||||
return self.render_json({'error': self.lang.stop_start_error()})
|
return self.render_json({'error': request.session.lang.stop_start_error()})
|
||||||
return self.render_json({'ok': self.lang.app_stopped()})
|
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):
|
class InvalidRecordException(Exception):
|
||||||
pass
|
pass
|
||||||
|
35
basic/srv/spotter/appmgr/wsgilang.py
Normal file
35
basic/srv/spotter/appmgr/wsgilang.py
Normal 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
|
32
basic/srv/spotter/appmgr/wsgisession.py
Normal file
32
basic/srv/spotter/appmgr/wsgisession.py
Normal 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)
|
@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import getpass
|
||||||
import sys
|
import sys
|
||||||
sys.path.append('/srv/spotter')
|
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('--email', help='Administrative e-mail address')
|
||||||
parser_update_common.add_argument('--gmaps-api-key', help='Google Maps API key')
|
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 = 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')
|
parser_request_cert.set_defaults(action='request-cert')
|
||||||
|
|
||||||
@ -90,6 +94,10 @@ elif args.action == 'update-host':
|
|||||||
mgr.update_host(args.domain, args.port)
|
mgr.update_host(args.domain, args.port)
|
||||||
elif args.action == 'update-common':
|
elif args.action == 'update-common':
|
||||||
mgr.update_common(args.email, args.gmaps_api_key)
|
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':
|
elif args.action == 'request-cert':
|
||||||
mgr.request_cert()
|
mgr.request_cert()
|
||||||
elif args.action == 'install-cert':
|
elif args.action == 'install-cert':
|
||||||
|
@ -132,6 +132,7 @@
|
|||||||
"gmaps-api-key": ""
|
"gmaps-api-key": ""
|
||||||
},
|
},
|
||||||
"host": {
|
"host": {
|
||||||
|
"adminpwd": "$2b$12$nLrIefUoWN.pK6j90gsfkO0/tg4EGXDmdjN8HOGB0U.9BcHTFxzWS",
|
||||||
"domain": "spotter.vm",
|
"domain": "spotter.vm",
|
||||||
"port": "443"
|
"port": "443"
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,35 @@ img {
|
|||||||
|
|
||||||
nav {
|
nav {
|
||||||
float: right;
|
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 {
|
nav a {
|
||||||
display: block;
|
display: block;
|
||||||
color: #00c;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
@ -44,13 +67,13 @@ header p,
|
|||||||
.setup-box {
|
.setup-box {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin-top: 13px;
|
margin-top: 13px;
|
||||||
margin-right: 13px;
|
|
||||||
border: solid 1px #000;
|
border: solid 1px #000;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-box {
|
.portal-box {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin-right: 13px;
|
||||||
width: 365px;
|
width: 365px;
|
||||||
float: left;
|
float: left;
|
||||||
height: 175px;
|
height: 175px;
|
||||||
@ -98,6 +121,7 @@ header p,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.setup-box input[type="text"],
|
.setup-box input[type="text"],
|
||||||
|
.setup-box input[type="password"],
|
||||||
.setup-box input[type="submit"],
|
.setup-box input[type="submit"],
|
||||||
.setup-box input[type="button"],
|
.setup-box input[type="button"],
|
||||||
.setup-box input[type="file"],
|
.setup-box input[type="file"],
|
||||||
|
200
basic/srv/spotter/static/js/admin.js
Normal file
200
basic/srv/spotter/static/js/admin.js
Normal 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;
|
||||||
|
}
|
@ -1,163 +1,7 @@
|
|||||||
$(function() {
|
$(function() {
|
||||||
$('#update-host').on('submit', update_host);
|
$('#menu-button').on('click', toggle_menu);
|
||||||
$('#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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function update_host() {
|
function toggle_menu() {
|
||||||
$('#host-submit').hide();
|
$('#menu').toggle();
|
||||||
$('#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;
|
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,27 @@
|
|||||||
<link rel="stylesheet" href="static/css/style.css" type="text/css" media="screen">
|
<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/jquery-3.3.1.min.js"></script>
|
||||||
<script src="static/js/script.js"></script>
|
<script src="static/js/script.js"></script>
|
||||||
|
{% if session.admin %}
|
||||||
|
<script src="static/js/admin.js"></script>
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
{% set template = self._TemplateReference__context.name %}
|
<div id="menu-button">
|
||||||
{% if template != 'portal.html' %}<a href="/">Portál</a>{% endif %}
|
<div></div>
|
||||||
{% if template != 'setup-host.html' %}<a href="/setup-host">Nastavení hostitele</a>{% endif %}
|
<div></div>
|
||||||
{% if template != 'setup-apps.html' %}<a href="/setup-apps">Nastavení aplikací</a>{% endif %}
|
<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>
|
</nav>
|
||||||
<header>
|
<header>
|
||||||
<h1>CLUSTER NGO</h1>
|
<h1>CLUSTER NGO</h1>
|
||||||
|
28
basic/srv/spotter/templates/login.html
Normal file
28
basic/srv/spotter/templates/login.html
Normal 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> </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 %}
|
123
basic/srv/spotter/templates/portal-user.html
Normal file
123
basic/srv/spotter/templates/portal-user.html
Normal 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 %}
|
@ -55,4 +55,40 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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> </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 %}
|
{% endblock %}
|
||||||
|
@ -10,10 +10,7 @@ application = WSGIApp()
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import os
|
import os
|
||||||
|
from werkzeug.contrib.fixers import ProxyFix
|
||||||
from werkzeug.serving import run_simple
|
from werkzeug.serving import run_simple
|
||||||
from werkzeug.wsgi import SharedDataMiddleware
|
|
||||||
|
|
||||||
application = SharedDataMiddleware(application, {
|
run_simple('127.0.0.1', 8080, ProxyFix(application))
|
||||||
'/static': os.path.join(os.path.dirname(__file__), 'static')
|
|
||||||
})
|
|
||||||
run_simple('127.0.0.1', 8080, application, use_reloader=True)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user