User Accounts, Legacy Routers, Delete Routers

This commit is contained in:
Dominik Heidler 2016-02-29 18:51:58 +01:00
parent 0a57b6e5c7
commit 19329ef85c
15 changed files with 798 additions and 25 deletions

10
ffmap/db/users.py Normal file
View File

@ -0,0 +1,10 @@
#!/usr/bin/python3
from pymongo import MongoClient
client = MongoClient()
db = client.freifunk
# create db indexes
db.users.create_index("email")
db.users.create_index("nickname")

View File

@ -74,21 +74,22 @@ def import_nodewatcher_xml(mac, xml):
router_id = db.routers.insert_one(router_update).inserted_id
status = router_update["status"]
except ValueError as e:
print("Warning: Unable to parse xml from %s: %s" % (mac, e))
import traceback
print("Warning: Unable to parse xml from %s: %s\n__%s" % (mac, e, traceback.format_exc().replace("\n", "\n__")))
if router:
db.routers.update_one({"_id": router_id}, {"$set": {"status": "unknown"}})
status = "unknown"
if router_id:
# fire events
with suppress(KeyError, TypeError):
with suppress(KeyError, TypeError, UnboundLocalError):
if router["system"]["uptime"] > router_update["system"]["uptime"]:
events.append({
"time": datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc),
"type": "reboot",
})
with suppress(KeyError, TypeError):
with suppress(KeyError, TypeError, UnboundLocalError):
if router["software"]["firmware"] != router_update["software"]["firmware"]:
events.append({
"time": datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc),
@ -96,7 +97,7 @@ def import_nodewatcher_xml(mac, xml):
"comment": "%s -> %s" % (router["software"]["firmware"], router_update["software"]["firmware"]),
})
with suppress(KeyError, TypeError):
with suppress(KeyError, TypeError, UnboundLocalError):
if router["hostname"] != router_update["hostname"]:
events.append({
"time": datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc),
@ -104,7 +105,7 @@ def import_nodewatcher_xml(mac, xml):
"comment": "%s -> %s" % (router["hostname"], router_update["hostname"]),
})
with suppress(KeyError, TypeError):
with suppress(KeyError, TypeError, UnboundLocalError):
if router["hood"] != router_update["hood"]:
events.append({
"time": datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc),
@ -213,7 +214,7 @@ def parse_nodewatcher_xml(xml):
},
"hardware": {
"chipset": tree.xpath("/data/system_data/chipset/text()")[0],
"name": tree.xpath("/data/system_data/model/text()")[0],
#"name": tree.xpath("/data/system_data/model/text()")[0],
"cpu": tree.xpath("/data/system_data/cpu/text()")[0]
},
"software": {
@ -228,6 +229,12 @@ def parse_nodewatcher_xml(xml):
}
}
# data.system_data.model
if len(tree.xpath("/data/system_data/model/text()")) > 0:
router_update["hardware"]["name"] = tree.xpath("/data/system_data/model/text()")[0]
else:
router_update["hardware"]["name"] = "Legacy"
# data.system_data.description
if len(tree.xpath("/data/system_data/description/text()")) > 0:
router_update["description"] = tree.xpath("/data/system_data/description/text()")[0]
@ -247,6 +254,10 @@ def parse_nodewatcher_xml(xml):
# data.system_data.contact
if len(tree.xpath("/data/system_data/contact/text()")) > 0:
router_update["system"]["contact"] = tree.xpath("/data/system_data/contact/text()")[0]
user = db.users.find_one({"email": router_update["system"]["contact"]})
if user:
# post-netmon router gets its user assigned
router_update["user"] = {"nickname": user["nickname"], "_id": user["_id"]}
# data.system_data.geo
with suppress(AssertionError, IndexError):
@ -341,14 +352,19 @@ def netmon_fetch_router_info(mac):
for r in tree.xpath("/netmon_response/router"):
user_netmon_id = int(r.xpath("user_id/text()")[0])
user = db.users.find_one({"netmon_id": user_netmon_id})
if user:
user_id = user["_id"]
else:
user_id = db.users.insert({
"netmon_id": user_netmon_id,
"nickname": r.xpath("user/nickname/text()")[0]
})
user = db.users.find_one({"_id": user_id})
if not user:
nickname = r.xpath("user/nickname/text()")[0]
user = db.users.find_one({"nickname": nickname})
if user:
# non-netmon user with email key for new routers gets old router
db.users.update_one({"_id": user['_id']}, {"$set": {"netmon_id": user_netmon_id}})
else:
# netmon user gets old user
user_id = db.users.insert({
"netmon_id": user_netmon_id,
"nickname": nickname
})
user = db.users.find_one({"_id": user_id})
router = {
"netmon_id": int(r.xpath("router_id/text()")[0]),

View File

@ -79,3 +79,16 @@ def record_global_stats():
"router_status": router_status(),
"total_clients": total_clients()
})
def router_user_sum():
r = db.routers.aggregate([{"$group": {
"_id": "$user.nickname",
"count": {"$sum": 1},
"clients": {"$sum": "$system.clients"}
}}])
result = {}
for rs in r:
if rs["_id"]:
result[rs["_id"]] = {"routers": rs["count"], "clients": rs["clients"]}
return result

