portal: refactor bridges views into modules

This commit is contained in:
Iain Learmonth 2022-05-04 14:03:04 +01:00
parent 22f850cf6b
commit 9987c996c9
7 changed files with 173 additions and 153 deletions

View file

@ -1,14 +1,15 @@
from datetime import datetime, timedelta, timezone
from flask import Blueprint, render_template, Response, flash, redirect, url_for, request
from flask import Blueprint, render_template, flash, redirect, url_for, request
from sqlalchemy import exc, desc, or_
from app.extensions import db
from app.models.bridges import BridgeConf, Bridge
from app.models.alarms import Alarm
from app import Origin, Proxy
from app.models.base import Group, MirrorList
from app.portal.forms import LifecycleForm, NewBridgeConfForm, EditBridgeConfForm, NewMirrorListForm
from app.portal.forms import LifecycleForm, NewMirrorListForm
from app.portal.bridgeconf import bp as bridgeconf
from app.portal.bridge import bp as bridge
from app.portal.group import bp as group
from app.portal.origin import bp as origin
from app.portal.proxy import bp as proxy
@ -16,6 +17,8 @@ from app.portal.util import response_404, view_lifecycle
portal = Blueprint("portal", __name__, template_folder="templates", static_folder="static")
portal.register_blueprint(bridgeconf, url_prefix="/bridgeconf")
portal.register_blueprint(bridge, url_prefix="/bridge")
portal.register_blueprint(group, url_prefix="/group")
portal.register_blueprint(origin, url_prefix="/origin")
portal.register_blueprint(proxy, url_prefix="/proxy")
@ -120,128 +123,3 @@ def new_mirror_list(group_id=None):
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("list.html.j2",
section="bridgeconf",
title="Tor Bridge Configurations",
item="bridge configuration",
items=bridgeconfs,
new_link=url_for("portal.new_bridgeconf"))
@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("list.html.j2",
section="bridge",
title="Tor Bridges",
item="bridge",
items=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="bridge",
header="404 Bridge Configuration Not Found",
message="The requested bridge configuration 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 = 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(reason="manual")
db.session.commit()
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)
@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"
)

37
app/portal/bridge.py Normal file
View file

@ -0,0 +1,37 @@
from flask import render_template, Response, flash, redirect, url_for, Blueprint
from app.extensions import db
from app.models.bridges import Bridge
from app.portal.forms import LifecycleForm
bp = Blueprint("bridge", __name__)
@bp.route("/list")
def bridge_list():
bridges = Bridge.query.filter(Bridge.destroyed == None).all()
return render_template("list.html.j2",
section="bridge",
title="Tor Bridges",
item="bridge",
items=bridges)
@bp.route("/block/<bridge_id>", methods=['GET', 'POST'])
def bridge_blocked(bridge_id):
bridge: 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(reason="manual")
db.session.commit()
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)

122
app/portal/bridgeconf.py Normal file
View file

@ -0,0 +1,122 @@
from datetime import datetime
from flask import render_template, url_for, flash, redirect, Response, Blueprint
from flask_wtf import FlaskForm
from sqlalchemy import exc
from wtforms import SelectField, StringField, IntegerField, SubmitField
from wtforms.validators import DataRequired, NumberRange
from app.extensions import db
from app.models.base import Group
from app.models.bridges import BridgeConf
from app.portal.util import response_404, view_lifecycle
bp = Blueprint("bridgeconf", __name__)
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')
@bp.route("/list")
def bridgeconf_list():
bridgeconfs = BridgeConf.query.filter(BridgeConf.destroyed == None).all()
return render_template("list.html.j2",
section="bridgeconf",
title="Tor Bridge Configurations",
item="bridge configuration",
items=bridgeconfs,
new_link=url_for("portal.bridgeconf.bridgeconf_new"))
@bp.route("/new", methods=['GET', 'POST'])
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
def bridgeconf_new(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.bridgeconf.bridgeconf_list"))
except exc.SQLAlchemyError as e:
print(e)
flash("Failed to create new bridge configuration.", "danger")
return redirect(url_for("portal.bridgeconf.bridgeconf_list"))
if group_id:
form.group.data = group_id
return render_template("new.html.j2", section="bridgeconf", form=form)
@bp.route('/edit/<bridgeconf_id>', methods=['GET', 'POST'])
def bridgeconf_edit(bridgeconf_id):
bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id).first()
if bridgeconf is None:
return Response(render_template("error.html.j2",
section="bridge",
header="404 Bridge Configuration Not Found",
message="The requested bridge configuration 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)
@bp.route("/destroy/<bridgeconf_id>", methods=['GET', 'POST'])
def bridgeconf_destroy(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.bridgeconf.bridgeconf_list",
success_message="All bridges from the destroyed configuration will shortly be destroyed at their providers.",
section="bridgeconf",
resource=bridgeconf,
action="destroy"
)

View file

@ -1,8 +1,6 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, SelectField, BooleanField, IntegerField
from wtforms.validators import DataRequired, NumberRange
from wtforms import StringField, SubmitField, SelectField
from wtforms.validators import DataRequired
class EditMirrorForm(FlaskForm):
@ -20,21 +18,6 @@ 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()])

View file

@ -90,7 +90,7 @@
</li>
<li class="nav-item">
<a class="nav-link{% if section == "bridgeconf" %} active{% endif %}"
href="{{ url_for("portal.view_bridgeconfs") }}">
href="{{ url_for("portal.bridgeconf.bridgeconf_list") }}">
Tor Bridges
</a>
</li>
@ -113,7 +113,7 @@
</li>
<li class="nav-item">
<a class="nav-link{% if section == "bridge" %} active{% endif %}"
href="{{ url_for("portal.view_bridges") }}">
href="{{ url_for("portal.bridge.bridge_list") }}">
Tor Bridges
</a>
</li>

View file

@ -181,9 +181,9 @@
<td>{{ bridgeconf.method }}</td>
<td>{{ bridgeconf.number }}</td>
<td>
<a href="{{ url_for("portal.edit_bridgeconf", bridgeconf_id=bridgeconf.id) }}"
<a href="{{ url_for("portal.bridgeconf.bridgeconf_edit", bridgeconf_id=bridgeconf.id) }}"
class="btn btn-primary btn-sm">View/Edit</a>
<a href="{{ url_for("portal.destroy_bridgeconf", bridgeconf_id=bridgeconf.id) }}"
<a href="{{ url_for("portal.bridgeconf.bridgeconf_destroy", bridgeconf_id=bridgeconf.id) }}"
class="btn btn-danger btn-sm">Destroy</a>
</td>
</tr>
@ -253,7 +253,7 @@
<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) }}"
<a href="{{ url_for("portal.bridge.bridge_blocked", bridge_id=bridge.id) }}"
class="btn btn-warning btn-sm">Mark blocked</a>
{% endif %}
</td>

View file

@ -35,4 +35,4 @@ def view_lifecycle(*,
header=header,
message=message,
section=section,
form=form)
form=form)