From ea1c04528c3beff92fdbe6789dc7710b00b484b9 Mon Sep 17 00:00:00 2001 From: KF7EEL Date: Mon, 26 Jul 2021 15:41:40 -0700 Subject: [PATCH] implement edit of system rule, update requirements --- requirements.txt | 1 - web/app.py | 5127 +++++++++++++++++ web/config-SAMPLE.py | 87 + web/gen_script_template-SAMPLE.py | 5 + web/static/HBnet.png | Bin 0 -> 64462 bytes .../flask_user/edit_user_profile.html | 30 + .../flask_user/emails/base_message.html | 8 + web/templates/flask_user/login.html | 69 + web/templates/flask_user/register.html | 50 + web/templates/flask_user_layout.html | 144 + web/templates/help.html | 4 + web/templates/index.html | 4 + web/templates/view_passphrase.html | 34 + 13 files changed, 5562 insertions(+), 1 deletion(-) create mode 100644 web/app.py create mode 100644 web/config-SAMPLE.py create mode 100644 web/gen_script_template-SAMPLE.py create mode 100644 web/static/HBnet.png create mode 100644 web/templates/flask_user/edit_user_profile.html create mode 100644 web/templates/flask_user/emails/base_message.html create mode 100644 web/templates/flask_user/login.html create mode 100644 web/templates/flask_user/register.html create mode 100644 web/templates/flask_user_layout.html create mode 100644 web/templates/help.html create mode 100644 web/templates/index.html create mode 100644 web/templates/view_passphrase.html diff --git a/requirements.txt b/requirements.txt index bd5a2b5..2972670 100755 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,5 @@ email_validator flask_babelex pymysql folium -json requests libsrc diff --git a/web/app.py b/web/app.py new file mode 100644 index 0000000..e391739 --- /dev/null +++ b/web/app.py @@ -0,0 +1,5127 @@ +# HBNet Web Server +############################################################################### +# HBNet Web Server - Copyright (C) 2020 Eric Craw, KF7EEL +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +############################################################################### + +''' +Flask based application that is the web server for HBNet. Controls user authentication, DMR server config, etc. +''' + +from flask import Flask, render_template_string, request, make_response, jsonify, render_template, Markup, flash, redirect, url_for, current_app +from flask_sqlalchemy import SQLAlchemy +from flask_user import login_required, UserManager, UserMixin, user_registered, roles_required +from werkzeug.security import check_password_hash +from flask_login import current_user, login_user, logout_user +from wtforms import StringField, SubmitField +import requests +import base64, hashlib +from dmr_utils3.utils import int_id, bytes_4 +from config import * +import ast +import json +import datetime, time +from flask_babelex import Babel +import libscrc +import random +from flask_mail import Message, Mail +from socket import gethostbyname + + +try: + from gen_script_template import gen_script +except: + pass + +import os, ast +##import hb_config + +script_links = {} +active_tgs = {} + +# Query radioid.net for list of IDs +def get_ids(callsign): + try: + url = "https://www.radioid.net" + response = requests.get(url+"/api/dmr/user/?callsign=" + callsign) + result = response.json() +## print(result) + # id_list = [] + id_list = {} + f_name = result['results'][0]['fname'] + l_name = result['results'][0]['surname'] + try: + city = str(result['results'][0]['city'] + ', ' + result['results'][0]['state'] + ', ' + result['results'][0]['country']) + except: + city = result['results'][0]['country'] + for i in result['results']: + id_list[i['id']] = 0 + return str([id_list, f_name, l_name, city]) + except: + return str([{}, '', '', '']) + + +# Return string in NATO phonetics +def convert_nato(string): + d_nato = { 'A': 'ALPHA', 'B': 'BRAVO', 'C': 'CHARLIE', 'D': 'DELTA', + 'E': 'ECHO', 'F': 'FOXTROT', 'G': 'GOLF', 'H': 'HOTEL', + 'I': 'INDIA', 'J': 'JULIETT','K': 'KILO', 'L': 'LIMA', + 'M': 'MIKE', 'N': 'NOVEMBER','O': 'OSCAR', 'P': 'PAPA', + 'Q': 'QUEBEC', 'R': 'ROMEO', 'S': 'SIERRA', 'T': 'TANGO', + 'U': 'UNIFORM', 'V': 'VICTOR', 'W': 'WHISKEY', 'X': 'X-RAY', + 'Y': 'YANKEE', 'Z': 'ZULU', '0': 'zero(0)', '1': 'one(1)', + '2': 'two(2)', '3': 'three(3)', '4': 'four(4)', '5': 'five(5)', + '6': 'six(6)', '7': 'seven(7)', '8': 'eight(8)', '9': 'nine(9)', + 'a': 'alpha', 'b': 'bravo', 'c': 'charlie', 'd': 'delta', + 'e': 'echo', 'f': 'foxtrot', 'g': 'golf', 'h': 'hotel', + 'i': 'india', 'j': 'juliett','k': 'kilo', 'l': 'lima', + 'm': 'mike', 'n': 'november','o': 'oscar', 'p': 'papa', + 'q': 'quebec', 'r': 'romeo', 's': 'sierra', 't': 'tango', + 'u': 'uniform', 'v': 'victor', 'w': 'whiskey', 'x': 'x-ray', + 'y': 'yankee', 'z': 'Zulu'} + ns = '' + for c in string: + try: + ns = ns + d_nato[c] + ' ' + except: + ns = ns + c + ' ' + return ns + +# Class-based application configuration +class ConfigClass(object): + from config import MAIL_SERVER, MAIL_PORT, MAIL_USE_SSL, MAIL_USE_TLS, MAIL_USERNAME, MAIL_PASSWORD, MAIL_DEFAULT_SENDER, USER_ENABLE_EMAIL, USER_ENABLE_USERNAME, USER_REQUIRE_RETYPE_PASSWORD, USER_ENABLE_CHANGE_USERNAME, USER_ENABLE_MULTIPLE_EMAILS, USER_ENABLE_CONFIRM_EMAIL, USER_ENABLE_REGISTER, USER_AUTO_LOGIN_AFTER_CONFIRM, USER_SHOW_USERNAME_DOES_NOT_EXIST + """ Flask application config """ + + # Flask settings + SECRET_KEY = secret_key + + # Flask-SQLAlchemy settings + SQLALCHEMY_DATABASE_URI = db_location # File-based SQL database + SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids SQLAlchemy warning + + # Flask-User settings + USER_APP_NAME = title # Shown in and email templates and page footers + USER_EMAIL_SENDER_EMAIL = MAIL_DEFAULT_SENDER + USER_EDIT_USER_PROFILE_TEMPLATE = 'flask_user/edit_user_profile.html' + + + + + +# Setup Flask-User +def create_app(): + """ Flask application factory """ + + # Create Flask app load app.config + mail = Mail() + app = Flask(__name__) + app.config.from_object(__name__+'.ConfigClass') + + # Initialize Flask-BabelEx + babel = Babel(app) + + # Initialize Flask-SQLAlchemy + db = SQLAlchemy(app) + + # Define the User data-model. + # NB: Make sure to add flask_user UserMixin !!! + class User(db.Model, UserMixin): + __tablename__ = 'users' + id = db.Column(db.Integer, primary_key=True) + active = db.Column('is_active', db.Boolean(), nullable=False, server_default='1') + + # User authentication information. The collation='NOCASE' is required + # to search case insensitively when USER_IFIND_MODE is 'nocase_collation'. + username = db.Column(db.String(100,), nullable=False, unique=True) + password = db.Column(db.String(255), nullable=False, server_default='') + email_confirmed_at = db.Column(db.DateTime()) + email = db.Column(db.String(255), nullable=False, unique=True) + + # User information + first_name = db.Column(db.String(100), nullable=False, server_default='') + last_name = db.Column(db.String(100), nullable=False, server_default='') + dmr_ids = db.Column(db.String(100), nullable=False, server_default='') + city = db.Column(db.String(100), nullable=False, server_default='') + notes = db.Column(db.String(100), nullable=False, server_default='') + #Used for initial approval + initial_admin_approved = db.Column('initial_admin_approved', db.Boolean(), nullable=False, server_default='1') + # Define the relationship to Role via UserRoles + roles = db.relationship('Role', secondary='user_roles') + + # Define the Role data-model + class Role(db.Model): + __tablename__ = 'roles' + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(50), unique=True) + + # Define the UserRoles association table + class UserRoles(db.Model): + __tablename__ = 'user_roles' + id = db.Column(db.Integer(), primary_key=True) + user_id = db.Column(db.Integer(), db.ForeignKey('users.id', ondelete='CASCADE')) + role_id = db.Column(db.Integer(), db.ForeignKey('roles.id', ondelete='CASCADE')) + class BurnList(db.Model): + __tablename__ = 'burn_list' +## id = db.Column(db.Integer(), primary_key=True) + dmr_id = db.Column(db.Integer(), unique=True, primary_key=True) + version = db.Column(db.Integer(), primary_key=True) + class AuthLog(db.Model): + __tablename__ = 'auth_log' + id = db.Column(db.Integer(), primary_key=True) + login_dmr_id = db.Column(db.Integer()) + login_time = db.Column(db.DateTime()) + peer_ip = db.Column(db.String(100), nullable=False, server_default='') + server_name = db.Column(db.String(100)) + login_auth_method = db.Column(db.String(100), nullable=False, server_default='') + portal_username = db.Column(db.String(100), nullable=False, server_default='') + login_type = db.Column(db.String(100), nullable=False, server_default='') + class mmdvmPeer(db.Model): + __tablename__ = 'MMDVM_peers' + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(100), nullable=False, server_default='') + enabled = db.Column(db.Boolean(), nullable=False, server_default='1') + loose = db.Column(db.Boolean(), nullable=False, server_default='1') + ip = db.Column(db.String(100), nullable=False, server_default='127.0.0.1') + port = db.Column(db.Integer(), primary_key=False) + master_ip = db.Column(db.String(100), nullable=False, server_default='') + master_port = db.Column(db.Integer(), primary_key=False) + passphrase = db.Column(db.String(100), nullable=False, server_default='') + callsign = db.Column(db.String(100), nullable=False, server_default='') + radio_id = db.Column(db.Integer(), primary_key=False) + rx_freq = db.Column(db.String(100), nullable=False, server_default='') + tx_freq = db.Column(db.String(100), nullable=False, server_default='') + tx_power = db.Column(db.String(100), nullable=False, server_default='') + color_code = db.Column(db.String(100), nullable=False, server_default='') + latitude = db.Column(db.String(100), nullable=False, server_default='') + longitude = db.Column(db.String(100), nullable=False, server_default='') + height = db.Column(db.String(100), nullable=False, server_default='') + location = db.Column(db.String(100), nullable=False, server_default='') + description = db.Column(db.String(100), nullable=False, server_default='') + slots = db.Column(db.String(100), nullable=False, server_default='') + url = db.Column(db.String(100), nullable=False, server_default='') + group_hangtime = db.Column(db.String(100), nullable=False, server_default='') + enable_unit = db.Column(db.Boolean(), nullable=False, server_default='1') + options = db.Column(db.String(100), nullable=False, server_default='') + use_acl = db.Column(db.Boolean(), nullable=False, server_default='0') + sub_acl = db.Column(db.String(100), nullable=False, server_default='') + tg1_acl = db.Column(db.String(100), nullable=False, server_default='') + tg2_acl = db.Column(db.String(100), nullable=False, server_default='') + server = db.Column(db.String(100), nullable=False, server_default='') + notes = db.Column(db.String(100), nullable=False, server_default='') + + class xlxPeer(db.Model): + __tablename__ = 'XLX_peers' + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(100), nullable=False, server_default='') + enabled = db.Column(db.Boolean(), nullable=False, server_default='1') + loose = db.Column(db.Boolean(), nullable=False, server_default='1') + ip = db.Column(db.String(100), nullable=False, server_default='127.0.0.1') + port = db.Column(db.Integer(), primary_key=False) + master_ip = db.Column(db.String(100), nullable=False, server_default='') + master_port = db.Column(db.Integer(), primary_key=False) + passphrase = db.Column(db.String(100), nullable=False, server_default='') + callsign = db.Column(db.String(100), nullable=False, server_default='') + radio_id = db.Column(db.Integer(), primary_key=False) + rx_freq = db.Column(db.String(100), nullable=False, server_default='') + tx_freq = db.Column(db.String(100), nullable=False, server_default='') + tx_power = db.Column(db.String(100), nullable=False, server_default='') + color_code = db.Column(db.String(100), nullable=False, server_default='') + latitude = db.Column(db.String(100), nullable=False, server_default='') + longitude = db.Column(db.String(100), nullable=False, server_default='') + height = db.Column(db.String(100), nullable=False, server_default='') + location = db.Column(db.String(100), nullable=False, server_default='') + description = db.Column(db.String(100), nullable=False, server_default='') + slots = db.Column(db.String(100), nullable=False, server_default='') + url = db.Column(db.String(100), nullable=False, server_default='') + group_hangtime = db.Column(db.String(100), nullable=False, server_default='') + xlxmodule = db.Column(db.String(100), nullable=False, server_default='') + options = db.Column(db.String(100), nullable=False, server_default='') + enable_unit = db.Column(db.Boolean(), nullable=False, server_default='1') + use_acl = db.Column(db.Boolean(), nullable=False, server_default='0') + sub_acl = db.Column(db.String(100), nullable=False, server_default='') + tg1_acl = db.Column(db.String(100), nullable=False, server_default='') + tg2_acl = db.Column(db.String(100), nullable=False, server_default='') + server = db.Column(db.String(100), nullable=False, server_default='') + notes = db.Column(db.String(100), nullable=False, server_default='') + class ServerList(db.Model): + __tablename__ = 'server_list' + name = db.Column(db.String(100), unique=True, primary_key=True) + secret = db.Column(db.String(255), nullable=False, server_default='') +## public_list = db.Column(db.Boolean(), nullable=False, server_default='1') + id = db.Column(db.Integer(), primary_key=False) + ip = db.Column(db.String(100), nullable=False, server_default='') + port = db.Column(db.Integer(), primary_key=False) + global_path = db.Column(db.String(100), nullable=False, server_default='./') + global_ping_time = db.Column(db.Integer(), primary_key=False) + global_max_missed = db.Column(db.Integer(), primary_key=False) + global_use_acl = db.Column(db.Boolean(), nullable=False, server_default='1') + global_reg_acl = db.Column(db.String(100), nullable=False, server_default='PERMIT:ALL') + global_sub_acl = db.Column(db.String(100), nullable=False, server_default='DENY:1') + global_tg1_acl = db.Column(db.String(100), nullable=False, server_default='PERMIT:ALL') + global_tg2_acl = db.Column(db.String(100), nullable=False, server_default='PERMIT:ALL') + ai_try_download = db.Column(db.Boolean(), nullable=False, server_default='1') + ai_path = db.Column(db.String(100), nullable=False, server_default='./') + ai_peer_file = db.Column(db.String(100), nullable=False, server_default='peer_ids.json') + ai_subscriber_file = db.Column(db.String(100), nullable=False, server_default='subscriber_ids.json') + ai_tgid_file = db.Column(db.String(100), nullable=False, server_default='talkgroup_ids.json') + ai_peer_url = db.Column(db.String(100), nullable=False, server_default='https://www.radioid.net/static/rptrs.json') + ai_subs_url = db.Column(db.String(100), nullable=False, server_default='https://www.radioid.net/static/users.json') + ai_stale = db.Column(db.Integer(), primary_key=False, server_default='7') + # Pull from config file for now +## um_append_int = db.Column(db.Integer(), primary_key=False, server_default='2') + um_shorten_passphrase = db.Column(db.Boolean(), nullable=False, server_default='0') + um_burn_file = db.Column(db.String(100), nullable=False, server_default='./burned_ids.txt') + # Pull from config file for now +## um_burn_int = db.Column(db.Integer(), primary_key=False, server_default='6') + report_enable = db.Column(db.Boolean(), nullable=False, server_default='1') + report_interval = db.Column(db.Integer(), primary_key=False, server_default='60') + report_port = db.Column(db.Integer(), primary_key=False, server_default='4321') + report_clients =db.Column(db.String(100), nullable=False, server_default='127.0.0.1') + unit_time = db.Column(db.Integer(), primary_key=False, server_default='10080') + notes = db.Column(db.String(100), nullable=False, server_default='') + + class MasterList(db.Model): + __tablename__ = 'master_list' + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(100), nullable=False, server_default='') + static_positions = db.Column(db.Boolean(), nullable=False, server_default='0') + repeat = db.Column(db.Boolean(), nullable=False, server_default='1') + active = db.Column(db.Boolean(), nullable=False, server_default='1') + max_peers = db.Column(db.Integer(), primary_key=False, server_default='10') + ip = db.Column(db.String(100), nullable=False, server_default='') + port = db.Column(db.Integer(), primary_key=False) + enable_um = db.Column(db.Boolean(), nullable=False, server_default='1') + passphrase = db.Column(db.String(100), nullable=False, server_default='') + group_hang_time = db.Column(db.Integer(), primary_key=False, server_default='5') + use_acl = db.Column(db.Boolean(), nullable=False, server_default='1') + reg_acl = db.Column(db.String(100), nullable=False, server_default='') + sub_acl = db.Column(db.String(100), nullable=False, server_default='') + tg1_acl = db.Column(db.String(100), nullable=False, server_default='') + tg2_acl = db.Column(db.String(100), nullable=False, server_default='') + enable_unit = db.Column(db.Boolean(), nullable=False, server_default='1') + server = db.Column(db.String(100), nullable=False, server_default='') + notes = db.Column(db.String(100), nullable=False, server_default='') + public_list = db.Column(db.Boolean(), nullable=False, server_default='1') + + + class ProxyList(db.Model): + __tablename__ = 'proxy_list' + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(100), nullable=False, server_default='') + active = db.Column(db.Boolean(), nullable=False, server_default='1') + static_positions = db.Column(db.Boolean(), nullable=False, server_default='0') + repeat = db.Column(db.Boolean(), nullable=False, server_default='1') + enable_um = db.Column(db.Boolean(), nullable=False, server_default='1') + passphrase = db.Column(db.String(100), nullable=False, server_default='') + external_proxy = db.Column(db.Boolean(), nullable=False, server_default='0') + external_port = db.Column(db.Integer(), primary_key=False) + group_hang_time = db.Column(db.Integer(), primary_key=False) + internal_start_port = db.Column(db.Integer(), primary_key=False) + internal_stop_port = db.Column(db.Integer(), primary_key=False) + use_acl = db.Column(db.Boolean(), nullable=False, server_default='1') + reg_acl = db.Column(db.String(100), nullable=False, server_default='') + sub_acl = db.Column(db.String(100), nullable=False, server_default='') + tg1_acl = db.Column(db.String(100), nullable=False, server_default='') + tg2_acl = db.Column(db.String(100), nullable=False, server_default='') + enable_unit = db.Column(db.Boolean(), nullable=False, server_default='1') + server = db.Column(db.String(100), nullable=False, server_default='') + notes = db.Column(db.String(100), nullable=False, server_default='') + public_list = db.Column(db.Boolean(), nullable=False, server_default='1') + + + class OBP(db.Model): + __tablename__ = 'OpenBridge' + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(100), nullable=False, server_default='') + enabled = db.Column(db.Boolean(), nullable=False, server_default='1') + network_id = db.Column(db.Integer(), primary_key=False) + ip = db.Column(db.String(100), nullable=False, server_default='') + port = db.Column(db.Integer(), primary_key=False) + passphrase = db.Column(db.String(100), nullable=False, server_default='') + target_ip = db.Column(db.String(100), nullable=False, server_default='') + target_port = db.Column(db.Integer(), primary_key=False) + both_slots = db.Column(db.Boolean(), nullable=False, server_default='1') + use_acl = db.Column(db.Boolean(), nullable=False, server_default='1') + sub_acl = db.Column(db.String(100), nullable=False, server_default='') + tg_acl = db.Column(db.String(100), nullable=False, server_default='') + enable_unit = db.Column(db.Boolean(), nullable=False, server_default='1') + server = db.Column(db.String(100), nullable=False, server_default='') + notes = db.Column(db.String(100), nullable=False, server_default='') + + class BridgeRules(db.Model): + __tablename__ = 'bridge_rules' + id = db.Column(db.Integer(), primary_key=True) + bridge_name = db.Column(db.String(100), nullable=False, server_default='') + system_name = db.Column(db.String(100), nullable=False, server_default='') + ts = db.Column(db.Integer(), primary_key=False) + tg = db.Column(db.Integer(), primary_key=False) + active = db.Column(db.Boolean(), nullable=False, server_default='1') + timeout = db.Column(db.Integer(), primary_key=False) + to_type = db.Column(db.String(100), nullable=False, server_default='') + on = db.Column(db.String(100), nullable=False, server_default='') + off = db.Column(db.String(100), nullable=False, server_default='') + reset = db.Column(db.String(100), nullable=False, server_default='') + server = db.Column(db.String(100), nullable=False, server_default='') + public_list = db.Column(db.Boolean(), nullable=False, server_default='0') + proxy = db.Column(db.Boolean(), nullable=False, server_default='0') + + class BridgeList(db.Model): + __tablename__ = 'bridge_list' + id = db.Column(db.Integer(), primary_key=True) + bridge_name = db.Column(db.String(100), nullable=False, server_default='') + description = db.Column(db.String(100), nullable=False, server_default='') + public_list = db.Column(db.Boolean(), nullable=False, server_default='0') + tg = db.Column(db.Integer(), primary_key=False) + + class GPS_LocLog(db.Model): + __tablename__ = 'gps_locations' + id = db.Column(db.Integer(), primary_key=True) + callsign = db.Column(db.String(100), nullable=False, server_default='') + comment = db.Column(db.String(100), nullable=False, server_default='') + lat = db.Column(db.String(100), nullable=False, server_default='') + lon = db.Column(db.String(100), nullable=False, server_default='') + time = db.Column(db.DateTime()) + server = db.Column(db.String(100), nullable=False, server_default='') + system_name = db.Column(db.String(100), nullable=False, server_default='') + dmr_id = db.Column(db.Integer(), primary_key=False) + + class BulletinBoard(db.Model): + __tablename__ = 'sms_bb' + id = db.Column(db.Integer(), primary_key=True) + callsign = db.Column(db.String(100), nullable=False, server_default='') + bulletin = db.Column(db.String(100), nullable=False, server_default='') + time = db.Column(db.DateTime()) + server = db.Column(db.String(100), nullable=False, server_default='') + system_name = db.Column(db.String(100), nullable=False, server_default='') + dmr_id = db.Column(db.Integer(), primary_key=False) + + class SMSLog(db.Model): + __tablename__ = 'sms_log' + id = db.Column(db.Integer(), primary_key=True) + snd_callsign = db.Column(db.String(100), nullable=False, server_default='') + rcv_callsign = db.Column(db.String(100), nullable=False, server_default='') + message = db.Column(db.String(100), nullable=False, server_default='') + time = db.Column(db.DateTime()) + server = db.Column(db.String(100), nullable=False, server_default='') + system_name = db.Column(db.String(100), nullable=False, server_default='') + snd_id = db.Column(db.Integer(), primary_key=False) + rcv_id = db.Column(db.Integer(), primary_key=False) + + class MailBox(db.Model): + __tablename__ = 'sms_aprs_mailbox' + id = db.Column(db.Integer(), primary_key=True) + snd_callsign = db.Column(db.String(100), nullable=False, server_default='') + rcv_callsign = db.Column(db.String(100), nullable=False, server_default='') + message = db.Column(db.String(100), nullable=False, server_default='') + time = db.Column(db.DateTime()) + server = db.Column(db.String(100), nullable=False, server_default='') + system_name = db.Column(db.String(100), nullable=False, server_default='') + snd_id = db.Column(db.Integer(), primary_key=False) + rcv_id = db.Column(db.Integer(), primary_key=False) + + + + + # Customize Flask-User + class CustomUserManager(UserManager): + # Override or extend the default login view method + def login_view(self): + """Prepare and process the login form.""" + + # Authenticate username/email and login authenticated users. + + safe_next_url = self._get_safe_next_url('next', self.USER_AFTER_LOGIN_ENDPOINT) + safe_reg_next = self._get_safe_next_url('reg_next', self.USER_AFTER_REGISTER_ENDPOINT) + + # Immediately redirect already logged in users + if self.call_or_get(current_user.is_authenticated) and self.USER_AUTO_LOGIN_AT_LOGIN: + return redirect(safe_next_url) + + # Initialize form + login_form = self.LoginFormClass(request.form) # for login.html + register_form = self.RegisterFormClass() # for login_or_register.html + if request.method != 'POST': + login_form.next.data = register_form.next.data = safe_next_url + login_form.reg_next.data = register_form.reg_next.data = safe_reg_next + + # Process valid POST + if request.method == 'POST' and login_form.validate(): + # Retrieve User + user = None + user_email = None + if self.USER_ENABLE_USERNAME: + # Find user record by username + user = self.db_manager.find_user_by_username(login_form.username.data) + + # Find user record by email (with form.username) + if not user and self.USER_ENABLE_EMAIL: + user, user_email = self.db_manager.get_user_and_user_email_by_email(login_form.username.data) + else: + # Find user by email (with form.email) + user, user_email = self.db_manager.get_user_and_user_email_by_email(login_form.email.data) + #Add aditional message + if not user.initial_admin_approved: + flash('You account is waiting for approval from an administrator. See the Help page for more information. You will receive an email when your account is approved.', 'success') + + if user: + # Log user in + safe_next_url = self.make_safe_url(login_form.next.data) + return self._do_login_user(user, safe_next_url, login_form.remember_me.data) + + # Render form + self.prepare_domain_translations() + template_filename = self.USER_LOGIN_AUTH0_TEMPLATE if self.USER_ENABLE_AUTH0 else self.USER_LOGIN_TEMPLATE + return render_template(template_filename, + form=login_form, + login_form=login_form, + register_form=register_form) + + #user_manager = UserManager(app, db, User) + user_manager = CustomUserManager(app, db, User) + + + # Create all database tables + db.create_all() + + + if not User.query.filter(User.username == 'admin').first(): + user = User( + username='admin', + email='admin@no.reply', + email_confirmed_at=datetime.datetime.utcnow(), + password=user_manager.hash_password('admin'), + initial_admin_approved = True, + notes='Default admin account created during installation.', + dmr_ids='{}' + ) + user.roles.append(Role(name='Admin')) + user.roles.append(Role(name='User')) + db.session.add(user) + db.session.commit() + + # Query radioid.net for list of DMR IDs, then add to DB + @user_registered.connect_via(app) + def _after_user_registered_hook(sender, user, **extra): + edit_user = User.query.filter(User.username == user.username).first() + radioid_data = ast.literal_eval(get_ids(user.username)) + edit_user.dmr_ids = str(radioid_data[0]) + edit_user.first_name = str(radioid_data[1]) + edit_user.last_name = str(radioid_data[2]) + edit_user.city = str(radioid_data[3]) + user_role = UserRoles( + user_id=edit_user.id, + role_id=2, + ) + db.session.add(user_role) + if default_account_state == False: + edit_user.active = default_account_state + edit_user.initial_admin_approved = False + db.session.commit() + + def gen_passphrase(dmr_id): + _new_peer_id = bytes_4(int(str(dmr_id)[:7])) + trimmed_id = int(str(dmr_id)[:7]) + b_list = get_burnlist() + # print(b_list) + burned = False + for ui in b_list.items(): + # print(ui) + #print(b_list) + if ui[0] == trimmed_id: + if ui[0] != 0: + calc_passphrase = hashlib.sha256(str(extra_1).encode() + str(extra_int_1).encode() + str(_new_peer_id).encode()[-3:]).hexdigest().upper().encode()[::14] + base64.b64encode(bytes.fromhex(str(hex(libscrc.ccitt((_new_peer_id) + b_list[trimmed_id].to_bytes(2, 'big') + burn_int.to_bytes(2, 'big') + append_int.to_bytes(2, 'big') + bytes.fromhex(str(hex(libscrc.posix((_new_peer_id) + b_list[trimmed_id].to_bytes(2, 'big') + burn_int.to_bytes(2, 'big') + append_int.to_bytes(2, 'big'))))[2:].zfill(8)))))[2:].zfill(4)) + (_new_peer_id) + b_list[trimmed_id].to_bytes(2, 'big') + burn_int.to_bytes(2, 'big') + append_int.to_bytes(2, 'big') + bytes.fromhex(str(hex(libscrc.posix((_new_peer_id) + b_list[trimmed_id].to_bytes(2, 'big') + burn_int.to_bytes(2, 'big') + append_int.to_bytes(2, 'big'))))[2:].zfill(8))) + hashlib.sha256(str(extra_2).encode() + str(extra_int_2).encode() + str(_new_peer_id).encode()[-3:]).hexdigest().upper().encode()[::14] + burned = True + if burned == False: + calc_passphrase = hashlib.sha256(str(extra_1).encode() + str(extra_int_1).encode() + str(_new_peer_id).encode()[-3:]).hexdigest().upper().encode()[::14] + base64.b64encode(bytes.fromhex(str(hex(libscrc.ccitt((_new_peer_id) + append_int.to_bytes(2, 'big') + bytes.fromhex(str(hex(libscrc.posix((_new_peer_id) + append_int.to_bytes(2, 'big'))))[2:].zfill(8)))))[2:].zfill(4)) + (_new_peer_id) + append_int.to_bytes(2, 'big') + bytes.fromhex(str(hex(libscrc.posix((_new_peer_id) + append_int.to_bytes(2, 'big'))))[2:].zfill(8))) + hashlib.sha256(str(extra_2).encode() + str(extra_int_2).encode() + str(_new_peer_id).encode()[-3:]).hexdigest().upper().encode()[::14] + if use_short_passphrase == True: + trim_pass = str(calc_passphrase)[2:-1] + new_pass = trim_pass[::int(shorten_sample)][-int(shorten_length):] + return str(new_pass) + elif use_short_passphrase ==False: + return str(calc_passphrase)[2:-1] + + + def update_from_radioid(callsign): + edit_user = User.query.filter(User.username == callsign).first() + #edit_user.dmr_ids = str(ast.literal_eval(get_ids(callsign))[0]) + radioid_dict = ast.literal_eval(get_ids(callsign))[0] + db_id_dict = ast.literal_eval(edit_user.dmr_ids) + new_id_dict = db_id_dict.copy() + for i in radioid_dict.items(): + if i[0] in db_id_dict: + pass + elif i[0] not in db_id_dict: + new_id_dict[i[0]] = 0 + edit_user.dmr_ids = str(new_id_dict) + edit_user.first_name = str(ast.literal_eval(get_ids(callsign))[1]) + edit_user.last_name = str(ast.literal_eval(get_ids(callsign))[2]) + edit_user.city = str(ast.literal_eval(get_ids(callsign))[3]) + + db.session.commit() + + # The Home page is accessible to anyone + @app.route('/') + def home_page(): + #content = Markup('Index') + return render_template('index.html') #, markup_content = content) + + @app.route('/help') + def help_page(): + #content = Markup('Index') + + return render_template('help.html') + + @app.route('/generate_passphrase/pi-star', methods = ['GET']) + @login_required + def gen_pi_star(): + try: + u = current_user + ## print(u.username) + id_dict = ast.literal_eval(u.dmr_ids) + #u = User.query.filter_by(username=user).first() + ## print(user_id) + ## print(request.args.get('mode')) + ## if request.args.get('mode') == 'generated': + content = ''' + + + + + + +
+

Pi-Star Instructions

+

 

+

1: Log into your Pi-Star device.
2: Change to Read-Write mode of the device by issuing the command:

+
rpi-rw
+


3a: Change to the root user by issuing the command:

+
sudo su -
+


3b: Now type pwd and verify you get a return indicating you are in the /root directory. If you are in the wrong directory, it is because you're not following the instructions and syntax above! This is a show stopper, and your attempt to load the files correctly, will fail !

4: Issue one of the commands below for the chosen DMR ID:

+

Note: Link can be used only once. To run the script again, simply reload the page and paste a new command into the command line.

+ +''' + for i in id_dict.items(): + #if i[1] == '': + link_num = str(random.randint(1,99999999)).zfill(8) + str(time.time()) + str(random.randint(1,99999999)).zfill(8) + script_links[i[0]] = link_num + content = content + '''\n +

DMR ID: ''' + str(i[0]) + ''':

+

bash <(curl -s "''' + str(url) + '/get_script?dmr_id=' + str(i[0]) + '&number=' + str(link_num) + '''")

+

 

+ ''' + #else: + # content = content + '''\n

Error

''' + content = content + '''\n


5: When asked for server ports, use the information above to populate the correct fields.
6: Reboot your Pi-Star device

+
+

 

''' + except: + content = Markup('No DMR IDs found or other error.') + + + #return str(content) + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + + + @app.route('/generate_passphrase', methods = ['GET']) + @login_required + def gen(): + #print(str(gen_passphrase(3153591))) #(int(i[0]))) + try: + #content = Markup('The HTML String') + #user_id = request.args.get('user_id') + u = current_user + ## print(u.username) + id_dict = ast.literal_eval(u.dmr_ids) + #u = User.query.filter_by(username=user).first() + ## print(user_id) + ## print(request.args.get('mode')) + ## if request.args.get('mode') == 'generated': + #print(id_dict) + content = '\n' + for i in id_dict.items(): + if isinstance(i[1], int) == True and i[1] != 0: + link_num = str(random.randint(1,99999999)).zfill(8) + str(time.time()) + str(random.randint(1,99999999)).zfill(8) + script_links[i[0]] = link_num + #print(script_links) + content = content + '''\n + + + + + + +
+

Your passphrase for ''' + str(i[0]) + ''':

+

Copy and paste: ''' + str(gen_passphrase(int(i[0]))) + '''

+
+ +

Phonetically spelled: ''' + convert_nato(str(gen_passphrase(int(i[0])))) + '''

+ +
+

 

+ ''' + elif i[1] == 0: + link_num = str(random.randint(1,99999999)).zfill(8) + str(time.time()) + str(random.randint(1,99999999)).zfill(8) + script_links[i[0]] = link_num + #print(script_links) + content = content + '''\n + + + + + + +
+

Your passphrase for ''' + str(i[0]) + ''':

+

Copy and paste: ''' + str(gen_passphrase(int(i[0]))) + '''

+
+ +

Phonetically spelled: ''' + convert_nato(str(gen_passphrase(int(i[0])))) + '''

+ +
+

 

+ ''' + elif i[1] == '': + content = content + ''' + + + + + + +
+

Your passphrase for ''' + str(i[0]) + ''':

+

Copy and paste: ''' + legacy_passphrase + '''

+
+

Phonetically spelled: ''' + convert_nato(legacy_passphrase) + '''

+
+

 

''' + else: + content = content + ''' + + + + + + +
+

Your passphrase for ''' + str(i[0]) + ''':

+

Copy and paste: ''' + str(i[1]) + '''

+
+

Phonetically spelled: ''' + convert_nato(str(i[1])) + '''

+
+

 

+ ''' + #content = content + '\n\n' + str(script_links[i[0]]) + except: + content = Markup('No DMR IDs found or other error.') + + + #return str(content) + return render_template('view_passphrase.html', markup_content = Markup(content)) + +## # The Members page is only accessible to authenticated users via the @login_required decorator +## @app.route('/members') +## @login_required # User must be authenticated +## def member_page(): +## content = 'Mem only' +## return render_template('flask_user_layout.html', markup_content = content) + + @app.route('/update_ids', methods=['POST', 'GET']) + @login_required # User must be authenticated + def update_info(): + #print(request.args.get('callsign')) + #print(current_user.username) + if request.args.get('callsign') == current_user.username or request.args.get('callsign') and request.args.get('callsign') != current_user.username and current_user.has_roles('Admin'): + content = '

Updated your information.

' + update_from_radioid(request.args.get('callsign')) + else: + content = ''' +

Use this page to sync changes from RadioID.net with this system (such as a new DMR ID, name change, etc.).

+

Updating your information from RadioID.net will overwrite any custom authentication passphrases, your city, and name in the database. Are you sure you want to continue?

+

 

+

Yes, update my information.

+ +''' + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + + @app.route('/email_user', methods=['POST', 'GET']) + @roles_required('Admin') + @login_required # User must be authenticated + def email_user(): + + if request.method == 'GET' and request.args.get('callsign'): + content = ''' +

Send email to user: ''' + request.args.get('callsign') + '''

+ + + + + + +
+







+
+

 

''' + elif request.method == 'POST': # and request.form.get('callsign') and request.form.get('subject') and request.form.get('message'): + u = User.query.filter_by(username=request.args.get('callsign')).first() + msg = Message(recipients=[u.email], + sender=(title, MAIL_DEFAULT_SENDER), + subject=request.form.get('subject'), + body=request.form.get('message')) + mail.send(msg) + content = '

Sent email to: ' + u.email + '

' + else: + content = '''

Find user in "List Users", then click on the email link.'

''' + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + + + @app.route('/list_users') + @roles_required('Admin') + @login_required # User must be authenticated + def list_users(): + u = User.query.all() + # Broken for now, link taken out -

List/edit users:

 

Enter Callsign

+ u_list = '''

 

+ + + + + + + +''' + for i in u: + u_list = u_list + ''' + + + + + + + +'''+ '\n' + content = u_list + ''' +
CallsignNameEnabledDMR ID:AuthenticationNotes
 ''' + str(i.username) + '''  ''' + str(i.first_name) + ' ' + str(i.last_name) + '''  ''' + str(i.active) + '''  ''' + str(i.dmr_ids) + '''  ''' + str(i.notes) + ''' 
+

 

''' + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + @app.route('/approve_users', methods=['POST', 'GET']) + @login_required + @roles_required('Admin') # Use of @roles_required decorator + def approve_list(): + u = User.query.all() + wait_list = '''

Users waiting for approval:

 

+ + + + + + +''' + for i in u: +## print(i.username) +## print(i.initial_admin_approved) + if i.initial_admin_approved == False: + wait_list = wait_list+ ''' + + + + + + +'''+ '\n' + content = wait_list + ''' +
CallsignNameEnabledDMR ID:Authentication
 ''' + str(i.username) + '''  ''' + str(i.first_name) + ' ' + str(i.last_name) + '''  ''' + str(i.active) + '''  ''' + str(i.dmr_ids) + ''' 
+

 

''' + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + + + # The Admin page requires an 'Admin' role. + @app.route('/edit_user', methods=['POST', 'GET']) + @login_required + @roles_required('Admin') # Use of @roles_required decorator + def admin_page(): + #print(request.args.get('callsign')) + #print(request.args.get('callsign')) +## if request.method == 'POST' and request.form.get('callsign'): +## #result = request.json +## callsign = request.form.get('callsign') +## u = User.query.filter_by(username=callsign).first() +## content = u.dmr_ids + if request.method == 'POST' and request.args.get('callsign') == None: + content = 'Not found' + elif request.method == 'POST' and request.args.get('callsign') and request.form.get('user_status'): + user = request.args.get('callsign') + #print(user) + edit_user = User.query.filter(User.username == user).first() + content = '' + if request.form.get('user_status') != edit_user.active: + if request.form.get('user_status') == "True": + edit_user.active = True + content = content + '''

User ''' + str(user) + ''' has been enabled.

\n''' + if request.form.get('user_status') == "False": + edit_user.active = False + content = content + '''

User ''' + str(user) + ''' has been disabled.

\n''' +## print(request.form.get('username')) + if user != request.form.get('username'): +#### #print(edit_user.username) + content = content + '''

User ''' + str(user) + ''' changed to ''' + request.form.get('username') + '''.

\n''' + edit_user.username = request.form.get('username') + if request.form.get('email') != edit_user.email: + edit_user.email = request.form.get('email') + content = content + '''

Changed email for user: ''' + str(user) + ''' to ''' + request.form.get('email') + '''

\n''' + if request.form.get('notes') != edit_user.notes: + edit_user.notes = request.form.get('notes') + content = content + '''

Changed notes for user: ''' + str(user) + '''.

\n''' + if request.form.get('password') != '': + edit_user.password = user_manager.hash_password(request.form.get('password')) + content = content + '''

Changed password for user: ''' + str(user) + '''

\n''' + if request.form.get('dmr_ids') != edit_user.dmr_ids: + edit_user.dmr_ids = request.form.get('dmr_ids') + dmr_auth_dict = ast.literal_eval(request.form.get('dmr_ids')) + for id_user in dmr_auth_dict: + if isinstance(dmr_auth_dict[id_user], int) == True and dmr_auth_dict[id_user] != 0: + #print('burn it') + if id_user in get_burnlist(): +## print('burned') + if get_burnlist()[id_user] != dmr_auth_dict[id_user]: +## print('update vers') + update_burnlist(id_user, dmr_auth_dict[id_user]) + else: + pass +## print('no update') + else: + add_burnlist(id_user, dmr_auth_dict[id_user]) +## print('not in list, adding') + elif isinstance(dmr_auth_dict[id_user], int) == False and id_user in get_burnlist(): + delete_burnlist(id_user) +## print('remove from burn list - string') + elif dmr_auth_dict[id_user] == 0: +## print('remove from burn list') + if id_user in get_burnlist(): + delete_burnlist(id_user) + + + + content = content + '''

Changed authentication settings for user: ''' + str(user) + '''

\n''' + db.session.commit() + #edit_user = User.query.filter(User.username == request.args.get('callsign')).first() + elif request.method == 'GET' and request.args.get('callsign') and request.args.get('delete_user') == 'true': + delete_user = User.query.filter(User.username == request.args.get('callsign')).first() + db.session.delete(delete_user) + db.session.commit() + content = '''

Deleted user: ''' + str(delete_user.username) + '''

\n''' + + elif request.method == 'GET' and request.args.get('callsign') and request.args.get('make_user_admin') == 'true': + u = User.query.filter_by(username=request.args.get('callsign')).first() + u_role = UserRoles.query.filter_by(user_id=u.id).first() + u_role.role_id = 1 + db.session.commit() + content = '''

User now Admin: ''' + str(request.args.get('callsign')) + '''

\n''' + + elif request.method == 'GET' and request.args.get('callsign') and request.args.get('make_user_admin') == 'false': + u = User.query.filter_by(username=request.args.get('callsign')).first() + u_role = UserRoles.query.filter_by(user_id=u.id).first() + u_role.role_id = 2 + db.session.commit() + content = '''

Admin now a user: ''' + str(request.args.get('callsign') ) + '''

\n''' + + elif request.method == 'GET' and request.args.get('callsign') and request.args.get('admin_approve') == 'true': + edit_user = User.query.filter(User.username == request.args.get('callsign')).first() + edit_user.active = True + edit_user.initial_admin_approved = True + db.session.commit() + msg = Message(recipients=[edit_user.email], + sender=(title, MAIL_DEFAULT_SENDER), + subject='Account Approval', + body='''You are receiving this message because an administrator has approved your account. You may now login and use ''' + title + '''.''') + mail.send(msg) + content = '''

User approved: ''' + str(request.args.get('callsign')) + '''

\n''' + + elif request.method == 'GET' and request.args.get('callsign') and request.args.get('email_verified') == 'true': + edit_user = User.query.filter(User.username == request.args.get('callsign')).first() + edit_user.email_confirmed_at = datetime.datetime.utcnow() + db.session.commit() + content = '''

Email verified for: ''' + str(request.args.get('callsign')) + '''

\n''' + + elif request.method == 'POST' and request.form.get('callsign') and not request.form.get('user_status') or request.method == 'GET' and request.args.get('callsign'):# and request.form.get('user_status') : + if request.args.get('callsign'): + callsign = request.args.get('callsign') + if request.form.get('callsign'): + callsign = request.form.get('callsign') + u = User.query.filter_by(username=callsign).first() + confirm_link = '' + if u.email_confirmed_at == None: + confirm_link = '''

Verify email - ''' + str(u.username) + '''

\n''' + u_role = UserRoles.query.filter_by(user_id=u.id).first() + if u_role.role_id == 2: + # Link to promote to Admin + role_link = '''

Give Admin role: ''' + str(u.username) + '''

\n''' + if u_role.role_id == 1: + # Link to promote to User + role_link = '''

Revert to User role: ''' + str(u.username) + '''

\n''' + id_dict = ast.literal_eval(u.dmr_ids) + passphrase_list = ''' + + + + + + ''' + for i in id_dict.items(): + print(i[1]) + if isinstance(i[1], int) == True: + passphrase_list = passphrase_list + ''' + + + + \n''' + if i[1] == '': + passphrase_list = passphrase_list + ''' + + + \n''' + if not isinstance(i[1], int) == True and i[1] != '': + passphrase_list = passphrase_list + ''' + + + \n''' + + passphrase_list = passphrase_list + '
DMR IDPassphrase
''' + str(i[0]) + '''''' + str(gen_passphrase(int(i[0]))) + '''
''' + str(i[0]) + '''''' + legacy_passphrase + '''
''' + str(i[0]) + '''''' + str(i[1]) + '''
' + content = ''' +

 

+ + + + + + + + + + + + + + + + +
First NameLast Name
 ''' + u.first_name + ''' ''' + u.last_name + '''
City''' + u.city + '''
+

 

+ +''' + passphrase_list + ''' + +

 Options for: ''' + u.username + ''' 

+ + + + + + + + + + + + + + + + +
  +

Update from RadioID.net

 ''' + confirm_link + ''' 

Email confirmed: ''' + str(u.email_confirmed_at) + '''

  +

Send user an email

 ''' + role_link + ''' 
 

View user auth log

  +

Deleted user

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+


+
+
+ + + + +

 

+ +

 Passphrase Authentication Method Key

+ + + + + + + + + + + + + +
CalculatedLegacy (config)Custom
0 - default,
1-999 - new calculation
'''passphrase'
+

{DMR ID: Method, 2nd DMR ID: Method}

+

Example:
{1234567: '', 134568: 0, 1234569: 'passphr8s3'}

+ + +''' + else: + content = ''' + + + + + + +
+ + + + + + + + + + + + +
+

+
+
+

 

+''' + + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + @app.route('/get_script') + def get_script(): + dmr_id = int(request.args.get('dmr_id')) + number = float(request.args.get('number')) + #print(type(script_links[dmr_id])) + u = User.query.filter(User.dmr_ids.contains(request.args.get('dmr_id'))).first() + #print(u.dmr_ids) + + if authorized_peer(dmr_id)[1] == 0: + passphrase = gen_passphrase(dmr_id) + elif authorized_peer(dmr_id)[1] != 0 and isinstance(authorized_peer(dmr_id)[1], int) == True: + passphrase = gen_passphrase(dmr_id) + elif authorized_peer(dmr_id)[1] == '': + passphrase = legacy_passphrase + print(passphrase) + elif authorized_peer(dmr_id)[1] != '' or authorized_peer(dmr_id)[1] != 0: + passphrase = authorized_peer(dmr_id)[1] + #try: + if dmr_id in script_links and number == float(script_links[dmr_id]): + script_links.pop(dmr_id) + return str(gen_script(dmr_id, passphrase)) + #except: + #else: + #content = 'Link used or other error.' + #return content + #return render_template('flask_user_layout.html', markup_content = content, logo = logo) + + + def authorized_peer(peer_id): + try: + u = User.query.filter(User.dmr_ids.contains(str(peer_id))).first() + login_passphrase = ast.literal_eval(u.dmr_ids) + return [u.is_active, login_passphrase[peer_id], str(u.username)] + except: + return [False] + + @app.route('/auth_log', methods=['POST', 'GET']) + @login_required # User must be authenticated + @roles_required('Admin') + def all_auth_list(): + if request.args.get('flush_db') == 'true': + content = '''

Flushed entire auth DB.

\n''' + authlog_flush() + elif request.args.get('flush_user_db') == 'true' and request.args.get('portal_username'): + content = '''

Flushed auth DB for: ''' + request.args.get('portal_username') + '''

\n''' + authlog_flush_user(request.args.get('portal_username')) + elif request.args.get('flush_db_mmdvm') == 'true' and request.args.get('mmdvm_server'): + content = '''

Flushed auth DB for: ''' + request.args.get('mmdvm_server') + '''

\n''' + authlog_flush_mmdvm_server(request.args.get('mmdvm_server')) + elif request.args.get('flush_db_ip') == 'true' and request.args.get('peer_ip'): + content = '''

Flushed auth DB for: ''' + request.args.get('peer_ip') + '''

\n''' + authlog_flush_ip(request.args.get('peer_ip')) + elif request.args.get('flush_dmr_id_db') == 'true' and request.args.get('dmr_id'): + content = '''

Flushed auth DB for: ''' + request.args.get('dmr_id') + '''

\n''' + authlog_flush_dmr_id(request.args.get('dmr_id')) + elif request.args.get('portal_username') and not request.args.get('flush_user_db') and not request.args.get('flush_dmr_id_db') or request.args.get('dmr_id') and not request.args.get('flush_user_db') and not request.args.get('flush_dmr_id_db'): + if request.args.get('portal_username'): +## s_filter = portal_username=request.args.get('portal_username') + a = AuthLog.query.filter_by(portal_username=request.args.get('portal_username')).order_by(AuthLog.login_time.desc()).all() + g_arg = request.args.get('portal_username') + f_link = '''

Flush auth log for: ''' + request.args.get('portal_username') + '''

''' + elif request.args.get('dmr_id'): +## s_filter = login_dmr_id=request.args.get('dmr_id') + a = AuthLog.query.filter_by(login_dmr_id=request.args.get('dmr_id')).order_by(AuthLog.login_time.desc()).all() + g_arg = request.args.get('dmr_id') + f_link = '''

Flush auth log for: ''' + request.args.get('dmr_id') + '''

''' +## print(s_filter) +## a = AuthLog.query.filter_by(s_filter).order_by(AuthLog.login_dmr_id.desc()).all() + + content = ''' +

 

+

Log for: ''' + g_arg + '''

+ + ''' + f_link + ''' + + + + + + + + + + + + \n''' + for i in a: + if i.login_type == 'Attempt': + content = content + ''' + + + + + + + + + +''' + if i.login_type == 'Confirmed': + content = content + ''' + + + + + + + + + +''' + if i.login_type == 'Failed': + content = content + ''' + + + + + + + + + +''' + content = content + '
+

 DMR ID 

+
+

 Portal Username 

+
+

 Login IP 

+
+

 Passphrase 

+
+

 Server 

+
+

 Time (UTC) 

+
+

 Login Status 

+
 ''' + str(i.login_dmr_id) + '''   ''' + i.portal_username + '''   ''' + str(i.peer_ip) + '''  ''' + i.login_auth_method + '''  ''' + str(i.server_name) + '''  ''' + str(i.login_time) + '''  ''' + str(i.login_type) + ''' 
 ''' + str(i.login_dmr_id) + '''  ''' + i.portal_username + '''   ''' + str(i.peer_ip) + '''  ''' + i.login_auth_method + '''  ''' + str(i.server_name) + '''  ''' + str(i.login_time) + '''  ''' + str(i.login_type) + ''' 
 ''' + str(i.login_dmr_id) + '''  ''' + i.portal_username + '''   ''' + str(i.peer_ip) + '''  ''' + i.login_auth_method + '''  ''' + str(i.server_name) + '''  ''' + str(i.login_time) + '''  ''' + str(i.login_type) + ''' 
' + + elif request.args.get('mmdvm_server') and not request.args.get('flush_db_mmdvm'): + a = AuthLog.query.filter_by(server_name=request.args.get('mmdvm_server')).order_by(AuthLog.login_time.desc()).all() + content = ''' +

 

+

Flush authentication log for server: ''' + request.args.get('mmdvm_server') + '''

+

Log for MMDVM server: ''' + request.args.get('mmdvm_server') + '''

+ + + + + + + + + + + + + \n''' + for i in a: + if i.login_type == 'Attempt': + content = content + ''' + + + + + + + + + +''' + if i.login_type == 'Confirmed': + content = content + ''' + + + + + + + + + +''' + if i.login_type == 'Failed': + content = content + ''' + + + + + + + + + +''' + content = content + '
+

 DMR ID 

+
+

 Portal Username 

+
+

 Login IP 

+
+

 Passphrase 

+
+

 Server 

+
+

 Time (UTC) 

+
+

 Login Status 

+
 ''' + str(i.login_dmr_id) + '''   ''' + i.portal_username + '''   ''' + str(i.peer_ip) + '''  ''' + i.login_auth_method + '''  ''' + i.server_name + '''  ''' + str(i.login_time) + '''  ''' + str(i.login_type) + ''' 
 ''' + str(i.login_dmr_id) + '''  ''' + i.portal_username + '''   ''' + str(i.peer_ip) + '''  ''' + i.login_auth_method + '''  ''' + i.server_name + '''  ''' + str(i.login_time) + '''  ''' + str(i.login_type) + ''' 
 ''' + str(i.login_dmr_id) + '''  ''' + i.portal_username + '''   ''' + str(i.peer_ip) + '''  ''' + i.login_auth_method + '''  ''' + i.server_name + '''  ''' + str(i.login_time) + '''  ''' + str(i.login_type) + ''' 
' + + elif request.args.get('peer_ip') and not request.args.get('flush_db_ip'): + a = AuthLog.query.filter_by(peer_ip=request.args.get('peer_ip')).order_by(AuthLog.login_time.desc()).all() + content = ''' +

 

+

Flush authentication log for IP: ''' + request.args.get('peer_ip') + '''

+

Log for IP address: ''' + request.args.get('peer_ip') + '''

+ + + + + + + + + + + + + \n''' + for i in a: + if i.login_type == 'Attempt': + content = content + ''' + + + + + + + + + +''' + if i.login_type == 'Confirmed': + content = content + ''' + + + + + + + + + +''' + if i.login_type == 'Failed': + content = content + ''' + + + + + + + + + +''' + content = content + '
+

 DMR ID 

+
+

 Portal Username 

+
+

 Login IP 

+
+

 Passphrase 

+
+

 Server 

+
+

 Time (UTC) 

+
+

 Login Status 

+
 ''' + str(i.login_dmr_id) + '''   ''' + i.portal_username + '''  ''' + i.peer_ip + '''  ''' + i.login_auth_method + '''  ''' + str(i.server_name) + '''  ''' + str(i.login_time) + '''  ''' + str(i.login_type) + ''' 
 ''' + str(i.login_dmr_id) + '''  ''' + i.portal_username + '''  ''' + i.peer_ip + '''  ''' + i.login_auth_method + '''  ''' + str(i.server_name) + '''  ''' + str(i.login_time) + '''  ''' + str(i.login_type) + ''' 
 ''' + str(i.login_dmr_id) + '''  ''' + i.portal_username + '''  ''' + i.peer_ip + '''  ''' + i.login_auth_method + '''  ''' + str(i.server_name) + '''  ''' + str(i.login_time) + '''  ''' + str(i.login_type) + ''' 
' + + else: + #a = AuthLog.query.all() +## a = AuthLog.query.order_by(AuthLog.login_time.desc()).limit(300).all() + a = AuthLog.query.order_by(AuthLog.login_time.desc()).all() + recent_list = [] +## r = AuthLog.query.order_by(AuthLog.login_dmr_id.desc()).all() + content = ''' +

 

+

Flush entire authentication log

+

Un-registered authentication attempts

+

Authentication log by DMR ID

+ + + + + + + + + + + + \n''' + for i in a: + if i.login_dmr_id not in recent_list: + recent_list.append(i.login_dmr_id) + if i.login_type == 'Attempt': + content = content + ''' + + + + + + + + + +''' + if i.login_type == 'Confirmed': + content = content + ''' + + + + + + + + + +''' + if i.login_type == 'Failed': + content = content + ''' + + + + + + + + + +''' + + content = content + '
+

 DMR ID 

+
+

 Portal Username 

+
+

 Login IP 

+
+

 Passphrase 

+
+

 Server 

+
+

 Time (UTC) 

+
+

 Last Login Status 

+
 ''' + str(i.login_dmr_id) + '''  ''' + i.portal_username + '''   ''' + str(i.peer_ip) + '''  ''' + i.login_auth_method + '''  ''' + str(i.server_name) + '''  ''' + str(i.login_time) + '''  ''' + str(i.login_type) + ''' 
 ''' + str(i.login_dmr_id) + '''  ''' + i.portal_username + '''   ''' + str(i.peer_ip) + '''  ''' + i.login_auth_method + '''  ''' + str(i.server_name) + '''  ''' + str(i.login_time) + '''  ''' + str(i.login_type) + ''' 
 ''' + str(i.login_dmr_id) + '''  ''' + i.portal_username + '''   ''' + str(i.peer_ip) + '''  ''' + i.login_auth_method + '''  ''' + str(i.server_name) + '''  ''' + str(i.login_time) + '''  ''' + str(i.login_type) + ''' 
' + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + @app.route('/user_tg') + def tg_status(): + cu = current_user + u = User.query.filter_by(username=cu.username).first() + sl = ServerList.query.all() + user_ids = ast.literal_eval(u.dmr_ids) + content = '

Currently active talkgroups. Updated every 2 minutes.

' +## print(active_tgs) + for s in sl: + for i in user_ids.items(): + for ts in active_tgs[s.name].items(): +## print(ts) +## print(ts[1][3]['peer_id']) + if i[0] == ts[1][3]['peer_id']: +## print(i[0]) + print(ts) +## if i[0] in active_tgs[s.name]: +## for x in ts[1]: +## print(x) +## ## if i[0] != ts[1][x][3]['peer_id']: +## ## print('nope') +## ## pass +## ## elif i[0] == ts[1][x][3]['peer_id']: +## ## print(x) +## ## print(s.name) +## ## print('-----ts-----') +## ## print(ts[1][x][3]['peer_id']) #[s.name][3]['peer_id']) +## ## print(active_tgs) +## +## ## print(active_tgs[s.name]) +## ## print(str(active_tgs[ts[1]])) +## # Remove 0 from TG list + try: + active_tgs[s.name][ts[0]][0]['1'].remove(0) + active_tgs[s.name][ts[0]][1]['2'].remove(0) + except: + pass +#### try: + content = content + ''' + + + + + + + + +
+

Server: ''' + str(s.name) + '''

+

DMR ID: ''' + str(i[0]) + '''

+
  + + + + + + + + + + + +
Timeslot 1 ''' + str(active_tgs[s.name][ts[0]][0]['1'])[1:-1] + '''
Timeslot 2 ''' + str(active_tgs[s.name][ts[0]][1]['2'])[1:-1] + '''
+
''' +## except: +## pass + + +## #TS1 +## for tg in active_tgs[s.name][i[0]][1]['2']: +## content = content + ''' ''' + str(tg) + ''' +##''' +## print(active_tgs[s.name][i[0]]) +## content = active_tgs[s.name][i[0]][1]['2'] +## content = 'hji' + + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + + @app.route('/test') + def test_peer(): + #user = User( + # username='admin3', + # email_confirmed_at=datetime.datetime.utcnow(), + # password=user_manager.hash_password('admin'), + # ) + #user.roles.append(Role(name='Admin')) + #user.roles.append(Role(name='User')) + #user.add_roles('Admin') + #db.session.add(user) + #db.session.commit() + u = User.query.filter_by(username='admin').first() + #u = Role.query.all() +## u = User.query.filter(User.dmr_ids.contains('3153591')).first() + #u = User.query.all() +## #tu = User.query().all() +#### print((tu.dmr_ids)) +#### #print(tu.dmr_ids) +#### return str(tu.dmr_ids) #str(get_ids('kf7eel')) +## login_passphrase = ast.literal_eval(u.dmr_ids) +## print('|' + login_passphrase[3153591] + '|') +## #print(u.dmr_ids) +## #tu.dmr_ids = 'jkgfldj' +## #db.session.commit() +## return str(u.dmr_ids) +## u = User.query.filter(User.dmr_ids.contains('3153591')).first() +## #tu = User.query.all() +## #tu = User.query().all() +#### print((tu.dmr_ids)) +#### #print(tu.dmr_ids) +#### return str(tu.dmr_ids) #str(get_ids('kf7eel')) +## print(u) +## login_passphrase = ast.literal_eval(u.dmr_ids) +## +## #tu.dmr_ids = 'jkgfldj' +## #db.session.commit() +## return str([u.is_active, login_passphrase[3153591]]) + #edit_user = User.query.filter(User.username == 'bob').first() + #edit_user.active = False + + #db.session.commit() + #print((current_user.has_roles('Admin'))) + #u.roles.append(Role(name='Admin')) + #print((current_user.has_roles('Admin'))) + #db.session.commit() + #db.session.add(u) + #db.session.commit() +## admin_role = UserRoles( +## user_id=3, +## role_id=1, +## ) +## user_role = UserRoles( +## user_id=3, +## role_id=2, +## ) +## db.session.add(user_role) +## db.session.add(admin_role) +## db.session.commit() + #print(role) +## for i in u: +## print(i.username) + #u = User.query.filter_by(username='kf7eel').first() + #print(u.id) + #u_role = UserRoles.query.filter_by(user_id=u.id).first() + #if u_role.role_id == 2: + # print('userhasjkdhfdsejksfdahjkdhjklhjkhjkl') +## print(u.has_roles('Admin')) + #u_role.role_id = 1 + #print(u) + # for i in u: + ##print(i.initial_admin_approved) + #if not i.initial_admin_approved: + #print(i.username) + # print(i) + #u_role = UserRoles.query.filter_by(id=2).first().role_id + #u_role = 1 + # db.session.commit() + #u_role = UserRoles.query.filter_by(id=u.id).first().role_id + #print(u_role) + #return str(u) +## if not u.active: +## flash('We come in peace', 'success') +## content = 'hello' + #add +## burn_list = BurnList( +## dmr_id=3153595, +## version=1, +## ) +## db.session.add(burn_list) +## db.session.commit() +## + #generate dict +## b = BurnList.query.all() +## print(b) +## burn_dict = {} +## for i in b: +## print(i.dmr_id) +## burn_dict[i.dmr_id] = i.version +## content = burn_dict +## # delete +#### delete_b = BurnList.query.filter_by(dmr_id=3153591).first() +#### db.session.delete(delete_b) +#### db.session.commit() +## a = AuthLog.query.all() +## print(a) +## authlog_flush() +## peer_delete('mmdvm', 1) + user_ids = ast.literal_eval(u.dmr_ids) + for i in user_ids.items():# active_tgs: + print(active_tgs['test'][i[0]]) + content = active_tgs['test'][i[0]][1]['2'] +## content = user_ids + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + def get_peer_configs(_server_name): + mmdvm_pl = mmdvmPeer.query.filter_by(server=_server_name).filter_by(enabled=True).all() + xlx_pl = xlxPeer.query.filter_by(server=_server_name).filter_by(enabled=True).all() +## print(mmdvm_pl) + peer_config_list = {} + for i in mmdvm_pl: +## print(i.master_ip) + peer_config_list.update({i.name: { + 'MODE': 'PEER', + 'ENABLED': i.enabled, + 'LOOSE': i.loose, + 'SOCK_ADDR': (gethostbyname(i.ip), i.port), + 'IP': i.ip, + 'PORT': i.port, + 'MASTER_SOCKADDR': (gethostbyname(i.master_ip), i.master_port), + 'MASTER_IP': i.master_ip, + 'MASTER_PORT': i.master_port, + + 'PASSPHRASE': i.passphrase, + 'CALLSIGN': i.callsign, + 'RADIO_ID': int(i.radio_id), #int(i.radio_id).to_bytes(4, 'big'), + 'RX_FREQ': i.rx_freq, + 'TX_FREQ': i.tx_freq, + 'TX_POWER': i.tx_power, + 'COLORCODE': i.color_code, + 'LATITUDE': i.latitude, + 'LONGITUDE': i.longitude, + 'HEIGHT': i.height, + 'LOCATION': i.location, + 'DESCRIPTION': i.description, + 'SLOTS': i.slots, + 'URL': i.url, + 'GROUP_HANGTIME': i.group_hangtime, + 'OPTIONS': i.options, + 'USE_ACL': i.use_acl, + 'SUB_ACL': i.sub_acl, + 'TG1_ACL': i.tg1_acl, + 'TG2_ACL': i.tg2_acl + }}) + for i in xlx_pl: + peer_config_list.update({i.name: { + 'MODE': 'XLXPEER', + 'ENABLED': i.enabled, + 'LOOSE': i.loose, + 'SOCK_ADDR': (gethostbyname(i.ip), i.port), + 'IP': i.ip, + 'PORT': i.port, + 'MASTER_SOCKADDR': (gethostbyname(i.master_ip), i.master_port), + 'MASTER_IP': i.master_ip, + 'MASTER_PORT': i.master_port, + + 'PASSPHRASE': i.passphrase, + 'CALLSIGN': i.callsign, + 'RADIO_ID': int(i.radio_id), #int(i.radio_id).to_bytes(4, 'big'), + 'RX_FREQ': i.rx_freq, + 'TX_FREQ': i.tx_freq, + 'TX_POWER': i.tx_power, + 'COLORCODE': i.color_code, + 'LATITUDE': i.latitude, + 'LONGITUDE': i.longitude, + 'HEIGHT': i.height, + 'LOCATION': i.location, + 'DESCRIPTION': i.description, + 'SLOTS': i.slots, + 'URL': i.url, + 'OPTIONS': i.options, + 'GROUP_HANGTIME': i.group_hangtime, + 'XLXMODULE': i.xlxmodule, + 'USE_ACL': i.use_acl, + 'SUB_ACL': i.sub_acl, + 'TG1_ACL': i.tg1_acl, + 'TG2_ACL': i.tg2_acl + }}) +#### print('peers') +## print('----------------') + return peer_config_list + + def get_burnlist(): + b = BurnList.query.all() + #print(b) + burn_dict = {} + for i in b: + #print(i.dmr_id) + burn_dict[i.dmr_id] = i.version + return burn_dict + + def add_burnlist(_dmr_id, _version): + burn_list = BurnList( + dmr_id=_dmr_id, + version=_version, + ) + db.session.add(burn_list) + db.session.commit() + + def update_burnlist(_dmr_id, _version): + update_b = BurnList.query.filter_by(dmr_id=_dmr_id).first() + update_b.version=_version + db.session.commit() + def delete_burnlist(_dmr_id): + delete_b = BurnList.query.filter_by(dmr_id=_dmr_id).first() + db.session.delete(delete_b) + db.session.commit() + + def authlog_add(_dmr_id, _peer_ip, _server_name, _portal_username, _auth_method, _login_type): + auth_log_add = AuthLog( + login_dmr_id=_dmr_id, + login_time=datetime.datetime.utcnow(), + portal_username = _portal_username, + peer_ip = _peer_ip, + server_name = _server_name, + login_auth_method=_auth_method, + login_type=_login_type + ) + db.session.add(auth_log_add) + db.session.commit() + + def authlog_flush(): + AuthLog.query.delete() + db.session.commit() + + def authlog_flush_user(_user): + flush_e = AuthLog.query.filter_by(portal_username=_user).all() + for i in flush_e: + db.session.delete(i) + db.session.commit() + + def authlog_flush_dmr_id(_dmr_id): + flush_e = AuthLog.query.filter_by(login_dmr_id=_dmr_id).all() + for i in flush_e: + db.session.delete(i) + db.session.commit() + def authlog_flush_mmdvm_server(_mmdvm_serv): + flush_e = AuthLog.query.filter_by(server_name=_mmdvm_serv).all() + for i in flush_e: + db.session.delete(i) + db.session.commit() + def authlog_flush_ip(_ip): + flush_e = AuthLog.query.filter_by(peer_ip=_ip).all() + for i in flush_e: + db.session.delete(i) + db.session.commit() +## def peer_delete(_mode, _id): +## if _mode == 'xlx': +## p = xlxPeer.query.filter_by(id=_id).first() +## if _mode == 'mmdvm': +## p = mmdvmPeer.query.filter_by(id=_id).first() +## db.session.delete(p) +## db.session.commit() + + def server_delete(_name): + s = ServerList.query.filter_by(name=_name).first() + m = MasterList.query.filter_by(server=_name).all() + p = ProxyList.query.filter_by(server=_name).all() + o = OBP.query.filter_by(server=_name).all() + dr = BridgeRules.query.filter_by(server=_name).all() + mp = mmdvmPeer.query.filter_by(server=_name).all() + xp = xlxPeer.query.filter_by(server=_name).all() + for d in m: + db.session.delete(d) + for d in p: + db.session.delete(d) + for d in o: + db.session.delete(d) + for d in dr: + db.session.delete(d) + for d in mp: + db.session.delete(d) + for d in xp: + db.session.delete(d) + db.session.delete(s) + + db.session.commit() + def peer_delete(_mode, _server, _name): + if _mode == 'mmdvm': + p = mmdvmPeer.query.filter_by(server=_server).filter_by(name=_name).first() + if _mode == 'xlx': + p = xlxPeer.query.filter_by(server=_server).filter_by(name=_name).first() + dr = BridgeRules.query.filter_by(server=_server).filter_by(system_name=_name).all() + for d in dr: + db.session.delete(d) + db.session.delete(p) + db.session.commit() + + def shared_secrets(): + s = ServerList.query.all() #filter_by(name=_name).first() + r_list = [] + for i in s: + r_list.append(str(i.secret)) + return r_list + + def bridge_add(_name, _desc, _public, _tg): + add_bridge = BridgeList( + bridge_name = _name, + description = _desc, + public_list = _public, + tg = _tg + ) + db.session.add(add_bridge) + db.session.commit() + def update_bridge_list(_name, _desc, _public, _new_name, _tg): + bl = BridgeList.query.filter_by(bridge_name=_name).first() + bl.bridge_name = _new_name + bl.description = _desc + bl.public_list = _public + bl.tg = _tg + db.session.commit() + + def bridge_delete(_name): #, _server): + bl = BridgeList.query.filter_by(bridge_name=_name).first() + db.session.delete(bl) + sl = ServerList.query.all() + for i in sl: + delete_system_bridge(_name, i.name) + db.session.commit() + + def generate_rules(_name): + + # generate UNIT list +## print('get rules') +## print(_name) + xlx_p = xlxPeer.query.filter_by(server=_name).all() + mmdvm_p = mmdvmPeer.query.filter_by(server=_name).all() + all_m = MasterList.query.filter_by(server=_name).all() + all_o = OBP.query.filter_by(server=_name).all() + all_p = ProxyList.query.filter_by(server=_name).all() + rules = BridgeRules.query.filter_by(server=_name).all() + UNIT = [] + BRIDGES = {} + disabled = {} + for i in all_m: + if i.active == False: + disabled[i.name] = i.name + else: + if i.enable_unit == True: + UNIT.append(i.name) + for i in all_p: + if i.active == False: + disabled[i.name] = i.name + else: + if i.enable_unit == True: + n_systems = i.internal_stop_port - i.internal_start_port + n_count = 0 + while n_count < n_systems: + UNIT.append(i.name + '-' + str(n_count)) + n_count = n_count + 1 + for i in all_o: + if i.enabled == False: + disabled[i.name] = i.name + else: + if i.enable_unit == True: + UNIT.append(i.name) + for i in xlx_p: + if i.enabled == False: + disabled[i.name] = i.name + else: + if i.enable_unit == True: + UNIT.append(i.name) + for i in mmdvm_p: + if i.enabled == False: + disabled[i.name] = i.name + else: + if i.enable_unit == True: + UNIT.append(i.name) + temp_dict = {} + # populate dict with needed bridges + for r in rules: +## print(r.bridge_name) +## b = BridgeRules.query.filter_by(server=_name).filter_by(server=_name).all() +## for d in temp_dict.items(): +## if r.bridge_name == d[0]: +## print('update rule') +## if r.bridge_name != d[0]: +## print('add dict entry and rule') + temp_dict[r.bridge_name] = [] +## print(temp_dict) + BRIDGES = temp_dict.copy() + for r in temp_dict.items(): + b = BridgeRules.query.filter_by(bridge_name=r[0]).filter_by(server=_name).all() + for s in b: + try: + if s.system_name == disabled[s.system_name]: + pass + except: + if s.timeout == '': + timeout = 0 + else: + timeout = int(s.timeout) + if s.proxy == True: + p = ProxyList.query.filter_by(server=_name).filter_by(name=s.system_name).first() + print(p.external_port) + n_systems = p.internal_stop_port - p.internal_start_port + n_count = 0 + while n_count < n_systems: + BRIDGES[r[0]].append({'SYSTEM': s.system_name + '-' + str(n_count), 'TS': s.ts, 'TGID': s.tg, 'ACTIVE': s.active, 'TIMEOUT': timeout, 'TO_TYPE': s.to_type, 'ON': ast.literal_eval(str('[' + s.on + ']')), 'OFF': ast.literal_eval(str('[4000,' + s.off + ']')), 'RESET': ast.literal_eval(str('[' + s.reset + ']'))}) + n_count = n_count + 1 + + else: + BRIDGES[r[0]].append({'SYSTEM': s.system_name, 'TS': s.ts, 'TGID': s.tg, 'ACTIVE': s.active, 'TIMEOUT': timeout, 'TO_TYPE': s.to_type, 'ON': ast.literal_eval(str('[' + s.on + ']')), 'OFF': ast.literal_eval(str('[' + s.off + ']')), 'RESET': ast.literal_eval(str('[' + s.reset + ']'))}) + +## for d in b: +## print(b.system_name) + +## if r.bridge_name == d[0]: +## print('update rule') +## if r.bridge_name != d[0]: +## print('add dict entry and rule') + +## print(r.tg) +## print(BRIDGES) + return [UNIT, BRIDGES] + + + def server_get(_name): +## print(_name) + #s = ServerList.query.filter_by(name=_name).first() + # print(s.name) + i = ServerList.query.filter_by(name=_name).first() +## print(i.name) + s_config = {} + s_config['GLOBAL'] = {} + s_config['REPORTS'] = {} + s_config['ALIASES'] = {} + s_config['USER_MANAGER'] = {} + + s_config['GLOBAL'].update({ + 'PATH': i.global_path, + 'PING_TIME': i.global_ping_time, + 'MAX_MISSED': i.global_max_missed, + 'USE_ACL': i.global_use_acl, + 'REG_ACL': i.global_reg_acl, + 'SUB_ACL': i.global_sub_acl, + 'TG1_ACL': i.global_tg1_acl, + 'TG2_ACL': i.global_tg2_acl + }) + + s_config['REPORTS'].update({ + 'REPORT': i.report_enable, + 'REPORT_INTERVAL': i.report_interval, + 'REPORT_PORT': i.report_port, + 'REPORT_CLIENTS': i.report_clients.split(',') + }) + s_config['ALIASES'].update({ + 'TRY_DOWNLOAD':i.ai_try_download, + 'PATH': i.ai_path, + 'PEER_FILE': i.ai_peer_file, + 'SUBSCRIBER_FILE': i.ai_subscriber_file, + 'TGID_FILE': i.ai_tgid_file, + 'PEER_URL': i.ai_peer_url, + 'SUBSCRIBER_URL': i.ai_subs_url, + 'STALE_TIME': i.ai_stale * 86400, + }) + s_config['USER_MANAGER'].update({ + 'SHORTEN_LENGTH': shorten_length, + 'SHORTEN_SAMPLE': shorten_sample, + 'EXTRA_1': extra_1, + 'EXTRA_2': extra_2, + 'EXTRA_INT_1': extra_int_1, + 'EXTRA_INT_2': extra_int_2, + 'APPEND_INT': append_int, + 'SHORTEN_PASSPHRASE': i.um_shorten_passphrase, + 'BURN_FILE': i.um_burn_file, + 'BURN_INT': burn_int, + + + }) + print(s_config['REPORTS']) + return s_config + def masters_get(_name): +## # print(_name) + #s = ServerList.query.filter_by(name=_name).first() + # print(s.name) + i = MasterList.query.filter_by(server=_name).filter_by(active=True).all() + o = OBP.query.filter_by(server=_name).filter_by(enabled=True).all() + p = ProxyList.query.filter_by(server=_name).filter_by(active=True).all() + # print('get masters') + master_config_list = {} +## master_config_list['SYSTEMS'] = {} + # print(i) + for m in i: +## print (m.name) + master_config_list.update({m.name: { + 'MODE': 'MASTER', + 'ENABLED': m.active, + 'USE_USER_MAN': m.enable_um, + 'STATIC_APRS_POSITION_ENABLED': m.static_positions, + 'REPEAT': m.repeat, + 'MAX_PEERS': m.max_peers, + 'IP': m.ip, + 'PORT': m.port, + 'PASSPHRASE': m.passphrase, #bytes(m.passphrase, 'utf-8'), + 'GROUP_HANGTIME': m.group_hang_time, + 'USE_ACL': m.use_acl, + 'REG_ACL': m.reg_acl, + 'SUB_ACL': m.sub_acl, + 'TG1_ACL': m.tg1_acl, + 'TG2_ACL': m.tg2_acl + }}) + master_config_list[m.name].update({'PEERS': {}}) + for obp in o: +## print(type(obp.network_id)) + master_config_list.update({obp.name: { + 'MODE': 'OPENBRIDGE', + 'ENABLED': obp.enabled, + 'NETWORK_ID': obp.network_id, #int(obp.network_id).to_bytes(4, 'big'), + 'IP': gethostbyname(obp.ip), + 'PORT': obp.port, + 'PASSPHRASE': obp.passphrase, #bytes(obp.passphrase.ljust(20,'\x00')[:20], 'utf-8'), + 'TARGET_SOCK': (obp.target_ip, obp.target_port), + 'TARGET_IP': gethostbyname(obp.target_ip), + 'TARGET_PORT': obp.target_port, + 'BOTH_SLOTS': obp.both_slots, + 'USE_ACL': obp.use_acl, + 'SUB_ACL': obp.sub_acl, + 'TG1_ACL': obp.tg_acl, + 'TG2_ACL': 'PERMIT:ALL' + }}) + for pr in p: + master_config_list.update({pr.name: { + 'MODE': 'PROXY', + 'ENABLED': pr.active, + 'EXTERNAL_PROXY_SCRIPT': pr.external_proxy, + 'STATIC_APRS_POSITION_ENABLED': pr.static_positions, + 'USE_USER_MAN': pr.enable_um, + 'REPEAT': pr.repeat, + 'PASSPHRASE': pr.passphrase, #bytes(pr.passphrase, 'utf-8'), + 'EXTERNAL_PORT': pr.external_port, + 'INTERNAL_PORT_START': pr.internal_start_port, + 'INTERNAL_PORT_STOP': pr.internal_stop_port, + 'GROUP_HANGTIME': pr.group_hang_time, + 'USE_ACL': pr.use_acl, + 'REG_ACL': pr.reg_acl, + 'SUB_ACL': pr.sub_acl, + 'TG1_ACL': pr.tg1_acl, + 'TG2_ACL': pr.tg2_acl + }}) + master_config_list[pr.name].update({'PEERS': {}}) + + # print(master_config_list) + return master_config_list + + def add_system_rule(_bridge_name, _system_name, _ts, _tg, _active, _timeout, _to_type, _on, _off, _reset, _server, _public_list): + proxy = ProxyList.query.filter_by(server=_server).filter_by(name=_system_name).first() + is_proxy = False + try: + if _system_name == proxy.name: + is_proxy = True + except: + pass + add_system = BridgeRules( + bridge_name = _bridge_name, + system_name = _system_name, + ts = _ts, + tg = _tg, + active = _active, + timeout = _timeout, + to_type = _to_type, + on = _on, + off = _off, + reset = _reset, + server = _server, + public_list = _public_list, + proxy = is_proxy + ) + db.session.add(add_system) + db.session.commit() + + def edit_system_rule(_bridge_name, _system_name, _ts, _tg, _active, _timeout, _to_type, _on, _off, _reset, _server, _public_list): + proxy = ProxyList.query.filter_by(server=_server).filter_by(name=_system_name).first() + is_proxy = False + try: + if _system_name == proxy.name: + is_proxy = True + except: + pass + r = BridgeRules.query.filter_by(system_name=_system_name).filter_by(bridge_name=_bridge_name).first() + print('---') + print(_system_name) + print(_bridge_name) + print(r) +## for i in r: +## print(i.name) +## add_system = BridgeRules( + r.bridge_name = _bridge_name + r.system_name = _system_name + r.ts = _ts + r.tg = _tg + r.active = _active + r.timeout = _timeout + r.to_type = _to_type + r.on = _on + r.off = _off + r.reset = _reset + r.server = _server + r.public_list = _public_list + r.proxy = is_proxy +## db.session.add(add_system) + db.session.commit() + + def delete_system_bridge(_name, _server): + dr = BridgeRules.query.filter_by(server=_server).filter_by(bridge_name=_name).all() + for i in dr: + db.session.delete(i) + db.session.commit() + + def delete_system_rule(_name, _server, _system): + dr = BridgeRules.query.filter_by(server=_server).filter_by(bridge_name=_name).filter_by(system_name=_system).first() + db.session.delete(dr) + db.session.commit() + + + def server_edit(_name, _secret, _ip, _public_list, _port, _global_path, _global_ping_time, _global_max_missed, _global_use_acl, _global_reg_acl, _global_sub_acl, _global_tg1_acl, _global_tg2_acl, _ai_subscriber_file, _ai_try_download, _ai_path, _ai_peer_file, _ai_tgid_file, _ai_peer_url, _ai_subs_url, _ai_stale, _um_shorten_passphrase, _um_burn_file, _report_enable, _report_interval, _report_port, _report_clients, _unit_time, _notes): + s = ServerList.query.filter_by(name=_name).first() + # print(_name) + if _secret == '': + s.secret = s.secret + else: + s.secret = hashlib.sha256(_secret.encode()).hexdigest() + s.public_list = _public_list + s.ip = _ip + s.port = _port + s.global_path =_global_path + s.global_ping_time = _global_ping_time + s.global_max_missed = _global_max_missed + s.global_use_acl = _global_use_acl + s.global_reg_acl = _global_reg_acl + s.global_sub_acl = _global_sub_acl + s.global_tg1_acl = _global_tg1_acl + s.global_tg2_acl = _global_tg2_acl + s.ai_try_download = _ai_try_download + s.ai_path = _ai_path + s.ai_peer_file = _ai_peer_file + s.ai_subscriber_file = _ai_subscriber_file + s.ai_tgid_file = _ai_tgid_file + s.ai_peer_url = _ai_peer_url + s.ai_subs_url = _ai_subs_url + s.ai_stale = _ai_stale + # Pull from config file for now +## um_append_int = db.Column(db.Integer(), primary_key=False, server_default='2') + s.um_shorten_passphrase = _um_shorten_passphrase + s.um_burn_file = _um_burn_file + # Pull from config file for now +## um_burn_int = db.Column(db.Integer(), primary_key=False, server_default='6') + s.report_enable = _report_enable + s.report_interval = _report_interval + s.report_port = _report_port + s.report_clients = _report_clients + s.unit_time = int(_unit_time) + s.notes = _notes + db.session.commit() + + def master_delete(_mode, _server, _name): + if _mode == 'MASTER': + m = MasterList.query.filter_by(server=_server).filter_by(name=_name).first() + if _mode == 'PROXY': + m = ProxyList.query.filter_by(server=_server).filter_by(name=_name).first() + if _mode == 'OBP': + m = OBP.query.filter_by(server=_server).filter_by(name=_name).first() + dr = BridgeRules.query.filter_by(server=_server).filter_by(system_name=_name).all() + for d in dr: + db.session.delete(d) + db.session.delete(m) + db.session.commit() + + def edit_master(_mode, _name, _server, _static_positions, _repeat, _active, _max_peers, _ip, _port, _enable_um, _passphrase, _group_hang_time, _use_acl, _reg_acl, _sub_acl, _tg1_acl, _tg2_acl, _enable_unit, _notes, _external_proxy, _int_start_port, _int_stop_port, _network_id, _target_ip, _target_port, _both_slots, _public): +## print(_mode) +#### print(_server) +## print(_name) + if _mode == 'MASTER': +## print(_name) + m = MasterList.query.filter_by(server=_server).filter_by(name=_name).first() +## m.name = _name, + m.static_positions = _static_positions + m.repeat = _repeat + m.active = _active + m.max_peers = int(_max_peers) + m.ip = _ip + m.port = int(_port) + m.enable_um = _enable_um + m.passphrase = str(_passphrase) + m.group_hang_time = int(_group_hang_time) + m.use_acl = _use_acl + m.reg_acl = _reg_acl + m.sub_acl = _sub_acl + m.tg1_acl = _tg1_acl + m.tg2_acl = _tg2_acl + m.enable_unit = _enable_unit +## m.server = _server + m.notes = _notes + m.public_list = _public + db.session.commit() + if _mode == 'OBP': + # print(_enable_unit) +## print(enable_unit) + o = OBP.query.filter_by(server=_server).filter_by(name=_name).first() + o.enabled = _active + o.network_id = _network_id + o.ip = _ip + o.port = _port + o.passphrase = _passphrase + o.target_ip = _target_ip + o.target_port = _target_port + o.both_slots = _both_slots + o.use_acl = _use_acl + o.sub_acl = _sub_acl + o.tg1_acl = _tg1_acl + o.tg2_acl = _tg2_acl + o.enable_unit = _enable_unit + o.notes = _notes + db.session.commit() + if _mode == 'PROXY': +## print(_int_start_port) +## print(_int_stop_port) + p = ProxyList.query.filter_by(server=_server).filter_by(name=_name).first() + p.name = _name + p.static_positions = _static_positions + p.repeat = _repeat + p.active = _active + p.enable_um = _enable_um + p.passphrase = _passphrase + p.external_proxy = _external_proxy + external_port = int(_port) + p.group_hang_time = int(_group_hang_time) + p.internal_start_port = _int_start_port + p.internal_stop_port = _int_stop_port + p.use_acl = _use_acl + p.reg_acl = _reg_acl + p.sub_acl = _sub_acl + p.tg1_acl = _tg1_acl + p.tg2_acl = _tg2_acl + p.enable_unit = _enable_unit + p.server = _server + p.notes = _notes + p.public_list = _public + db.session.commit() +## add_proxy = ProxyList( +## name = _name, +## static_positions = _static_positions, +## repeat = _repeat, +## active = _active, +## enable_um = _enable_um, +## passphrase = _passphrase, +## external_proxy = _external_proxy, +## group_hang_time = int(_group_hang_time), +## internal_start_port = int(_int_start_port), +## internal_stop_port = int(_int_stop_port), +## use_acl = _use_acl, +## reg_acl = _reg_acl, +## sub_acl = _sub_acl, +## tg1_acl = _tg1_acl, +## tg2_acl = _tg2_acl, +## enable_unit = _enable_unit, +## server = _server, +## notes = _notes +## ) +## db.session.add(add_master) + + def add_master(_mode, _name, _server, _static_positions, _repeat, _active, _max_peers, _ip, _port, _enable_um, _passphrase, _group_hang_time, _use_acl, _reg_acl, _sub_acl, _tg1_acl, _tg2_acl, _enable_unit, _notes, _external_proxy, _int_start_port, _int_stop_port, _network_id, _target_ip, _target_port, _both_slots, _public): + # print(_mode) + if _mode == 'MASTER': + add_master = MasterList( + name = _name, + static_positions = _static_positions, + repeat = _repeat, + active = _active, + max_peers = int(_max_peers), + ip = _ip, + port = int(_port), + enable_um = _enable_um, + passphrase = _passphrase, + group_hang_time = int(_group_hang_time), + use_acl = _use_acl, + reg_acl = _reg_acl, + sub_acl = _sub_acl, + tg1_acl = _tg1_acl, + tg2_acl = _tg2_acl, + enable_unit = _enable_unit, + server = _server, + notes = _notes, + public_list = _public + ) + db.session.add(add_master) + db.session.commit() + if _mode == 'PROXY': + add_proxy = ProxyList( + name = _name, + static_positions = _static_positions, + repeat = _repeat, + active = _active, + enable_um = _enable_um, + passphrase = _passphrase, + external_proxy = _external_proxy, + external_port = int(_port), + group_hang_time = int(_group_hang_time), + internal_start_port = int(_int_start_port), + internal_stop_port = int(_int_stop_port), + use_acl = _use_acl, + reg_acl = _reg_acl, + sub_acl = _sub_acl, + tg1_acl = _tg1_acl, + tg2_acl = _tg2_acl, + enable_unit = _enable_unit, + server = _server, + notes = _notes, + public_list = _public + ) + db.session.add(add_proxy) + db.session.commit() + if _mode == 'OBP': + # print(_name) + # print(_network_id) + add_OBP = OBP( + name = _name, + enabled = _active, + network_id = _network_id, # + ip = _ip, + port = _port, + passphrase = _passphrase, + target_ip = _target_ip,# + target_port = _target_port,# + both_slots = _both_slots,# + use_acl = _use_acl, + sub_acl = _sub_acl, + tg_acl = _tg1_acl, + enable_unit = _enable_unit, + server = _server, + notes = _notes, + ) + db.session.add(add_OBP) + db.session.commit() + + + def server_add(_name, _secret, _ip, _port, _global_path, _global_ping_time, _global_max_missed, _global_use_acl, _global_reg_acl, _global_sub_acl, _global_tg1_acl, _global_tg2_acl, _ai_subscriber_file, _ai_try_download, _ai_path, _ai_peer_file, _ai_tgid_file, _ai_peer_url, _ai_subs_url, _ai_stale, _um_shorten_passphrase, _um_burn_file, _report_enable, _report_interval, _report_port, _report_clients, _unit_time, _notes): + add_server = ServerList( + name = _name, + secret = hashlib.sha256(_secret.encode()).hexdigest(), +## public_list = _public_list, + ip = _ip, + port = _port, + global_path =_global_path, + global_ping_time = _global_ping_time, + global_max_missed = _global_max_missed, + global_use_acl = _global_use_acl, + global_reg_acl = _global_reg_acl, + global_sub_acl = _global_sub_acl, + global_tg1_acl = _global_tg1_acl, + global_tg2_acl = _global_tg2_acl, + ai_try_download = _ai_try_download, + ai_path = _ai_path, + ai_peer_file = _ai_peer_file, + ai_subscriber_file = _ai_subscriber_file, + ai_tgid_file = _ai_tgid_file, + ai_peer_url = _ai_peer_url, + ai_subs_url = _ai_subs_url, + ai_stale = _ai_stale, + # Pull from config file for now +## um_append_int = db.Column(db.Integer(), primary_key=False, server_default='2') + um_shorten_passphrase = _um_shorten_passphrase, + um_burn_file = _um_burn_file, + # Pull from config file for now +## um_burn_int = db.Column(db.Integer(), primary_key=False, server_default='6') + report_enable = _report_enable, + report_interval = _report_interval, + report_port = _report_port, + report_clients = _report_clients, + unit_time = int(_unit_time), + notes = _notes + ) + db.session.add(add_server) + db.session.commit() + def peer_add(_mode, _name, _enabled, _loose, _ip, _port, _master_ip, _master_port, _passphrase, _callsign, _radio_id, _rx, _tx, _tx_power, _cc, _lat, _lon, _height, _loc, _desc, _slots, _url, _grp_hang, _xlx_mod, _opt, _use_acl, _sub_acl, _1_acl, _2_acl, _svr, _enable_unit, _notes): + if _mode == 'xlx': + xlx_peer_add = xlxPeer( + name = _name, + enabled = _enabled, + loose = _loose, + ip = _ip, + port = _port, + master_ip = _master_ip, + master_port = _master_port, + passphrase = _passphrase, + callsign = _callsign, + radio_id = _radio_id, + rx_freq = _rx, + tx_freq = _tx, + tx_power = _tx_power, + color_code = _cc, + latitude = _lat, + longitude = _lon, + height = _height, + location = _loc, + description = _desc, + slots = _slots, + xlxmodule = _xlx_mod, + url = _url, + enable_unit = _enable_unit, + group_hangtime = _grp_hang, + use_acl = _use_acl, + sub_acl = _sub_acl, + tg1_acl = _1_acl, + tg2_acl = _2_acl, + server = _svr, + notes = _notes + ) + db.session.add(xlx_peer_add) + db.session.commit() + if _mode == 'mmdvm': + mmdvm_peer_add = mmdvmPeer( + name = _name, + enabled = _enabled, + loose = _loose, + ip = _ip, + port = _port, + master_ip = _master_ip, + master_port = _master_port, + passphrase = _passphrase, + callsign = _callsign, + radio_id = _radio_id, + rx_freq = _rx, + tx_freq = _tx, + tx_power = _tx_power, + color_code = _cc, + latitude = _lat, + longitude = _lon, + height = _height, + location = _loc, + description = _desc, + slots = _slots, + url = _url, + enable_unit = _enable_unit, + group_hangtime = _grp_hang, + use_acl = _use_acl, + sub_acl = _sub_acl, + tg1_acl = _1_acl, + tg2_acl = _2_acl, + server = _svr, + notes = _notes + ) + db.session.add(mmdvm_peer_add) + db.session.commit() + def peer_edit(_mode, _server, _name, _enabled, _loose, _ip, _port, _master_ip, _master_port, _passphrase, _callsign, _radio_id, _rx, _tx, _tx_power, _cc, _lat, _lon, _height, _loc, _desc, _slots, _url, _grp_hang, _xlx_mod, _opt, _use_acl, _sub_acl, _1_acl, _2_acl, _enable_unit, _notes): +## print(_mode) + if _mode == 'mmdvm': +## print(_server) +## print(_name) +## print(_name) +## s = mmdvmPeer.query.filter_by(server=_server).filter_by(name=_name).first() + p = mmdvmPeer.query.filter_by(server=_server).filter_by(name=_name).first() + p.enabled = _enabled + p.loose = _loose + p.ip = _ip + p.port = _port + p.master_ip = _master_ip + p.master_port = _master_port + p.passphrase = _passphrase + p.callsign = _callsign + p.radio_id = _radio_id + p.rx_freq = _rx + p.tx_freq = _tx + p.tx_power = _tx_power + p.color_code = _cc + p.latitude = _lat + p.longitude = _lon + p.height = _height + p.location = _loc + p.description = _desc + p.slots = _slots + p.url = _url + p.enable_unit = _enable_unit + p.group_hangtime = _grp_hang + p.options = _opt + p.use_acl = _use_acl + p.sub_acl = _sub_acl + p.tg1_acl = _1_acl + p.tg2_acl = _2_acl + p.notes = _notes + if _mode == 'xlx': +## print(type(_server)) +## print(type(_name)) +## print(type(_enabled)) +## print((_enable_unit)) +## print(type(_use_acl)) +#### print(_port) + + +## s = mmdvmPeer.query.filter_by(server=_server).filter_by(name=_name).first() + p = xlxPeer.query.filter_by(server=_server).filter_by(name=_name).first() + # print(type(p.enable_unit)) + p.enabled = _enabled + p.loose = _loose + p.ip = _ip + p.port = _port + p.master_ip = _master_ip + p.master_port = _master_port + p.passphrase = _passphrase + p.callsign = _callsign + p.radio_id = _radio_id + p.rx_freq = _rx + p.tx_freq = _tx + p.tx_power = _tx_power + p.color_code = _cc + p.latitude = _lat + p.longitude = _lon + p.height = _height + p.location = _loc + p.description = _desc + p.slots = _slots + p.url = _url + p.options = _opt + p.enable_unit = _enable_unit + p.xlxmodule = _xlx_mod + p.group_hangtime = _grp_hang + p.use_acl = _use_acl + p.sub_acl = _sub_acl + p.tg1_acl = _1_acl + p.tg2_acl = _2_acl + p.notes = _notes + db.session.commit() + + + + +# Test server configs + + @app.route('/manage_servers', methods=['POST', 'GET']) + @login_required + @roles_required('Admin') + def edit_server_db(): + # Edit server + if request.args.get('save_mode'):# == 'new' and request.form.get('server_name'): + _port = int(request.form.get('server_port')) + _global_ping_time = int(request.form.get('ping_time')) + _global_max_missed = int(request.form.get('max_missed')) + _ai_stale = int(request.form.get('stale_days')) + _report_interval = int(request.form.get('report_interval')) + _report_port = int(request.form.get('report_port')) + if request.form.get('use_acl') == 'True': + _global_use_acl = True + if request.form.get('aliases_enabled') == 'True': + _ai_try_download = True + if request.form.get('um_shorten_passphrase') == 'True': + _um_shorten_passphrase = True + if request.form.get('report') == 'True': + _report_enabled = True +## if request.form.get('public_list') == 'True': +## public_list = True + else: + _global_use_acl = False + _ai_try_download = False + _um_shorten_passphrase = False + _report_enabled = False +## public_list = False + + if request.args.get('save_mode') == 'new': + if request.form.get('server_name') == '': + content = '''