90
ffmap/usertools.py Normal file
View File

@ -0,0 +1,90 @@
#!/usr/bin/python3
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..'))
from ffmap.dbtools import FreifunkDB
from werkzeug.security import generate_password_hash, check_password_hash
import datetime
db = FreifunkDB().handle()
class AccountWithEmailExists(Exception):
pass
class AccountWithNicknameExists(Exception):
pass
class AccountNotExisting(Exception):
pass
class InvalidToken(Exception):
pass
def register_user(nickname, email, password):
user_with_nick = db.users.find_one({"nickname": nickname})
user_with_email = db.users.find_one({"email": email})
if user_with_email:
raise AccountWithEmailExists()
elif user_with_nick and "email" in user_with_nick:
raise AccountWithNicknameExists()
else:
user_update = {
"nickname": nickname,
"password": generate_password_hash(password),
"email": email,
"created": datetime.datetime.utcnow()
}
if user_with_nick:
db.users.update_one({"_id": user_with_nick["_id"]}, {"$set": user_update})
return user_with_nick["_id"]
else:
return db.users.insert_one(user_update).inserted_id
def check_login_details(nickname, password):
user = db.users.find_one({"nickname": nickname})
if user and check_password_hash(user.get('password', ''), password):
return user
else:
return False
def reset_user_password(email, token=None, password=None):
user = db.users.find_one({"email": email})
if not user:
raise AccountNotExisting()
elif password:
if user.get("token") == token:
db.users.update_one({"_id": user["_id"]}, {
"$set": {"password": generate_password_hash(password)},
"$unset": {"token": 1},
})
else:
raise InvalidToken()
elif token:
db.users.update_one({"_id": user["_id"]}, {"$set": {"token": token}})
def set_user_password(nickname, password):
user = db.users.find_one({"nickname": nickname})
if not user:
raise AccountNotExisting()
elif password:
db.users.update_one({"_id": user["_id"]}, {
"$set": {"password": generate_password_hash(password)},
})
def set_user_email(nickname, email):
user = db.users.find_one({"nickname": nickname})
user_with_email = db.users.find_one({"email": email})
if user_with_email:
raise AccountWithEmailExists()
if not user:
raise AccountNotExisting()
elif email:
db.users.update_one({"_id": user["_id"]}, {
"$set": {"email": email},
})
def set_user_admin(nickname, admin):
db.users.update({"nickname": nickname}, {"$set": {"admin": admin}})

View File

