diff options
Diffstat (limited to '')
| -rw-r--r-- | webmentions_ssg/__init__.py | 3 | ||||
| -rw-r--r-- | webmentions_ssg/forms/__init__.py | 4 | ||||
| -rw-r--r-- | webmentions_ssg/templates/received.html | 97 | ||||
| -rw-r--r-- | webmentions_ssg/views.py | 72 |
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}) |
