aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--webmentions_ssg/__init__.py3
-rw-r--r--webmentions_ssg/forms/__init__.py4
-rw-r--r--webmentions_ssg/templates/received.html97
-rw-r--r--webmentions_ssg/views.py72
4 files changed, 165 insertions, 11 deletions
diff --git a/webmentions_ssg/__init__.py b/webmentions_ssg/__init__.py
index f66e478..b799678 100644
--- a/webmentions_ssg/__init__.py
+++ b/webmentions_ssg/__init__.py
@@ -14,6 +14,7 @@ from flask import Flask
from flask_bootstrap import Bootstrap5
from flask_migrate import Migrate, upgrade
from flask_sqlalchemy import SQLAlchemy
+from flask_wtf.csrf import CSRFProtect
from sqlalchemy.orm import DeclarativeBase
from webmentions_ssg.config import DefaultConfig, DevelopmentConfig, ProductionConfig
@@ -28,6 +29,7 @@ class Base(DeclarativeBase):
pass
+CSRF = CSRFProtect()
BOOTSTRAP = Bootstrap5()
DATABASE = SQLAlchemy(model_class=Base)
MIGRATE = Migrate()
@@ -94,6 +96,7 @@ def create_app(config_class: type[DefaultConfig] | None = None) -> Flask:
DATABASE.init_app(app)
MIGRATE.init_app(app, db=DATABASE)
+ CSRF.init_app(app)
from .auth import AUTH
diff --git a/webmentions_ssg/forms/__init__.py b/webmentions_ssg/forms/__init__.py
index 0c54999..c386eb1 100644
--- a/webmentions_ssg/forms/__init__.py
+++ b/webmentions_ssg/forms/__init__.py
@@ -41,3 +41,7 @@ class EndpointForm(FlaskForm):
AllowedHostname(),
],
)
+
+
+class AdminActionForm(FlaskForm):
+ submit = SubmitField("Confirm")
diff --git a/webmentions_ssg/templates/received.html b/webmentions_ssg/templates/received.html
index ea74557..64f225e 100644
--- a/webmentions_ssg/templates/received.html
+++ b/webmentions_ssg/templates/received.html
@@ -1,4 +1,5 @@
{% extends "base.html" %}
+{% from "bootstrap5/form.html" import render_form %}
{% from "bootstrap5/pagination.html" import render_pagination %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
@@ -16,16 +17,20 @@
<th scope="col">Received</th>
<th scope="col">Updated</th>
<th scope="col">Reason</th>
+ <th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for webmention in webmentions %}
- {% if webmention.status == "verified" %}
+ {% if webmention.status == "received" %}
+ {% set row_class = "table-info" %}
+ {% set display_status = "Pending" %}
+ {% elif webmention.status == "verifying" %}
+ {% set row_class = "table-warning" %}
+ {% set display_status = "Verifying" %}
+ {% elif webmention.status == "verified" %}
{% set row_class = "table-success" %}
{% set display_status = "Verified" %}
- {% elif webmention.status in ("received", "verifying") %}
- {% set row_class = "table-warning" %}
- {% set display_status = "Pending" %}
{% else %}
{% set row_class = "table-danger" %}
{% set display_status = "Declined" %}
@@ -51,19 +56,93 @@
</time>
</td>
<td>{{ webmention.failure_reason or "—" }}</td>
+ <td class="text-nowrap">
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ data-bs-toggle="modal"
+ data-bs-target="#confirmation-modal"
+ data-action="{{ url_for('root.reverify_received_webmention', identifier=webmention.uuid, page=webmentions.page) }}"
+ data-title="Reverify webmention?"
+ data-message="The webmention will be fetched and verified again."
+ data-confirm-label="Reverify"
+ data-confirm-class="btn-primary">Reverify</button>
+ <button type="button"
+ class="btn btn-sm btn-danger"
+ data-bs-toggle="modal"
+ data-bs-target="#confirmation-modal"
+ data-action="{{ url_for('root.delete_received_webmention', identifier=webmention.uuid, page=webmentions.page) }}"
+ data-title="Delete webmention?"
+ data-message="This permanently deletes the webmention and cannot be undone."
+ data-confirm-label="Delete"
+ data-confirm-class="btn-danger">Delete</button>
+ </td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if webmentions.pages > 1 %}
- <div class="mt-3">
- {{ render_pagination(webmentions,
- align="center",
- size="sm") }}
- </div>
+ <div class="mt-3">{{ render_pagination(webmentions) }}</div>
{% endif %}
+ <div class="modal fade"
+ id="confirmation-modal"
+ tabindex="-1"
+ aria-labelledby="confirmation-modal-title"
+ aria-hidden="true">
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h1 class="modal-title fs-5" id="confirmation-modal-title"></h1>
+ <button type="button"
+ class="btn-close"
+ data-bs-dismiss="modal"
+ aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <p id="confirmation-modal-message" class="mb-0"></p>
+ </div>
+ <div class="modal-footer">
+ {{ render_form(action_form,
+ id="confirmation-modal-form",
+ button_style="primary",
+ form_group_classes="mb-0") }}
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
+ </div>
+ </div>
+ </div>
+ </div>
{% else %}
<p class="text-body-secondary">No Webmentions have been received yet.</p>
{% endif %}
{% endblock %}
+{% block scripts %}
+ {{ super() }}
+ <script>
+ const confirmationModal = document.getElementById(
+ "confirmation-modal"
+ );
+
+ confirmationModal.addEventListener("show.bs.modal", (event) => {
+ const button = event.relatedTarget;
+
+ const form = confirmationModal.querySelector(
+ "#confirmation-modal-form"
+ );
+ const title = confirmationModal.querySelector(
+ "#confirmation-modal-title"
+ );
+ const message = confirmationModal.querySelector(
+ "#confirmation-modal-message"
+ );
+ const submit = form.querySelector("#submit");
+
+ form.action = button.dataset.action;
+
+ title.textContent = button.dataset.title;
+ message.textContent = button.dataset.message;
+
+ submit.value = button.dataset.confirmLabel;
+ submit.className = `btn ${button.dataset.confirmClass}`;
+ });
+ </script>
+{% endblock %}
diff --git a/webmentions_ssg/views.py b/webmentions_ssg/views.py
index 5408fa4..b52ae07 100644
--- a/webmentions_ssg/views.py
+++ b/webmentions_ssg/views.py
@@ -17,8 +17,8 @@ from flask import (
from flask.typing import ResponseReturnValue
from flask_login import current_user, login_required, login_user, logout_user
+from . import CSRF, forms
from . import DATABASE as db
-from . import forms
from .models import ReceivedWebmention, User
from .tasks.receiver import verify_webmention
@@ -74,10 +74,78 @@ def received() -> ResponseReturnValue:
"received.html",
title="Received Webmentions",
webmentions=webmentions,
+ action_form=forms.AdminActionForm(),
)
-@root_page.route("/endpoint", methods=["POST"])
+@root_page.post("/received/<uuid:identifier>/delete")
+@login_required
+def delete_received_webmention(
+ identifier: uuid.UUID,
+) -> ResponseReturnValue:
+ form = forms.AdminActionForm()
+
+ if not form.validate_on_submit():
+ return abort(400)
+
+ webmention = db.session.get(
+ ReceivedWebmention,
+ identifier,
+ )
+
+ if webmention is None:
+ return abort(404)
+
+ db.session.delete(webmention)
+ db.session.commit()
+
+ flash(f"Webmention {webmention.uuid} deleted.", "success")
+
+ return redirect(
+ url_for(
+ "root.received",
+ page=request.args.get("page", 1, type=int),
+ )
+ )
+
+
+@root_page.post("/received/<uuid:identifier>/reverify")
+@login_required
+def reverify_received_webmention(
+ identifier: uuid.UUID,
+) -> ResponseReturnValue:
+ form = forms.AdminActionForm()
+
+ if not form.validate_on_submit():
+ return abort(400)
+
+ webmention = db.session.get(
+ ReceivedWebmention,
+ identifier,
+ )
+
+ if webmention is None:
+ return abort(404)
+
+ webmention.status = "received"
+ webmention.failure_reason = None
+
+ db.session.commit()
+
+ verify_webmention(identifier)
+
+ flash(f"Webmention {webmention.uuid} queued for reverification.", "success")
+
+ return redirect(
+ url_for(
+ "root.received",
+ page=request.args.get("page", 1, type=int),
+ )
+ )
+
+
+@root_page.post("/endpoint")
+@CSRF.exempt
def endpoint() -> ResponseReturnValue:
form = forms.EndpointForm(meta={"csrf": False})