lint: reformat python code with black

This commit is contained in:
Iain Learmonth 2024-12-06 18:15:47 +00:00
parent 331beb01b4
commit a406a7974b
88 changed files with 2579 additions and 1608 deletions

View file

@ -32,7 +32,9 @@ from app.portal.static import bp as static
from app.portal.storage import bp as storage
from app.portal.webhook import bp as webhook
portal = Blueprint("portal", __name__, template_folder="templates", static_folder="static")
portal = Blueprint(
"portal", __name__, template_folder="templates", static_folder="static"
)
portal.register_blueprint(automation, url_prefix="/automation")
portal.register_blueprint(bridgeconf, url_prefix="/bridgeconf")
portal.register_blueprint(bridge, url_prefix="/bridge")
@ -54,7 +56,10 @@ portal.register_blueprint(webhook, url_prefix="/webhook")
@portal.app_template_filter("bridge_expiry")
def calculate_bridge_expiry(b: Bridge) -> str:
if b.deprecated is None:
logging.warning("Bridge expiry requested by template for a bridge %s that was not expiring.", b.id)
logging.warning(
"Bridge expiry requested by template for a bridge %s that was not expiring.",
b.id,
)
return "Not expiring"
expiry = b.deprecated + timedelta(hours=b.conf.expiry_hours)
countdown = expiry - datetime.now(tz=timezone.utc)
@ -85,27 +90,27 @@ def describe_brn(s: str) -> ResponseReturnValue:
if parts[3] == "mirror":
if parts[5].startswith("origin/"):
origin = Origin.query.filter(
Origin.domain_name == parts[5][len("origin/"):]
Origin.domain_name == parts[5][len("origin/") :]
).first()
if not origin:
return s
return f"Origin: {origin.domain_name} ({origin.group.group_name})"
if parts[5].startswith("proxy/"):
proxy = Proxy.query.filter(
Proxy.id == int(parts[5][len("proxy/"):])
Proxy.id == int(parts[5][len("proxy/") :])
).first()
if not proxy:
return s
return Markup(
f"Proxy: {proxy.url}<br>({proxy.origin.group.group_name}: {proxy.origin.domain_name})")
f"Proxy: {proxy.url}<br>({proxy.origin.group.group_name}: {proxy.origin.domain_name})"
)
if parts[5].startswith("quota/"):
if parts[4] == "cloudfront":
return f"Quota: CloudFront {parts[5][len('quota/'):]}"
if parts[3] == "eotk":
if parts[5].startswith("instance/"):
eotk = Eotk.query.filter(
Eotk.group_id == parts[2],
Eotk.region == parts[5][len("instance/"):]
Eotk.group_id == parts[2], Eotk.region == parts[5][len("instance/") :]
).first()
if not eotk:
return s
@ -138,9 +143,16 @@ def portal_home() -> ResponseReturnValue:
proxies = Proxy.query.filter(Proxy.destroyed.is_(None)).all()
last24 = len(Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=1))).all())
last72 = len(Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=3))).all())
lastweek = len(Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=7))).all())
lastweek = len(
Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=7))).all()
)
alarms = {
s: len(Alarm.query.filter(Alarm.alarm_state == s.upper(), Alarm.last_updated > (now - timedelta(days=1))).all())
s: len(
Alarm.query.filter(
Alarm.alarm_state == s.upper(),
Alarm.last_updated > (now - timedelta(days=1)),
).all()
)
for s in ["critical", "warning", "ok", "unknown"]
}
bridges = Bridge.query.filter(Bridge.destroyed.is_(None)).all()
@ -148,13 +160,36 @@ def portal_home() -> ResponseReturnValue:
d: len(Bridge.query.filter(Bridge.deprecated > (now - timedelta(days=d))).all())
for d in [1, 3, 7]
}
activity = Activity.query.filter(Activity.added > (now - timedelta(days=2))).order_by(desc(Activity.added)).all()
onionified = len([o for o in Origin.query.filter(Origin.destroyed.is_(None)).all() if o.onion() is not None])
activity = (
Activity.query.filter(Activity.added > (now - timedelta(days=2)))
.order_by(desc(Activity.added))
.all()
)
onionified = len(
[
o
for o in Origin.query.filter(Origin.destroyed.is_(None)).all()
if o.onion() is not None
]
)
ooni_blocked = total_origins_blocked()
total_origins = len(Origin.query.filter(Origin.destroyed.is_(None)).all())
return render_template("home.html.j2", section="home", groups=groups, last24=last24, last72=last72,
lastweek=lastweek, proxies=proxies, **alarms, activity=activity, total_origins=total_origins,
onionified=onionified, br_last=br_last, ooni_blocked=ooni_blocked, bridges=bridges)
return render_template(
"home.html.j2",
section="home",
groups=groups,
last24=last24,
last72=last72,
lastweek=lastweek,
proxies=proxies,
**alarms,
activity=activity,
total_origins=total_origins,
onionified=onionified,
br_last=br_last,
ooni_blocked=ooni_blocked,
bridges=bridges,
)
@portal.route("/search")
@ -163,19 +198,27 @@ def search() -> ResponseReturnValue:
if query is None:
return redirect(url_for("portal.portal_home"))
proxies = Proxy.query.filter(
or_(func.lower(Proxy.url).contains(query.lower())), Proxy.destroyed.is_(None)).all()
or_(func.lower(Proxy.url).contains(query.lower())), Proxy.destroyed.is_(None)
).all()
origins = Origin.query.filter(
or_(func.lower(Origin.description).contains(query.lower()),
func.lower(Origin.domain_name).contains(query.lower()))).all()
return render_template("search.html.j2", section="home", proxies=proxies, origins=origins)
or_(
func.lower(Origin.description).contains(query.lower()),
func.lower(Origin.domain_name).contains(query.lower()),
)
).all()
return render_template(
"search.html.j2", section="home", proxies=proxies, origins=origins
)
@portal.route('/alarms')
@portal.route("/alarms")
def view_alarms() -> ResponseReturnValue:
one_day_ago = datetime.now(timezone.utc) - timedelta(days=1)
alarms = Alarm.query.filter(Alarm.last_updated >= one_day_ago).order_by(
desc(Alarm.alarm_state), desc(Alarm.state_changed)).all()
return render_template("list.html.j2",
section="alarm",
title="Alarms",
items=alarms)
alarms = (
Alarm.query.filter(Alarm.last_updated >= one_day_ago)
.order_by(desc(Alarm.alarm_state), desc(Alarm.state_changed))
.all()
)
return render_template(
"list.html.j2", section="alarm", title="Alarms", items=alarms
)

View file

@ -17,40 +17,52 @@ bp = Blueprint("automation", __name__)
_SECTION_TEMPLATE_VARS = {
"section": "automation",
"help_url": "https://bypass.censorship.guide/user/automation.html"
"help_url": "https://bypass.censorship.guide/user/automation.html",
}
class EditAutomationForm(FlaskForm): # type: ignore
enabled = BooleanField('Enabled')
submit = SubmitField('Save Changes')
enabled = BooleanField("Enabled")
submit = SubmitField("Save Changes")
@bp.route("/list")
def automation_list() -> ResponseReturnValue:
automations = list(filter(
lambda a: a.short_name not in current_app.config.get('HIDDEN_AUTOMATIONS', []),
Automation.query.filter(
Automation.destroyed.is_(None)).order_by(Automation.description).all()
))
automations = list(
filter(
lambda a: a.short_name
not in current_app.config.get("HIDDEN_AUTOMATIONS", []),
Automation.query.filter(Automation.destroyed.is_(None))
.order_by(Automation.description)
.all(),
)
)
states = {tfs.key: tfs for tfs in TerraformState.query.all()}
return render_template("list.html.j2",
title="Automation Jobs",
item="automation",
items=automations,
states=states,
**_SECTION_TEMPLATE_VARS)
return render_template(
"list.html.j2",
title="Automation Jobs",
item="automation",
items=automations,
states=states,
**_SECTION_TEMPLATE_VARS
)
@bp.route('/edit/<automation_id>', methods=['GET', 'POST'])
@bp.route("/edit/<automation_id>", methods=["GET", "POST"])
def automation_edit(automation_id: int) -> ResponseReturnValue:
automation: Optional[Automation] = Automation.query.filter(Automation.id == automation_id).first()
automation: Optional[Automation] = Automation.query.filter(
Automation.id == automation_id
).first()
if automation is None:
return Response(render_template("error.html.j2",
header="404 Automation Job Not Found",
message="The requested automation job could not be found.",
**_SECTION_TEMPLATE_VARS),
status=404)
return Response(
render_template(
"error.html.j2",
header="404 Automation Job Not Found",
message="The requested automation job could not be found.",
**_SECTION_TEMPLATE_VARS
),
status=404,
)
form = EditAutomationForm(enabled=automation.enabled)
if form.validate_on_submit():
automation.enabled = form.enabled.data
@ -59,21 +71,30 @@ def automation_edit(automation_id: int) -> ResponseReturnValue:
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")
logs = AutomationLogs.query.filter(AutomationLogs.automation_id == automation.id).order_by(
desc(AutomationLogs.added)).limit(5).all()
return render_template("automation.html.j2",
automation=automation,
logs=logs,
form=form,
**_SECTION_TEMPLATE_VARS)
flash(
"An error occurred saving the changes to the bridge configuration.",
"danger",
)
logs = (
AutomationLogs.query.filter(AutomationLogs.automation_id == automation.id)
.order_by(desc(AutomationLogs.added))
.limit(5)
.all()
)
return render_template(
"automation.html.j2",
automation=automation,
logs=logs,
form=form,
**_SECTION_TEMPLATE_VARS
)
@bp.route("/kick/<automation_id>", methods=['GET', 'POST'])
@bp.route("/kick/<automation_id>", methods=["GET", "POST"])
def automation_kick(automation_id: int) -> ResponseReturnValue:
automation = Automation.query.filter(
Automation.id == automation_id,
Automation.destroyed.is_(None)).first()
Automation.id == automation_id, Automation.destroyed.is_(None)
).first()
if automation is None:
return response_404("The requested bridge configuration could not be found.")
return view_lifecycle(
@ -83,5 +104,5 @@ def automation_kick(automation_id: int) -> ResponseReturnValue:
success_view="portal.automation.automation_list",
success_message="This automation job will next run within 1 minute.",
resource=automation,
action="kick"
action="kick",
)

View file

@ -1,7 +1,6 @@
from typing import Optional
from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask import Blueprint, Response, flash, redirect, render_template, url_for
from flask.typing import ResponseReturnValue
from app.extensions import db
@ -12,57 +11,79 @@ bp = Blueprint("bridge", __name__)
_SECTION_TEMPLATE_VARS = {
"section": "bridge",
"help_url": "https://bypass.censorship.guide/user/bridges.html"
"help_url": "https://bypass.censorship.guide/user/bridges.html",
}
@bp.route("/list")
def bridge_list() -> ResponseReturnValue:
bridges = Bridge.query.filter(Bridge.destroyed.is_(None)).all()
return render_template("list.html.j2",
title="Tor Bridges",
item="bridge",
items=bridges,
**_SECTION_TEMPLATE_VARS)
return render_template(
"list.html.j2",
title="Tor Bridges",
item="bridge",
items=bridges,
**_SECTION_TEMPLATE_VARS,
)
@bp.route("/block/<bridge_id>", methods=['GET', 'POST'])
@bp.route("/block/<bridge_id>", methods=["GET", "POST"])
def bridge_blocked(bridge_id: int) -> ResponseReturnValue:
bridge: Optional[Bridge] = Bridge.query.filter(Bridge.id == bridge_id, Bridge.destroyed.is_(None)).first()
bridge: Optional[Bridge] = Bridge.query.filter(
Bridge.id == bridge_id, Bridge.destroyed.is_(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.",
**_SECTION_TEMPLATE_VARS))
return Response(
render_template(
"error.html.j2",
header="404 Proxy Not Found",
message="The requested bridge could not be found.",
**_SECTION_TEMPLATE_VARS,
)
)
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.bridgeconf.bridgeconf_edit", bridgeconf_id=bridge.conf_id))
return render_template("lifecycle.html.j2",
header=f"Mark bridge {bridge.hashed_fingerprint} as blocked?",
message=bridge.hashed_fingerprint,
form=form,
**_SECTION_TEMPLATE_VARS)
return redirect(
url_for("portal.bridgeconf.bridgeconf_edit", bridgeconf_id=bridge.conf_id)
)
return render_template(
"lifecycle.html.j2",
header=f"Mark bridge {bridge.hashed_fingerprint} as blocked?",
message=bridge.hashed_fingerprint,
form=form,
**_SECTION_TEMPLATE_VARS,
)
@bp.route("/expire/<bridge_id>", methods=['GET', 'POST'])
@bp.route("/expire/<bridge_id>", methods=["GET", "POST"])
def bridge_expire(bridge_id: int) -> ResponseReturnValue:
bridge: Optional[Bridge] = Bridge.query.filter(Bridge.id == bridge_id, Bridge.destroyed.is_(None)).first()
bridge: Optional[Bridge] = Bridge.query.filter(
Bridge.id == bridge_id, Bridge.destroyed.is_(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.",
**_SECTION_TEMPLATE_VARS))
return Response(
render_template(
"error.html.j2",
header="404 Proxy Not Found",
message="The requested bridge could not be found.",
**_SECTION_TEMPLATE_VARS,
)
)
form = LifecycleForm()
if form.validate_on_submit():
bridge.destroy()
db.session.commit()
flash("Bridge will be shortly destroyed.", "success")
return redirect(url_for("portal.bridgeconf.bridgeconf_edit", bridgeconf_id=bridge.conf_id))
return render_template("lifecycle.html.j2",
header=f"Destroy bridge {bridge.hashed_fingerprint}?",
message=bridge.hashed_fingerprint,
form=form,
**_SECTION_TEMPLATE_VARS)
return redirect(
url_for("portal.bridgeconf.bridgeconf_edit", bridgeconf_id=bridge.conf_id)
)
return render_template(
"lifecycle.html.j2",
header=f"Destroy bridge {bridge.hashed_fingerprint}?",
message=bridge.hashed_fingerprint,
form=form,
**_SECTION_TEMPLATE_VARS,
)

