portal: additional buttons on list pages
link to onion services page from origin page link to previews of distribution lists from lists
This commit is contained in:
parent
928edc46c2
commit
d54fae7423
9 changed files with 151 additions and 74 deletions
|
@ -4,7 +4,6 @@ from typing import Dict, List
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from tldextract import extract
|
from tldextract import extract
|
||||||
|
|
||||||
from app import app
|
|
||||||
from app.models.base import Group
|
from app.models.base import Group
|
||||||
from app.models.mirrors import Proxy
|
from app.models.mirrors import Proxy
|
||||||
|
|
||||||
|
@ -31,6 +30,7 @@ class MirrorMapping(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
def mirror_mapping():
|
def mirror_mapping():
|
||||||
|
from app import app
|
||||||
return MirrorMapping(
|
return MirrorMapping(
|
||||||
version="1.1",
|
version="1.1",
|
||||||
mappings={
|
mappings={
|
||||||
|
|
|
@ -24,11 +24,25 @@ class Group(AbstractConfiguration):
|
||||||
class MirrorList(AbstractConfiguration):
|
class MirrorList(AbstractConfiguration):
|
||||||
provider = db.Column(db.String(255), nullable=False)
|
provider = db.Column(db.String(255), nullable=False)
|
||||||
format = db.Column(db.String(20), nullable=False)
|
format = db.Column(db.String(20), nullable=False)
|
||||||
|
# obfuscate = db.Column(db.Boolean(), nullable=False)
|
||||||
container = db.Column(db.String(255), nullable=False)
|
container = db.Column(db.String(255), nullable=False)
|
||||||
branch = db.Column(db.String(255), nullable=False)
|
branch = db.Column(db.String(255), nullable=False)
|
||||||
role = db.Column(db.String(255), nullable=True)
|
role = db.Column(db.String(255), nullable=True)
|
||||||
filename = db.Column(db.String(255), nullable=False)
|
filename = db.Column(db.String(255), nullable=False)
|
||||||
|
|
||||||
|
providers_supported = {
|
||||||
|
"github": "GitHub",
|
||||||
|
"gitlab": "GitLab",
|
||||||
|
"s3": "AWS S3",
|
||||||
|
}
|
||||||
|
|
||||||
|
formats_supported = {
|
||||||
|
"bc2": "Bypass Censorship v2",
|
||||||
|
"bc3": "Bypass Censorship v3",
|
||||||
|
"bca": "Bypass Censorship Analytics",
|
||||||
|
"bridgelines": "Tor Bridge Lines"
|
||||||
|
}
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self):
|
||||||
self.destroyed = datetime.utcnow()
|
self.destroyed = datetime.utcnow()
|
||||||
self.updated = datetime.utcnow()
|
self.updated = datetime.utcnow()
|
||||||
|
|
|
@ -4,7 +4,7 @@ from flask import Blueprint, render_template, request
|
||||||
from sqlalchemy import desc, or_
|
from sqlalchemy import desc, or_
|
||||||
|
|
||||||
from app.models.alarms import Alarm
|
from app.models.alarms import Alarm
|
||||||
from app import Origin, Proxy
|
from app.models.mirrors import Origin, Proxy
|
||||||
from app.models.base import Group
|
from app.models.base import Group
|
||||||
from app.portal.list import NewMirrorListForm
|
from app.portal.list import NewMirrorListForm
|
||||||
from app.portal.automation import bp as automation
|
from app.portal.automation import bp as automation
|
||||||
|
@ -76,5 +76,3 @@ def view_alarms():
|
||||||
section="alarm",
|
section="alarm",
|
||||||
title="Alarms",
|
title="Alarms",
|
||||||
items=alarms)
|
items=alarms)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,32 @@
|
||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import render_template, url_for, flash, redirect, Blueprint
|
from flask import render_template, url_for, flash, redirect, Blueprint, Response
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from sqlalchemy import exc
|
from sqlalchemy import exc
|
||||||
from wtforms import SelectField, StringField, SubmitField
|
from wtforms import SelectField, StringField, SubmitField
|
||||||
from wtforms.validators import DataRequired
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
from app import db
|
from app.extensions import db
|
||||||
|
from app.lists.bc2 import mirror_sites
|
||||||
|
from app.lists.bridgelines import bridgelines
|
||||||
|
from app.lists.mirror_mapping import mirror_mapping
|
||||||
from app.models.base import MirrorList
|
from app.models.base import MirrorList
|
||||||
from app.portal.util import response_404, view_lifecycle
|
from app.portal.util import response_404, view_lifecycle
|
||||||
|
|
||||||
bp = Blueprint("list", __name__)
|
bp = Blueprint("list", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.app_template_filter("provider_name")
|
||||||
|
def list_provider_name(s: str) -> str:
|
||||||
|
return MirrorList.providers_supported.get(s, "Unknown")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.app_template_filter("format_name")
|
||||||
|
def list_format_name(s: str) -> str:
|
||||||
|
return MirrorList.formats_supported.get(s, "Unknown")
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/list')
|
@bp.route('/list')
|
||||||
def list_list():
|
def list_list():
|
||||||
lists = MirrorList.query.filter(MirrorList.destroyed == None).all()
|
lists = MirrorList.query.filter(MirrorList.destroyed == None).all()
|
||||||
|
@ -21,7 +35,27 @@ def list_list():
|
||||||
title="Mirror Lists",
|
title="Mirror Lists",
|
||||||
item="mirror list",
|
item="mirror list",
|
||||||
new_link=url_for("portal.list.list_new"),
|
new_link=url_for("portal.list.list_new"),
|
||||||
items=lists)
|
items=lists,
|
||||||
|
extra_buttons=[
|
||||||
|
{
|
||||||
|
"link": url_for("portal.list.list_preview", format_=k),
|
||||||
|
"text": f"Preview {v}",
|
||||||
|
"style": "secondary"
|
||||||
|
}
|
||||||
|
for k, v in MirrorList.formats_supported.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/preview/<format_>')
|
||||||
|
def list_preview(format_: str):
|
||||||
|
if format_ == "bca":
|
||||||
|
return Response(json.dumps(mirror_mapping()), content_type="application/json")
|
||||||
|
if format_ == "bc2":
|
||||||
|
return Response(json.dumps(mirror_sites()), content_type="application/json")
|
||||||
|
if format_ == "bridgelines":
|
||||||
|
return Response(json.dumps(bridgelines()), 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'])
|
||||||
|
@ -44,17 +78,8 @@ def list_destroy(list_id: int):
|
||||||
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
|
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
|
||||||
def list_new(group_id=None):
|
def list_new(group_id=None):
|
||||||
form = NewMirrorListForm()
|
form = NewMirrorListForm()
|
||||||
form.provider.choices = [
|
form.provider.choices = [(k, v) for k, v in MirrorList.providers_supported]
|
||||||
("github", "GitHub"),
|
form.format.choices = [(k, v) for k, v in MirrorList.formats_supported]
|
||||||
("gitlab", "GitLab"),
|
|
||||||
("s3", "AWS S3"),
|
|
||||||
]
|
|
||||||
form.format.choices = [
|
|
||||||
("bc2", "Bypass Censorship v2"),
|
|
||||||
("bc3", "Bypass Censorship v3"),
|
|
||||||
("bca", "Bypass Censorship Analytics"),
|
|
||||||
("bridgelines", "Tor Bridge Lines")
|
|
||||||
]
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
list_ = MirrorList()
|
list_ = MirrorList()
|
||||||
list_.provider = form.provider.data
|
list_.provider = form.provider.data
|
||||||
|
|
|
@ -89,10 +89,15 @@ def origin_list():
|
||||||
origins = Origin.query.order_by(Origin.domain_name).all()
|
origins = Origin.query.order_by(Origin.domain_name).all()
|
||||||
return render_template("list.html.j2",
|
return render_template("list.html.j2",
|
||||||
section="origin",
|
section="origin",
|
||||||
title="Origins",
|
title="Web Origins",
|
||||||
item="origin",
|
item="origin",
|
||||||
new_link=url_for("portal.origin.origin_new"),
|
new_link=url_for("portal.origin.origin_new"),
|
||||||
items=origins)
|
items=origins,
|
||||||
|
extra_buttons=[{
|
||||||
|
"link": url_for("portal.origin.origin_onion"),
|
||||||
|
"text": "Onion services",
|
||||||
|
"style": "onion"
|
||||||
|
}])
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/onion")
|
@bp.route("/onion")
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
body {
|
body {
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feather {
|
.feather {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -13,56 +13,56 @@ body {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
/* rtl:raw:
|
/* rtl:raw:
|
||||||
right: 0;
|
right: 0;
|
||||||
*/
|
*/
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
/* rtl:remove */
|
/* rtl:remove */
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 100; /* Behind the navbar */
|
z-index: 100; /* Behind the navbar */
|
||||||
padding: 48px 0 0; /* Height of navbar */
|
padding: 48px 0 0; /* Height of navbar */
|
||||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
top: 5rem;
|
top: 5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-sticky {
|
.sidebar-sticky {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: calc(100vh - 48px);
|
height: calc(100vh - 48px);
|
||||||
padding-top: .5rem;
|
padding-top: .5rem;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link {
|
.sidebar .nav-link {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link .feather {
|
.sidebar .nav-link .feather {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
color: #727272;
|
color: #727272;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link.active {
|
.sidebar .nav-link.active {
|
||||||
color: #2470dc;
|
color: #2470dc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link:hover .feather,
|
.sidebar .nav-link:hover .feather,
|
||||||
.sidebar .nav-link.active .feather {
|
.sidebar .nav-link.active .feather {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-heading {
|
.sidebar-heading {
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -70,31 +70,63 @@ body {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
padding-top: .75rem;
|
padding-top: .75rem;
|
||||||
padding-bottom: .75rem;
|
padding-bottom: .75rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
background-color: rgba(0, 0, 0, .25);
|
background-color: rgba(0, 0, 0, .25);
|
||||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
|
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar-toggler {
|
.navbar .navbar-toggler {
|
||||||
top: .25rem;
|
top: .25rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .form-control {
|
.navbar .form-control {
|
||||||
padding: .75rem 1rem;
|
padding: .75rem 1rem;
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control-dark {
|
.form-control-dark {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: rgba(255, 255, 255, .1);
|
background-color: rgba(255, 255, 255, .1);
|
||||||
border-color: rgba(255, 255, 255, .1);
|
border-color: rgba(255, 255, 255, .1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control-dark:focus {
|
.form-control-dark:focus {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-onion {
|
||||||
|
background-color: #7d4698;
|
||||||
|
border-color: #7d4698;
|
||||||
|
color: #fff
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-check:focus + .btn-onion, .btn-onion:focus, .btn-onion:hover {
|
||||||
|
background-color: #6a3c81;
|
||||||
|
border-color: #64387a;
|
||||||
|
color: #fff
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-check:focus + .btn-onion, .btn-onion:focus {
|
||||||
|
box-shadow: 0 0 0 .25rem rgba(145, 98, 167, .5)
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-check:active + .btn-onion, .btn-check:checked + .btn-onion, .btn-onion.active, .btn-onion:active, .show > .btn-onion.dropdown-toggle {
|
||||||
|
background-color: #64387a;
|
||||||
|
border-color: #5e3572;
|
||||||
|
color: #fff
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-check:active + .btn-onion:focus, .btn-check:checked + .btn-onion:focus, .btn-onion.active:focus, .btn-onion:active:focus, .show > .btn-onion.dropdown-toggle:focus {
|
||||||
|
box-shadow: 0 0 0 .25rem rgba(145, 98, 167, .5)
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-onion.disabled, .btn-onion:disabled {
|
||||||
|
background-color: #7d4698;
|
||||||
|
border-color: #7d4698;
|
||||||
|
color: #fff
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link{% if section == "blocklist" %} active{% endif %}"
|
<a class="nav-link{% if section == "blocklist" %} active{% endif %} disabled text-secondary"
|
||||||
href="#">
|
href="#">
|
||||||
{{ icon("file-earmark-excel") }} Block Lists
|
{{ icon("file-earmark-excel") }} Block Lists
|
||||||
</a>
|
</a>
|
||||||
|
@ -119,13 +119,13 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link{% if section == "smart_proxy" %} active{% endif %}"
|
<a class="nav-link{% if section == "smart_proxy" %} active{% endif %} disabled text-secondary"
|
||||||
href="#">
|
href="#">
|
||||||
{{ icon("globe") }} Smart Proxy Instances
|
{{ icon("globe") }} Smart Proxy Instances
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link{% if section == "eotk" %} active{% endif %}"
|
<a class="nav-link{% if section == "eotk" %} active{% endif %} disabled text-secondary"
|
||||||
href="#">
|
href="#">
|
||||||
{{ icon("server") }} EOTK Instances
|
{{ icon("server") }} EOTK Instances
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -5,7 +5,10 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="h2 mt-3">{{ title }}</h1>
|
<h1 class="h2 mt-3">{{ title }}</h1>
|
||||||
{% if new_link %}
|
{% if new_link %}
|
||||||
<a href="{{ new_link }}" class="btn btn-success">Create new {{ item }}</a>
|
<a href="{{ new_link }}" class="btn btn-success">Create new {{ item }}</a>
|
||||||
|
{% for extra_button in extra_buttons %}
|
||||||
|
<a href="{{ extra_button.link }}" class="btn btn-{{ extra_button.style }}">{{ extra_button.text }}</a>
|
||||||
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if section == "alarm" %}
|
{% if section == "alarm" %}
|
||||||
{{ alarms_table(items) }}
|
{{ alarms_table(items) }}
|
||||||
|
|
|
@ -465,9 +465,9 @@
|
||||||
{% for list in mirrorlists %}
|
{% for list in mirrorlists %}
|
||||||
{% if not list.destroyed %}
|
{% if not list.destroyed %}
|
||||||
<tr class="align-middle">
|
<tr class="align-middle">
|
||||||
<td>{{ list.provider }}</td>
|
<td>{{ list.provider | provider_name }}</td>
|
||||||
<td>{{ list.format }}</td>
|
<td>{{ list.format | format_name }}</td>
|
||||||
<td>{{ list.url() }}</td>
|
<td><a href="{{ list.url() }}">{{ list.url() }}</a></td>
|
||||||
<td>{{ list.description }}</td>
|
<td>{{ list.description }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for("portal.list.list_destroy", list_id=list.id) }}"
|
<a href="{{ url_for("portal.list.list_destroy", list_id=list.id) }}"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue