lint: reformat python code with black
This commit is contained in:
parent
331beb01b4
commit
a406a7974b
88 changed files with 2579 additions and 1608 deletions
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue