Initial import
This commit is contained in:
commit
09f0b0672d
64 changed files with 3735 additions and 0 deletions
384
app/portal/__init__.py
Normal file
384
app/portal/__init__.py
Normal file
|
@ -0,0 +1,384 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from flask import Blueprint, render_template, Response, flash, redirect, url_for, request
|
||||
from sqlalchemy import exc, desc, or_
|
||||
|
||||
from app.extensions import db
|
||||
from app.models import Group, Origin, Proxy, Alarm, BridgeConf, Bridge, MirrorList, AbstractResource
|
||||
from app.portal.forms import EditGroupForm, NewGroupForm, NewOriginForm, EditOriginForm, LifecycleForm, \
|
||||
NewBridgeConfForm, EditBridgeConfForm, NewMirrorListForm
|
||||
|
||||
portal = Blueprint("portal", __name__, template_folder="templates", static_folder="static")
|
||||
|
||||
|
||||
@portal.app_template_filter("mirror_expiry")
|
||||
def calculate_mirror_expiry(s):
|
||||
expiry = s + timedelta(days=3)
|
||||
countdown = expiry - datetime.utcnow()
|
||||
if countdown.days == 0:
|
||||
return f"{countdown.seconds // 3600} hours"
|
||||
return f"{countdown.days} days"
|
||||
|
||||
|
||||
@portal.route("/")
|
||||
def portal_home():
|
||||
return render_template("home.html.j2", section="home")
|
||||
|
||||
|
||||
@portal.route("/groups")
|
||||
def view_groups():
|
||||
groups = Group.query.order_by(Group.group_name).all()
|
||||
return render_template("groups.html.j2", section="group", groups=groups)
|
||||
|
||||
|
||||
@portal.route("/group/new", methods=['GET', 'POST'])
|
||||
def new_group():
|
||||
form = NewGroupForm()
|
||||
if form.validate_on_submit():
|
||||
group = Group()
|
||||
group.group_name = form.group_name.data
|
||||
group.description = form.description.data
|
||||
group.eotk = form.eotk.data
|
||||
group.created = datetime.utcnow()
|
||||
group.updated = datetime.utcnow()
|
||||
try:
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
flash(f"Created new group {group.group_name}.", "success")
|
||||
return redirect(url_for("portal.edit_group", group_id=group.id))
|
||||
except exc.SQLAlchemyError as e:
|
||||
print(e)
|
||||
flash("Failed to create new group.", "danger")
|
||||
return redirect(url_for("portal.view_groups"))
|
||||
return render_template("new.html.j2", section="group", form=form)
|
||||
|
||||
|
||||
@portal.route('/group/edit/<group_id>', methods=['GET', 'POST'])
|
||||
def edit_group(group_id):
|
||||
group = Group.query.filter(Group.id == group_id).first()
|
||||
if group is None:
|
||||
return Response(render_template("error.html.j2",
|
||||
section="group",
|
||||
header="404 Group Not Found",
|
||||
message="The requested group could not be found."),
|
||||
status=404)
|
||||
form = EditGroupForm(description=group.description,
|
||||
eotk=group.eotk)
|
||||
if form.validate_on_submit():
|
||||
group.description = form.description.data
|
||||
group.eotk = form.eotk.data
|
||||
group.updated = datetime.utcnow()
|
||||
try:
|
||||
db.session.commit()
|
||||
flash("Saved changes to group.", "success")
|
||||
except exc.SQLAlchemyError:
|
||||
flash("An error occurred saving the changes to the group.", "danger")
|
||||
return render_template("group.html.j2",
|
||||
section="group",
|
||||
group=group, form=form)
|
||||
|
||||
|
||||
@portal.route("/origin/new", methods=['GET', 'POST'])
|
||||
@portal.route("/origin/new/<group_id>", methods=['GET', 'POST'])
|
||||
def new_origin(group_id=None):
|
||||
form = NewOriginForm()
|
||||
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
||||
if form.validate_on_submit():
|
||||
origin = Origin()
|
||||
origin.group_id = form.group.data
|
||||
origin.domain_name = form.domain_name.data
|
||||
origin.description = form.description.data
|
||||
origin.created = datetime.utcnow()
|
||||
origin.updated = datetime.utcnow()
|
||||
try:
|
||||
db.session.add(origin)
|
||||
db.session.commit()
|
||||
flash(f"Created new origin {origin.domain_name}.", "success")
|
||||
return redirect(url_for("portal.edit_origin", origin_id=origin.id))
|
||||
except exc.SQLAlchemyError as e:
|
||||
print(e)
|
||||
flash("Failed to create new origin.", "danger")
|
||||
return redirect(url_for("portal.view_origins"))
|
||||
if group_id:
|
||||
form.group.data = group_id
|
||||
return render_template("new.html.j2", section="origin", form=form)
|
||||
|
||||
|
||||
@portal.route('/origin/edit/<origin_id>', methods=['GET', 'POST'])
|
||||
def edit_origin(origin_id):
|
||||
origin = Origin.query.filter(Origin.id == origin_id).first()
|
||||
if origin is None:
|
||||
return Response(render_template("error.html.j2",
|
||||
section="origin",
|
||||
header="404 Origin Not Found",
|
||||
message="The requested origin could not be found."),
|
||||
status=404)
|
||||
form = EditOriginForm(group=origin.group_id,
|
||||
description=origin.description)
|
||||
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
||||
if form.validate_on_submit():
|
||||
origin.group_id = form.group.data
|
||||
origin.description = form.description.data
|
||||
origin.updated = datetime.utcnow()
|
||||
try:
|
||||
db.session.commit()
|
||||
flash("Saved changes to group.", "success")
|
||||
except exc.SQLAlchemyError:
|
||||
flash("An error occurred saving the changes to the group.", "danger")
|
||||
return render_template("origin.html.j2",
|
||||
section="origin",
|
||||
origin=origin, form=form)
|
||||
|
||||
|
||||
@portal.route("/origins")
|
||||
def view_origins():
|
||||
origins = Origin.query.order_by(Origin.domain_name).all()
|
||||
return render_template("origins.html.j2", section="origin", origins=origins)
|
||||
|
||||
|
||||
@portal.route("/proxies")
|
||||
def view_proxies():
|
||||
proxies = Proxy.query.filter(Proxy.destroyed == None).order_by(desc(Proxy.updated)).all()
|
||||
return render_template("proxies.html.j2", section="proxy", proxies=proxies)
|
||||
|
||||
|
||||
@portal.route("/proxy/block/<proxy_id>", methods=['GET', 'POST'])
|
||||
def blocked_proxy(proxy_id):
|
||||
proxy = Proxy.query.filter(Proxy.id == proxy_id, Proxy.destroyed == None).first()
|
||||
if proxy is None:
|
||||
return Response(render_template("error.html.j2",
|
||||
header="404 Proxy Not Found",
|
||||
message="The requested proxy could not be found."))
|
||||
form = LifecycleForm()
|
||||
if form.validate_on_submit():
|
||||
proxy.deprecate()
|
||||
flash("Proxy will be shortly replaced.", "success")
|
||||
return redirect(url_for("portal.edit_origin", origin_id=proxy.origin.id))
|
||||
return render_template("lifecycle.html.j2",
|
||||
header=f"Mark proxy for {proxy.origin.domain_name} as blocked?",
|
||||
message=proxy.url,
|
||||
section="proxy",
|
||||
form=form)
|
||||
|
||||
|
||||
@portal.route("/search")
|
||||
def search():
|
||||
query = request.args.get("query")
|
||||
proxies = Proxy.query.filter(or_(Proxy.url.contains(query)), Proxy.destroyed == None).all()
|
||||
origins = Origin.query.filter(or_(Origin.description.contains(query), Origin.domain_name.contains(query))).all()
|
||||
return render_template("search.html.j2", section="home", proxies=proxies, origins=origins)
|
||||
|
||||
|
||||
@portal.route('/alarms')
|
||||
def view_alarms():
|
||||
alarms = Alarm.query.order_by(Alarm.alarm_state, desc(Alarm.state_changed)).all()
|
||||
return render_template("alarms.html.j2", section="alarm", alarms=alarms)
|
||||
|
||||
|
||||
@portal.route('/lists')
|
||||
def view_mirror_lists():
|
||||
mirrorlists = MirrorList.query.filter(MirrorList.destroyed == None).all()
|
||||
return render_template("mirrorlists.html.j2", section="list", mirrorlists=mirrorlists)
|
||||
|
||||
|
||||
@portal.route("/list/destroy/<list_id>")
|
||||
def destroy_mirror_list(list_id):
|
||||
return "not implemented"
|
||||
|
||||
@portal.route("/list/new", methods=['GET', 'POST'])
|
||||
@portal.route("/list/new/<group_id>", methods=['GET', 'POST'])
|
||||
def new_mirror_list(group_id=None):
|
||||
form = NewMirrorListForm()
|
||||
form.provider.choices = [
|
||||
("github", "GitHub"),
|
||||
("gitlab", "GitLab"),
|
||||
("s3", "AWS S3"),
|
||||
]
|
||||
form.format.choices = [
|
||||
("bc2", "Bypass Censorship v2"),
|
||||
("bc3", "Bypass Censorship v3"),
|
||||
("bca", "Bypass Censorship Analytics"),
|
||||
("bridgelines", "Tor Bridge Lines")
|
||||
]
|
||||
form.container.description = "GitHub Project, GitLab Project or AWS S3 bucket name."
|
||||
form.branch.description = "Ignored for AWS S3."
|
||||
if form.validate_on_submit():
|
||||
mirror_list = MirrorList()
|
||||
mirror_list.provider = form.provider.data
|
||||
mirror_list.format = form.format.data
|
||||
mirror_list.description = form.description.data
|
||||
mirror_list.container = form.container.data
|
||||
mirror_list.branch = form.branch.data
|
||||
mirror_list.filename = form.filename.data
|
||||
mirror_list.created = datetime.utcnow()
|
||||
mirror_list.updated = datetime.utcnow()
|
||||
try:
|
||||
db.session.add(mirror_list)
|
||||
db.session.commit()
|
||||
flash(f"Created new mirror list.", "success")
|
||||
return redirect(url_for("portal.view_mirror_lists"))
|
||||
except exc.SQLAlchemyError as e:
|
||||
print(e)
|
||||
flash("Failed to create new mirror list.", "danger")
|
||||
return redirect(url_for("portal.view_mirror_lists"))
|
||||
if group_id:
|
||||
form.group.data = group_id
|
||||
return render_template("new.html.j2", section="list", form=form)
|
||||
|
||||
|
||||
@portal.route("/bridgeconfs")
|
||||
def view_bridgeconfs():
|
||||
bridgeconfs = BridgeConf.query.filter(BridgeConf.destroyed == None).all()
|
||||
return render_template("bridgeconfs.html.j2", section="bridgeconf", bridgeconfs=bridgeconfs)
|
||||
|
||||
|
||||
@portal.route("/bridgeconf/new", methods=['GET', 'POST'])
|
||||
@portal.route("/bridgeconf/new/<group_id>", methods=['GET', 'POST'])
|
||||
def new_bridgeconf(group_id=None):
|
||||
form = NewBridgeConfForm()
|
||||
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
||||
form.provider.choices = [
|
||||
("aws", "AWS Lightsail"),
|
||||
("hcloud", "Hetzner Cloud"),
|
||||
("ovh", "OVH Public Cloud"),
|
||||
("gandi", "GandiCloud VPS")
|
||||
]
|
||||
form.method.choices = [
|
||||
("any", "Any (BridgeDB)"),
|
||||
("email", "E-Mail (BridgeDB)"),
|
||||
("moat", "Moat (BridgeDB)"),
|
||||
("https", "HTTPS (BridgeDB)"),
|
||||
("none", "None (Private)")
|
||||
]
|
||||
if form.validate_on_submit():
|
||||
bridge_conf = BridgeConf()
|
||||
bridge_conf.group_id = form.group.data
|
||||
bridge_conf.provider = form.provider.data
|
||||
bridge_conf.method = form.method.data
|
||||
bridge_conf.description = form.description.data
|
||||
bridge_conf.number = form.number.data
|
||||
bridge_conf.created = datetime.utcnow()
|
||||
bridge_conf.updated = datetime.utcnow()
|
||||
try:
|
||||
db.session.add(bridge_conf)
|
||||
db.session.commit()
|
||||
flash(f"Created new bridge configuration {bridge_conf.id}.", "success")
|
||||
return redirect(url_for("portal.view_bridgeconfs"))
|
||||
except exc.SQLAlchemyError as e:
|
||||
print(e)
|
||||
flash("Failed to create new bridge configuration.", "danger")
|
||||
return redirect(url_for("portal.view_bridgeconfs"))
|
||||
if group_id:
|
||||
form.group.data = group_id
|
||||
return render_template("new.html.j2", section="bridgeconf", form=form)
|
||||
|
||||
|
||||
@portal.route("/bridges")
|
||||
def view_bridges():
|
||||
bridges = Bridge.query.filter(Bridge.destroyed == None).all()
|
||||
return render_template("bridges.html.j2", section="bridge", bridges=bridges)
|
||||
|
||||
|
||||
@portal.route('/bridgeconf/edit/<bridgeconf_id>', methods=['GET', 'POST'])
|
||||
def edit_bridgeconf(bridgeconf_id):
|
||||
bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id).first()
|
||||
if bridgeconf is None:
|
||||
return Response(render_template("error.html.j2",
|
||||
section="origin",
|
||||
header="404 Origin Not Found",
|
||||
message="The requested origin could not be found."),
|
||||
status=404)
|
||||
form = EditBridgeConfForm(description=bridgeconf.description,
|
||||
number=bridgeconf.number)
|
||||
if form.validate_on_submit():
|
||||
bridgeconf.description = form.description.data
|
||||
bridgeconf.number = form.number.data
|
||||
bridgeconf.updated = datetime.utcnow()
|
||||
try:
|
||||
db.session.commit()
|
||||
flash("Saved changes to bridge configuration.", "success")
|
||||
except exc.SQLAlchemyError:
|
||||
flash("An error occurred saving the changes to the bridge configuration.", "danger")
|
||||
return render_template("bridgeconf.html.j2",
|
||||
section="bridgeconf",
|
||||
bridgeconf=bridgeconf, form=form)
|
||||
|
||||
|
||||
@portal.route("/bridge/block/<bridge_id>", methods=['GET', 'POST'])
|
||||
def blocked_bridge(bridge_id):
|
||||
bridge = Bridge.query.filter(Bridge.id == bridge_id, Bridge.destroyed == None).first()
|
||||
if bridge is None:
|
||||
return Response(render_template("error.html.j2",
|
||||
header="404 Proxy Not Found",
|
||||
message="The requested bridge could not be found."))
|
||||
form = LifecycleForm()
|
||||
if form.validate_on_submit():
|
||||
bridge.deprecate()
|
||||
flash("Bridge will be shortly replaced.", "success")
|
||||
return redirect(url_for("portal.edit_bridgeconf", bridgeconf_id=bridge.conf_id))
|
||||
return render_template("lifecycle.html.j2",
|
||||
header=f"Mark bridge {bridge.hashed_fingerprint} as blocked?",
|
||||
message=bridge.hashed_fingerprint,
|
||||
section="bridge",
|
||||
form=form)
|
||||
|
||||
|
||||
def response_404(message: str):
|
||||
return Response(render_template("error.html.j2",
|
||||
header="404 Not Found",
|
||||
message=message))
|
||||
|
||||
|
||||
def view_lifecycle(*,
|
||||
header: str,
|
||||
message: str,
|
||||
success_message: str,
|
||||
success_view: str,
|
||||
section: str,
|
||||
resource: AbstractResource,
|
||||
action: str):
|
||||
form = LifecycleForm()
|
||||
if form.validate_on_submit():
|
||||
if action == "destroy":
|
||||
resource.destroy()
|
||||
elif action == "deprecate":
|
||||
resource.deprecate()
|
||||
flash(success_message, "success")
|
||||
return redirect(url_for(success_view))
|
||||
return render_template("lifecycle.html.j2",
|
||||
header=header,
|
||||
message=message,
|
||||
section=section,
|
||||
form=form)
|
||||
|
||||
|
||||
@portal.route("/bridgeconf/destroy/<bridgeconf_id>", methods=['GET', 'POST'])
|
||||
def destroy_bridgeconf(bridgeconf_id: int):
|
||||
bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id, BridgeConf.destroyed == None).first()
|
||||
if bridgeconf is None:
|
||||
return response_404("The requested bridge configuration could not be found.")
|
||||
return view_lifecycle(
|
||||
header=f"Destroy bridge configuration?",
|
||||
message=bridgeconf.description,
|
||||
success_view="portal.view_bridgeconfs",
|
||||
success_message="All bridges from the destroyed configuration will shortly be destroyed at their providers.",
|
||||
section="bridgeconf",
|
||||
resource=bridgeconf,
|
||||
action="destroy"
|
||||
)
|
||||
|
||||
|
||||
@portal.route("/origin/destroy/<origin_id>", methods=['GET', 'POST'])
|
||||
def destroy_origin(origin_id: int):
|
||||
origin = Origin.query.filter(Origin.id == origin_id, Origin.destroyed == None).first()
|
||||
if origin is None:
|
||||
return response_404("The requested origin could not be found.")
|
||||
return view_lifecycle(
|
||||
header=f"Destroy origin {origin.domain_name}",
|
||||
message=origin.description,
|
||||
success_message="All proxies from the destroyed origin will shortly be destroyed at their providers.",
|
||||
success_view="portal.view_origins",
|
||||
section="origin",
|
||||
resource=origin,
|
||||
action="destroy"
|
||||
)
|
69
app/portal/forms.py
Normal file
69
app/portal/forms.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, SelectField, BooleanField, IntegerField
|
||||
from wtforms.validators import DataRequired, NumberRange
|
||||
|
||||
|
||||
class NewGroupForm(FlaskForm):
|
||||
group_name = StringField("Short Name", validators=[DataRequired()])
|
||||
description = StringField("Description", validators=[DataRequired()])
|
||||
eotk = BooleanField("Deploy EOTK instances?")
|
||||
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
|
||||
|
||||
|
||||
class EditGroupForm(FlaskForm):
|
||||
description = StringField('Description', validators=[DataRequired()])
|
||||
eotk = BooleanField("Deploy EOTK instances?")
|
||||
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
|
||||
|
||||
|
||||
class NewOriginForm(FlaskForm):
|
||||
domain_name = StringField('Domain Name', validators=[DataRequired()])
|
||||
description = StringField('Description', validators=[DataRequired()])
|
||||
group = SelectField('Group', validators=[DataRequired()])
|
||||
submit = SubmitField('Save Changes')
|
||||
|
||||
|
||||
class EditOriginForm(FlaskForm):
|
||||
description = StringField('Description', validators=[DataRequired()])
|
||||
group = SelectField('Group', validators=[DataRequired()])
|
||||
submit = SubmitField('Save Changes')
|
||||
|
||||
|
||||
class EditMirrorForm(FlaskForm):
|
||||
origin = SelectField('Origin')
|
||||
url = StringField('URL')
|
||||
submit = SubmitField('Save Changes')
|
||||
|
||||
|
||||
class EditProxyForm(FlaskForm):
|
||||
origin = SelectField('Origin')
|
||||
submit = SubmitField('Save Changes')
|
||||
|
||||
|
||||
class LifecycleForm(FlaskForm):
|
||||
submit = SubmitField('Confirm')
|
||||
|
||||
|
||||
class NewBridgeConfForm(FlaskForm):
|
||||
provider = SelectField('Provider', validators=[DataRequired()])
|
||||
method = SelectField('Distribution Method', validators=[DataRequired()])
|
||||
description = StringField('Description')
|
||||
group = SelectField('Group', validators=[DataRequired()])
|
||||
number = IntegerField('Number', validators=[NumberRange(1, message="One or more bridges must be created")])
|
||||
submit = SubmitField('Save Changes')
|
||||
|
||||
|
||||
class EditBridgeConfForm(FlaskForm):
|
||||
description = StringField('Description')
|
||||
number = IntegerField('Number', validators=[NumberRange(1, message="One or more bridges must be created")])
|
||||
submit = SubmitField('Save Changes')
|
||||
|
||||
|
||||
class NewMirrorListForm(FlaskForm):
|
||||
provider = SelectField('Provider', validators=[DataRequired()])
|
||||
format = SelectField('Distribution Method', validators=[DataRequired()])
|
||||
description = StringField('Description', validators=[DataRequired()])
|
||||
container = StringField('Container', validators=[DataRequired()])
|
||||
branch = StringField('Branch')
|
||||
filename = StringField('Filename', validators=[DataRequired()])
|
||||
submit = SubmitField('Save Changes')
|
100
app/portal/static/portal.css
Normal file
100
app/portal/static/portal.css
Normal file
|
@ -0,0 +1,100 @@
|
|||
body {
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.feather {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sidebar
|
||||
*/
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
/* rtl:raw:
|
||||
right: 0;
|
||||
*/
|
||||
bottom: 0;
|
||||
/* rtl:remove */
|
||||
left: 0;
|
||||
z-index: 100; /* Behind the navbar */
|
||||
padding: 48px 0 0; /* Height of navbar */
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar {
|
||||
top: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-sticky {
|
||||
position: relative;
|
||||
top: 0;
|
||||
height: calc(100vh - 48px);
|
||||
padding-top: .5rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link .feather {
|
||||
margin-right: 4px;
|
||||
color: #727272;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #2470dc;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover .feather,
|
||||
.sidebar .nav-link.active .feather {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/*
|
||||
* Navbar
|
||||
*/
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: .75rem;
|
||||
padding-bottom: .75rem;
|
||||
font-size: 1rem;
|
||||
background-color: rgba(0, 0, 0, .25);
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
||||
.navbar .navbar-toggler {
|
||||
top: .25rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.navbar .form-control {
|
||||
padding: .75rem 1rem;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.form-control-dark {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
border-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
.form-control-dark:focus {
|
||||
border-color: transparent;
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
||||
}
|
31
app/portal/templates/alarms.html.j2
Normal file
31
app/portal/templates/alarms.html.j2
Normal file
|
@ -0,0 +1,31 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from 'bootstrap5/form.html' import render_form %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Alarms</h1>
|
||||
<h2 class="h3">Proxies</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Resource</th>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">State</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for alarm in alarms %}
|
||||
<tr class="bg-{% if alarm.alarm_state.name == "OK" %}success{% elif alarm.alarm_state.name == "UNKNOWN" %}dark{% else %}danger{% endif %} text-light">
|
||||
{% if alarm.target == "proxy" %}
|
||||
<td>Proxy: {{ alarm.proxy.url }} ({{ alarm.proxy.origin.domain_name }})</td>
|
||||
{% elif alarm.target == "service/cloudfront" %}
|
||||
<td>AWS CloudFront</td>
|
||||
{% endif %}
|
||||
<td>{{ alarm.alarm_type }}</td>
|
||||
<td>{{ alarm.alarm_state.name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
162
app/portal/templates/base.html.j2
Normal file
162
app/portal/templates/base.html.j2
Normal file
|
@ -0,0 +1,162 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% block head %}
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="generator" content="Bypass Censorship Portal">
|
||||
|
||||
{% block styles %}
|
||||
<!-- Bootstrap CSS -->
|
||||
{{ bootstrap.load_css() }}
|
||||
{% endblock %}
|
||||
|
||||
<title>Bypass Censorship Portal</title>
|
||||
|
||||
<style>
|
||||
.bd-placeholder-img {
|
||||
font-size: 1.125rem;
|
||||
text-anchor: middle;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.bd-placeholder-img-lg {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Custom styles for this template -->
|
||||
<link href="/portal/static/portal.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
|
||||
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="#">Bypass Censorship</a>
|
||||
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<form class="w-100" action="{{ url_for("portal.search") }}">
|
||||
<input class="form-control form-control-dark w-100" type="text" name="query" placeholder="Search"
|
||||
aria-label="Search">
|
||||
</form>
|
||||
<div class="navbar-nav">
|
||||
<div class="nav-item text-nowrap">
|
||||
<a class="nav-link px-3" href="#">{{ request.headers.get('X-User-Name', 'Default User') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
|
||||
<div class="position-sticky pt-3">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if section == "home" %} active{% endif %}"
|
||||
href="{{ url_for("portal.portal_home") }}">
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Configuration</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if section == "group" %} active{% endif %}"
|
||||
href="{{ url_for("portal.view_groups") }}">
|
||||
Groups
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if section == "origin" %} active{% endif %}"
|
||||
href="{{ url_for("portal.view_origins") }}">
|
||||
Origins
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if section == "list" %} active{% endif %}"
|
||||
href="{{ url_for("portal.view_mirror_lists") }}">
|
||||
Mirror Lists
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if section == "bridgeconf" %} active{% endif %}"
|
||||
href="{{ url_for("portal.view_bridgeconfs") }}">
|
||||
Tor Bridges
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Infrastructure</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="disabled nav-link{% if section == "eotk" %} active{% endif %}"
|
||||
href="#">
|
||||
EOTK Instances
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if section == "proxy" %} active{% endif %}"
|
||||
href="{{ url_for("portal.view_proxies") }}">
|
||||
Proxies
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if section == "bridge" %} active{% endif %}"
|
||||
href="{{ url_for("portal.view_bridges") }}">
|
||||
Tor Bridges
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Monitoring</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if section == "alarm" %} active{% endif %}"
|
||||
href="{{ url_for("portal.view_alarms") }}">
|
||||
Alarms
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} mt-2">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
<!-- Optional JavaScript -->
|
||||
{{ bootstrap.load_js() }}
|
||||
{% endblock %}
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js"
|
||||
integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE"
|
||||
crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
16
app/portal/templates/bridgeconf.html.j2
Normal file
16
app/portal/templates/bridgeconf.html.j2
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from 'bootstrap5/form.html' import render_form %}
|
||||
{% from "tables.html.j2" import bridges_table %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Tor Bridge Configuration</h1>
|
||||
<h2 class="h3">{{ bridgeconf.group.group_name }}: {{ bridgeconf.provider }}/{{ bridgeconf.method }}</h2>
|
||||
|
||||
<div style="border: 1px solid #666;" class="p-3">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
|
||||
<h3>Bridges</h3>
|
||||
{{ bridges_table(bridgeconf.bridges) }}
|
||||
|
||||
{% endblock %}
|
8
app/portal/templates/bridgeconfs.html.j2
Normal file
8
app/portal/templates/bridgeconfs.html.j2
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from "tables.html.j2" import bridgeconfs_table %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Tor Bridge Configurations</h1>
|
||||
<a href="{{ url_for("portal.new_bridgeconf") }}" class="btn btn-success">Create new configuration</a>
|
||||
{{ bridgeconfs_table(bridgeconfs) }}
|
||||
{% endblock %}
|
7
app/portal/templates/bridges.html.j2
Normal file
7
app/portal/templates/bridges.html.j2
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from "tables.html.j2" import bridges_table %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Tor Bridges</h1>
|
||||
{{ bridges_table(bridges) }}
|
||||
{% endblock %}
|
6
app/portal/templates/error.html.j2
Normal file
6
app/portal/templates/error.html.j2
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% extends "base.html.j2" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">{{ header }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
{% endblock %}
|
18
app/portal/templates/group.html.j2
Normal file
18
app/portal/templates/group.html.j2
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from 'bootstrap5/form.html' import render_form %}
|
||||
{% from "tables.html.j2" import origins_table %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Groups</h1>
|
||||
<h2 class="h3">{{ group.group_name }}</h2>
|
||||
|
||||
<div style="border: 1px solid #666;" class="p-3">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
|
||||
<h3 class="mt-3">Origins</h3>
|
||||
<a href="{{ url_for("portal.new_origin", group_id=group.id) }}" class="btn btn-success btn-sm">Create new origin</a>
|
||||
{% if group.origins %}
|
||||
{{ origins_table(group.origins) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
30
app/portal/templates/groups.html.j2
Normal file
30
app/portal/templates/groups.html.j2
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% extends "base.html.j2" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Groups</h1>
|
||||
<a href="{{ url_for("portal.new_group") }}" class="btn btn-success">Create new group</a>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Description</th>
|
||||
<th scope="col">EOTK</th>
|
||||
<th scope="col">Sites</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td>{{ group.group_name }}</td>
|
||||
<td>{{ group.description }}</td>
|
||||
<td>{% if group.eotk %}√{% else %}x{% endif %}</td>
|
||||
<td>{{ group.origins | length }}</td>
|
||||
<td><a href="{{ url_for("portal.edit_group", group_id=group.id) }}" class="btn btn-primary btn-sm">View/Edit</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
6
app/portal/templates/home.html.j2
Normal file
6
app/portal/templates/home.html.j2
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% extends "base.html.j2" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Welcome</h1>
|
||||
<p>Welcome to the Bypass Censorship portal.</p>
|
||||
{% endblock %}
|
8
app/portal/templates/lifecycle.html.j2
Normal file
8
app/portal/templates/lifecycle.html.j2
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from 'bootstrap5/form.html' import render_form %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">{{ header }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
{{ render_form(form) }}
|
||||
{% endblock %}
|
8
app/portal/templates/mirrorlists.html.j2
Normal file
8
app/portal/templates/mirrorlists.html.j2
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from "tables.html.j2" import mirrorlists_table %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Mirror Lists</h1>
|
||||
<a href="{{ url_for("portal.new_mirror_list") }}" class="btn btn-success">Create new mirror list</a>
|
||||
{{ mirrorlists_table(mirrorlists) }}
|
||||
{% endblock %}
|
12
app/portal/templates/new.html.j2
Normal file
12
app/portal/templates/new.html.j2
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from 'bootstrap5/form.html' import render_form %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">{{ (resource_p or (section + "s")).title() }}</h1>
|
||||
<h2 class="h3">New {{ section.lower() }}</h2>
|
||||
|
||||
<div style="border: 1px solid #666;" class="p-3">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
20
app/portal/templates/origin.html.j2
Normal file
20
app/portal/templates/origin.html.j2
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from 'bootstrap5/form.html' import render_form %}
|
||||
{% from "tables.html.j2" import proxies_table %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Origins</h1>
|
||||
<h2 class="h3">
|
||||
{{ origin.group.group_name }}: {{ origin.domain_name }}
|
||||
<a href="{{ origin.domain_name }}" class="btn btn-secondary btn-sm" target="_bypass"
|
||||
rel="noopener noreferer">⎋</a>
|
||||
</h2>
|
||||
|
||||
<div style="border: 1px solid #666;" class="p-3">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
|
||||
<h3>Proxies</h3>
|
||||
{{ proxies_table(origin.proxies) }}
|
||||
|
||||
{% endblock %}
|
8
app/portal/templates/origins.html.j2
Normal file
8
app/portal/templates/origins.html.j2
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from "tables.html.j2" import origins_table %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Origins</h1>
|
||||
<a href="{{ url_for("portal.new_origin") }}" class="btn btn-success">Create new origin</a>
|
||||
{{ origins_table(origins) }}
|
||||
{% endblock %}
|
7
app/portal/templates/proxies.html.j2
Normal file
7
app/portal/templates/proxies.html.j2
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from "tables.html.j2" import proxies_table %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Proxies</h1>
|
||||
{{ proxies_table(proxies) }}
|
||||
{% endblock %}
|
14
app/portal/templates/search.html.j2
Normal file
14
app/portal/templates/search.html.j2
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from "tables.html.j2" import origins_table %}
|
||||
{% from "tables.html.j2" import proxies_table %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Search Results</h1>
|
||||
{% if origins %}
|
||||
<h2 class="h3">Origins</h2>
|
||||
{{ origins_table(origins) }}
|
||||
{% endif %}{% if proxies %}
|
||||
<h2 class="h3">Proxies</h2>
|
||||
{{ proxies_table(proxies) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
253
app/portal/templates/tables.html.j2
Normal file
253
app/portal/templates/tables.html.j2
Normal file
|
@ -0,0 +1,253 @@
|
|||
{% macro origins_table(origins) %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Description</th>
|
||||
<th scope="col">Mirrors</th>
|
||||
<th scope="col">Proxies</th>
|
||||
<th scope="col">Group</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for origin in origins %}
|
||||
{% if not origin.destroyed %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://{{ origin.domain_name }}" target="_bypass" rel="noopener noreferer"
|
||||
class="btn btn-secondary btn-sm">⎋</a>
|
||||
{{ origin.domain_name }}
|
||||
</td>
|
||||
<td>{{ origin.description }}</td>
|
||||
<td>{{ origin.mirrors | length }}</td>
|
||||
<td>{{ origin.proxies | length }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for("portal.edit_group", group_id=origin.group.id) }}">{{ origin.group.group_name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for("portal.edit_origin", origin_id=origin.id) }}"
|
||||
class="btn btn-primary btn-sm">View/Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro proxies_table(proxies) %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Origin Domain Name</th>
|
||||
<th scope="col">Group</th>
|
||||
<th scope="col">Provider</th>
|
||||
<th scope="col">URL</th>
|
||||
<th scope="col">Alarms</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for proxy in proxies %}
|
||||
{% if not proxy.destroyed %}
|
||||
<tr class="align-middle{% if proxy.deprecated %} bg-warning{% endif %}">
|
||||
<td>
|
||||
<a href="https://{{ proxy.origin.domain_name }}" class="btn btn-secondary btn-sm"
|
||||
target="_bypass"
|
||||
rel="noopener noreferer">⎋</a>
|
||||
<a href="{{ url_for("portal.edit_origin", origin_id=proxy.origin.id) }}">{{ proxy.origin.domain_name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for("portal.edit_group", group_id=proxy.origin.group.id) }}">{{ proxy.origin.group.group_name }}</a>
|
||||
</td>
|
||||
<td>{{ proxy.provider }}</td>
|
||||
<td>
|
||||
<a href="{{ proxy.url }}" class="btn btn-secondary btn-sm" target="_bypass"
|
||||
rel="noopener noreferer">⎋</a>
|
||||
<span{% if proxy.deprecated %}
|
||||
class="text-decoration-line-through"{% endif %}>{{ proxy.url }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% for alarm in proxy.alarms %}
|
||||
<span title="{{ alarm.alarm_type }}">
|
||||
{% if alarm.alarm_state.name == "OK" %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-check-circle text-success" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/>
|
||||
</svg>
|
||||
{% elif alarm.alarm_state.name == "UNKNOWN" %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-question-circle text-muted" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-exclamation-circle text-danger" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{% if proxy.deprecated %}
|
||||
<a href="#" class="disabled btn btn-sm btn-outline-dark">Expiring
|
||||
in {{ proxy.deprecated | mirror_expiry }}</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for("portal.blocked_proxy", proxy_id=proxy.id) }}"
|
||||
class="btn btn-warning btn-sm">Mark blocked</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro bridgeconfs_table(bridgeconfs) %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Group</th>
|
||||
<th scope="col">Provider</th>
|
||||
<th scope="col">Distribution Method</th>
|
||||
<th scope="col">Number</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for bridgeconf in bridgeconfs %}
|
||||
{% if not bridgeconf.destroyed %}
|
||||
<tr class="align-middle">
|
||||
<td>
|
||||
<a href="{{ url_for("portal.edit_group", group_id=bridgeconf.group.id) }}">{{ bridgeconf.group.group_name }}</a>
|
||||
</td>
|
||||
<td>{{ bridgeconf.provider }}</td>
|
||||
<td>{{ bridgeconf.method }}</td>
|
||||
<td>{{ bridgeconf.number }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for("portal.edit_bridgeconf", bridgeconf_id=bridgeconf.id) }}"
|
||||
class="btn btn-primary btn-sm">View/Edit</a>
|
||||
<a href="{{ url_for("portal.destroy_bridgeconf", bridgeconf_id=bridgeconf.id) }}"
|
||||
class="btn btn-danger btn-sm">Destroy</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro bridges_table(bridges) %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Group</th>
|
||||
<th scope="col">Configuration</th>
|
||||
<th scope="col">Nickname</th>
|
||||
<th scope="col">Hashed Fingerprint</th>
|
||||
<th scope="col">Alarms</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for bridge in bridges %}
|
||||
{% if not bridge.destroyed %}
|
||||
<tr class="align-middle{% if bridge.deprecated %} bg-warning{% endif %}">
|
||||
<td>
|
||||
<a href="{{ url_for("portal.edit_group", group_id=bridge.conf.group.id) }}">{{ bridge.conf.group.group_name }}</a>
|
||||
</td>
|
||||
<td>{{ bridge.conf.description }} ({{ bridge.conf.provider }}/{{ bridge.conf.method }})</td>
|
||||
<td>
|
||||
<a href="https://metrics.torproject.org/rs.html#details/{{ bridge.hashed_fingerprint }}">
|
||||
{{ bridge.nickname }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ bridge.hashed_fingerprint }}</code>
|
||||
</td>
|
||||
<td>
|
||||
{% for alarm in bridge.alarms %}
|
||||
<span title="{{ alarm.alarm_type }}">
|
||||
{% if alarm.alarm_state.name == "OK" %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-check-circle text-success" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/>
|
||||
</svg>
|
||||
{% elif alarm.alarm_state.name == "UNKNOWN" %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-question-circle text-muted" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-exclamation-circle text-danger" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{% if bridge.deprecated %}
|
||||
<a href="#" class="disabled btn btn-sm btn-outline-dark">Expiring
|
||||
in {{ bridge.deprecated | mirror_expiry }}</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for("portal.blocked_bridge", bridge_id=bridge.id) }}"
|
||||
class="btn btn-warning btn-sm">Mark blocked</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro mirrorlists_table(mirrorlists) %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Provider</th>
|
||||
<th scope="col">Format</th>
|
||||
<th scope="col">URI</th>
|
||||
<th scope="col">Description</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for list in mirrorlists %}
|
||||
{% if not list.destroyed %}
|
||||
<tr class="align-middle">
|
||||
<td>{{ list.provider }}</td>
|
||||
<td>{{ list.format }}</td>
|
||||
<td>{{ list.url() }}</td>
|
||||
<td>{{ list.description }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for("portal.destroy_mirror_list", list_id=list.id) }}"
|
||||
class="btn btn-danger btn-sm">Destroy</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
Loading…
Add table
Add a link
Reference in a new issue