Server can't have blank name.

+

Redirecting in 3 seconds.

+''' + else: + server_add(request.form.get('server_name'), request.form.get('server_secret'), request.form.get('server_ip'), _port, request.form.get('global_path'), _global_ping_time, _global_max_missed, _global_use_acl, request.form.get('reg_acl'), request.form.get('sub_acl'), request.form.get('global_ts1_acl'), request.form.get('global_ts2_acl'), request.form.get('sub_file'), _ai_try_download, request.form.get('aliases_path'), request.form.get('peer_file'), request.form.get('tgid_file'), request.form.get('peer_url'), request.form.get('sub_url'), _ai_stale, _um_shorten_passphrase, request.form.get('um_burn_file'), _report_enabled, _report_interval, _report_port, request.form.get('report_clients'), request.form.get('unit_time'), request.form.get('notes')) + content = '''

Server saved.

+

Redirecting in 3 seconds.

+ ''' + if request.args.get('save_mode') == 'edit': +## print(request.args.get('server')) + server_edit(request.args.get('server'), request.form.get('server_secret'), request.form.get('server_ip'), _port, request.form.get('global_path'), _global_ping_time, _global_max_missed, _global_use_acl, request.form.get('reg_acl'), request.form.get('sub_acl'), request.form.get('global_ts1_acl'), request.form.get('global_ts2_acl'), request.form.get('sub_file'), _ai_try_download, request.form.get('aliases_path'), request.form.get('peer_file'), request.form.get('tgid_file'), request.form.get('peer_url'), request.form.get('sub_url'), _ai_stale, _um_shorten_passphrase, request.form.get('um_burn_file'), _report_enabled, _report_interval, _report_port, request.form.get('report_clients'), request.form.get('unit_time'), request.form.get('notes')) + content = '''