@ -8,13 +8,15 @@ from ffmap.web.api import api
from ffmap.web.filters import filters
from ffmap.dbtools import FreifunkDB
from ffmap import stattools
from ffmap.usertools import *
from ffmap.web.helpers import *
from flask import Flask, render_template, request, Response, redirect, url_for, flash
from flask import Flask, render_template, request, Response, redirect, url_for, flash, session
import bson
import pymongo
from bson.json_util import dumps as bson2json
from bson.objectid import ObjectId
import base64
app = Flask(__name__)
app.register_blueprint(api, url_prefix='/api')
@ -52,22 +54,96 @@ def router_list():
@app.route('/routers/<dbid>', methods=['GET', 'POST'])
def router_info(dbid):
try:
router = db.routers.find_one({"_id": ObjectId(dbid)})
assert router
if request.method == 'POST':
if request.form.get("act") == "netmon_resync":
r = db.routers.update_one({"_id": ObjectId(dbid)}, {"$unset": {"netmon_id": 1}})
assert r.matched_count > 0
flash("<b>Netmon Sync triggered!</b>", "success")
return redirect(url_for("router_info", dbid=dbid))
router = db.routers.find_one({"_id": ObjectId(dbid)})
assert router
if request.form.get("act") == "delete":
if is_authorized(router["user"]["nickname"], session):
db.routers.delete_one({"_id": ObjectId(dbid)})
flash("<b>Router <i>%s</i> deleted!</b>" % router["hostname"], "success")
return redirect(url_for("index"))
else:
flash("<b>You are not authorized to perform this action!</b>", "danger")
except (bson.errors.InvalidId, AssertionError):
return "Router not found"
if request.args.get('json', None) != None:
del router["stats"]
#FIXME: Only as admin
return Response(bson2json(router, sort_keys=True, indent=4), mimetype='application/json')
else:
return render_template("router.html", router=router, tileurls=tileurls)
@app.route('/users')
def user_list():
return render_template("user_list.html",
user_routers = stattools.router_user_sum(),
users = db.users.find({}, {"nickname": 1, "email": 1, "created": 1, "admin": 1}).sort("nickname", pymongo.ASCENDING)
)
@app.route('/users/<nickname>', methods=['GET', 'POST'])
def user_info(nickname):
try:
user = db.users.find_one({"nickname": nickname})
assert user
except AssertionError:
return "User not found"
if request.method == 'POST':
if is_authorized(user["nickname"], session):
if request.form.get("action") == "changepw":
if request.form["password"] != request.form["password_rep"]:
flash("<b>Passwords did not match!</b>", "danger")
elif request.form["password"] == "":
flash("<b>Password must not be empty!</b>", "danger")
else:
set_user_password(user["nickname"], request.form["password"])
flash("<b>Password changed!</b>", "success")
elif request.form.get("action") == "changemail":
if request.form["email"] != request.form["email_rep"]:
flash("<b>E-Mail addresses do not match!</b>", "danger")
elif not "@" in request.form["email"]:
flash("<b>Invalid E-Mail addresse!</b>", "danger")
else:
try:
set_user_email(user["nickname"], request.form["email"])
flash("<b>E-Mail changed!</b>", "success")
if not session.get('admin'):
password = base64.b32encode(os.urandom(10)).decode()
set_user_password(user["nickname"], password)
send_email(
recipient = request.form['email'],
subject = "Password for %s" % user['nickname'],
content = "Your Password: %s" % password
)
return logout()
else:
# force db data reload
user = db.users.find_one({"nickname": nickname})
except AccountWithEmailExists:
flash("<b>There is already an account with this E-Mail Address!</b>", "danger")
elif request.form.get("action") == "changeadmin":
if session.get('admin'):
set_user_admin(nickname, request.form.get("admin") == "true")
# force db data reload
user = db.users.find_one({"nickname": nickname})
else:
flash("<b>You are not authorized to perform this action!</b>", "danger")
routers=db.routers.find({"user._id": user["_id"]}, {
"hostname": 1,
"status": 1,
"hood": 1,
"software.firmware": 1,
"hardware.name": 1,
"created": 1,
"system.uptime": 1,
"system.clients": 1,
}).sort("hostname", pymongo.ASCENDING)
return render_template("user.html", user=user, routers=routers)
@app.route('/statistics')
def global_statistics():
hoods = stattools.hoods()
@ -79,9 +155,81 @@ def global_statistics():
router_firmwares = stattools.router_firmwares(),
hoods = hoods,
hoods_sum = stattools.hoods_sum(),
newest_routers = db.routers.find({}, {"hostname": 1, "hood": 1, "created": 1}).sort("created", pymongo.DESCENDING).limit(len(hoods)+1)
newest_routers = db.routers.find({"hardware.name": {"$ne": "Legacy"}}, {"hostname": 1, "hood": 1, "created": 1}).sort("created", pymongo.DESCENDING).limit(len(hoods)+1)
)
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
try:
password = base64.b32encode(os.urandom(10)).decode()
register_user(request.form['user'], request.form['email'], password)
send_email(
recipient = request.form['email'],
subject = "Password for %s" % request.form['user'],
content = "Your Password: %s" % password
)
flash("<b>Registration successful!</b> - Your password was sent to %s" % request.form['email'], "success")
except AccountWithEmailExists:
flash("<b>There is already an account with this E-Mail Address!</b>", "danger")
except AccountWithNicknameExists:
flash("<b>There is already an active account with this Nickname!</b>", "danger")
return render_template("register.html")
@app.route('/resetpw', methods=['GET', 'POST'])
def resetpw():
try:
if request.method == 'POST':
token = base64.b32encode(os.urandom(10)).decode()
reset_user_password(request.form['email'], token)
send_email(
recipient = request.form['email'],
subject = "Password reset link",
content = url_for('resetpw', email=request.form['email'], token=token, _external=True)
)
flash("<b>A password reset link was sent to %s</b>" % request.form['email'], "success")
elif "token" in request.args:
password = base64.b32encode(os.urandom(10)).decode()
reset_user_password(request.args['email'], request.args['token'], password)
send_email(
recipient = request.args['email'],
subject = "Password",
content = "Your Password: %s" % password
)
flash("<b>Password reset successful!</b> - Your password was sent to %s" % request.args['email'], "success")
except AccountNotExisting:
flash("<b>No Account found with this E-Mail address!</b>", "danger")
except InvalidToken:
flash("<b>Invalid password token!</b>", "danger")
return render_template("resetpw.html")
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
referrer = request.form["referrer"]
user_login = check_login_details(request.form["user"], request.form["password"])
if user_login:
session['user'] = user_login["nickname"]
session['admin'] = user_login.get("admin", False)
return redirect(referrer)
else:
flash("<b>Invalid login details!</b>", "danger")
else:
referrer = request.referrer or url_for("index")
return render_template("login.html", referrer=referrer)
@app.route('/logout')
def logout():
session.pop('user', None)
return redirect(request.referrer or url_for("index"))
@app.context_processor
def register_helpers():
return {
"is_authorized_for": lambda owner: is_authorized(owner, session)
}
app.secret_key = os.urandom(24)

