diff --git a/basic.sh b/basic.sh index af56af3..ce5f959 100755 --- a/basic.sh +++ b/basic.sh @@ -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 diff --git a/basic/srv/spotter/appmgr/__init__.py b/basic/srv/spotter/appmgr/__init__.py index e1c8269..dce96a5 100644 --- a/basic/srv/spotter/appmgr/__init__.py +++ b/basic/srv/spotter/appmgr/__init__.py @@ -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')] diff --git a/basic/srv/spotter/appmgr/tools.py b/basic/srv/spotter/appmgr/tools.py index a9c53b5..55e6816 100644 --- a/basic/srv/spotter/appmgr/tools.py +++ b/basic/srv/spotter/appmgr/tools.py @@ -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']) diff --git a/basic/srv/spotter/appmgr/wsgiapp.py b/basic/srv/spotter/appmgr/wsgiapp.py index 3bdd3f8..fb0b7d3 100644 --- a/basic/srv/spotter/appmgr/wsgiapp.py +++ b/basic/srv/spotter/appmgr/wsgiapp.py @@ -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 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': 'Spuštěna (zastavit)', - 'app_stopped': 'Zastavena (spustit)', - '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([ - Rule('/', endpoint='portal_view'), + 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 diff --git a/basic/srv/spotter/appmgr/wsgilang.py b/basic/srv/spotter/appmgr/wsgilang.py new file mode 100644 index 0000000..98a2d0a --- /dev/null +++ b/basic/srv/spotter/appmgr/wsgilang.py @@ -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 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': 'Spuštěna (zastavit)', + 'app_stopped': 'Zastavena (spustit)', + '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 diff --git a/basic/srv/spotter/appmgr/wsgisession.py b/basic/srv/spotter/appmgr/wsgisession.py new file mode 100644 index 0000000..938ba06 --- /dev/null +++ b/basic/srv/spotter/appmgr/wsgisession.py @@ -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) diff --git a/basic/srv/spotter/cli.py b/basic/srv/spotter/cli.py index 529b6dd..159301d 100755 --- a/basic/srv/spotter/cli.py +++ b/basic/srv/spotter/cli.py @@ -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': diff --git a/basic/srv/spotter/config.json b/basic/srv/spotter/config.json index a16de79..0de0323 100644 --- a/basic/srv/spotter/config.json +++ b/basic/srv/spotter/config.json @@ -132,6 +132,7 @@ "gmaps-api-key": "" }, "host": { + "adminpwd": "$2b$12$nLrIefUoWN.pK6j90gsfkO0/tg4EGXDmdjN8HOGB0U.9BcHTFxzWS", "domain": "spotter.vm", "port": "443" } diff --git a/basic/srv/spotter/static/css/style.css b/basic/srv/spotter/static/css/style.css index 2b307a6..bd748bd 100644 --- a/basic/srv/spotter/static/css/style.css +++ b/basic/srv/spotter/static/css/style.css @@ -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"], diff --git a/basic/srv/spotter/static/js/admin.js b/basic/srv/spotter/static/js/admin.js new file mode 100644 index 0000000..3c5b03a --- /dev/null +++ b/basic/srv/spotter/static/js/admin.js @@ -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('
'); + $.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('
'); + $.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; +} diff --git a/basic/srv/spotter/static/js/script.js b/basic/srv/spotter/static/js/script.js index 13625c9..8fd36e7 100644 --- a/basic/srv/spotter/static/js/script.js +++ b/basic/srv/spotter/static/js/script.js @@ -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('
'); - $.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('
'); - $.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(); } diff --git a/basic/srv/spotter/templates/layout.html b/basic/srv/spotter/templates/layout.html index 3a2b61b..a6ceee6 100644 --- a/basic/srv/spotter/templates/layout.html +++ b/basic/srv/spotter/templates/layout.html @@ -11,13 +11,27 @@ + {% if session.admin %} + + {% endif %}

CLUSTER NGO

diff --git a/basic/srv/spotter/templates/login.html b/basic/srv/spotter/templates/login.html new file mode 100644 index 0000000..61ecc79 --- /dev/null +++ b/basic/srv/spotter/templates/login.html @@ -0,0 +1,28 @@ +{% extends 'layout.html' %} +{% block title %}Přihlášení{% endblock %} +{% block body %} +
+

Přihlášení

+
+ + + + + + + + + + + + + +
Jméno:admin
Heslo
  + +
+ {% if message is defined %} +

{{ message }}

+ {% endif %} +
+
+{% endblock %} diff --git a/basic/srv/spotter/templates/portal.html b/basic/srv/spotter/templates/portal-admin.html similarity index 100% rename from basic/srv/spotter/templates/portal.html rename to basic/srv/spotter/templates/portal-admin.html diff --git a/basic/srv/spotter/templates/portal-user.html b/basic/srv/spotter/templates/portal-user.html new file mode 100644 index 0000000..fe041c5 --- /dev/null +++ b/basic/srv/spotter/templates/portal-user.html @@ -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'] %} +
+

Řízení humanítární činnosti

+

Přístup určený k bezpečnému vyzkoušení aplikace. Zde můžete přidávat i mazat testovací data.

+
+{% endif %} + +{% if conf['apps']['sambro']['visible'] %} +
+

Centrum hlášení a výstrah

+

Samostatná instance s šablonou pro centrum hlášení a výstrah.

+
+{% endif %} + +{% if conf['apps']['crisiscleanup']['visible'] %} +
+

Mapování následků katastrof

+

Mapování krizové pomoci při odstraňování následků katastrof a koordinaci práce. Jde o majetek, ne o lidi.

+
+{% endif %} + +{% if conf['apps']['ckan']['visible'] %} +
+

Datový sklad

+

Repository management a datová analýza pro vytváření otevřených dat.

+
+{% endif %} + +{% if conf['apps']['opendatakit-build']['visible'] %} +
+

Sběr formulářových dat

+

Sběr dat s pomocí smartphone.
Aplikace pro návrh formulářů

+
+{% endif %} + +{% if conf['apps']['opendatakit']['visible'] %} +
+

Sběr formulářových dat

+

Sběr dat s pomocí smartphone.

+
+{% endif %} + +{% if conf['apps']['openmapkit']['visible'] %} +
+

Sběr mapových dat

+

Sběr dat s pomocí smartphone.
+

+{% endif %} + +{% if conf['apps']['frontlinesms']['visible'] %} +
+

Hromadné odesílání zpráv

+

SMS messaging přes veřejné datové brány

+
+{% endif %} + +{% if conf['apps']['seeddms']['visible'] %} +
+

Archiv dokumentace

+

Dokument management na dokumentaci a projektovou dokumentaci

+
+{% endif %} + +{% if conf['apps']['pandora']['visible'] %} +
+

Archiv medií

+

Media management na foto a video z krizové události. Tvorba metadat, komentářů, lokalizace v čase a na mapě.

+
+{% endif %} + +{% if conf['apps']['ushahidi']['visible'] %} +
+

Skupinová reakce na události

+

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.

+
+{% endif %} + +{% if conf['apps']['kanboard']['visible'] %} +
+

Kanban řízení projektů

+

Usnadňuje tvorbu a řízení projektů s pomocí Kanban metodiky.

+
+{% endif %} + +{% if conf['apps']['gnuhealth']['visible'] %} +
+

Lékařské záznamy pacientů

+

Zdravotní a nemocniční informační systém.

+
+{% endif %} + +{% if conf['apps']['sigmah']['visible'] %} +
+

Finanční řízení sbírek

+

Rozpočtování získávání finančních prostředků.

+
+{% endif %} + +{% if conf['apps']['motech']['visible'] %} +
+

Automatizace komunikace

+

Integrace zdravotnických a komunikačních služeb.

+
+{% endif %} + +{% if conf['apps']['mifosx']['visible'] %} +
+

Mikrofinancování rozvojových projektů

+

Nástroj na rozvojovou, humanitární pomoc a mikrofinancování.

+
+{% endif %} + +
+

Cluster SpotterCluster Spotter

+

Info o Misi a Vizi projektu, včetně kontaktu. Zachovejte data bezpečná a neposkytujte je nepovolaným osobám.
+ CC 4.0 CZ by TS. Content is based on PD, CC, GNU/GPL. Brand names, trademarks belong to their respective holders. +

+
+{% endblock %} diff --git a/basic/srv/spotter/templates/setup-apps.html b/basic/srv/spotter/templates/setup-apps.html index 10cf7ba..b853bea 100644 --- a/basic/srv/spotter/templates/setup-apps.html +++ b/basic/srv/spotter/templates/setup-apps.html @@ -55,4 +55,40 @@ + +
+

Správce virtuálního stroje

+

Změna hesla k šifrovanému diskovému oddílu a administračnímu rozhraní.

+
+ + + + + + + + + + + + + + + + + +
Stávající heslo:
Nové heslo:
Kontrola nového hesla:
  + +
+
+
+ Provádí se změna hesla, prosím čekejte... +
+
+
+

Restartování nebo vypnutí virtuálního stroje.

+ + +
+
{% endblock %} diff --git a/basic/srv/spotter/wsgi.py b/basic/srv/spotter/wsgi.py index 8a34b03..64e8896 100755 --- a/basic/srv/spotter/wsgi.py +++ b/basic/srv/spotter/wsgi.py @@ -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))