View file

@ -1,8 +1,7 @@
from datetime import datetime, timezone
from typing import List, Optional
from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask import Blueprint, Response, flash, redirect, render_template, url_for
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from sqlalchemy import exc
@ -19,77 +18,109 @@ bp = Blueprint("bridgeconf", __name__)
_SECTION_TEMPLATE_VARS = {
"section": "bridgeconf",
"help_url": "https://bypass.censorship.guide/user/bridges.html"
"help_url": "https://bypass.censorship.guide/user/bridges.html",
}
class NewBridgeConfForm(FlaskForm): # type: ignore
method = SelectField('Distribution Method', validators=[DataRequired()])
description = StringField('Description')
pool = SelectField('Pool', validators=[DataRequired()])
target_number = IntegerField('Target Number',
description="The number of active bridges to deploy (excluding deprecated bridges).",
validators=[NumberRange(1, message="One or more bridges must be created.")])
max_number = IntegerField('Maximum Number',
description="The maximum number of bridges to deploy (including deprecated bridges).",
validators=[
NumberRange(1, message="Must be at least 1, ideally greater than target number.")])
expiry_hours = IntegerField('Expiry Timer (hours)',
description=("The number of hours to wait after a bridge is deprecated before its "
"destruction."))
provider_allocation = SelectField('Provider Allocation Method',
description="How to allocate new bridges to providers.",
choices=[
("COST", "Use cheapest provider first"),
("RANDOM", "Use providers randomly"),
])
submit = SubmitField('Save Changes')
method = SelectField("Distribution Method", validators=[DataRequired()])
description = StringField("Description")
pool = SelectField("Pool", validators=[DataRequired()])
target_number = IntegerField(
"Target Number",
description="The number of active bridges to deploy (excluding deprecated bridges).",
validators=[NumberRange(1, message="One or more bridges must be created.")],
)
max_number = IntegerField(
"Maximum Number",
description="The maximum number of bridges to deploy (including deprecated bridges).",
validators=[
NumberRange(
1, message="Must be at least 1, ideally greater than target number."
)
],
)
expiry_hours = IntegerField(
"Expiry Timer (hours)",
description=(
"The number of hours to wait after a bridge is deprecated before its "
"destruction."
),
)
provider_allocation = SelectField(
"Provider Allocation Method",
description="How to allocate new bridges to providers.",
choices=[
("COST", "Use cheapest provider first"),
("RANDOM", "Use providers randomly"),
],
)
submit = SubmitField("Save Changes")
class EditBridgeConfForm(FlaskForm): # type: ignore
description = StringField('Description')
target_number = IntegerField('Target Number',
description="The number of active bridges to deploy (excluding deprecated bridges).",
validators=[NumberRange(1, message="One or more bridges must be created.")])
max_number = IntegerField('Maximum Number',
description="The maximum number of bridges to deploy (including deprecated bridges).",
validators=[
NumberRange(1, message="Must be at least 1, ideally greater than target number.")])
expiry_hours = IntegerField('Expiry Timer (hours)',
description=("The number of hours to wait after a bridge is deprecated before its "
"destruction."))
provider_allocation = SelectField('Provider Allocation Method',
description="How to allocate new bridges to providers.",
choices=[
("COST", "Use cheapest provider first"),
("RANDOM", "Use providers randomly"),
])
submit = SubmitField('Save Changes')
description = StringField("Description")
target_number = IntegerField(
"Target Number",
description="The number of active bridges to deploy (excluding deprecated bridges).",
validators=[NumberRange(1, message="One or more bridges must be created.")],
)
max_number = IntegerField(
"Maximum Number",
description="The maximum number of bridges to deploy (including deprecated bridges).",
validators=[
NumberRange(
1, message="Must be at least 1, ideally greater than target number."
)
],
)
expiry_hours = IntegerField(
"Expiry Timer (hours)",
description=(
"The number of hours to wait after a bridge is deprecated before its "
"destruction."
),
)
provider_allocation = SelectField(
"Provider Allocation Method",
description="How to allocate new bridges to providers.",
choices=[
("COST", "Use cheapest provider first"),
("RANDOM", "Use providers randomly"),
],
)
submit = SubmitField("Save Changes")
@bp.route("/list")
def bridgeconf_list() -> ResponseReturnValue:
bridgeconfs: List[BridgeConf] = BridgeConf.query.filter(BridgeConf.destroyed.is_(None)).all()
return render_template("list.html.j2",
title="Tor Bridge Configurations",
item="bridge configuration",
items=bridgeconfs,
new_link=url_for("portal.bridgeconf.bridgeconf_new"),
**_SECTION_TEMPLATE_VARS)
bridgeconfs: List[BridgeConf] = BridgeConf.query.filter(
BridgeConf.destroyed.is_(None)
).all()
return render_template(
"list.html.j2",
title="Tor Bridge Configurations",
item="bridge configuration",
items=bridgeconfs,
new_link=url_for("portal.bridgeconf.bridgeconf_new"),
**_SECTION_TEMPLATE_VARS,
)
@bp.route("/new", methods=['GET', 'POST'])
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
@bp.route("/new", methods=["GET", "POST"])
@bp.route("/new/<group_id>", methods=["GET", "POST"])
def bridgeconf_new(group_id: Optional[int] = None) -> ResponseReturnValue:
form = NewBridgeConfForm()
form.pool.choices = [(x.id, x.pool_name) for x in Pool.query.filter(Pool.destroyed.is_(None)).all()]
form.pool.choices = [
(x.id, x.pool_name) for x in Pool.query.filter(Pool.destroyed.is_(None)).all()
]
form.method.choices = [
("any", "Any (BridgeDB)"),
("email", "E-Mail (BridgeDB)"),
("moat", "Moat (BridgeDB)"),
("settings", "Settings (BridgeDB)"),
("https", "HTTPS (BridgeDB)"),
("none", "None (Private)")
("none", "None (Private)"),
]
if form.validate_on_submit():
bridgeconf = BridgeConf()
@ -99,7 +130,9 @@ def bridgeconf_new(group_id: Optional[int] = None) -> ResponseReturnValue:
bridgeconf.target_number = form.target_number.data
bridgeconf.max_number = form.max_number.data
bridgeconf.expiry_hours = form.expiry_hours.data
bridgeconf.provider_allocation = ProviderAllocation[form.provider_allocation.data]
bridgeconf.provider_allocation = ProviderAllocation[
form.provider_allocation.data
]
bridgeconf.added = datetime.now(tz=timezone.utc)
bridgeconf.updated = datetime.now(tz=timezone.utc)
try:
@ -112,47 +145,56 @@ def bridgeconf_new(group_id: Optional[int] = None) -> ResponseReturnValue:
return redirect(url_for("portal.bridgeconf.bridgeconf_list"))
if group_id:
form.group.data = group_id
return render_template("new.html.j2",
form=form,
**_SECTION_TEMPLATE_VARS)
return render_template("new.html.j2", form=form, **_SECTION_TEMPLATE_VARS)
@bp.route('/edit/<bridgeconf_id>', methods=['GET', 'POST'])
@bp.route("/edit/<bridgeconf_id>", methods=["GET", "POST"])
def bridgeconf_edit(bridgeconf_id: int) -> ResponseReturnValue:
bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id).first()
if bridgeconf is None:
return Response(render_template("error.html.j2",
header="404 Bridge Configuration Not Found",
message="The requested bridge configuration could not be found.",
**_SECTION_TEMPLATE_VARS),
status=404)
form = EditBridgeConfForm(description=bridgeconf.description,
target_number=bridgeconf.target_number,
max_number=bridgeconf.max_number,
expiry_hours=bridgeconf.expiry_hours,
provider_allocation=bridgeconf.provider_allocation.name,
)
return Response(
render_template(
"error.html.j2",
header="404 Bridge Configuration Not Found",
message="The requested bridge configuration could not be found.",
**_SECTION_TEMPLATE_VARS,
),
status=404,
)
form = EditBridgeConfForm(
description=bridgeconf.description,
target_number=bridgeconf.target_number,
max_number=bridgeconf.max_number,
expiry_hours=bridgeconf.expiry_hours,
provider_allocation=bridgeconf.provider_allocation.name,
)
if form.validate_on_submit():
bridgeconf.description = form.description.data
bridgeconf.target_number = form.target_number.data
bridgeconf.max_number = form.max_number.data
bridgeconf.expiry_hours = form.expiry_hours.data
bridgeconf.provider_allocation = ProviderAllocation[form.provider_allocation.data]
bridgeconf.provider_allocation = ProviderAllocation[
form.provider_allocation.data
]
bridgeconf.updated = datetime.now(tz=timezone.utc)
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",
bridgeconf=bridgeconf,
form=form,
**_SECTION_TEMPLATE_VARS)
flash(
"An error occurred saving the changes to the bridge configuration.",
"danger",
)
return render_template(
"bridgeconf.html.j2", bridgeconf=bridgeconf, form=form, **_SECTION_TEMPLATE_VARS
)
@bp.route("/destroy/<bridgeconf_id>", methods=['GET', 'POST'])
@bp.route("/destroy/<bridgeconf_id>", methods=["GET", "POST"])
def bridgeconf_destroy(bridgeconf_id: int) -> ResponseReturnValue:
bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id, BridgeConf.destroyed.is_(None)).first()
bridgeconf = BridgeConf.query.filter(
BridgeConf.id == bridgeconf_id, BridgeConf.destroyed.is_(None)
).first()
if bridgeconf is None:
return response_404("The requested bridge configuration could not be found.")
return view_lifecycle(
@ -162,5 +204,5 @@ def bridgeconf_destroy(bridgeconf_id: int) -> ResponseReturnValue:
success_message="All bridges from the destroyed configuration will shortly be destroyed at their providers.",
section="bridgeconf",
resource=bridgeconf,
action="destroy"
action="destroy",
)

View file

@ -3,8 +3,15 @@ from typing import Dict, List, Optional, Type, Union
from flask import Blueprint, redirect, render_template, url_for
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from wtforms import (BooleanField, Form, FormField, IntegerField, SelectField,
StringField, SubmitField)
from wtforms import (
BooleanField,
Form,
FormField,
IntegerField,
SelectField,
StringField,
SubmitField,
)
from wtforms.validators import InputRequired
from app.extensions import db
@ -14,54 +21,72 @@ bp = Blueprint("cloud", __name__)
_SECTION_TEMPLATE_VARS = {
"section": "cloud",
"help_url": "https://bypass.censorship.guide/user/cloud.html"
"help_url": "https://bypass.censorship.guide/user/cloud.html",
}
class NewCloudAccountForm(FlaskForm): # type: ignore
provider = SelectField('Cloud Provider', validators=[InputRequired()])
submit = SubmitField('Next')
provider = SelectField("Cloud Provider", validators=[InputRequired()])
submit = SubmitField("Next")
class AWSAccountForm(FlaskForm): # type: ignore
provider = StringField('Platform', render_kw={"disabled": ""})
description = StringField('Description', validators=[InputRequired()])
aws_access_key = StringField('AWS Access Key', validators=[InputRequired()])
aws_secret_key = StringField('AWS Secret Key', validators=[InputRequired()])
aws_region = StringField('AWS Region', default='us-east-2', validators=[InputRequired()])
max_distributions = IntegerField('Cloudfront Distributions Quota', default=200,
description="This is the quota for number of distributions per account.",
validators=[InputRequired()])
max_instances = IntegerField('EC2 Instance Quota', default=2,
description="This can be impacted by a number of quotas including instance limits "
"and IP address limits.",
validators=[InputRequired()])
enabled = BooleanField('Enable this account', default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.")
submit = SubmitField('Save Changes')
provider = StringField("Platform", render_kw={"disabled": ""})
description = StringField("Description", validators=[InputRequired()])
aws_access_key = StringField("AWS Access Key", validators=[InputRequired()])
aws_secret_key = StringField("AWS Secret Key", validators=[InputRequired()])
aws_region = StringField(
"AWS Region", default="us-east-2", validators=[InputRequired()]
)
max_distributions = IntegerField(
"Cloudfront Distributions Quota",
default=200,
description="This is the quota for number of distributions per account.",
validators=[InputRequired()],
)
max_instances = IntegerField(
"EC2 Instance Quota",
default=2,
description="This can be impacted by a number of quotas including instance limits "
"and IP address limits.",
validators=[InputRequired()],
)
enabled = BooleanField(
"Enable this account",
default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.",
)
submit = SubmitField("Save Changes")
class HcloudAccountForm(FlaskForm): # type: ignore
provider = StringField('Platform', render_kw={"disabled": ""})
description = StringField('Description', validators=[InputRequired()])
hcloud_token = StringField('Hetzner Cloud Token', validators=[InputRequired()])
max_instances = IntegerField('Server Limit', default=10,
validators=[InputRequired()])
enabled = BooleanField('Enable this account', default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.")
submit = SubmitField('Save Changes')
provider = StringField("Platform", render_kw={"disabled": ""})
description = StringField("Description", validators=[InputRequired()])
hcloud_token = StringField("Hetzner Cloud Token", validators=[InputRequired()])
max_instances = IntegerField(
"Server Limit", default=10, validators=[InputRequired()]
)
enabled = BooleanField(
"Enable this account",
default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.",
)
submit = SubmitField("Save Changes")
class GitlabAccountForm(FlaskForm): # type: ignore
provider = StringField('Platform', render_kw={"disabled": ""})
description = StringField('Description', validators=[InputRequired()])
gitlab_token = StringField('GitLab Access Token', validators=[InputRequired()])
enabled = BooleanField('Enable this account', default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.")
submit = SubmitField('Save Changes')
provider = StringField("Platform", render_kw={"disabled": ""})
description = StringField("Description", validators=[InputRequired()])
gitlab_token = StringField("GitLab Access Token", validators=[InputRequired()])
enabled = BooleanField(
"Enable this account",
default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.",
)
submit = SubmitField("Save Changes")
class OvhHorizonForm(Form): # type: ignore[misc]
@ -77,16 +102,20 @@ class OvhApiForm(Form): # type: ignore[misc]
class OvhAccountForm(FlaskForm): # type: ignore
provider = StringField('Platform', render_kw={"disabled": ""})
description = StringField('Description', validators=[InputRequired()])
horizon = FormField(OvhHorizonForm, 'OpenStack Horizon API')
ovh_api = FormField(OvhApiForm, 'OVH API')
max_instances = IntegerField('Server Limit', default=10,
validators=[InputRequired()])
enabled = BooleanField('Enable this account', default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.")
submit = SubmitField('Save Changes')
provider = StringField("Platform", render_kw={"disabled": ""})
description = StringField("Description", validators=[InputRequired()])
horizon = FormField(OvhHorizonForm, "OpenStack Horizon API")
ovh_api = FormField(OvhApiForm, "OVH API")
max_instances = IntegerField(
"Server Limit", default=10, validators=[InputRequired()]
)
enabled = BooleanField(
"Enable this account",
default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.",
)
submit = SubmitField("Save Changes")
class GandiHorizonForm(Form): # type: ignore[misc]
@ -96,18 +125,24 @@ class GandiHorizonForm(Form): # type: ignore[misc]
class GandiAccountForm(FlaskForm): # type: ignore
provider = StringField('Platform', render_kw={"disabled": ""})
description = StringField('Description', validators=[InputRequired()])
horizon = FormField(GandiHorizonForm, 'OpenStack Horizon API')
max_instances = IntegerField('Server Limit', default=10,
validators=[InputRequired()])
enabled = BooleanField('Enable this account', default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.")
submit = SubmitField('Save Changes')
provider = StringField("Platform", render_kw={"disabled": ""})
description = StringField("Description", validators=[InputRequired()])
horizon = FormField(GandiHorizonForm, "OpenStack Horizon API")
max_instances = IntegerField(
"Server Limit", default=10, validators=[InputRequired()]
)
enabled = BooleanField(
"Enable this account",
default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.",
)
submit = SubmitField("Save Changes")
CloudAccountForm = Union[AWSAccountForm, HcloudAccountForm, GandiAccountForm, OvhAccountForm]
CloudAccountForm = Union[
AWSAccountForm, HcloudAccountForm, GandiAccountForm, OvhAccountForm
]
provider_forms: Dict[str, Type[CloudAccountForm]] = {
CloudProvider.AWS.name: AWSAccountForm,
@ -118,7 +153,9 @@ provider_forms: Dict[str, Type[CloudAccountForm]] = {
}
def cloud_account_save(account: Optional[CloudAccount], provider: CloudProvider, form: CloudAccountForm) -> None:
def cloud_account_save(
account: Optional[CloudAccount], provider: CloudProvider, form: CloudAccountForm
) -> None:
if not account:
account = CloudAccount()
account.provider = provider
@ -162,7 +199,9 @@ def cloud_account_save(account: Optional[CloudAccount], provider: CloudProvider,
"ovh_openstack_password": form.horizon.data["ovh_openstack_password"],
"ovh_openstack_tenant_id": form.horizon.data["ovh_openstack_tenant_id"],
"ovh_cloud_application_key": form.ovh_api.data["ovh_cloud_application_key"],
"ovh_cloud_application_secret": form.ovh_api.data["ovh_cloud_application_secret"],
"ovh_cloud_application_secret": form.ovh_api.data[
"ovh_cloud_application_secret"
],
"ovh_cloud_consumer_key": form.ovh_api.data["ovh_cloud_consumer_key"],
}
account.max_distributions = 0
@ -182,53 +221,82 @@ def cloud_account_populate(form: CloudAccountForm, account: CloudAccount) -> Non
form.aws_region.data = account.credentials["aws_region"]
form.max_distributions.data = account.max_distributions
form.max_instances.data = account.max_instances
elif account.provider == CloudProvider.HCLOUD and isinstance(form, HcloudAccountForm):
elif account.provider == CloudProvider.HCLOUD and isinstance(
form, HcloudAccountForm
):
form.hcloud_token.data = account.credentials["hcloud_token"]
form.max_instances.data = account.max_instances
elif account.provider == CloudProvider.GANDI and isinstance(form, GandiAccountForm):
form.horizon.form.gandi_openstack_user.data = account.credentials["gandi_openstack_user"]
form.horizon.form.gandi_openstack_password.data = account.credentials["gandi_openstack_password"]
form.horizon.form.gandi_openstack_tenant_id.data = account.credentials["gandi_openstack_tenant_id"]
form.horizon.form.gandi_openstack_user.data = account.credentials[
"gandi_openstack_user"
]
form.horizon.form.gandi_openstack_password.data = account.credentials[
"gandi_openstack_password"
]
form.horizon.form.gandi_openstack_tenant_id.data = account.credentials[
"gandi_openstack_tenant_id"
]
form.max_instances.data = account.max_instances
elif account.provider == CloudProvider.GITLAB and isinstance(form, GitlabAccountForm):
elif account.provider == CloudProvider.GITLAB and isinstance(
form, GitlabAccountForm
):
form.gitlab_token.data = account.credentials["gitlab_token"]
elif account.provider == CloudProvider.OVH and isinstance(form, OvhAccountForm):
form.horizon.form.ovh_openstack_user.data = account.credentials["ovh_openstack_user"]
form.horizon.form.ovh_openstack_password.data = account.credentials["ovh_openstack_password"]
form.horizon.form.ovh_openstack_tenant_id.data = account.credentials["ovh_openstack_tenant_id"]
form.ovh_api.form.ovh_cloud_application_key.data = account.credentials["ovh_cloud_application_key"]
form.ovh_api.form.ovh_cloud_application_secret.data = account.credentials["ovh_cloud_application_secret"]
form.ovh_api.form.ovh_cloud_consumer_key.data = account.credentials["ovh_cloud_consumer_key"]
form.horizon.form.ovh_openstack_user.data = account.credentials[
"ovh_openstack_user"
]
form.horizon.form.ovh_openstack_password.data = account.credentials[
"ovh_openstack_password"
]
form.horizon.form.ovh_openstack_tenant_id.data = account.credentials[
"ovh_openstack_tenant_id"
]
form.ovh_api.form.ovh_cloud_application_key.data = account.credentials[
"ovh_cloud_application_key"
]
form.ovh_api.form.ovh_cloud_application_secret.data = account.credentials[
"ovh_cloud_application_secret"
]
form.ovh_api.form.ovh_cloud_consumer_key.data = account.credentials[
"ovh_cloud_consumer_key"
]
form.max_instances.data = account.max_instances
else:
raise RuntimeError(f"Unknown provider {account.provider} or form data {type(form)} did not match provider.")
raise RuntimeError(
f"Unknown provider {account.provider} or form data {type(form)} did not match provider."
)
@bp.route("/list")
def cloud_account_list() -> ResponseReturnValue:
accounts: List[CloudAccount] = CloudAccount.query.filter(CloudAccount.destroyed.is_(None)).all()
return render_template("list.html.j2",
title="Cloud Accounts",
item="cloud account",
items=accounts,
new_link=url_for("portal.cloud.cloud_account_new"),
**_SECTION_TEMPLATE_VARS)
accounts: List[CloudAccount] = CloudAccount.query.filter(
CloudAccount.destroyed.is_(None)
).all()
return render_template(
"list.html.j2",
title="Cloud Accounts",
item="cloud account",
items=accounts,
new_link=url_for("portal.cloud.cloud_account_new"),
**_SECTION_TEMPLATE_VARS,
)
@bp.route("/new", methods=['GET', 'POST'])
@bp.route("/new", methods=["GET", "POST"])
def cloud_account_new() -> ResponseReturnValue:
form = NewCloudAccountForm()
form.provider.choices = sorted([
(provider.name, provider.description) for provider in CloudProvider
], key=lambda p: p[1].lower())
form.provider.choices = sorted(
[(provider.name, provider.description) for provider in CloudProvider],
key=lambda p: p[1].lower(),
)
if form.validate_on_submit():
return redirect(url_for("portal.cloud.cloud_account_new_for", provider=form.provider.data))
return render_template("new.html.j2",
form=form,
**_SECTION_TEMPLATE_VARS)
return redirect(
url_for("portal.cloud.cloud_account_new_for", provider=form.provider.data)
)
return render_template("new.html.j2", form=form, **_SECTION_TEMPLATE_VARS)
@bp.route("/new/<provider>", methods=['GET', 'POST'])
@bp.route("/new/<provider>", methods=["GET", "POST"])
def cloud_account_new_for(provider: str) -> ResponseReturnValue:
form = provider_forms[provider]()
form.provider.data = CloudProvider[provider].description
@ -236,12 +304,10 @@ def cloud_account_new_for(provider: str) -> ResponseReturnValue:
cloud_account_save(None, CloudProvider[provider], form)
db.session.commit()
return redirect(url_for("portal.cloud.cloud_account_list"))
return render_template("new.html.j2",
form=form,
**_SECTION_TEMPLATE_VARS)
return render_template("new.html.j2", form=form, **_SECTION_TEMPLATE_VARS)
@bp.route("/edit/<account_id>", methods=['GET', 'POST'])
@bp.route("/edit/<account_id>", methods=["GET", "POST"])
def cloud_account_edit(account_id: int) -> ResponseReturnValue:
account = CloudAccount.query.filter(
CloudAccount.id == account_id,
@ -256,6 +322,4 @@ def cloud_account_edit(account_id: int) -> ResponseReturnValue:
db.session.commit()
return redirect(url_for("portal.cloud.cloud_account_list"))
cloud_account_populate(form, account)
return render_template("new.html.j2",
form=form,
**_SECTION_TEMPLATE_VARS)
return render_template("new.html.j2", form=form, **_SECTION_TEMPLATE_VARS)

View file

@ -13,7 +13,7 @@ bp = Blueprint("country", __name__)
_SECTION_TEMPLATE_VARS = {
"section": "country",
"help_url": "https://bypass.censorship.guide/user/countries.html"
"help_url": "https://bypass.censorship.guide/user/countries.html",
}
@ -22,42 +22,51 @@ def filter_country_flag(country_code: str) -> str:
country_code = country_code.upper()
# Calculate the regional indicator symbol for each letter in the country code
base = ord('\U0001F1E6') - ord('A')
flag = ''.join([chr(ord(char) + base) for char in country_code])
base = ord("\U0001F1E6") - ord("A")
flag = "".join([chr(ord(char) + base) for char in country_code])
return flag
@bp.route('/list')
@bp.route("/list")
def country_list() -> ResponseReturnValue:
countries = Country.query.filter(Country.destroyed.is_(None)).all()
print(len(countries))
return render_template("list.html.j2",
title="Countries",
item="country",
new_link=None,
items=sorted(countries, key=lambda x: x.country_code),
**_SECTION_TEMPLATE_VARS
)
return render_template(
"list.html.j2",
title="Countries",
item="country",
new_link=None,
items=sorted(countries, key=lambda x: x.country_code),
**_SECTION_TEMPLATE_VARS
)
class EditCountryForm(FlaskForm): # type: ignore[misc]
risk_level_override = BooleanField("Force Risk Level Override?")
risk_level_override_number = IntegerField("Forced Risk Level", description="Number from 0 to 20", default=0)
submit = SubmitField('Save Changes')
risk_level_override_number = IntegerField(
"Forced Risk Level", description="Number from 0 to 20", default=0
)
submit = SubmitField("Save Changes")
@bp.route('/edit/<country_id>', methods=['GET', 'POST'])
@bp.route("/edit/<country_id>", methods=["GET", "POST"])
def country_edit(country_id: int) -> ResponseReturnValue:
country = Country.query.filter(Country.id == country_id).first()
if country is None:
return Response(render_template("error.html.j2",
section="country",
header="404 Country Not Found",
message="The requested country could not be found."),
status=404)
form = EditCountryForm(risk_level_override=country.risk_level_override is not None,
risk_level_override_number=country.risk_level_override)
return Response(
render_template(
"error.html.j2",
section="country",
header="404 Country Not Found",
message="The requested country could not be found.",
),
status=404,
)
form = EditCountryForm(
risk_level_override=country.risk_level_override is not None,
risk_level_override_number=country.risk_level_override,
)
if form.validate_on_submit():
if form.risk_level_override.data:
country.risk_level_override = form.risk_level_override_number.data
@ -69,6 +78,6 @@ def country_edit(country_id: int) -> ResponseReturnValue:
flash("Saved changes to country.", "success")
except sqlalchemy.exc.SQLAlchemyError:
flash("An error occurred saving the changes to the country.", "danger")
return render_template("country.html.j2",
section="country",
country=country, form=form)
return render_template(
"country.html.j2", section="country", country=country, form=form
)

View file

@ -10,23 +10,32 @@ bp = Blueprint("eotk", __name__)
_SECTION_TEMPLATE_VARS = {
"section": "eotk",
"help_url": "https://bypass.censorship.guide/user/eotk.html"
"help_url": "https://bypass.censorship.guide/user/eotk.html",
}
@bp.route("/list")
def eotk_list() -> ResponseReturnValue:
instances = Eotk.query.filter(Eotk.destroyed.is_(None)).order_by(desc(Eotk.added)).all()
return render_template("list.html.j2",
title="EOTK Instances",
item="eotk",
items=instances,
**_SECTION_TEMPLATE_VARS)
instances = (
Eotk.query.filter(Eotk.destroyed.is_(None)).order_by(desc(Eotk.added)).all()
)
return render_template(
"list.html.j2",
title="EOTK Instances",
item="eotk",
items=instances,
**_SECTION_TEMPLATE_VARS
)
@bp.route("/conf/<group_id>")
def eotk_conf(group_id: int) -> ResponseReturnValue:
group = Group.query.filter(Group.id == group_id).first()
return Response(render_template("sites.conf.j2",
bypass_token=current_app.config["BYPASS_TOKEN"],
group=group), content_type="text/plain")
return Response(
render_template(
"sites.conf.j2",
bypass_token=current_app.config["BYPASS_TOKEN"],
group=group,
),
content_type="text/plain",
)

View file

@ -3,6 +3,6 @@ from wtforms import SelectField, StringField, SubmitField
class EditMirrorForm(FlaskForm): # type: ignore
origin = SelectField('Origin')
url = StringField('URL')
submit = SubmitField('Save Changes')
origin = SelectField("Origin")
url = StringField("URL")
submit = SubmitField("Save Changes")

View file

@ -1,8 +1,7 @@
from datetime import datetime, timezone
import sqlalchemy
from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask import Blueprint, Response, flash, redirect, render_template, url_for
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from wtforms import BooleanField, StringField, SubmitField
@ -18,27 +17,29 @@ class NewGroupForm(FlaskForm): # type: ignore
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"})
submit = SubmitField("Save Changes", render_kw={"class": "btn btn-success"})
class EditGroupForm(FlaskForm): # type: ignore
description = StringField('Description', validators=[DataRequired()])
description = StringField("Description", validators=[DataRequired()])
eotk = BooleanField("Deploy EOTK instances?")
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
submit = SubmitField("Save Changes", render_kw={"class": "btn btn-success"})
@bp.route("/list")
def group_list() -> ResponseReturnValue:
groups = Group.query.order_by(Group.group_name).all()
return render_template("list.html.j2",
section="group",
title="Groups",
item="group",
items=groups,
new_link=url_for("portal.group.group_new"))
return render_template(
"list.html.j2",
section="group",
title="Groups",
item="group",
items=groups,
new_link=url_for("portal.group.group_new"),
)
@bp.route("/new", methods=['GET', 'POST'])
@bp.route("/new", methods=["GET", "POST"])
def group_new() -> ResponseReturnValue:
form = NewGroupForm()
if form.validate_on_submit():
@ -59,17 +60,20 @@ def group_new() -> ResponseReturnValue:
return render_template("new.html.j2", section="group", form=form)
@bp.route('/edit/<group_id>', methods=['GET', 'POST'])
@bp.route("/edit/<group_id>", methods=["GET", "POST"])
def group_edit(group_id: int) -> ResponseReturnValue:
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)
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
@ -79,6 +83,4 @@ def group_edit(group_id: int) -> ResponseReturnValue:
flash("Saved changes to group.", "success")
except sqlalchemy.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)
return render_template("group.html.j2", section="group", group=group, form=form)

View file

@ -2,8 +2,7 @@ import json
from datetime import datetime, timezone
from typing import Any, Optional
from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask import Blueprint, Response, flash, redirect, render_template, url_for
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from sqlalchemy import exc
@ -23,7 +22,7 @@ bp = Blueprint("list", __name__)
_SECTION_TEMPLATE_VARS = {
"section": "list",
"help_url": "https://bypass.censorship.guide/user/lists.html"
"help_url": "https://bypass.censorship.guide/user/lists.html",
}
@ -42,37 +41,44 @@ def list_encoding_name(key: str) -> str:
return MirrorList.encodings_supported.get(key, "Unknown")
@bp.route('/list')
@bp.route("/list")
def list_list() -> ResponseReturnValue:
lists = MirrorList.query.filter(MirrorList.destroyed.is_(None)).all()
return render_template("list.html.j2",
title="Distribution Lists",
item="distribution list",
new_link=url_for("portal.list.list_new"),
items=lists,
**_SECTION_TEMPLATE_VARS
)
return render_template(
"list.html.j2",
title="Distribution Lists",
item="distribution list",
new_link=url_for("portal.list.list_new"),
items=lists,
**_SECTION_TEMPLATE_VARS
)
@bp.route('/preview/<format_>/<pool_id>')
@bp.route("/preview/<format_>/<pool_id>")
def list_preview(format_: str, pool_id: int) -> ResponseReturnValue:
pool = Pool.query.filter(Pool.id == pool_id).first()
if not pool:
return response_404(message="Pool not found")
if format_ == "bca":
return Response(json.dumps(mirror_mapping(pool)), content_type="application/json")
return Response(
json.dumps(mirror_mapping(pool)), content_type="application/json"
)
if format_ == "bc2":
return Response(json.dumps(mirror_sites(pool)), content_type="application/json")
if format_ == "bridgelines":
return Response(json.dumps(bridgelines(pool)), content_type="application/json")
if format_ == "rdr":
return Response(json.dumps(redirector_data(pool)), content_type="application/json")
return Response(
json.dumps(redirector_data(pool)), content_type="application/json"
)
return response_404(message="Format not found")
@bp.route("/destroy/<list_id>", methods=['GET', 'POST'])
@bp.route("/destroy/<list_id>", methods=["GET", "POST"])
def list_destroy(list_id: int) -> ResponseReturnValue:
list_ = MirrorList.query.filter(MirrorList.id == list_id, MirrorList.destroyed.is_(None)).first()
list_ = MirrorList.query.filter(
MirrorList.id == list_id, MirrorList.destroyed.is_(None)
).first()
if list_ is None:
return response_404("The requested bridge configuration could not be found.")
return view_lifecycle(
@ -82,12 +88,12 @@ def list_destroy(list_id: int) -> ResponseReturnValue:
success_message="This list will no longer be updated and may be deleted depending on the provider.",
section="list",
resource=list_,
action="destroy"
action="destroy",
)
@bp.route("/new", methods=['GET', 'POST'])
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
@bp.route("/new", methods=["GET", "POST"])
@bp.route("/new/<group_id>", methods=["GET", "POST"])
def list_new(group_id: Optional[int] = None) -> ResponseReturnValue:
form = NewMirrorListForm()
form.provider.choices = list(MirrorList.providers_supported.items())
@ -116,43 +122,53 @@ def list_new(group_id: Optional[int] = None) -> ResponseReturnValue:
return redirect(url_for("portal.list.list_list"))
if group_id:
form.group.data = group_id
return render_template("new.html.j2",
form=form,
**_SECTION_TEMPLATE_VARS)
return render_template("new.html.j2", form=form, **_SECTION_TEMPLATE_VARS)
class NewMirrorListForm(FlaskForm): # type: ignore
pool = SelectField('Resource Pool', validators=[DataRequired()])
provider = SelectField('Provider', validators=[DataRequired()])
format = SelectField('Distribution Method', validators=[DataRequired()])
encoding = SelectField('Encoding', validators=[DataRequired()])
description = StringField('Description', validators=[DataRequired()])
container = StringField('Container', validators=[DataRequired()],
description="GitHub Project, GitLab Project or AWS S3 bucket name.")
branch = StringField('Git Branch/AWS Region', validators=[DataRequired()],
description="For GitHub/GitLab, set this to the desired branch name, e.g. main. For AWS S3, "
"set this field to the desired region, e.g. us-east-1.")
role = StringField('Role ARN',
description="(Optional) ARN for IAM role to assume for interaction with the S3 bucket.")
filename = StringField('Filename', validators=[DataRequired()])
submit = SubmitField('Save Changes')
pool = SelectField("Resource Pool", validators=[DataRequired()])
provider = SelectField("Provider", validators=[DataRequired()])
format = SelectField("Distribution Method", validators=[DataRequired()])
encoding = SelectField("Encoding", validators=[DataRequired()])
description = StringField("Description", validators=[DataRequired()])
container = StringField(
"Container",
validators=[DataRequired()],
description="GitHub Project, GitLab Project or AWS S3 bucket name.",
)
branch = StringField(
"Git Branch/AWS Region",
validators=[DataRequired()],
description="For GitHub/GitLab, set this to the desired branch name, e.g. main. For AWS S3, "
"set this field to the desired region, e.g. us-east-1.",
)
role = StringField(
"Role ARN",
description="(Optional) ARN for IAM role to assume for interaction with the S3 bucket.",
)
filename = StringField("Filename", validators=[DataRequired()])
submit = SubmitField("Save Changes")
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.pool.choices = [
(pool.id, pool.pool_name) for pool in Pool.query.all()
]
self.pool.choices = [(pool.id, pool.pool_name) for pool in Pool.query.all()]
@bp.route('/edit/<list_id>', methods=['GET', 'POST'])
@bp.route("/edit/<list_id>", methods=["GET", "POST"])
def list_edit(list_id: int) -> ResponseReturnValue:
list_: Optional[MirrorList] = MirrorList.query.filter(MirrorList.id == list_id).first()
list_: Optional[MirrorList] = MirrorList.query.filter(
MirrorList.id == list_id
).first()
if list_ is None:
return Response(render_template("error.html.j2",
header="404 Distribution List Not Found",
message="The requested distribution list could not be found.",
**_SECTION_TEMPLATE_VARS),
status=404)
return Response(
render_template(
"error.html.j2",
header="404 Distribution List Not Found",
message="The requested distribution list could not be found.",
**_SECTION_TEMPLATE_VARS
),
status=404,
)
form = NewMirrorListForm(
pool=list_.pool_id,
provider=list_.provider,
@ -162,7 +178,7 @@ def list_edit(list_id: int) -> ResponseReturnValue:
container=list_.container,
branch=list_.branch,
role=list_.role,
filename=list_.filename
filename=list_.filename,
)
form.provider.choices = list(MirrorList.providers_supported.items())
form.format.choices = list(MirrorList.formats_supported.items())
@ -182,7 +198,10 @@ def list_edit(list_id: int) -> ResponseReturnValue:
db.session.commit()
flash("Saved changes to group.", "success")
except exc.SQLAlchemyError:
flash("An error occurred saving the changes to the distribution list.", "danger")
return render_template("distlist.html.j2",
list=list_, form=form,
**_SECTION_TEMPLATE_VARS)
flash(
"An error occurred saving the changes to the distribution list.",
"danger",
)
return render_template(
"distlist.html.j2", list=list_, form=form, **_SECTION_TEMPLATE_VARS
)

View file

@ -9,13 +9,13 @@ from app.portal.util import response_404, view_lifecycle
bp = Blueprint("onion", __name__)
@bp.route("/new", methods=['GET', 'POST'])
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
@bp.route("/new", methods=["GET", "POST"])
@bp.route("/new/<group_id>", methods=["GET", "POST"])
def onion_new(group_id: Optional[int] = None) -> ResponseReturnValue:
return redirect("/ui/web/onions/new")
@bp.route('/edit/<onion_id>', methods=['GET', 'POST'])
@bp.route("/edit/<onion_id>", methods=["GET", "POST"])
def onion_edit(onion_id: int) -> ResponseReturnValue:
return redirect("/ui/web/onions/edit/{}".format(onion_id))
@ -25,9 +25,11 @@ def onion_list() -> ResponseReturnValue:
return redirect("/ui/web/onions")
@bp.route("/destroy/<onion_id>", methods=['GET', 'POST'])
@bp.route("/destroy/<onion_id>", methods=["GET", "POST"])
def onion_destroy(onion_id: str) -> ResponseReturnValue:
onion = Onion.query.filter(Onion.id == int(onion_id), Onion.destroyed.is_(None)).first()
onion = Onion.query.filter(
Onion.id == int(onion_id), Onion.destroyed.is_(None)
).first()
if onion is None:
return response_404("The requested onion service could not be found.")
return view_lifecycle(
@ -37,5 +39,5 @@ def onion_destroy(onion_id: str) -> ResponseReturnValue:
success_view="portal.onion.onion_list",
section="onion",
resource=onion,
action="destroy"
action="destroy",
)

View file

@ -4,13 +4,11 @@ from typing import List, Optional
import requests
import sqlalchemy
from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask import Blueprint, Response, flash, redirect, render_template, url_for
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from sqlalchemy import exc
from wtforms import (BooleanField, IntegerField, SelectField, StringField,
SubmitField)
from wtforms import BooleanField, IntegerField, SelectField, StringField, SubmitField
from wtforms.validators import DataRequired
from app.extensions import db
@ -22,29 +20,31 @@ bp = Blueprint("origin", __name__)
class NewOriginForm(FlaskForm): # type: ignore
domain_name = StringField('Domain Name', validators=[DataRequired()])
description = StringField('Description', validators=[DataRequired()])
group = SelectField('Group', validators=[DataRequired()])
domain_name = StringField("Domain Name", validators=[DataRequired()])
description = StringField("Description", validators=[DataRequired()])
group = SelectField("Group", validators=[DataRequired()])
auto_rotate = BooleanField("Enable auto-rotation?", default=True)
smart_proxy = BooleanField("Requires smart proxy?", default=False)
asset_domain = BooleanField("Used to host assets for other domains?", default=False)
submit = SubmitField('Save Changes')
submit = SubmitField("Save Changes")
class EditOriginForm(FlaskForm): # type: ignore[misc]
description = StringField('Description', validators=[DataRequired()])
group = SelectField('Group', validators=[DataRequired()])
description = StringField("Description", validators=[DataRequired()])
group = SelectField("Group", validators=[DataRequired()])
auto_rotate = BooleanField("Enable auto-rotation?")
smart_proxy = BooleanField("Requires smart proxy?")
asset_domain = BooleanField("Used to host assets for other domains?", default=False)
risk_level_override = BooleanField("Force Risk Level Override?")
risk_level_override_number = IntegerField("Forced Risk Level", description="Number from 0 to 20", default=0)
submit = SubmitField('Save Changes')
risk_level_override_number = IntegerField(
"Forced Risk Level", description="Number from 0 to 20", default=0
)
submit = SubmitField("Save Changes")
class CountrySelectForm(FlaskForm): # type: ignore[misc]
country = SelectField("Country", validators=[DataRequired()])
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
submit = SubmitField("Save Changes", render_kw={"class": "btn btn-success"})
def final_domain_name(domain_name: str) -> str:
@ -53,8 +53,8 @@ def final_domain_name(domain_name: str) -> str:
return urllib.parse.urlparse(r.url).netloc
@bp.route("/new", methods=['GET', 'POST'])
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
@bp.route("/new", methods=["GET", "POST"])
@bp.route("/new/<group_id>", methods=["GET", "POST"])
def origin_new(group_id: Optional[int] = None) -> ResponseReturnValue:
form = NewOriginForm()
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
@ -81,22 +81,28 @@ def origin_new(group_id: Optional[int] = None) -> ResponseReturnValue:
return render_template("new.html.j2", section="origin", form=form)
@bp.route('/edit/<origin_id>', methods=['GET', 'POST'])
@bp.route("/edit/<origin_id>", methods=["GET", "POST"])
def origin_edit(origin_id: int) -> ResponseReturnValue:
origin: Optional[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,
auto_rotate=origin.auto_rotation,
smart_proxy=origin.smart,
asset_domain=origin.assets,
risk_level_override=origin.risk_level_override is not None,
risk_level_override_number=origin.risk_level_override)
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,
auto_rotate=origin.auto_rotation,
smart_proxy=origin.smart,
asset_domain=origin.assets,
risk_level_override=origin.risk_level_override is not None,
risk_level_override_number=origin.risk_level_override,
)
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
@ -114,41 +120,47 @@ def origin_edit(origin_id: int) -> ResponseReturnValue:
flash(f"Saved changes for origin {origin.domain_name}.", "success")
except exc.SQLAlchemyError:
flash("An error occurred saving the changes to the origin.", "danger")
return render_template("origin.html.j2",
section="origin",
origin=origin, form=form)
return render_template("origin.html.j2", section="origin", origin=origin, form=form)
@bp.route("/list")
def origin_list() -> ResponseReturnValue:
origins: List[Origin] = Origin.query.order_by(Origin.domain_name).all()
return render_template("list.html.j2",
section="origin",
title="Web Origins",
item="origin",
new_link=url_for("portal.origin.origin_new"),
items=origins,
extra_buttons=[{
"link": url_for("portal.origin.origin_onion"),
"text": "Onion services",
"style": "onion"
}])
return render_template(
"list.html.j2",
section="origin",
title="Web Origins",
item="origin",
new_link=url_for("portal.origin.origin_new"),
items=origins,
extra_buttons=[
{
"link": url_for("portal.origin.origin_onion"),
"text": "Onion services",
"style": "onion",
}
],
)
@bp.route("/onion")
def origin_onion() -> ResponseReturnValue:
origins = Origin.query.order_by(Origin.domain_name).all()
return render_template("list.html.j2",
section="origin",
title="Onion Sites",
item="onion service",
new_link=url_for("portal.onion.onion_new"),
items=origins)
return render_template(
"list.html.j2",
section="origin",
title="Onion Sites",
item="onion service",
new_link=url_for("portal.onion.onion_new"),
items=origins,
)
@bp.route("/destroy/<origin_id>", methods=['GET', 'POST'])
@bp.route("/destroy/<origin_id>", methods=["GET", "POST"])
def origin_destroy(origin_id: int) -> ResponseReturnValue:
origin = Origin.query.filter(Origin.id == origin_id, Origin.destroyed.is_(None)).first()
origin = Origin.query.filter(
Origin.id == origin_id, Origin.destroyed.is_(None)
).first()
if origin is None:
return response_404("The requested origin could not be found.")
return view_lifecycle(
@ -158,32 +170,44 @@ def origin_destroy(origin_id: int) -> ResponseReturnValue:
success_view="portal.origin.origin_list",
section="origin",
resource=origin,
action="destroy"
action="destroy",
)
@bp.route('/country_remove/<origin_id>/<country_id>', methods=['GET', 'POST'])
@bp.route("/country_remove/<origin_id>/<country_id>", methods=["GET", "POST"])
def origin_country_remove(origin_id: int, country_id: int) -> ResponseReturnValue:
origin = Origin.query.filter(Origin.id == origin_id).first()
if origin is None:
return Response(render_template("error.html.j2",
section="origin",
header="404 Pool Not Found",
message="The requested origin could not be found."),
status=404)
return Response(
render_template(
"error.html.j2",
section="origin",
header="404 Pool Not Found",
message="The requested origin could not be found.",
),
status=404,
)
country = Country.query.filter(Country.id == country_id).first()
if country is None:
return Response(render_template("error.html.j2",
section="origin",
header="404 Country Not Found",
message="The requested country could not be found."),
status=404)
return Response(
render_template(
"error.html.j2",
section="origin",
header="404 Country Not Found",
message="The requested country could not be found.",
),
status=404,
)
if country not in origin.countries:
return Response(render_template("error.html.j2",
section="origin",
header="404 Country Not In Pool",
message="The requested country could not be found in the specified origin."),
status=404)
return Response(
render_template(
"error.html.j2",
section="origin",
header="404 Country Not In Pool",
message="The requested country could not be found in the specified origin.",
),
status=404,
)
form = LifecycleForm()
if form.validate_on_submit():
origin.countries.remove(country)
@ -193,32 +217,45 @@ def origin_country_remove(origin_id: int, country_id: int) -> ResponseReturnValu
return redirect(url_for("portal.origin.origin_edit", origin_id=origin.id))
except sqlalchemy.exc.SQLAlchemyError:
flash("An error occurred saving the changes to the origin.", "danger")
return render_template("lifecycle.html.j2",
header=f"Remove {country.description} from the {origin.domain_name} origin?",
message="Stop monitoring in this country.",
section="origin",
origin=origin, form=form)
return render_template(
"lifecycle.html.j2",
header=f"Remove {country.description} from the {origin.domain_name} origin?",
message="Stop monitoring in this country.",
section="origin",
origin=origin,
form=form,
)
@bp.route('/country_add/<origin_id>', methods=['GET', 'POST'])
@bp.route("/country_add/<origin_id>", methods=["GET", "POST"])
def origin_country_add(origin_id: int) -> ResponseReturnValue:
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)
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 = CountrySelectForm()
form.country.choices = [(x.id, f"{x.country_code} - {x.description}") for x in Country.query.all()]
form.country.choices = [
(x.id, f"{x.country_code} - {x.description}") for x in Country.query.all()
]
if form.validate_on_submit():
country = Country.query.filter(Country.id == form.country.data).first()
if country is None:
return Response(render_template("error.html.j2",
section="origin",
header="404 Country Not Found",
message="The requested country could not be found."),
status=404)
return Response(
render_template(
"error.html.j2",
section="origin",
header="404 Country Not Found",
message="The requested country could not be found.",
),
status=404,
)
origin.countries.append(country)
try:
db.session.commit()
@ -226,8 +263,11 @@ def origin_country_add(origin_id: int) -> ResponseReturnValue:
return redirect(url_for("portal.origin.origin_edit", origin_id=origin.id))
except sqlalchemy.exc.SQLAlchemyError:
flash("An error occurred saving the changes to the origin.", "danger")
return render_template("lifecycle.html.j2",
header=f"Add a country to {origin.domain_name}",
message="Enable monitoring from this country:",
section="origin",
origin=origin, form=form)
return render_template(
"lifecycle.html.j2",
header=f"Add a country to {origin.domain_name}",
message="Enable monitoring from this country:",
section="origin",
origin=origin,
form=form,
)

View file

@ -3,8 +3,7 @@ import secrets
from datetime import datetime, timezone
import sqlalchemy
from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask import Blueprint, Response, flash, redirect, render_template, url_for
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from wtforms import SelectField, StringField, SubmitField
@ -21,41 +20,50 @@ class NewPoolForm(FlaskForm): # type: ignore[misc]
group_name = StringField("Short Name", validators=[DataRequired()])
description = StringField("Description", validators=[DataRequired()])
redirector_domain = StringField("Redirector Domain")
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
submit = SubmitField("Save Changes", render_kw={"class": "btn btn-success"})
class EditPoolForm(FlaskForm): # type: ignore[misc]
description = StringField("Description", validators=[DataRequired()])
redirector_domain = StringField("Redirector Domain")
api_key = StringField("API Key", description=("Any change to this field (e.g. clearing it) will result in the "
"API key being regenerated."))
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
api_key = StringField(
"API Key",
description=(
"Any change to this field (e.g. clearing it) will result in the "
"API key being regenerated."
),
)
submit = SubmitField("Save Changes", render_kw={"class": "btn btn-success"})
class GroupSelectForm(FlaskForm): # type: ignore[misc]
group = SelectField("Group", validators=[DataRequired()])
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
submit = SubmitField("Save Changes", render_kw={"class": "btn btn-success"})
@bp.route("/list")
def pool_list() -> ResponseReturnValue:
pools = Pool.query.order_by(Pool.pool_name).all()
return render_template("list.html.j2",
section="pool",
title="Resource Pools",
item="pool",
items=pools,
new_link=url_for("portal.pool.pool_new"))
return render_template(
"list.html.j2",
section="pool",
title="Resource Pools",
item="pool",
items=pools,
new_link=url_for("portal.pool.pool_new"),
)
@bp.route("/new", methods=['GET', 'POST'])
@bp.route("/new", methods=["GET", "POST"])
def pool_new() -> ResponseReturnValue:
form = NewPoolForm()
if form.validate_on_submit():
pool = Pool()
pool.pool_name = form.group_name.data
pool.description = form.description.data
pool.redirector_domain = form.redirector_domain.data if form.redirector_domain.data != "" else None
pool.redirector_domain = (
form.redirector_domain.data if form.redirector_domain.data != "" else None
)
pool.api_key = secrets.token_urlsafe(nbytes=32)
pool.added = datetime.now(timezone.utc)
pool.updated = datetime.now(timezone.utc)
@ -71,21 +79,29 @@ def pool_new() -> ResponseReturnValue:
return render_template("new.html.j2", section="pool", form=form)
@bp.route('/edit/<pool_id>', methods=['GET', 'POST'])
@bp.route("/edit/<pool_id>", methods=["GET", "POST"])
def pool_edit(pool_id: int) -> ResponseReturnValue:
pool = Pool.query.filter(Pool.id == pool_id).first()
if pool is None:
return Response(render_template("error.html.j2",
section="pool",
header="404 Pool Not Found",
message="The requested pool could not be found."),
status=404)
form = EditPoolForm(description=pool.description,
api_key=pool.api_key,
redirector_domain=pool.redirector_domain)
return Response(
render_template(
"error.html.j2",
section="pool",
header="404 Pool Not Found",
message="The requested pool could not be found.",
),
status=404,
)
form = EditPoolForm(
description=pool.description,
api_key=pool.api_key,
redirector_domain=pool.redirector_domain,
)
if form.validate_on_submit():
pool.description = form.description.data
pool.redirector_domain = form.redirector_domain.data if form.redirector_domain.data != "" else None
pool.redirector_domain = (
form.redirector_domain.data if form.redirector_domain.data != "" else None
)
if form.api_key.data != pool.api_key:
pool.api_key = secrets.token_urlsafe(nbytes=32)
form.api_key.data = pool.api_key
@ -95,33 +111,43 @@ def pool_edit(pool_id: int) -> ResponseReturnValue:
flash("Saved changes to pool.", "success")
except sqlalchemy.exc.SQLAlchemyError:
flash("An error occurred saving the changes to the pool.", "danger")
return render_template("pool.html.j2",
section="pool",
pool=pool, form=form)
return render_template("pool.html.j2", section="pool", pool=pool, form=form)
@bp.route('/group_remove/<pool_id>/<group_id>', methods=['GET', 'POST'])
@bp.route("/group_remove/<pool_id>/<group_id>", methods=["GET", "POST"])
def pool_group_remove(pool_id: int, group_id: int) -> ResponseReturnValue:
pool = Pool.query.filter(Pool.id == pool_id).first()
if pool is None:
return Response(render_template("error.html.j2",
section="pool",
header="404 Pool Not Found",
message="The requested pool could not be found."),
status=404)
return Response(
render_template(
"error.html.j2",
section="pool",
header="404 Pool Not Found",
message="The requested pool could not be found.",
),
status=404,
)
group = Group.query.filter(Group.id == group_id).first()
if group is None:
return Response(render_template("error.html.j2",
section="pool",
header="404 Group Not Found",
message="The requested group could not be found."),
status=404)
return Response(
render_template(
"error.html.j2",
section="pool",
header="404 Group Not Found",
message="The requested group could not be found.",
),
status=404,
)
if group not in pool.groups:
return Response(render_template("error.html.j2",
section="pool",
header="404 Group Not In Pool",
message="The requested group could not be found in the specified pool."),
status=404)
return Response(
render_template(
"error.html.j2",
section="pool",
header="404 Group Not In Pool",
message="The requested group could not be found in the specified pool.",
),
status=404,
)
form = LifecycleForm()
if form.validate_on_submit():
pool.groups.remove(group)
@ -131,32 +157,43 @@ def pool_group_remove(pool_id: int, group_id: int) -> ResponseReturnValue:
return redirect(url_for("portal.pool.pool_edit", pool_id=pool.id))
except sqlalchemy.exc.SQLAlchemyError:
flash("An error occurred saving the changes to the pool.", "danger")
return render_template("lifecycle.html.j2",
header=f"Remove {group.group_name} from the {pool.pool_name} pool?",
message="Resources deployed and available in the pool will be destroyed soon.",
section="pool",
pool=pool, form=form)
return render_template(
"lifecycle.html.j2",
header=f"Remove {group.group_name} from the {pool.pool_name} pool?",
message="Resources deployed and available in the pool will be destroyed soon.",
section="pool",
pool=pool,
form=form,
)
@bp.route('/group_add/<pool_id>', methods=['GET', 'POST'])
@bp.route("/group_add/<pool_id>", methods=["GET", "POST"])
def pool_group_add(pool_id: int) -> ResponseReturnValue:
pool = Pool.query.filter(Pool.id == pool_id).first()
if pool is None:
return Response(render_template("error.html.j2",
section="pool",
header="404 Pool Not Found",
message="The requested pool could not be found."),
status=404)
return Response(
render_template(
"error.html.j2",
section="pool",
header="404 Pool Not Found",
message="The requested pool could not be found.",
),
status=404,
)
form = GroupSelectForm()
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
if form.validate_on_submit():
group = Group.query.filter(Group.id == form.group.data).first()
if group is None:
return Response(render_template("error.html.j2",
section="pool",
header="404 Group Not Found",
message="The requested group could not be found."),
status=404)
return Response(
render_template(
"error.html.j2",
section="pool",
header="404 Group Not Found",
message="The requested group could not be found.",
),
status=404,
)
pool.groups.append(group)
try:
db.session.commit()
@ -164,8 +201,11 @@ def pool_group_add(pool_id: int) -> ResponseReturnValue:
return redirect(url_for("portal.pool.pool_edit", pool_id=pool.id))
except sqlalchemy.exc.SQLAlchemyError:
flash("An error occurred saving the changes to the pool.", "danger")
return render_template("lifecycle.html.j2",
header=f"Add a group to {pool.pool_name}",
message="Resources will shortly be deployed and available for all origins in this group.",
section="pool",
pool=pool, form=form)
return render_template(
"lifecycle.html.j2",
header=f"Add a group to {pool.pool_name}",
message="Resources will shortly be deployed and available for all origins in this group.",
section="pool",
pool=pool,
form=form,
)

View file

@ -1,5 +1,4 @@
from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask import Blueprint, Response, flash, redirect, render_template, url_for
from flask.typing import ResponseReturnValue
from sqlalchemy import desc
@ -12,51 +11,63 @@ bp = Blueprint("proxy", __name__)
@bp.route("/list")
def proxy_list() -> ResponseReturnValue:
proxies = Proxy.query.filter(Proxy.destroyed.is_(None)).order_by(desc(Proxy.added)).all()
return render_template("list.html.j2",
section="proxy",
title="Proxies",
item="proxy",
items=proxies)
proxies = (
Proxy.query.filter(Proxy.destroyed.is_(None)).order_by(desc(Proxy.added)).all()
)
return render_template(
"list.html.j2", section="proxy", title="Proxies", item="proxy", items=proxies
)
@bp.route("/expire/<proxy_id>", methods=['GET', 'POST'])
@bp.route("/expire/<proxy_id>", methods=["GET", "POST"])
def proxy_expire(proxy_id: int) -> ResponseReturnValue:
proxy = Proxy.query.filter(Proxy.id == proxy_id, Proxy.destroyed.is_(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. It may have already been "
"destroyed."))
return Response(
render_template(
"error.html.j2",
header="404 Proxy Not Found",
message="The requested proxy could not be found. It may have already been "
"destroyed.",
)
)
form = LifecycleForm()
if form.validate_on_submit():
proxy.destroy()
db.session.commit()
flash("Proxy will be shortly retired.", "success")
return redirect(url_for("portal.origin.origin_edit", origin_id=proxy.origin.id))
return render_template("lifecycle.html.j2",
header=f"Expire proxy for {proxy.origin.domain_name} immediately?",
message=proxy.url,
section="proxy",
form=form)
return render_template(
"lifecycle.html.j2",
header=f"Expire proxy for {proxy.origin.domain_name} immediately?",
message=proxy.url,
section="proxy",
form=form,
)
@bp.route("/block/<proxy_id>", methods=['GET', 'POST'])
@bp.route("/block/<proxy_id>", methods=["GET", "POST"])
def proxy_block(proxy_id: int) -> ResponseReturnValue:
proxy = Proxy.query.filter(Proxy.id == proxy_id, Proxy.destroyed.is_(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. It may have already been "
"destroyed."))
return Response(
render_template(
"error.html.j2",
header="404 Proxy Not Found",
message="The requested proxy could not be found. It may have already been "
"destroyed.",
)
)
form = LifecycleForm()
if form.validate_on_submit():
proxy.deprecate(reason="manual")
db.session.commit()
flash("Proxy will be shortly replaced.", "success")
return redirect(url_for("portal.origin.origin_edit", 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)
return render_template(
"lifecycle.html.j2",
header=f"Mark proxy for {proxy.origin.domain_name} as blocked?",
message=proxy.url,
section="proxy",
form=form,
)

View file

@ -20,12 +20,12 @@ def generate_subqueries():
deprecations_24hr_subquery = (
db.session.query(
DeprecationAlias.resource_id,
func.count(DeprecationAlias.resource_id).label('deprecations_24hr')
func.count(DeprecationAlias.resource_id).label("deprecations_24hr"),
)
.filter(
DeprecationAlias.reason.like('block_%'),
DeprecationAlias.reason.like("block_%"),
DeprecationAlias.deprecated_at >= now - timedelta(hours=24),
DeprecationAlias.resource_type == 'Proxy'
DeprecationAlias.resource_type == "Proxy",
)
.group_by(DeprecationAlias.resource_id)
.subquery()
@ -33,12 +33,12 @@ def generate_subqueries():
deprecations_72hr_subquery = (
db.session.query(
DeprecationAlias.resource_id,
func.count(DeprecationAlias.resource_id).label('deprecations_72hr')
func.count(DeprecationAlias.resource_id).label("deprecations_72hr"),
)
.filter(
DeprecationAlias.reason.like('block_%'),
DeprecationAlias.reason.like("block_%"),
DeprecationAlias.deprecated_at >= now - timedelta(hours=72),
DeprecationAlias.resource_type == 'Proxy'
DeprecationAlias.resource_type == "Proxy",
)
.group_by(DeprecationAlias.resource_id)
.subquery()
@ -52,13 +52,23 @@ def countries_report():
return (
db.session.query(
Country,
func.coalesce(func.sum(deprecations_24hr_subquery.c.deprecations_24hr), 0).label('total_deprecations_24hr'),
func.coalesce(func.sum(deprecations_72hr_subquery.c.deprecations_72hr), 0).label('total_deprecations_72hr')
func.coalesce(
func.sum(deprecations_24hr_subquery.c.deprecations_24hr), 0
).label("total_deprecations_24hr"),
func.coalesce(
func.sum(deprecations_72hr_subquery.c.deprecations_72hr), 0
).label("total_deprecations_72hr"),
)
.join(Origin, Country.origins)
.join(Proxy, Origin.proxies)
.outerjoin(deprecations_24hr_subquery, Proxy.id == deprecations_24hr_subquery.c.resource_id)
.outerjoin(deprecations_72hr_subquery, Proxy.id == deprecations_72hr_subquery.c.resource_id)
.outerjoin(
deprecations_24hr_subquery,
Proxy.id == deprecations_24hr_subquery.c.resource_id,
)
.outerjoin(
deprecations_72hr_subquery,
Proxy.id == deprecations_72hr_subquery.c.resource_id,
)
.group_by(Country.id)
.all()
)
@ -70,12 +80,22 @@ def origins_report():
return (
db.session.query(
Origin,
func.coalesce(func.sum(deprecations_24hr_subquery.c.deprecations_24hr), 0).label('total_deprecations_24hr'),
func.coalesce(func.sum(deprecations_72hr_subquery.c.deprecations_72hr), 0).label('total_deprecations_72hr')
func.coalesce(
func.sum(deprecations_24hr_subquery.c.deprecations_24hr), 0
).label("total_deprecations_24hr"),
func.coalesce(
func.sum(deprecations_72hr_subquery.c.deprecations_72hr), 0
).label("total_deprecations_72hr"),
)
.outerjoin(Proxy, Origin.proxies)
.outerjoin(deprecations_24hr_subquery, Proxy.id == deprecations_24hr_subquery.c.resource_id)
.outerjoin(deprecations_72hr_subquery, Proxy.id == deprecations_72hr_subquery.c.resource_id)
.outerjoin(
deprecations_24hr_subquery,
Proxy.id == deprecations_24hr_subquery.c.resource_id,
)
.outerjoin(
deprecations_72hr_subquery,
Proxy.id == deprecations_72hr_subquery.c.resource_id,
)
.filter(Origin.destroyed.is_(None))
.group_by(Origin.id)
.order_by(desc("total_deprecations_24hr"))
@ -83,26 +103,37 @@ def origins_report():
)
@report.app_template_filter('country_name')
@report.app_template_filter("country_name")
def country_description_filter(country_code):
country = Country.query.filter_by(country_code=country_code).first()
return country.description if country else None
@report.route("/blocks", methods=['GET'])
@report.route("/blocks", methods=["GET"])
def report_blocks() -> ResponseReturnValue:
blocked_today = db.session.query( # type: ignore[no-untyped-call]
Origin.domain_name,
Origin.description,
Proxy.added,
Proxy.deprecated,
Proxy.deprecation_reason
).join(Origin, Origin.id == Proxy.origin_id
).filter(and_(Proxy.deprecated > datetime.now(tz=timezone.utc) - timedelta(days=1),
Proxy.deprecation_reason.like('block_%'))).all()
blocked_today = (
db.session.query( # type: ignore[no-untyped-call]
Origin.domain_name,
Origin.description,
Proxy.added,
Proxy.deprecated,
Proxy.deprecation_reason,
)
.join(Origin, Origin.id == Proxy.origin_id)
.filter(
and_(
Proxy.deprecated > datetime.now(tz=timezone.utc) - timedelta(days=1),
Proxy.deprecation_reason.like("block_%"),
)
)
.all()
)
return render_template("report_blocks.html.j2",
blocked_today=blocked_today,
origins=sorted(origins_report(), key=lambda o: o[1], reverse=True),
countries=sorted(countries_report(), key=lambda c: c[0].risk_level, reverse=True),
)
return render_template(
"report_blocks.html.j2",
blocked_today=blocked_today,
origins=sorted(origins_report(), key=lambda o: o[1], reverse=True),
countries=sorted(
countries_report(), key=lambda c: c[0].risk_level, reverse=True
),
)

View file

@ -9,9 +9,15 @@ bp = Blueprint("smart_proxy", __name__)
@bp.route("/list")
def smart_proxy_list() -> ResponseReturnValue:
instances = SmartProxy.query.filter(SmartProxy.destroyed.is_(None)).order_by(desc(SmartProxy.added)).all()
return render_template("list.html.j2",
section="smart_proxy",
title="Smart Proxy Instances",
item="smart proxy",
items=instances)
instances = (
SmartProxy.query.filter(SmartProxy.destroyed.is_(None))
.order_by(desc(SmartProxy.added))
.all()
)
return render_template(
"list.html.j2",
section="smart_proxy",
title="Smart Proxy Instances",
item="smart proxy",
items=instances,
)

View file

@ -2,13 +2,19 @@ import logging
from typing import Any, List, Optional
import sqlalchemy.exc
from flask import (Blueprint, Response, current_app, flash, redirect,
render_template, url_for)
from flask import (
Blueprint,
Response,
current_app,
flash,
redirect,
render_template,
url_for,
)
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from sqlalchemy import exc
from wtforms import (BooleanField, FileField, SelectField, StringField,
SubmitField)
from wtforms import BooleanField, FileField, SelectField, StringField, SubmitField
from wtforms.validators import DataRequired
from app.brm.static import create_static_origin
@ -22,87 +28,99 @@ bp = Blueprint("static", __name__)
class StaticOriginForm(FlaskForm): # type: ignore
description = StringField(
'Description',
"Description",
validators=[DataRequired()],
description='Enter a brief description of the static website that you are creating in this field. This is '
'also a required field.'
description="Enter a brief description of the static website that you are creating in this field. This is "
"also a required field.",
)
group = SelectField(
'Group',
"Group",
validators=[DataRequired()],
description='Select the group that you want the origin to belong to from the drop-down menu in this field. '
'This is a required field.'
description="Select the group that you want the origin to belong to from the drop-down menu in this field. "
"This is a required field.",
)
storage_cloud_account = SelectField(
'Storage Cloud Account',
"Storage Cloud Account",
validators=[DataRequired()],
description='Select the cloud account that you want the origin to be deployed to from the drop-down menu in '
'this field. This is a required field.'
description="Select the cloud account that you want the origin to be deployed to from the drop-down menu in "
"this field. This is a required field.",
)
source_cloud_account = SelectField(
'Source Cloud Account',
"Source Cloud Account",
validators=[DataRequired()],
description='Select the cloud account that will be used to modify the source repository for the web content '
'for this static origin. This is a required field.'
description="Select the cloud account that will be used to modify the source repository for the web content "
"for this static origin. This is a required field.",
)
source_project = StringField(
'Source Project',
"Source Project",
validators=[DataRequired()],
description='GitLab project path.'
description="GitLab project path.",
)
auto_rotate = BooleanField(
'Auto-Rotate',
"Auto-Rotate",
default=True,
description='Select this field if you want to enable auto-rotation for the mirror. This means that the mirror '
'will automatically redeploy with a new domain name if it is detected to be blocked. This field '
'is optional and is enabled by default.'
description="Select this field if you want to enable auto-rotation for the mirror. This means that the mirror "
"will automatically redeploy with a new domain name if it is detected to be blocked. This field "
"is optional and is enabled by default.",
)
matrix_homeserver = SelectField(
'Matrix Homeserver',
description='Select the Matrix homeserver from the drop-down box to enable Keanu Convene on mirrors of this '
'static origin.'
"Matrix Homeserver",
description="Select the Matrix homeserver from the drop-down box to enable Keanu Convene on mirrors of this "
"static origin.",
)
keanu_convene_path = StringField(
'Keanu Convene Path',
default='talk',
description='Enter the subdirectory to present the Keanu Convene application at on the mirror. This defaults '
'to "talk".'
"Keanu Convene Path",
default="talk",
description="Enter the subdirectory to present the Keanu Convene application at on the mirror. This defaults "
'to "talk".',
)
keanu_convene_logo = FileField(
'Keanu Convene Logo',
description='Logo to use for Keanu Convene'
"Keanu Convene Logo", description="Logo to use for Keanu Convene"
)
keanu_convene_color = StringField(
'Keanu Convene Accent Color',
default='#0047ab',
description='Accent color to use for Keanu Convene (HTML hex code)'
"Keanu Convene Accent Color",
default="#0047ab",
description="Accent color to use for Keanu Convene (HTML hex code)",
)
enable_clean_insights = BooleanField(
'Enable Clean Insights',
description='When enabled, a Clean Insights Measurement Proxy endpoint is deployed on the mirror to allow for '
'submission of results from any of the supported Clean Insights SDKs.'
"Enable Clean Insights",
description="When enabled, a Clean Insights Measurement Proxy endpoint is deployed on the mirror to allow for "
"submission of results from any of the supported Clean Insights SDKs.",
)
submit = SubmitField('Save Changes')
submit = SubmitField("Save Changes")
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
self.storage_cloud_account.choices = [(x.id, f"{x.provider.description} - {x.description}") for x in
CloudAccount.query.filter(
CloudAccount.provider == CloudProvider.AWS).all()]
self.source_cloud_account.choices = [(x.id, f"{x.provider.description} - {x.description}") for x in
CloudAccount.query.filter(
CloudAccount.provider == CloudProvider.GITLAB).all()]
self.matrix_homeserver.choices = [(x, x) for x in current_app.config['MATRIX_HOMESERVERS']]
self.storage_cloud_account.choices = [
(x.id, f"{x.provider.description} - {x.description}")
for x in CloudAccount.query.filter(
CloudAccount.provider == CloudProvider.AWS
).all()
]
self.source_cloud_account.choices = [
(x.id, f"{x.provider.description} - {x.description}")
for x in CloudAccount.query.filter(
CloudAccount.provider == CloudProvider.GITLAB
).all()
]
self.matrix_homeserver.choices = [
(x, x) for x in current_app.config["MATRIX_HOMESERVERS"]
]
@bp.route("/new", methods=['GET', 'POST'])
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
@bp.route("/new", methods=["GET", "POST"])
@bp.route("/new/<group_id>", methods=["GET", "POST"])
def static_new(group_id: Optional[int] = None) -> ResponseReturnValue:
form = StaticOriginForm()
if len(form.source_cloud_account.choices) == 0 or len(form.storage_cloud_account.choices) == 0:
flash("You must add at least one AWS account and at least one GitLab account before creating static origins.",
"warning")
if (
len(form.source_cloud_account.choices) == 0
or len(form.storage_cloud_account.choices) == 0
):
flash(
"You must add at least one AWS account and at least one GitLab account before creating static origins.",
"warning",
)
return redirect(url_for("portal.cloud.cloud_account_list"))
if form.validate_on_submit():
try:
@ -118,16 +136,22 @@ def static_new(group_id: Optional[int] = None) -> ResponseReturnValue:
form.keanu_convene_logo.data,
form.keanu_convene_color.data,
form.enable_clean_insights.data,
True
True,
)
flash(f"Created new static origin #{static.id}.", "success")
return redirect(url_for("portal.static.static_edit", static_id=static.id))
except ValueError as e: # may be returned by create_static_origin and from the int conversion
except (
ValueError
) as e: # may be returned by create_static_origin and from the int conversion
logging.warning(e)
flash("Failed to create new static origin due to an invalid input.", "danger")
flash(
"Failed to create new static origin due to an invalid input.", "danger"
)
return redirect(url_for("portal.static.static_list"))
except exc.SQLAlchemyError as e:
flash("Failed to create new static origin due to a database error.", "danger")
flash(
"Failed to create new static origin due to a database error.", "danger"
)
logging.warning(e)
return redirect(url_for("portal.static.static_list"))
if group_id:
@ -135,24 +159,32 @@ def static_new(group_id: Optional[int] = None) -> ResponseReturnValue:
return render_template("new.html.j2", section="static", form=form)
@bp.route('/edit/<static_id>', methods=['GET', 'POST'])
@bp.route("/edit/<static_id>", methods=["GET", "POST"])
def static_edit(static_id: int) -> ResponseReturnValue:
static_origin: Optional[StaticOrigin] = StaticOrigin.query.filter(StaticOrigin.id == static_id).first()
static_origin: Optional[StaticOrigin] = StaticOrigin.query.filter(
StaticOrigin.id == static_id
).first()
if static_origin is None:
return Response(render_template("error.html.j2",
section="static",
header="404 Origin Not Found",
message="The requested static origin could not be found."),
status=404)
form = StaticOriginForm(description=static_origin.description,
group=static_origin.group_id,
storage_cloud_account=static_origin.storage_cloud_account_id,
source_cloud_account=static_origin.source_cloud_account_id,
source_project=static_origin.source_project,
matrix_homeserver=static_origin.matrix_homeserver,
keanu_convene_path=static_origin.keanu_convene_path,
auto_rotate=static_origin.auto_rotate,
enable_clean_insights=bool(static_origin.clean_insights_backend))
return Response(
render_template(
"error.html.j2",
section="static",
header="404 Origin Not Found",
message="The requested static origin could not be found.",
),
status=404,
)
form = StaticOriginForm(
description=static_origin.description,
group=static_origin.group_id,
storage_cloud_account=static_origin.storage_cloud_account_id,
source_cloud_account=static_origin.source_cloud_account_id,
source_project=static_origin.source_project,
matrix_homeserver=static_origin.matrix_homeserver,
keanu_convene_path=static_origin.keanu_convene_path,
auto_rotate=static_origin.auto_rotate,
enable_clean_insights=bool(static_origin.clean_insights_backend),
)
form.group.render_kw = {"disabled": ""}
form.storage_cloud_account.render_kw = {"disabled": ""}
form.source_cloud_account.render_kw = {"disabled": ""}
@ -167,50 +199,68 @@ def static_edit(static_id: int) -> ResponseReturnValue:
form.keanu_convene_logo.data,
form.keanu_convene_color.data,
form.enable_clean_insights.data,
True
True,
)
flash("Saved changes to group.", "success")
except ValueError as e: # may be returned by create_static_origin and from the int conversion
except (
ValueError
) as e: # may be returned by create_static_origin and from the int conversion
logging.warning(e)
flash("An error occurred saving the changes to the static origin due to an invalid input.", "danger")
flash(
"An error occurred saving the changes to the static origin due to an invalid input.",
"danger",
)
except exc.SQLAlchemyError as e:
logging.warning(e)
flash("An error occurred saving the changes to the static origin due to a database error.", "danger")
flash(
"An error occurred saving the changes to the static origin due to a database error.",
"danger",
)
try:
origin = Origin.query.filter_by(domain_name=static_origin.origin_domain_name).one()
origin = Origin.query.filter_by(
domain_name=static_origin.origin_domain_name
).one()
proxies = origin.proxies
except sqlalchemy.exc.NoResultFound:
proxies = []
return render_template("static.html.j2",
section="static",
static=static_origin, form=form,
proxies=proxies)
return render_template(
"static.html.j2",
section="static",
static=static_origin,
form=form,
proxies=proxies,
)
@bp.route("/list")
def static_list() -> ResponseReturnValue:
statics: List[StaticOrigin] = StaticOrigin.query.order_by(StaticOrigin.description).all()
return render_template("list.html.j2",
section="static",
title="Static Origins",
item="static",
new_link=url_for("portal.static.static_new"),
items=statics
)
statics: List[StaticOrigin] = StaticOrigin.query.order_by(
StaticOrigin.description
).all()
return render_template(
"list.html.j2",
section="static",
title="Static Origins",
item="static",
new_link=url_for("portal.static.static_new"),
items=statics,
)
@bp.route("/destroy/<static_id>", methods=['GET', 'POST'])
@bp.route("/destroy/<static_id>", methods=["GET", "POST"])
def static_destroy(static_id: int) -> ResponseReturnValue:
static = StaticOrigin.query.filter(StaticOrigin.id == static_id, StaticOrigin.destroyed.is_(None)).first()
static = StaticOrigin.query.filter(
StaticOrigin.id == static_id, StaticOrigin.destroyed.is_(None)
).first()
if static is None:
return response_404("The requested static origin could not be found.")
return view_lifecycle(
header=f"Destroy static origin {static.description}",
message=static.description,
success_message="All proxies from the destroyed static origin will shortly be destroyed at their providers, "
"and the static content will be removed from the cloud provider.",
"and the static content will be removed from the cloud provider.",
success_view="portal.static.static_list",
section="static",
resource=static,
action="destroy"
action="destroy",
)

View file

@ -17,24 +17,30 @@ bp = Blueprint("storage", __name__)
_SECTION_TEMPLATE_VARS = {
"section": "automation",
"help_url": "https://bypass.censorship.guide/user/automation.html"
"help_url": "https://bypass.censorship.guide/user/automation.html",
}
class EditStorageForm(FlaskForm): # type: ignore
force_unlock = BooleanField('Force Unlock')
submit = SubmitField('Save Changes')
force_unlock = BooleanField("Force Unlock")
submit = SubmitField("Save Changes")
@bp.route('/edit/<storage_key>', methods=['GET', 'POST'])
@bp.route("/edit/<storage_key>", methods=["GET", "POST"])
def storage_edit(storage_key: str) -> ResponseReturnValue:
storage: Optional[TerraformState] = TerraformState.query.filter(TerraformState.key == storage_key).first()
storage: Optional[TerraformState] = TerraformState.query.filter(
TerraformState.key == storage_key
).first()
if storage is None:
return Response(render_template("error.html.j2",
header="404 Storage Key Not Found",
message="The requested storage could not be found.",
**_SECTION_TEMPLATE_VARS),
status=404)
return Response(
render_template(
"error.html.j2",
header="404 Storage Key Not Found",
message="The requested storage could not be found.",
**_SECTION_TEMPLATE_VARS
),
status=404,
)
form = EditStorageForm()
if form.validate_on_submit():
if form.force_unlock.data:
@ -45,17 +51,16 @@ def storage_edit(storage_key: str) -> ResponseReturnValue:
flash("Storage has been force unlocked.", "success")
except exc.SQLAlchemyError:
flash("An error occurred unlocking the storage.", "danger")
return render_template("storage.html.j2",
storage=storage,
form=form,
**_SECTION_TEMPLATE_VARS)
return render_template(
"storage.html.j2", storage=storage, form=form, **_SECTION_TEMPLATE_VARS
)
@bp.route("/kick/<automation_id>", methods=['GET', 'POST'])
@bp.route("/kick/<automation_id>", methods=["GET", "POST"])
def automation_kick(automation_id: int) -> ResponseReturnValue:
automation = Automation.query.filter(
Automation.id == automation_id,
Automation.destroyed.is_(None)).first()
Automation.id == automation_id, Automation.destroyed.is_(None)
).first()
if automation is None:
return response_404("The requested bridge configuration could not be found.")
return view_lifecycle(
@ -65,5 +70,5 @@ def automation_kick(automation_id: int) -> ResponseReturnValue:
success_view="portal.automation.automation_list",
success_message="This automation job will next run within 1 minute.",
resource=automation,
action="kick"
action="kick",
)

View file

@ -9,19 +9,21 @@ from app.models.activity import Activity
def response_404(message: str) -> ResponseReturnValue:
return Response(render_template("error.html.j2",
header="404 Not Found",
message=message))
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) -> ResponseReturnValue:
def view_lifecycle(
*,
header: str,
message: str,
success_message: str,
success_view: str,
section: str,
resource: AbstractResource,
action: str,
) -> ResponseReturnValue:
form = LifecycleForm()
if action == "destroy":
form.submit.render_kw = {"class": "btn btn-danger"}
@ -41,19 +43,17 @@ def view_lifecycle(*,
return redirect(url_for("portal.portal_home"))
activity = Activity(
activity_type="lifecycle",
text=f"Portal action: {message}. {success_message}"
text=f"Portal action: {message}. {success_message}",
)
db.session.add(activity)
db.session.commit()
activity.notify()
flash(success_message, "success")
return redirect(url_for(success_view))
return render_template("lifecycle.html.j2",
header=header,
message=message,
section=section,
form=form)
return render_template(
"lifecycle.html.j2", header=header, message=message, section=section, form=form
)
class LifecycleForm(FlaskForm): # type: ignore
submit = SubmitField('Confirm')
submit = SubmitField("Confirm")

View file

@ -1,8 +1,7 @@
from datetime import datetime, timezone
from typing import Optional
from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask import Blueprint, Response, flash, redirect, render_template, url_for
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from sqlalchemy import exc
@ -26,47 +25,54 @@ def webhook_format_name(key: str) -> str:
class NewWebhookForm(FlaskForm): # type: ignore
description = StringField('Description', validators=[DataRequired()])
format = SelectField('Format', choices=[
("telegram", "Telegram"),
("matrix", "Matrix")
], validators=[DataRequired()])
url = StringField('URL', validators=[DataRequired()])
submit = SubmitField('Save Changes')
description = StringField("Description", validators=[DataRequired()])
format = SelectField(
"Format",
choices=[("telegram", "Telegram"), ("matrix", "Matrix")],
validators=[DataRequired()],
)
url = StringField("URL", validators=[DataRequired()])
submit = SubmitField("Save Changes")
@bp.route("/new", methods=['GET', 'POST'])
@bp.route("/new", methods=["GET", "POST"])
def webhook_new() -> ResponseReturnValue:
form = NewWebhookForm()
if form.validate_on_submit():
webhook = Webhook(
description=form.description.data,
format=form.format.data,
url=form.url.data
url=form.url.data,
)
try:
db.session.add(webhook)
db.session.commit()
flash(f"Created new webhook {webhook.url}.", "success")
return redirect(url_for("portal.webhook.webhook_edit", webhook_id=webhook.id))
return redirect(
url_for("portal.webhook.webhook_edit", webhook_id=webhook.id)
)
except exc.SQLAlchemyError:
flash("Failed to create new webhook.", "danger")
return redirect(url_for("portal.webhook.webhook_list"))
return render_template("new.html.j2", section="webhook", form=form)
@bp.route('/edit/<webhook_id>', methods=['GET', 'POST'])
@bp.route("/edit/<webhook_id>", methods=["GET", "POST"])
def webhook_edit(webhook_id: int) -> ResponseReturnValue:
webhook = Webhook.query.filter(Webhook.id == webhook_id).first()
if webhook is None:
return Response(render_template("error.html.j2",
section="webhook",
header="404 Webhook Not Found",
message="The requested webhook could not be found."),
status=404)
form = NewWebhookForm(description=webhook.description,
format=webhook.format,
url=webhook.url)
return Response(
render_template(
"error.html.j2",
section="webhook",
header="404 Webhook Not Found",
message="The requested webhook could not be found.",
),
status=404,
)
form = NewWebhookForm(
description=webhook.description, format=webhook.format, url=webhook.url
)
if form.validate_on_submit():
webhook.description = form.description.data
webhook.format = form.description.data
@ -77,26 +83,29 @@ def webhook_edit(webhook_id: int) -> ResponseReturnValue:
flash("Saved changes to webhook.", "success")
except exc.SQLAlchemyError:
flash("An error occurred saving the changes to the webhook.", "danger")
return render_template("edit.html.j2",
section="webhook",
title="Edit Webhook",
item=webhook, form=form)
return render_template(
"edit.html.j2", section="webhook", title="Edit Webhook", item=webhook, form=form
)
@bp.route("/list")
def webhook_list() -> ResponseReturnValue:
webhooks = Webhook.query.all()
return render_template("list.html.j2",
section="webhook",
title="Webhooks",
item="webhook",
new_link=url_for("portal.webhook.webhook_new"),
items=webhooks)
return render_template(
"list.html.j2",
section="webhook",
title="Webhooks",
item="webhook",
new_link=url_for("portal.webhook.webhook_new"),
items=webhooks,
)
@bp.route("/destroy/<webhook_id>", methods=['GET', 'POST'])
@bp.route("/destroy/<webhook_id>", methods=["GET", "POST"])
def webhook_destroy(webhook_id: int) -> ResponseReturnValue:
webhook: Optional[Webhook] = Webhook.query.filter(Webhook.id == webhook_id, Webhook.destroyed.is_(None)).first()
webhook: Optional[Webhook] = Webhook.query.filter(
Webhook.id == webhook_id, Webhook.destroyed.is_(None)
).first()
if webhook is None:
return response_404("The requested webhook could not be found.")
return view_lifecycle(
@ -106,5 +115,5 @@ def webhook_destroy(webhook_id: int) -> ResponseReturnValue:
success_view="portal.webhook.webhook_list",
section="webhook",
resource=webhook,
action="destroy"
action="destroy",
)