Server changed.

+

Redirecting in 3 seconds.

+''' + elif request.args.get('delete_server'): + server_delete(request.args.get('delete_server')) + content = '''

Server deleted.

+

Redirecting in 3 seconds.

+''' + elif request.args.get('edit_server'): + s = ServerList.query.filter_by(name=request.args.get('edit_server')).first() + + content = ''' +

 

+ +

Delete server

+ +
+

 

+

Server

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Server Name: ''' + str(s.name) + '''
 Server Secret: 
 Host (IP/DNS, for listing on passphrase page): 
 Port (for listing on passphrase page): 
 Unit Call Timeout (minutes): 
 Notes: 
+

Global

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Path: 
 Ping Time: 
 Max Missed: 
 Use ACLs: 
 Regular ACLs: 
 Subscriber ACSs: 
 Timeslot 1 ACLs: 
 Timeslot 2 ACLs: 
+

 

+

Reports

+ + + + + + + + + + + + + + + + + + + +
 Enable: 
 Interval: 
 Port: 
 Clients: 
+ +

 

+

Aliases

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Download: 
 Path: 
 Peer File: 
 Subscriber File: 
 Talkgroup ID File: 
 Peer URL: 
 Subscriber URL: 
 Stale time(days): 
+
+

 

+

User Manager

+ + + + + + + + + + + + +
 Use short passphrase:
 Burned IDs File: 
+

 

+

+

 

+''' + # Add new server + elif request.args.get('add'): # == 'yes': + content = ''' +
+

 

+

Server

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Server Name: 
 Server Secret: 
 Host (IP/DNS): 
 Port: 
 Unit Call Timeout (minutes): 
 Notes: 
+

Global

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Path: 
 Ping Time: 
 Max Missed: 
 Use ACLs: 
 Regular ACLs: 
 Subscriber ACSs: 
 Timeslot 1 ACLs: 
 Timeslot 2 ACLs: 
+

 

+

Reports

+ + + + + + + + + + + + + + + + + + + +
 Enable: 
 Interval: 
 Port: 
 Clients: 
+ +

 

+

Aliases

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Download: 
 Path: 
 Peer File: 
 Subscriber File: 
 Talkgroup ID File: 
 Peer URL: 
 Subscriber URL: 
 Stale time(days): 
+
+

 

+

User Manager

+ + + + + + + + + + + + + +
 Use short passphrase:
 Burned IDs File: 
+

 

+

+

 

+''' + else: + all_s = ServerList.query.all() + p_list = ''' +

View/Edit Servers

+ + + + + + + +
Add Server Config
+

 

+ + + + + + +''' + for s in all_s: + p_list = p_list + ''' + + + +\n +''' + p_list = p_list + '''
Name
Notes
''' + str(s.name) + '''''' + s.notes + '''
''' + content = p_list + + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + @app.route('/manage_peers', methods=['POST', 'GET']) + @login_required + @roles_required('Admin') + def test_peer_db(): + if request.args.get('save_mode'): + if request.form.get('enabled') == 'true': + peer_enabled = True +## if request.form.get('loose') == 'true': +## peer_loose = True + if request.form.get('use_acl') == 'true': + use_acl = True + if request.form.get('enable_unit') == 'True': + unit_enabled = True +## else: +## peer_loose = False + peer_enabled = False + use_acl = False + unit_enabled = False + peer_loose = True +## print(request.form.get('enable_unit')) +## print(enable_unit) + if request.form.get('name_text') == '': + content = '''

Peer can't have blank name.

+

Redirecting in 3 seconds.

+''' + else: + if request.args.get('save_mode') == 'mmdvm_peer': + peer_add('mmdvm', request.form.get('name_text'), peer_enabled, peer_loose, request.form.get('ip'), request.form.get('port'), request.form.get('master_ip'), request.form.get('master_port'), request.form.get('passphrase'), request.form.get('callsign'), request.form.get('radio_id'), request.form.get('rx'), request.form.get('tx'), request.form.get('tx_power'), request.form.get('cc'), request.form.get('lat'), request.form.get('lon'), request.form.get('height'), request.form.get('location'), request.form.get('description'), request.form.get('slots'), request.form.get('url'), request.form.get('group_hangtime'), 'MMDVM', request.form.get('options'), use_acl, request.form.get('sub_acl'), request.form.get('tgid_ts1_acl'), request.form.get('tgid_ts2_acl'), request.form.get('server'), unit_enabled, request.form.get('notes')) + content = '''

MMDVM PEER saved.

+

Redirecting in 3 seconds.

+ ''' + if request.args.get('save_mode') == 'xlx_peer': + peer_add('xlx', request.form.get('name_text'), peer_enabled, peer_loose, request.form.get('ip'), request.form.get('port'), request.form.get('master_ip'), request.form.get('master_port'), request.form.get('passphrase'), request.form.get('callsign'), request.form.get('radio_id'), request.form.get('rx'), request.form.get('tx'), request.form.get('tx_power'), request.form.get('cc'), request.form.get('lat'), request.form.get('lon'), request.form.get('height'), request.form.get('location'), request.form.get('description'), request.form.get('slots'), request.form.get('url'), request.form.get('group_hangtime'), request.form.get('xlxmodule'), request.form.get('options'), use_acl, request.form.get('sub_acl'), request.form.get('tgid_ts1_acl'), request.form.get('tgid_ts2_acl'), request.form.get('server'), unit_enabled, request.form.get('notes')) + content = '''

XLX PEER saved.

+

Redirecting in 3 seconds.

+ ''' + elif request.args.get('add') == 'mmdvm' or request.args.get('add') == 'xlx': + s = ServerList.query.all() + if request.args.get('add') == 'mmdvm': + mode = 'MMDVM' + submit_link = 'manage_peers?save_mode=mmdvm_peer' + xlx_module = '' + if request.args.get('add') == 'xlx': + xlx_module = ''' + + XLX Module: + +''' + mode = 'XLX' + submit_link = 'manage_peers?save_mode=xlx_peer' + server_options = '' + for i in s: + server_options = server_options + '''\n''' + content = ''' +

 

+

Add an ''' + mode + ''' peer

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +''' + xlx_module + ''' + + + + + + + + + + + + + + + + + + + + + + + + + +
Assign to Server: 
Connection Name: 
 Active: 
 IP: 
 Port: 
 Passphrase: 
 Master IP: 
 Master Port: 
 Callsign: 
 Radio ID: 
 Transmit Frequency: 
 Receive Frequency: 
 Transmit Power: 
 Color Code: 
 Slots: 
 Latitude: 
 Longitude: 
 Height 
 Location: 
 Description: 
 URL: 
 Group Hangtime: 
 Options: 
 Enable Unit Calls: 
 Use ACLs: 
 Subscriber ACLs: 
 Talkgroup Slot 1 ACLs: 
 Talkgroup Slot 2 ACLs: 
 Notes: 
+

 

+

+''' + +## elif request.args.get('edit_server') and request.args.get('edit_peer') and request.args.get('mode') == 'mmdvm': + elif request.args.get('delete_peer') and request.args.get('peer_server'): + peer_delete(request.args.get('mode'), request.args.get('peer_server'), request.args.get('delete_peer')) + content = '''

PEER deleted.

+

Redirecting in 3 seconds.

+''' + elif request.args.get('edit_mmdvm') == 'save' or request.args.get('edit_xlx') == 'save': + peer_enabled = False + use_acl = False + peer_loose = True + unit_enabled = False + if request.form.get('enabled') == 'true': + peer_enabled = True +## if request.form.get('loose') == 'true': +## peer_loose = True + if request.form.get('use_acl') == 'True': + use_acl = True + if request.form.get('enable_unit') == 'True': + unit_enabled = True +## else: +## peer_loose = False +## print((unit_enabled)) +## print(type(peer_enabled)) +## print(type(use_acl)) + if request.args.get('edit_mmdvm') == 'save': + peer_edit('mmdvm', request.args.get('server'), request.args.get('name'), peer_enabled, peer_loose, request.form.get('ip'), request.form.get('port'), request.form.get('master_ip'), request.form.get('master_port'), request.form.get('passphrase'), request.form.get('callsign'), request.form.get('radio_id'), request.form.get('rx'), request.form.get('tx'), request.form.get('tx_power'), request.form.get('cc'), request.form.get('lat'), request.form.get('lon'), request.form.get('height'), request.form.get('location'), request.form.get('description'), request.form.get('slots'), request.form.get('url'), request.form.get('group_hangtime'), 'MMDVM', request.form.get('options'), use_acl, request.form.get('sub_acl'), request.form.get('tgid_ts1_acl'), request.form.get('tgid_ts2_acl'), unit_enabled, request.form.get('notes')) + content = '''

MMDVM PEER changed.

+

Redirecting in 3 seconds.

+''' + if request.args.get('edit_xlx') == 'save': + peer_edit('xlx', request.args.get('server'), request.args.get('name'), peer_enabled, peer_loose, request.form.get('ip'), request.form.get('port'), request.form.get('master_ip'), request.form.get('master_port'), request.form.get('passphrase'), request.form.get('callsign'), request.form.get('radio_id'), request.form.get('rx'), request.form.get('tx'), request.form.get('tx_power'), request.form.get('cc'), request.form.get('lat'), request.form.get('lon'), request.form.get('height'), request.form.get('location'), request.form.get('description'), request.form.get('slots'), request.form.get('url'), request.form.get('group_hangtime'), request.form.get('xlxmodule'), request.form.get('options'), use_acl, request.form.get('sub_acl'), request.form.get('tgid_ts1_acl'), request.form.get('tgid_ts2_acl'), unit_enabled, request.form.get('notes')) + content = '''

XLX PEER changed.

+

Redirecting in 3 seconds.

+''' + elif request.args.get('server') and request.args.get('peer_name') and request.args.get('mode'): # and request.args.get('edit_peer') and request.args.get('mode') == 'mmdvm': + if request.args.get('mode') == 'mmdvm': + p = mmdvmPeer.query.filter_by(server=request.args.get('server')).filter_by(name=request.args.get('peer_name')).first() + xlx_module = '' + mode = "MMDVM" + form_submit = '''
''' + if request.args.get('mode') == 'xlx': + p = xlxPeer.query.filter_by(server=request.args.get('server')).filter_by(name=request.args.get('peer_name')).first() + form_submit = '''''' + xlx_module = ''' + + XLX Module: + +''' + mode = "XLX" + + content = ''' +

 

+

View/Edit an ''' + mode + ''' peer

+ +

Delete peer

+ +''' + form_submit + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +''' + xlx_module + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Connection Name:  ''' + str(p.name) + '''
 Active: 
 IP: 
 Port: 
 Passphrase: 
 Master IP: 
 Master Port: 
 Callsign: 
 Radio ID: 
 Transmit Frequency: 
 Receive Frequency: 
 Transmit Power: 
 Color Code: 
 Slots: 
 Latitude: 
 Longitude: 
 Height 
 Location: 
 Description: 
 URL: 
 Group Call Hangtime: 
 Options: 
 Enable Unit Calls: 
 Use ACLs: 
 Subscriber ACLs: 
 Talkgroup Slot 1 ACLs: 
 Talkgroup Slot 2 ACLs: 
 Notes: 
+

 

+

+ +

 

+''' + else: + all_s = ServerList.query.all() + p_list = '' + for s in all_s: + # print(s.name) + p_list = p_list + ''' +

Server: ''' + str(s.name) + '''

+ + + + + + + +\n +''' + all_p = mmdvmPeer.query.filter_by(server=s.name).all() + all_x = xlxPeer.query.filter_by(server=s.name).all() + for p in all_p: + p_list = p_list + ''' + + + + + + +''' + for x in all_x: + p_list = p_list + ''' + + + + + + +''' + p_list = p_list + '''
NameModeNotes
''' + str(p.name) + '''MMDVM''' + p.notes + '''
''' + str(x.name) + '''XLX''' + x.notes + '''
\n''' + content = ''' + +

View/Edit Peers

+ + + + + + + + +
Add MMDVM peerAdd XLX peer
+

 

+ +''' + p_list + + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + + @app.route('/manage_masters', methods=['POST', 'GET']) + @login_required + @roles_required('Admin') + def manage_masters(): + #PROXY + if request.args.get('proxy_save'): + active = False + use_acl = False + enable_unit = False + repeat = True + aprs_pos = False + enable_um = True + external_proxy = False + public = False + if request.form.get('enable_um') == 'False': + enable_um = False + if request.form.get('aprs_pos') == 'True': + aprs_pos = True + if request.form.get('enabled') == 'True': + active = True + if request.form.get('use_acl') == 'True': + use_acl = True + if request.form.get('enable_unit') == 'True': + enable_unit = True + if request.form.get('repeat') == 'False': + repeat = False + if request.form.get('external_proxy') == 'True': + external_proxy = True + if request.form.get('public_list') == 'True': + public = True + if request.args.get('proxy_save') == 'add': + if request.form.get('name_text') == '': + content = '''

PROXY can't have blank name.

+

Redirecting in 3 seconds.

+''' + else: + add_master('PROXY', request.form.get('name_text'), request.form.get('server'), aprs_pos, repeat, active, 0, request.form.get('ip'), request.form.get('external_port'), enable_um, request.form.get('passphrase'), request.form.get('group_hangtime'), use_acl, request.form.get('reg_acl'), request.form.get('sub_acl'), request.form.get('ts1_acl'), request.form.get('ts2_acl'), enable_unit, request.form.get('notes'), external_proxy, request.form.get('int_port_start'), request.form.get('int_port_stop'), '', '', '', '', public) + content = '''

PROXY saved.

+

Redirecting in 3 seconds.

+ ''' + elif request.args.get('proxy_save') == 'edit': +## print(request.args.get('name')) + edit_master('PROXY', request.args.get('name'), request.args.get('server'), aprs_pos, repeat, active, 0, request.form.get('ip'), request.form.get('external_port'), enable_um, request.form.get('passphrase'), request.form.get('group_hangtime'), use_acl, request.form.get('reg_acl'), request.form.get('sub_acl'), request.form.get('ts1_acl'), request.form.get('ts2_acl'), enable_unit, request.form.get('notes'), external_proxy, request.form.get('int_port_start'), request.form.get('int_port_stop'), '', '', '', '', public) + content = '''

PROXY changed.

+

Redirecting in 3 seconds.

+''' + elif request.args.get('proxy_save') == 'delete': + master_delete('PROXY', request.args.get('server'), request.args.get('name')) + content = '''

PROXY deleted.

+

Redirecting in 3 seconds.

+''' + # OBP + elif request.args.get('OBP_save'): + enabled = False + use_acl = False + enable_unit = False + both_slots = True + if request.form.get('enabled') == 'True': + enabled = True + if request.form.get('use_acl') == 'True': + use_acl = True + if request.form.get('enable_unit') == 'True': + enable_unit = True + if request.form.get('both_slots') == 'False': + both_slots = False + if request.args.get('OBP_save') == 'add': + if request.form.get('name_text') == '': + content = '''

OpenBridge connection can't have blank name.

+

Redirecting in 3 seconds.

+''' + else: + add_master('OBP', request.form.get('name_text'), request.form.get('server'), '', '', enabled, request.form.get('max_peers'), request.form.get('ip'), request.form.get('port'), '', request.form.get('passphrase'), request.form.get('group_hangtime'), use_acl, request.form.get('reg_acl'), request.form.get('sub_acl'), request.form.get('tg_acl'), '', enable_unit, request.form.get('notes'), '', '', '', request.form.get('network_id'), request.form.get('target_ip'), request.form.get('target_port'), both_slots, '') + content = '''

OpenBridge connection saved.

+

Redirecting in 3 seconds.

+ ''' + elif request.args.get('OBP_save') == 'edit': + edit_master('OBP', request.args.get('name'), request.args.get('server'), '', '', enabled, request.form.get('max_peers'), request.form.get('ip'), request.form.get('port'), '', request.form.get('passphrase'), request.form.get('group_hangtime'), use_acl, request.form.get('reg_acl'), request.form.get('sub_acl'), request.form.get('tg_acl'), '', enable_unit, request.form.get('notes'), '', '', '', request.form.get('network_id'), request.form.get('target_ip'), request.form.get('target_port'), both_slots, '') + content = '''