View File

@ -1,12 +1,13 @@
#!/usr/bin/python3
from flask import Blueprint
from flask import Blueprint, session
from dateutil import tz
from bson.json_util import dumps as bson2json
import json
import datetime
import re
import pymongo
import hashlib
filters = Blueprint("filters", __name__)
@ -137,3 +138,39 @@ def status2css(status):
"update": "primary",
}
return "label label-%s" % status_map.get(status, "default")
@filters.app_template_filter('anon_email')
def anon_email(email, replacement_char='.'):
if 'user' in session:
return email
try:
def anon_str(s, full=False):
if full:
return replacement_char * len(s)
else:
hide_pos = int(len(s)/2)
return s[:hide_pos] + replacement_char + s[(hide_pos+1):]
prefix, tld = email.rsplit('.', 1)
user, domain = prefix.split('@')
return '%s@%s.%s' % (anon_str(user), anon_str(domain), anon_str(tld, True))
except:
return email
@filters.app_template_filter('anon_email_regex')
def anon_email_regex(email):
return anon_email(email, '*').replace('.', '\.').replace('*', '.').replace('+', '\+').replace('_', '\_')
@filters.app_template_filter('gravatar_url')
def gravatar_url(email):
return "https://www.gravatar.com/avatar/%s?d=identicon" % hashlib.md5(email.encode("UTF-8").lower()).hexdigest()
@filters.app_template_filter('webui_addr')
def webui_addr(router_netifs):
try:
for br_mesh in filter(lambda n: n["name"] == "br-mesh", router_netifs):
for ipv6 in br_mesh["ipv6_addrs"]:
if ipv6.startswith("fdff") and len(ipv6) == 20:
return ipv6
except (KeyError, TypeError):
return None

View File

@ -1,6 +1,9 @@
#!/usr/bin/python3
import bson
import re
import smtplib
from email.mime.text import MIMEText
def format_query(query_usr):
query_list = []
@ -12,7 +15,18 @@ def format_query(query_usr):
query_list.append("%s%s" % (qtag, value))
return " ".join(query_list)
allowed_filters = ('status', 'hood', 'community', 'user.nickname', 'hardware.name', 'software.firmware', 'netifs.mac', 'netmon_id', 'hostname')
allowed_filters = (
'status',
'hood',
'community',
'user.nickname',
'hardware.name',
'software.firmware',
'netifs.mac',
'netmon_id',
'hostname',
'system.contact',
)
def parse_router_list_search_query(args):
query_usr = bson.SON()
if "q" in args:
@ -38,6 +52,27 @@ def parse_router_list_search_query(args):
query[key] = {"$regex": value.replace('.', '\.').replace('_', ' '), "$options": 'i'}
elif key == 'netmon_id':
query[key] = int(value)
elif key == 'system.contact':
if not '\.' in value:
value = re.escape(value)
query[key] = {"$regex": value, "$options": 'i'}
else:
query[key] = value
return (query, format_query(query_usr))
def send_email(recipient, subject, content, sender="FFF Monitoring <noreply@monitoring.freifunk-franken.de>"):
msg = MIMEText(content)
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = recipient
s = smtplib.SMTP('localhost')
s.send_message(msg)
s.quit()
def is_authorized(owner, session):
if owner == session.get("user"):
return True
elif session.get("admin"):
return True
else:
return False

