User Accounts, Legacy Routers, Delete Routers
This commit is contained in:
parent
0a57b6e5c7
commit
19329ef85c
|
@ -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")
|
|
@ -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]),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}})
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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ö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 > 0.5.2) eingegeben werden,
|
||||
damit die Router dem Account zugeordnet werden kö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 %}
|
|
@ -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 %}
|
|
@ -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 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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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">×</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">×</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 %}
|
|
@ -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 %}
|
Loading…
Reference in New Issue