OpenBridge connection changed.

+

Redirecting in 3 seconds.

+''' + elif request.args.get('OBP_save') == 'delete': + master_delete('OBP', request.args.get('server'), request.args.get('name')) + content = '''

OpenBridge connection deleted.

+

Redirecting in 3 seconds.

+''' + # MASTER + elif request.args.get('master_save'): + aprs_pos = False + repeat = False + active = False + use_acl = False + enable_um = False + enable_unit = False + public = False + if request.form.get('aprs_pos') == 'True': + aprs_pos = True + if request.form.get('repeat') == 'True': + repeat = True + if request.form.get('enabled') == 'True': + active = True + if request.form.get('use_acl') == 'True': + use_acl = True + if request.form.get('enable_um') == 'True': + enable_um = True + if request.form.get('enable_unit') == 'True': + enable_unit = True + if request.form.get('public_list') == 'True': + public = True + if request.args.get('master_save') == 'add': + if request.form.get('name_text') == '': + content = '''

MASTER can't have blank name.

+

Redirecting in 3 seconds.

+''' + else: + add_master('MASTER', request.form.get('name_text'), request.form.get('server'), aprs_pos, repeat, active, request.form.get('max_peers'), request.form.get('ip'), request.form.get('port'), enable_um, request.form.get('passphrase'), request.form.get('group_hangtime'), use_acl, request.form.get('reg_acl'), request.form.get('sub_acl'), request.form.get('ts1_acl'), request.form.get('ts2_acl'), enable_unit, request.form.get('notes'), '', '', '', '', '', '', '', public) + content = '''

MASTER saved.

+

Redirecting in 3 seconds.

+ ''' + elif request.args.get('master_save') == 'edit': + edit_master('MASTER', request.args.get('name'), request.args.get('server'), aprs_pos, repeat, active, request.form.get('max_peers'), request.form.get('ip'), request.form.get('port'), enable_um, request.form.get('passphrase'), request.form.get('group_hangtime'), use_acl, request.form.get('reg_acl'), request.form.get('sub_acl'), request.form.get('ts1_acl'), request.form.get('ts2_acl'), enable_unit, request.form.get('notes'), '', '', '', '', '', '', '', public) + content = '''

MASTER changed.

+

Redirecting in 3 seconds.

+ ''' + elif request.args.get('master_save') == 'delete': + master_delete('MASTER', request.args.get('server'), request.args.get('name')) + content = '''

MASTER deleted.

+

Redirecting in 3 seconds.

+''' + elif request.args.get('add_OBP'): + s = ServerList.query.all() + server_options = '' + for i in s: + server_options = server_options + '''\n''' + content = ''' +

 

+ +

Add an OpenBridge Connection

+

 

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Name: 
 Assign to Server: 
 Active: 
 IP: 
 Port: 
 Passphrase: 
 Network ID: 
 Target IP: 
 Target Port: 
 Use ACLs: 
 Subscriber ACLs: 
 Talkgroup ACLs: 
 Use Both Slots: 
 Enable Unit Calls: 
 Notes: 
+

 

+

+

 

+ +''' + elif request.args.get('edit_proxy'): + # print(request.args.get('server')) + # print(request.args.get('edit_proxy')) + p = ProxyList.query.filter_by(server=request.args.get('server')).filter_by(name=request.args.get('edit_proxy')).first() + content = ''' +

 

+ +

View/Edit Proxy

+ +

Delete Proxy

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Name: ''' + str(p.name) + '''
 Active: 
 Repeat: 
 External Proxy Script: 
 Static APRS positions: 
 User Manager for login: 
 External Port: 
 Internal Port Start: 
 Internal Port Stop: 
 Passphrase: 
 Group Hangtime: 
 Use ACLs: 
 Register ACLs: 
 Subscriber ACLs: 
 Talkgroup Slot 1 ACLs: 
 Talkgroup Slot 2 ACLs: 
 Enable Unit Calls: 
 Public List: 
 Notes: 
+

 

+
+

 

+''' + + elif request.args.get('add_proxy'): + s = ServerList.query.all() + server_options = '' + for i in s: + server_options = server_options + '''\n''' + content = ''' +

 

+ +

Add a PROXY

+

 

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Assign to Server: 
 Name: 
 Active: 
 Repeat: 
 External Proxy Script: 
 Static APRS positions: 
 User Manager for login: 
 IP: 
 External Port: 
 Internal Port Start (lower than stop port): 
 Internal Port Stop: 
 Passphrase: 
 Group Hangtime: 
 Use ACLs: 
 Register ACLs: 
 Subscriber ACLs: 
 Talkgroup Slot 1 ACLs: 
 Talkgroup Slot 2 ACLs: 
 Enable Unit Calls: 
 Public List: 
 Notes: 
+

 

+

+

 

+''' + + + elif request.args.get('add_master'): + s = ServerList.query.all() + server_options = '' + for i in s: + server_options = server_options + '''\n''' + + content = ''' +

 

+

Add an MASTER

+

 

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Assign to Server: 
 Name: 
 Active: 
 Repeat: 
 Max Peers: 
 Static APRS positions: 
 User Manager for login: 
 IP: 
 PORT: 
 Passphrase: 
 Group Hangtime: 
 Use ACLs: 
 Register ACLs: 
 Subscriber ACLs: 
 Talkgroup Slot 1 ACLs: 
 Talkgroup Slot 2 ACLs: 
 Enable Unit Calls: 
 Public List: 
 Notes: 
+

 

+

+

 

+''' + elif request.args.get('edit_OBP'): +## print(request.args.get('server')) +## print(request.args.get('edit_OBP')) +## s = ServerList.query.all() + o = OBP.query.filter_by(server=request.args.get('server')).filter_by(name=request.args.get('edit_OBP')).first() +## print(o.notes) + content = ''' +

 

+

View/Edit OpenBridge Connection

+

Delete OpenBridge Connection

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Name: ''' + str(o.name) + '''
 Active: 
 IP: 
 Port: 
 Passphrase: 
 Network ID: 
 Target IP: 
 Target Port: 
 Use ACLs: 
 Subscriber ACLs: 
 Talkgroup ACLs: 
 Use Both Slots: 
 Enable Unit Calls: 
 Notes: 
+

 

+

+
+

 

+ + ''' + + elif request.args.get('edit_master'): +## s = ServerList.query.all() + m = MasterList.query.filter_by(server=request.args.get('server')).filter_by(name=request.args.get('edit_master')).first() + + content = ''' +

 

+

View/Edit a MASTER

+

Delete MASTER

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Name: ''' + str(m.name) + '''
 Active: 
 Repeat: 
 Max Peers: 
 Static APRS positions: 
 User Manager for login: 
 IP: 
 PORT: 
 Passphrase: 
 Group Hangtime: 
 Use ACLs: 
 Register ACLs: 
 Subscriber ACLs: 
 Talkgroup Slot 1 ACLs: 
 Talkgroup Slot 2 ACLs: 
 Enable Unit Calls: 
 Public List: 
 Notes: 
+

 

+

+

 

+''' +## elif not request.args.get('edit_master') and not request.args.get('edit_OBP') and not request.args.get('add_OBP') and not request.args.get('add_master'): +## content = 'jglkdjklsd' + else: + #elif not request.args.get('add_proxy') or not request.args.get('add_OBP') or not request.args.get('add_master'): # or not request.args.get('proxy_save') or not request.args.get('master_save') or not request.args.get('OBP_save'): + all_s = ServerList.query.all() + m_list = '' + for s in all_s: +## print(s.name) + m_list = m_list + ''' +

Server: ''' + str(s.name) + '''

+ + + + + + + + +''' + all_m = MasterList.query.filter_by(server=s.name).all() + all_p = ProxyList.query.filter_by(server=s.name).all() + all_o = OBP.query.filter_by(server=s.name).all() + for o in all_o: + m_list = m_list + ''' + + + + + + +''' + for p in all_p: + m_list = m_list + ''' + + + + + + +''' + for x in all_m: + m_list = m_list + ''' + + + + + + + +''' + m_list = m_list + '''
NameModeNotes
''' + str(o.name) + '''OpenBridge''' + str(o.notes) + '''
''' + str(p.name) + '''PROXY''' + str(p.notes) + '''
''' + str(x.name) + '''MASTER''' + str(x.notes) + '''
\n''' + content = ''' + +

View/Edit Masters

+ + + + + + + + + + +
Add MASTERAdd PROXYAdd OpenBridge
+

 

+ +''' + m_list + + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + + @app.route('/add_user', methods=['POST', 'GET']) + @login_required + @roles_required('Admin') + def add_admin(): + if request.method == 'GET': + content = ''' +
+ + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+ + + +

 

+''' + elif request.method == 'POST' and request.form.get('username'): + if not User.query.filter(User.username == request.form.get('username')).first(): + radioid_data = ast.literal_eval(get_ids(request.form.get('username'))) + user = User( + username=request.form.get('username'), + email=request.form.get('email'), + email_confirmed_at=datetime.datetime.utcnow(), + password=user_manager.hash_password(request.form.get('password')), + dmr_ids = str(radioid_data[0]), + initial_admin_approved = True, + first_name = str(radioid_data[1]), + last_name = str(radioid_data[2]), + city = str(radioid_data[3]) + + ) + + db.session.add(user) + u = User.query.filter_by(username=request.form.get('username')).first() + user_role = UserRoles( + user_id=u.id, + role_id=2, + ) + db.session.add(user_role) + db.session.commit() + content = '''

Created user: ''' + str(request.form.get('username')) + '''

\n''' + elif User.query.filter(User.username == request.form.get('username')).first(): + content = 'Existing user: ' + str(request.form.get('username') + '. New user not created.') + + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + @app.route('/manage_rules', methods=['POST', 'GET']) + @login_required + @roles_required('Admin') + def manage_rules(): + + if request.args.get('save_bridge') == 'save': + public = False + if request.form.get('public_list') == 'True': + public = True + if request.form.get('bridge_name') == '': + content = '''

Bridge can't have blank name.

+

Redirecting in 3 seconds.

+''' + else: + bridge_add(request.form.get('bridge_name'), request.form.get('description'), public, request.form.get('tg')) + content = '''

Bridge (talkgroup) saved.

+

Redirecting in 3 seconds.

+ ''' + elif request.args.get('save_bridge') == 'edit': + public = False + if request.form.get('public_list') == 'True': + public = True + update_bridge_list(request.args.get('bridge'), request.form.get('description'), public, request.form.get('bridge_name'), request.form.get('tg')) + content = '''

Bridge (talkgroup) changed.

+

Redirecting in 3 seconds.

+ ''' + elif request.args.get('save_bridge') == 'delete': + bridge_delete(request.args.get('bridge')) + content = '''

Bridge (talkgroup) deleted.

+

Redirecting in 3 seconds.

+ ''' + + + #Rules + elif request.args.get('save_rule'): + public_list = False + active = False + if request.form.get('active_dropdown') == 'True': + active = True + if request.args.get('save_rule') == 'new': + add_system_rule(request.form.get('bridge_dropdown'), request.form.get('system_text'), request.form.get('ts_dropdown'), request.form.get('tgid'), active, request.form.get('timer_time'), request.form.get('type_dropdown'), request.form.get('on'), request.form.get('off'), request.form.get('reset'), request.args.get('server'), public_list) + content = '''

Bridge (talkgroup) rule saved.

+

Redirecting in 3 seconds.

+ ''' + elif request.args.get('save_rule') == 'edit': + content = '''

Bridge (talkgroup) rule changed.

+

Redirecting in 3 seconds.

+ ''' + elif request.args.get('save_rule') == 'delete': + # print(request.args.get('bridge')) + # print(request.args.get('server')) + if request.args.get('system'): + delete_system_rule(request.args.get('bridge'), request.args.get('server'), request.args.get('system')) + else: + delete_system_bridge(request.args.get('bridge'), request.args.get('server')) + +## delete_system_rule(request.args.get('bridge'), request.args.get('server'), request.args.get('system')) + content = '''

System rule deleted.

+

Redirecting in 3 seconds.

+ ''' + + elif request.args.get('add_rule'): +## svl = ServerList.query.all() + bl = BridgeList.query.all() #filter(bridge_name== request.form.get('username')).all() + all_o = OBP.query.filter_by(server=request.args.get('add_rule')).all() + all_m = MasterList.query.filter_by(server=request.args.get('add_rule')).all() + all_p = ProxyList.query.filter_by(server=request.args.get('add_rule')).all() + m_l = mmdvmPeer.query.filter_by(server=request.args.get('add_rule')).all() + x_l = xlxPeer.query.filter_by(server=request.args.get('add_rule')).all() +## print(sl) +## print(bl) +## svl_option = '' + bl_option = '' + sl_option = '' + for i in all_o: + sl_option = sl_option + '''''' + for i in all_m: + sl_option = sl_option + '''''' + for i in all_p: + sl_option = sl_option + '''''' + for i in m_l: + sl_option = sl_option + '''''' + for i in x_l: + sl_option = sl_option + '''''' + for i in bl: + bl_option = bl_option + '''''' + content = ''' +

Add rule to server: ''' + request.args.get('add_rule') + '''

+ +
+

 

+ + + + + + + + + + + + + + + + + +
Bridge (Talkgroup): System: Timeslot: Talkgroup number: Activate on start:  
Timer Time (minutes):  Timer Type:  Trigger ON TGs:   Trigger OFF TGs:  Trigger Reset TGs:  
+

 

+

+
+

 

+ +''' + elif request.args.get('edit_rule') and request.args.get('bridge'): + br = BridgeRules.query.filter_by(server=request.args.get('edit_rule')).filter_by(bridge_name=request.args.get('bridge')).all() + print(br) + br_view = '''

Rules for bridge ''' + request.args.get('bridge') + ''' on server ''' + request.args.get('edit_rule') + '''.

''' + for i in br: + br_view = br_view + ''' + + + +  + + + + + +
Delete SYSTEM Rule
+

 

+ + + + + + + + + + + + + + + + + +
Bridge (Talkgroup): ''' + str(i.bridge_name) + '''System: ''' + str(i.system_name) + '''Timeslot: Talkgroup number: Activate on start:  
Timer Time (minutes):  Timer Type:  Trigger ON TGs:   Trigger OFF TGs:  Trigger Reset TGs:  
+

 

+

+
+

 

+
+

 

+ +''' + content = br_view + + elif request.args.get('edit_rule') == 'save' and request.args.get('bridge_edit'): + public_list = False + active = False + if request.form.get('active_dropdown') == 'True': + active = True + edit_system_rule(request.args.get('bridge_edit'), request.args.get('system'), request.form.get('ts_dropdown'), request.form.get('tgid'), active, request.form.get('timer_time'), request.form.get('type_dropdown'), request.form.get('on'), request.form.get('off'), request.form.get('reset'), request.args.get('server'), public_list) + content = '''

System rule changed.

+

Redirecting in 3 seconds.

+ ''' + + elif request.args.get('add_bridge'): + s = ServerList.query.all() +## server_options = '' +## for i in s: +## server_options = server_options + '''\n''' + + content = ''' +

 

+

Add a Talk Group

+
+ + + + + + + + + + + + + + + + + + + + +

 

+

 

+


 

+
+

 

+ +

+
+
+''' + elif request.args.get('edit_bridge'): + b = BridgeList.query.filter_by(bridge_name=request.args.get('edit_bridge')).first() +## s = ServerList.query.all() +## server_options = '' +## for i in s: +## server_options = server_options + '''\n''' + + content = ''' +

 

+

Edit a Talk Group

+

Delete Talk Group

+

 

+ +
+ + + + + + + + + + + + + + + + + + + + +

 

+

 

+


 

+
+

 

+ +

+
+
+''' + else: + all_b = BridgeList.query.all() + s = ServerList.query.all() + b_list = ''' +

View/Edit Bridges (Talk Groups)

+ + + + + + + + +
Add Bridge
+

 

+ + + + + + + + + + +''' + for i in all_b: + b_list = b_list + ''' + + + + + + +''' + b_list = b_list + '''
NamePublicDescriptionTGID
''' + str(i.bridge_name) + ''' +''' + str(i.public_list) + '''''' + str(i.description) + '''''' + str(i.tg) + '''
+

View/Edit Rules

+ +''' + r_list = '' + for i in s: + # print(i) + r_list = r_list + ''' + + + + + + +
Add a rule to server: ''' + str(i.name) + '''
+ + + + + + +''' + br = BridgeRules.query.filter_by(server=i.name).all() + temp_list = [] + for x in br: #.filter_by(bridge_name=request.args.get('bridge')).all() + if x.bridge_name in temp_list: + pass + else: + temp_list.append(x.bridge_name) + r_list = r_list + ''' + + + + + +''' + r_list = r_list + '''
Bridge Name--
''' + str(x.bridge_name) + '''Edit Bridge RulesDelete Bridge from this server

 