View File

@ -36,6 +36,7 @@
{%- for fkt, text in [(["index"], "Home"),
(["router_map"], "Map"),
(["router_list", "router_info"], "Routers"),
(["user_list", "user_info"], "Users"),
(["global_statistics"], "Statistics"),
] %}
<li class="{{ "active" if request.endpoint in fkt }}"><a href="{{ url_for(fkt[0]) }}">{{ text }}</a></li>
@ -43,6 +44,16 @@
</ul>
{%- block search %}
{%- endblock %}
{%- block nav_login %}
<p class="navbar-text navbar-right" style="margin-right: 0;">
{%- if "user" in session -%}
<a href="{{ url_for('user_info', nickname=session.user) }}" class="navbar-link">{{ session.user }}</a>
[<a href="{{ url_for('logout') }}" class="navbar-link">Logout</a>]
{%- else %}
<a href="{{ url_for('login') }}" class="navbar-link">Login</a>
{%- endif %}
</p>
{%- endblock %}
</div><!--/.nav-collapse -->
</div>
</nav>

View File

@ -0,0 +1,42 @@
{% extends "bootstrap.html" %}
{% block title %}{{super()}} :: Login{% endblock %}
{% block content %}
<div class="row text-center">
<div class="col-md-12">
<h2>Please sign in</h2>
</div>
</div>
<div class="row" style="padding-top: 40px;">
<div class="col-xs-12 col-sm-8 col-sm-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Login</div>
<div class="panel-body">
<form class="form-horizontal" method="post">
<div class="form-group">
<label class="col-sm-3 control-label">Nickname</label>
<div class="col-sm-8">
<input type="text" name="user" class="form-control" placeholder="Nickname" required autofocus />
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Password</label>
<div class="col-sm-8">
<input type="password" name="password" class="form-control" placeholder="Password" required />
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-8">
<input type="hidden" name="referrer" value="{{ referrer }}" />
<button type="submit" class="btn btn-primary">Login</button>
</div>
</div>
</form>
<div class="col-sm-12 text-center">
<a href="{{ url_for('register') }}">Create Account</a> -
<a href="{{ url_for('resetpw') }}">Reset Password</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends "bootstrap.html" %}
{% block title %}{{super()}} :: Register{% endblock %}
{% block content %}
<div class="row text-center">
<div class="col-md-12">
<h2>Registration</h2>
</div>
</div>
<div class="row" style="padding-top: 40px;">
<div class="col-xs-12 col-sm-8 col-sm-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Create User Account</div>
<div class="panel-body">
<form class="form-horizontal" method="post">
<div class="form-group">
<label class="col-sm-3 control-label">Nickname</label>
<div class="col-sm-8">
<input type="text" name="user" class="form-control" placeholder="Nickname" />
<p class="help-block">
Falls bereits ein Netmon Account existiert, <b>muss</b> hier der gleiche Nickname
verwendet werden, damit die alten Router (bis Firmware 0.5.2) dem Account
zugeordnet werden k&ouml;nnen.
</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">E-Mail</label>
<div class="col-sm-8">
<input type="email" name="email" class="form-control" placeholder="Email" />
<p class="help-block">
Die selbe E-Mail Adresse muss auf den Routern (mit Firmware &gt; 0.5.2) eingegeben werden,
damit die Router dem Account zugeordnet werden k&ouml;nnen.
</p>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-8">
<button type="submit" class="btn btn-success">Register</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "bootstrap.html" %}
{% block title %}{{super()}} :: Reset password{% endblock %}
{% block content %}
<div class="row text-center">
<div class="col-md-12">
<h2>Password forgotten</h2>
</div>
</div>
<div class="row" style="padding-top: 40px;">
<div class="col-xs-12 col-sm-8 col-sm-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Reset password</div>
<div class="panel-body">
<form class="form-horizontal" method="post">
<div class="form-group">
<label class="col-sm-3 control-label">E-Mail</label>
<div class="col-sm-8">
<input type="email" name="email" class="form-control" placeholder="Email" />
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-8">
<button type="submit" class="btn btn-primary">Reset password</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -49,6 +49,8 @@
</button>
<ul class="dropdown-menu">
<li><a href="#" onclick="$('#act').val('netmon_resync'); $('#actform').submit()">Trigger Netmon Sync</a></li>
{# FIXME: If authorized #}
<li><a href="#" onclick="$('#act').val('delete'); $('#actform').submit()">Delete Router</a></li>
</ul>
</div>
</form>
@ -94,6 +96,9 @@
{%- else %}
{{ router.hostname }}
{%- endif %}
{%- if router.netifs|webui_addr %}
(<a href="http://[{{ router.netifs|webui_addr }}]">WebUI</a>)
{%- endif %}
</td></tr>
<tr><th>Status</th><td><span class="{{ router.status|status2css }}">{{ router.status }}</span>
{%- if router.status == "online" %}
@ -123,8 +128,17 @@
{%- endif -%}
</td></tr>
{%- endif %}
{%- if router.user %}
<tr><th>User</th><td><a href="{{ url_for('router_list') }}?q=user.nickname:{{ router.user.nickname }}">{{ router.user.nickname }}</a></td></tr>
{%- if router.user or router.system.contact %}
<tr><th>User</th><td>
{%- if router.user -%}
<a href="{{ url_for('user_info', nickname=router.user.nickname) }}">{{ router.user.nickname }}</a>
{%- if router.system.contact %}
(<a href="{{ url_for('router_list', q='system.contact:%s' % router.system.contact|anon_email_regex) }}">{{ router.system.contact|anon_email }}</a>)
{%- endif -%}
{%- else -%}
<a href="{{ url_for('router_list', q='system.contact:%s' % router.system.contact|anon_email_regex) }}">{{ router.system.contact|anon_email }}</a>
{%- endif -%}
</td></tr>
{%- endif %}
<tr><th>Hardware</th><td><span title="{{ router.hardware.chipset }}">{{ router.hardware.name }}</span></td></tr>
<tr><th>WAN Uplink</th><td><span class="{{ "glyphicon glyphicon-ok" if router.system.has_wan_uplink else "glyphicon glyphicon-remove" }}"></span></td></tr>
@ -141,9 +155,9 @@
<div class="panel-heading">Software</div>
<div class="panel-body">
<table class="table table-condensed">
<tr><th>Firmware</th><td><span title="{{ router.software.firmware_rev }}">{{ router.software.firmware }}</span></td></tr>
<tr><th>Operating&nbsp;System</th><td>{{ router.software.os }}</td></tr>
<tr><th>Kernel</th><td>{{ router.software.kernel }}</td></tr>
<tr><th>Firmware</th><td><span title="{{ router.software.firmware_rev }}">{{ router.software.firmware }}</span></td></tr>
<tr><th>B.A.T.M.A.N. adv</th><td>{{ router.software.batman_adv }}</td></tr>
<tr><th>Nodewatcher</th><td>{{ router.software.nodewatcher }}</td></tr>
</table>

View File

@ -13,7 +13,7 @@
</style>
{% endblock %}
{%- block search %}
<form class="navbar-form navbar-right" role="search">
<form class="navbar-form navbar-left" role="search">
<div class="input-group">
<input type="text" class="form-control" placeholder="Search routers" name="q" value="{{ query_str }}">
<span class="input-group-btn">
@ -56,7 +56,7 @@
</table>
</div>
<div style="margin-bottom: 20px;">
{{ routers.count() }} Router{{ "s" if (routers.count() > 1) else "" }} found.
{{ routers.count() }} Router{{ "s" if (routers.count() == 1) else "" }} found.
</div>
<script type="text/javascript">
$(document).ready(function() {

View File

@ -0,0 +1,210 @@
{% extends "bootstrap.html" %}
{% block title %}{{super()}} :: {{ user.nickname }}{% endblock %}
{% block head %}{{super()}}
<script src="{{ url_for('static', filename='js/datatables/jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/datatables/dataTables.bootstrap.min.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/datatables/dataTables.bootstrap.min.css') }}">
<style type="text/css">
.navbar, .table-condensed {
margin-bottom: 0;
}
.table-condensed tr:last-child td, th {
border-bottom: 1px solid #ddd;
}
@media(min-width:991px) {
.text-nowrap-responsive {
white-space: nowrap;
}
}
</style>
{% endblock %}
{% block content %}
<div class="row" style="margin-top: 5px; margin-bottom: 5px;">
<div class="col-xs-12 col-sm-10"><h2 style="margin-top: 10px;">User: {{ user.nickname }}</h2></div>
{%- if user.created and is_authorized_for(user.nickname) -%}
<div class="col-xs-12 col-sm-2 text-right" style="margin-top: 10px; margin-bottom: 10px;">
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
{# FIXME: If authorized #}
<li><a href="#" data-toggle="modal" data-target="#changepw">Change Password</a></li>
<li><a href="#" data-toggle="modal" data-target="#changemail">Change E-Mail Address</a></li>
{%- if session.admin %}
<li><a href="#" onclick="$('#adminform').submit()">Toggle admin</a></li>
{%- endif %}
</ul>
</div>
</div>
{%- endif %}
</div>
<div class="row">
<div class="hidden-xs col-sm-2">
<div class="panel panel-default">
<div class="panel-body" style="padding: 5px;">
<img id="avatar" class="img-responsive center-block" src="{{ user.get('email', '')|gravatar_url }}&s=150" style="width: 150px; height: 150px;" />
</div>
</div>
</div>
<div class="col-xs-12 col-sm-10">
<div class="panel panel-default">
<div class="panel-heading">User Info</div>
<div class="panel-body">
<table class="table table-condensed">
<tr><th>Nickname</th><td>
{{ user.nickname }}
(<a href="{{ url_for('router_list', q='user.nickname:%s' % user.nickname) }}">Filter</a>)
</td></tr>
{%- if user.email %}
<tr><th>E-Mail</th><td>
{{ user.email|anon_email }}
(<a href="{{ url_for('router_list', q='system.contact:%s' % user.email|anon_email_regex) }}">Filter</a>)
</td></tr>
{%- endif %}
<tr><th>Created</th><td>
{%- if user.created -%}
{{ user.created|utc2local|format_dt_date }}
{%- else -%}
<span class="glyphicon glyphicon-remove"></span>
Automatically imported stub from netmon
{%- endif -%}
</td></tr>
<tr><th>Admin</th><td>
<span class="glyphicon glyphicon-{%- if user.admin -%}ok{%- else -%}remove{%- endif -%}"></span>
</td></tr>
</table>
</div>
</div>
</div>
</div>
<div class="table-responsive">
<table id="routerlist" class="table table-condensed table-striped table-bordered" style="margin-bottom: 8px;">
<thead>
<tr>
<th style="width: 1%; padding-right: 5px; min-width: 90px;">Hostname</th>
<th style="width: 45px; padding-right: 5px;">Status</th>
<th style="padding-right: 5px;">Hood</th>
<th style="padding-right: 5px;">Firmware</th>
<th style="padding-right: 5px;">Hardware</th>
<th style="padding-right: 5px;">Created</th>
<th style="padding-right: 5px;">Uptime</th>
<th>Clients</th>
</tr>
</thead>
<tbody>
{%- set total_clients = 0 %}
{%- for router in routers %}
<tr>
<td class="text-nowrap-responsive"><a href="{{ url_for("router_info", dbid=router._id) }}">{{ router.hostname }}</a></td>
<td class="text-center"><span class="{{ router.status|status2css }}">{{ router.status }}</span></td>
<td>{{ router.hood }}</td>
<td>{{ router.software.firmware }}</td>
<td class="text-nowrap">{{ router.get("hardware", {}).get("name", "") }}</td>
<td class="text-nowrap">{{ router.created|utc2local|format_dt_date }}</td>
<td class="text-nowrap">{{ router.system.uptime|format_ts_diff }}</td>
<td>{{ router.system.clients }}</td>
{%- set total_clients = total_clients + router.system.clients %}
</tr>
{%- endfor %}
</tbody>
</table>
</div>
<div style="margin-bottom: 20px;">
{{ routers.count() }} Router{{ "s" if (routers.count() == 1) else "" }} found.
</div>
<script type="text/javascript">
$(document).ready(function() {
$("#routerlist").DataTable({
"paging": false,
"info": false,
"searching": false,
/*"responsive": {
"details": false
},*/
"columnDefs": [
{"orderable": false, "targets": 1},
{"orderable": false, "targets": -2},
]
});
});
</script>
{# FIXME: User's router list #}
<div class="modal fade" id="changepw" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Change Password for {{ user.nickname }}</h4>
</div>
<form method="post" class="form-horizontal">
<div class="modal-body">
<div class="form-group">
<label class="col-sm-4 control-label">New password</label>
<div class="col-sm-8">
<input type="password" name="password" class="form-control" placeholder="Password" autocomplete="new-password" required />
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Repeat new Password</label>
<div class="col-sm-8">
<input type="password" name="password_rep" class="form-control" autocomplete="off" placeholder="Repeat password" required />
</div>
</div>
</div>
<div class="modal-footer">
<input type="hidden" name="action" value="changepw" />
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="changemail" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Change E-Mail address for {{ user.nickname }}</h4>
</div>
<form method="post" class="form-horizontal">
<div class="modal-body">
<div class="form-group">
<label class="col-sm-4 control-label">New E-Mail address</label>
<div class="col-sm-8">
<input type="email" name="email" class="form-control" placeholder="New E-Mail" required />
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Repeat E-Mail address</label>
<div class="col-sm-8">
<input type="email" name="email_rep" class="form-control" autocomplete="off" placeholder="Repeat E-Mail" required />
</div>
</div>
{%- if not session.admin %}
<b>Warning</b>:
When you change your E-Mail address, your password will be reset and sent to your new address.
{%- endif %}
</div>
<div class="modal-footer">
<input type="hidden" name="action" value="changemail" />
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
{%- if session.admin %}
<form method="post" id="adminform">
<input type="hidden" name="action" value="changeadmin" />
<input type="hidden" name="admin" value="{{ "false" if user.admin else "true" }}" />
</form>
{%- endif %}
{% endblock %}

View File

@ -0,0 +1,70 @@
{% extends "bootstrap.html" %}
{% block title %}{{super()}} :: Users{% endblock %}
{% block head %}{{super()}}
<script src="{{ url_for('static', filename='js/datatables/jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/datatables/dataTables.bootstrap.min.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/datatables/dataTables.bootstrap.min.css') }}">
<style type="text/css">
@media(min-width:991px) {
.text-nowrap-responsive {
white-space: nowrap;
}
}
</style>
{% endblock %}
{%- block search %}
{# FIXME: Search users #}
{%- endblock %}
{% block content %}
<div class="table-responsive">
<table id="userlist" class="table table-condensed table-striped table-bordered" style="margin-bottom: 8px;">
<thead>
<tr>
<th style="width: 1%; padding-right: 5px; min-width: 90px;">Nickname</th>
<th style="padding-right: 5px;">E-Mail</th>
<th style="padding-right: 5px;">Admin</th>
<th style="padding-right: 5px;">Created</th>
<th style="padding-right: 5px;">Routers</th>
<th style="padding-right: 5px;">Clients</th>
</tr>
</thead>
<tbody>
{%- for user in users %}
<tr>
<td class="text-nowrap-responsive"><a href="{{ url_for("user_info", nickname=user.nickname) }}">{{ user.nickname }}</a></td>
<td>{{ user.email|anon_email }}</td>
<td><span class="glyphicon glyphicon-{%- if user.admin -%}ok{%- else -%}remove{%- endif -%}"></span><span style="display: none;">{{ user.admin }}</span></td>
<td class="text-nowrap">
{%- if user.created -%}
{{ user.created|utc2local|format_dt_date }}
{%- else -%}
<span class="glyphicon glyphicon-remove" title="Automatically imported stub from netmon"></span>
{%- endif -%}
</td>
<td>{{ user_routers.get(user.nickname, {}).get('routers', 0) }}</td>
<td>{{ user_routers.get(user.nickname, {}).get('clients', 0) }}</td>
</tr>
{%- endfor %}
</tbody>
</table>
</div>
<div style="margin-bottom: 20px;">
{{ users.count() }} User{{ "s" if (users.count() > 1) else "" }} found.
</div>
<script type="text/javascript">
$(document).ready(function() {
$("#userlist").DataTable({
"paging": false,
"info": false,
"searching": false,
/*"responsive": {
"details": false
},*/
"columnDefs": [
//{"orderable": false, "targets": 1},
//{"orderable": false, "targets": -2},
]
});
});
</script>
{% endblock %}