''' + content = b_list + r_list + '''''' + + return render_template('flask_user_layout.html', markup_content = Markup(content)) + + @app.route('/svr', methods=['POST']) + def auth(): + hblink_req = request.json + # print((hblink_req)) + if hblink_req['secret'] in shared_secrets(): + if 'login_id' in hblink_req and 'login_confirmed' not in hblink_req: + if type(hblink_req['login_id']) == int: + if authorized_peer(hblink_req['login_id'])[0]: + print(active_tgs) + if isinstance(authorized_peer(hblink_req['login_id'])[1], int) == True: + authlog_add(hblink_req['login_id'], hblink_req['login_ip'], hblink_req['login_server'], authorized_peer(hblink_req['login_id'])[2], gen_passphrase(hblink_req['login_id']), 'Attempt') +## active_tgs[hblink_req['login_server']][hblink_req['system']] = [{'1':[]}, {'2':[]}, {'SYSTEM': ''}, {'peer_id':hblink_req['login_id']}] + response = jsonify( + allow=True, + mode='normal', + ) + elif authorized_peer(hblink_req['login_id'])[1] == '': + authlog_add(hblink_req['login_id'], hblink_req['login_ip'], hblink_req['login_server'], authorized_peer(hblink_req['login_id'])[2], 'Config Passphrase: ' + legacy_passphrase, 'Attempt') +## active_tgs[hblink_req['login_server']][hblink_req['system']] = [{'1':[]}, {'2':[]}, {'SYSTEM': ''}, {'peer_id':hblink_req['login_id']}] + response = jsonify( + allow=True, + mode='legacy', + ) + elif authorized_peer(hblink_req['login_id'])[1] != '' or isinstance(authorized_peer(hblink_req['login_id'])[1], int) == False: + authlog_add(hblink_req['login_id'], hblink_req['login_ip'], hblink_req['login_server'], authorized_peer(hblink_req['login_id'])[2], authorized_peer(hblink_req['login_id'])[1], 'Attempt') +## active_tgs[hblink_req['login_server']][hblink_req['system']] = [{'1':[]}, {'2':[]}, {'SYSTEM': ''}, {'peer_id':hblink_req['login_id']}] + # print(authorized_peer(hblink_req['login_id'])) + response = jsonify( + allow=True, + mode='override', + value=authorized_peer(hblink_req['login_id'])[1] + ) + try: + active_tgs[hblink_req['login_server']][hblink_req['system']] = [{'1':[]}, {'2':[]}, {'SYSTEM': ''}, {'peer_id':hblink_req['login_id']}] +## print('Restart ' + hblink_req['login_server'] + ' please.') + except: +## active_tgs[hblink_req['login_server']] = {} + pass + elif authorized_peer(hblink_req['login_id'])[0] == False: +## print('log fail') + authlog_add(hblink_req['login_id'], hblink_req['login_ip'], hblink_req['login_server'], 'Not Registered', '-', 'Failed') + response = jsonify( + allow=False) + elif not type(hblink_req['login_id']) == int: + user = hblink_req['login_id'] + u = User.query.filter_by(username=user).first() + + if not u: + msg = jsonify(auth=False, + reason='User not found') + response = make_response(msg, 401) + if u: + u_role = UserRoles.query.filter_by(user_id=u.id).first() + password = user_manager.verify_password(hblink_req['password'], u.password) + if u_role.role_id == 2: + role = 'user' + if u_role.role_id == 1: + role = 'admin' + if password: + response = jsonify(auth=True, role=role) + else: + msg = jsonify(auth=False, + reason='Incorrect password') + response = make_response(msg, 401) + elif 'login_id' in hblink_req and 'login_confirmed' in hblink_req: + if hblink_req['old_auth'] == True: + authlog_add(hblink_req['login_id'], hblink_req['login_ip'], hblink_req['login_server'], authorized_peer(hblink_req['login_id'])[2], 'CONFIG, NO UMS', 'Confirmed') + else: + authlog_add(hblink_req['login_id'], hblink_req['login_ip'], hblink_req['login_server'], authorized_peer(hblink_req['login_id'])[2], 'USER MANAGER', 'Confirmed') + response = jsonify( + logged=True + ) + elif 'burn_list' in hblink_req: # ['burn_list']: # == 'burn_list': + response = jsonify( + burn_list=get_burnlist() + ) + + elif 'get_config' in hblink_req: + if hblink_req['get_config']: + active_tgs[hblink_req['get_config']] = {} + print(active_tgs) + ## try: +## print(get_peer_configs(hblink_req['get_config'])) + response = jsonify( + config=server_get(hblink_req['get_config']), + peers=get_peer_configs(hblink_req['get_config']), + masters=masters_get(hblink_req['get_config']), + ## OBP=get_OBP(hblink_req['get_config']) + + ) + ## except: + ## message = jsonify(message='Config error') + ## response = make_response(message, 401) + elif 'get_rules' in hblink_req: + if hblink_req['get_rules']: # == 'burn_list': + + ## try: + response = jsonify( + rules=generate_rules(hblink_req['get_rules']), + ## OBP=get_OBP(hblink_req['get_config']) + + ) + ## except: + ## message = jsonify(message='Config error') + ## response = make_response(message, 401) + elif 'update_tg' in hblink_req: + if hblink_req['update_tg']: + print(hblink_req) +## print(hblink_req['data'][0]['SYSTEM']) + if 'on' == hblink_req['mode']: +## try: + if hblink_req['dmr_id'] == 0: + print('id 0') +## print(active_tgs) + for system in active_tgs[hblink_req['update_tg']].items(): + ## print(system) + ## print('sys') + if system[0] == hblink_req['data'][0]['SYSTEM']: + print(active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][0]['1']) +## print(hblink_req['data'][2]['tg']) + print('---------') + print(active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][1]['2']) + ## print(hblink_req['data'][1]['ts']) + if hblink_req['data'][1]['ts'] == 1: + #### print(active_tgs[hblink_req['update_tg']][system[0]][0]['1']) + + if active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][0]['1'] == hblink_req['data'][2]['tg']: + pass + else: + active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][0]['1'].append(hblink_req['data'][2]['tg']) + #### active_tgs[hblink_req['update_tg']][system[0]][0]['1'].append(0) + if hblink_req['data'][1]['ts'] == 2: + if active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][1]['2'] == hblink_req['data'][2]['tg']: + pass + #### print(active_tgs[hblink_req['update_tg']][system[0]][1]['2']) + else: + active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][1]['2'].append(hblink_req['data'][2]['tg']) + else: + try: + print('---------on------------') + print(hblink_req['data']) + print(active_tgs[hblink_req['update_tg']]) + print(hblink_req['data'][2]['ts2']) + print('-----------------------') + ## active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][2]['SYSTEM'] = hblink_req['data'][0]['SYSTEM'] + #### active_tgs[hblink_req['update_tg']][hblink_req['dmr_id']].update({hblink_req['data'][0]['SYSTEM']: [{1:[hblink_req['data'][1]['ts1']]}, {2:[hblink_req['data'][2]['ts2']]}]}) #.update({[hblink_req['dmr_id']]:hblink_req['data']}) + if hblink_req['data'][1]['ts1'] not in active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][0]['1']: + active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][0]['1'].append(hblink_req['data'][1]['ts1']) + active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][2]['SYSTEM'] = hblink_req['data'][0]['SYSTEM'] + if hblink_req['data'][2]['ts2'] not in active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][1]['2']: + print('---0---') + print(hblink_req['data'][0]['SYSTEM']) + active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][2]['SYSTEM'] = hblink_req['data'][0]['SYSTEM'] + active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][1]['2'].append(hblink_req['data'][2]['ts2']) +## print('append') + #### active_tgs[hblink_req['update_tg']][system[0]][1]['2'].append(0) + ## print(hblink_req['data'][0]['SYSTEM']) + + ## print(active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']]) + ## print(active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][2]['2']) + ## print(hblink_req['data'][1]['ts2']) + ## print(active_tgs[hblink_req['update_tg']]) + except: +## active_tgs[hblink_req['update_tg']] = {} + pass + +## except: +## pass + + + elif 'off' == hblink_req['mode']: + print('off') + for system in active_tgs[hblink_req['update_tg']].items(): + print(system) + if system[0] == hblink_req['data'][0]['SYSTEM']: + print('yes it is') +#### print(system[0]) +#### print(active_tgs[hblink_req['update_tg']][system[0]]) + if hblink_req['data'][1]['ts'] == 1: +#### print(active_tgs[hblink_req['update_tg']][system[0]][0]['1']) + active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][0]['1'].remove(hblink_req['data'][2]['tg']) +#### active_tgs[hblink_req['update_tg']][system[0]][0]['1'].append(0) + if hblink_req['data'][1]['ts'] == 2: +#### print(active_tgs[hblink_req['update_tg']][system[0]][1]['2']) + active_tgs[hblink_req['update_tg']][hblink_req['data'][0]['SYSTEM']][1]['2'].remove(hblink_req['data'][2]['tg']) +#### active_tgs[hblink_req['update_tg']][system[0]][1]['2'].append(0) + + + +## print() +## print(system) +## print(system[1][2]['SYSTEM']) +## print('off') +## print(hblink_req['data'][1]['ts']) +## print(hblink_req['data'][2]['tg']) + print(active_tgs) + response = 'got it' + else: + message = jsonify(message='Authentication error') + response = make_response(message, 401) + return response + + + + return app + + +if __name__ == '__main__': + app = create_app() + app.run(debug = True, port=hws_port, host=hws_host) diff --git a/web/config-SAMPLE.py b/web/config-SAMPLE.py new file mode 100644 index 0000000..db58108 --- /dev/null +++ b/web/config-SAMPLE.py @@ -0,0 +1,87 @@ + +''' +Settings for HBNet Web Server. +''' +# Database options +# Using SQLite is simple and easiest. Comment out this line and uncomment the MySQL +# line to use a MySQL/MariaDB server. +db_location = 'sqlite:///hbnet.sqlite' + +# Uncomment and change this line to use a MySQL DB. It is best to start with a fresh +# DB without data in it. + +#db_location = 'mysql+pymysql://DB_USERNAME:DB_PASSWORD@DB_HOST:MySQL_PORT/DB_NAME' + + +# Title of the HBNet Web Server +title = 'HBNet DMR server' +# Port to run server +hws_port = 8080 +# IP to run server on +hws_host = '127.0.0.1' +# Publicly accessible URL of the web server. THIS IS REQUIRED AND MUST BE CORRECT. +url = 'http://localhost:8080' +# Replace below with some random string such as an SHA256 +secret_key = 'SUPER SECRET LONG KEY' + +# Default state for newly created user accounts. Setting to False will require +# the approval of an admin user before the user can login. +default_account_state = True + +# Legacy passphrase used in hblink.cfg +#legacy_passphrase = 'passw0rd' + + +# Passphrase calculation config. If REMOTE_CONFIG is not used in your DMR server config +# (hblink.cfg), then the values in section [USER_MANAGER] MUST match the values below. +# If REMOTE_CONFIG is enabled, the DMR server (hblink) will automatically use the values below. +# These config options affect the generation of user passphrases. + +# Set to a value between 1 - 99. This value is used in the normal calculation. +append_int = 1 + +# Set to a value between 1 - 99. This value is used for compromised passphrases. +burn_int = 5 + +# Set to a value between 1 - 99 This value is used in the normal calculation. +extra_int_1 = 5 + +# Set to a value between 1 - 99 This value is used in the normal calculation. +extra_int_2 = 8 + +# Set to a length of about 10 characters. +extra_1 = 'TeSt' +extra_2 = 'DmR4' + +# Shorten generated passphrases +use_short_passphrase = True + +# Character length of shortened passphrase +shorten_length = 6 +# How often to pick character from long passphrase when shortening. +shorten_sample = 4 + +# Email settings +MAIL_SERVER = 'smtp.gmail.com' +MAIL_PORT = 465 +MAIL_USE_SSL = True +MAIL_USE_TLS = False +MAIL_USERNAME = 'app@gmail.com' +MAIL_PASSWORD = 'password' +MAIL_DEFAULT_SENDER = '"' + title + '" ' + +# User settings settings +USER_ENABLE_EMAIL = True +USER_ENABLE_USERNAME = True +USER_REQUIRE_RETYPE_PASSWORD = True +USER_ENABLE_CHANGE_USERNAME = False +USER_ENABLE_MULTIPLE_EMAILS = True +USER_ENABLE_CONFIRM_EMAIL = True +USER_ENABLE_REGISTER = True +USER_AUTO_LOGIN_AFTER_CONFIRM = False +USER_SHOW_USERNAME_DOES_NOT_EXIST = True + + +# Time format for display on some pages +time_format = '%H:%M:%S - %m/%d/%y' + diff --git a/web/gen_script_template-SAMPLE.py b/web/gen_script_template-SAMPLE.py new file mode 100644 index 0000000..459718e --- /dev/null +++ b/web/gen_script_template-SAMPLE.py @@ -0,0 +1,5 @@ +def gen_script(dmr_id, passphrase): + script = ''' +DMR ID: ''' + str(dmr_id) + ''' \n Passphrase: ''' + str(passphrase) + ''' +''' + return script diff --git a/web/static/HBnet.png b/web/static/HBnet.png new file mode 100644 index 0000000000000000000000000000000000000000..c670424c7079707a72ad8edfcd384033d8eb2c03 GIT binary patch literal 64462 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 GwEqJiqOsQi literal 0 HcmV?d00001 diff --git a/web/templates/flask_user/edit_user_profile.html b/web/templates/flask_user/edit_user_profile.html new file mode 100644 index 0000000..ad30a56 --- /dev/null +++ b/web/templates/flask_user/edit_user_profile.html @@ -0,0 +1,30 @@ +{% extends 'flask_user/_authorized_base.html' %} + +{% block content %} +{% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %} +

{%trans%}User profile{%endtrans%}

+ +
+ {{ form.hidden_tag() }} + {% for field in form %} + {% if not field.flags.hidden %} + {% if field.type=='SubmitField' %} + {{ render_submit_field(field, tabindex=loop.index*10) }} + {% else %} + {{ render_field(field, tabindex=loop.index*10) }} + {% endif %} + {% endif %} + {% endfor %} +
+
+

Update your information from RadioID.net

+{% if not user_manager.USER_ENABLE_AUTH0 %} + {% if user_manager.USER_ENABLE_CHANGE_USERNAME %} +

{%trans%}Change username{%endtrans%}

+ {% endif %} + {% if user_manager.USER_ENABLE_CHANGE_PASSWORD %} +

{%trans%}Change password{%endtrans%}

+ {% endif %} +{% endif %} + +{% endblock %} diff --git a/web/templates/flask_user/emails/base_message.html b/web/templates/flask_user/emails/base_message.html new file mode 100644 index 0000000..5a4c0d7 --- /dev/null +++ b/web/templates/flask_user/emails/base_message.html @@ -0,0 +1,8 @@ +

Dear {{ user.email }} - {{ user.username }},

+ +{% block message %} +{% endblock %} + +

Sincerely,
+{{ app_name }} +

diff --git a/web/templates/flask_user/login.html b/web/templates/flask_user/login.html new file mode 100644 index 0000000..3dcab9e --- /dev/null +++ b/web/templates/flask_user/login.html @@ -0,0 +1,69 @@ +{% extends 'flask_user/_public_base.html' %} + +{% block content %} +{% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %} +

{%trans%}Sign in{%endtrans%}

+

 

+ +Your username MUST be your callsign or email address. +

 

+ +
+ {{ form.hidden_tag() }} + + {# Username or Email field #} + {% set field = form.username if user_manager.USER_ENABLE_USERNAME else form.email %} +
+ {# Label on left, "New here? Register." on right #} +
+
+ +
+
+ {% if user_manager.USER_ENABLE_REGISTER and not user_manager.USER_REQUIRE_INVITATION %} + + {%trans%}New here? Register.{%endtrans%} + {% endif %} +
+
+ {{ field(class_='form-control', tabindex=110) }} + {% if field.errors %} + {% for e in field.errors %} +

{{ e }}

+ {% endfor %} + {% endif %} +
+ + {# Password field #} + {% set field = form.password %} +
+ {# Label on left, "Forgot your Password?" on right #} +
+
+ +
+
+ {% if user_manager.USER_ENABLE_FORGOT_PASSWORD %} + + {%trans%}Forgot your Password?{%endtrans%} + {% endif %} +
+
+ {{ field(class_='form-control', tabindex=120) }} + {% if field.errors %} + {% for e in field.errors %} +

{{ e }}

+ {% endfor %} + {% endif %} +
+ + {# Remember me #} + {% if user_manager.USER_ENABLE_REMEMBER_ME %} + {{ render_checkbox_field(login_form.remember_me, tabindex=130) }} + {% endif %} + + {# Submit button #} + {{ render_submit_field(form.submit, tabindex=180) }} +
+ +{% endblock %} diff --git a/web/templates/flask_user/register.html b/web/templates/flask_user/register.html new file mode 100644 index 0000000..f938afb --- /dev/null +++ b/web/templates/flask_user/register.html @@ -0,0 +1,50 @@ +{% extends 'flask_user/_public_base.html' %} + +{% block content %} +{% from "flask_user/_macros.html" import render_field, render_submit_field %} +

{%trans%}Register{%endtrans%}

+

 

+ +Your username MUST be your callsign. After filling out the fields, a confirmation link will be emailed to you. +

 

+ +
+ {{ form.hidden_tag() }} + + {# Username or Email #} + {% set field = form.username if user_manager.USER_ENABLE_USERNAME else form.email %} +
+ {# Label on left, "Already registered? Sign in." on right #} +
+
+ +
+
+ {% if user_manager.USER_ENABLE_REGISTER %} + + {%trans%}Already registered? Sign in.{%endtrans%} + {% endif %} +
+
+ {{ field(class_='form-control', tabindex=210) }} + {% if field.errors %} + {% for e in field.errors %} +

{{ e }}

+ {% endfor %} + {% endif %} +
+ + {% if user_manager.USER_ENABLE_EMAIL and user_manager.USER_ENABLE_USERNAME %} + {{ render_field(form.email, tabindex=220) }} + {% endif %} + + {{ render_field(form.password, tabindex=230) }} + + {% if user_manager.USER_REQUIRE_RETYPE_PASSWORD %} + {{ render_field(form.retype_password, tabindex=240) }} + {% endif %} + + {{ render_submit_field(form.submit, tabindex=280) }} +
+ +{% endblock %} diff --git a/web/templates/flask_user_layout.html b/web/templates/flask_user_layout.html new file mode 100644 index 0000000..9d7a848 --- /dev/null +++ b/web/templates/flask_user_layout.html @@ -0,0 +1,144 @@ + + + + + + + {{ user_manager.USER_APP_NAME }} + + + + + + + + + + + + {# *** Allow sub-templates to insert extra html to the head section *** #} + {% block extra_css %}{% endblock %} + + + + +

{{ user_manager.USER_APP_NAME }}

+

Logo

+

{{title}}

+
+ + + + + + {% if not call_or_get(current_user.is_authenticated) %} + + + {% endif %} + {% if call_or_get(current_user.is_authenticated) %} + {% if call_or_get(current_user.has_roles('Admin')) %} + + + + + + {% endif %} + + + + + + {% endif %} + + +
HomeRegisterSign inAdd a UserEdit UsersWaiting ApprovalAuth LogHelpView Passphrase(s)Current TGsEdit {{ current_user.username or current_user.email }}Sign out
+ +{% if call_or_get(current_user.is_authenticated) %} + {% if call_or_get(current_user.has_roles('Admin')) %} + + + + + + + + + +
Manage ServersManage PeersManage MastersManage Rules
+ {% endif %} + {% endif %} + +
+ {% block body %} + +
+ +
+ {# One-time system messages called Flash messages #} + {% block flash_messages %} + {%- with messages = get_flashed_messages(with_categories=true) -%} + {% if messages %} + {% for category, message in messages %} + {% if category=='error' %} + {% set category='danger' %} + {% endif %} +
{{ message|safe }}
+ {% endfor %} + {% endif %} + {%- endwith %} + {% endblock %} + {% block main %} + {% block content %} + + {{markup_content}} + + + {% endblock %} + {% endblock %} +
+ +
+
+ + {% endblock %} + + + + + + + {# *** Allow sub-templates to insert extra html to the bottom of the body *** #} + {% block extra_js %}{% endblock %} + + + diff --git a/web/templates/help.html b/web/templates/help.html new file mode 100644 index 0000000..3a063f2 --- /dev/null +++ b/web/templates/help.html @@ -0,0 +1,4 @@ +{% extends 'flask_user/_public_base.html' %} +{% block content %} +This is a help page.

+{% endblock %} diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..8cb812a --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,4 @@ +{% extends 'flask_user/_public_base.html' %} +{% block content %} +

Welcome to the {{ user_manager.USER_APP_NAME }}. This tool is used to manage your access.

+{% endblock %} diff --git a/web/templates/view_passphrase.html b/web/templates/view_passphrase.html new file mode 100644 index 0000000..aaf231e --- /dev/null +++ b/web/templates/view_passphrase.html @@ -0,0 +1,34 @@ +{% extends 'flask_user/_public_base.html' %} +{% block content %} +

 

Click here for automated Pi-Star script.

 

+ + + + + + + + +
+ + + + + + + + + + + + + + + + +
Name:My Server
Host/IP:127.0.0.1
Port:62030
+ +
{{markup_content}} +
+

 

+{% endblock %}