splent-feature-recaptcha 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- splent_feature_recaptcha-0.2.1/MANIFEST.in +8 -0
- splent_feature_recaptcha-0.2.1/PKG-INFO +5 -0
- splent_feature_recaptcha-0.2.1/pyproject.toml +55 -0
- splent_feature_recaptcha-0.2.1/setup.cfg +4 -0
- splent_feature_recaptcha-0.2.1/src/splent_feature_recaptcha.egg-info/PKG-INFO +5 -0
- splent_feature_recaptcha-0.2.1/src/splent_feature_recaptcha.egg-info/SOURCES.txt +14 -0
- splent_feature_recaptcha-0.2.1/src/splent_feature_recaptcha.egg-info/dependency_links.txt +1 -0
- splent_feature_recaptcha-0.2.1/src/splent_feature_recaptcha.egg-info/top_level.txt +1 -0
- splent_feature_recaptcha-0.2.1/src/splent_io/splent_feature_recaptcha/__init__.py +22 -0
- splent_feature_recaptcha-0.2.1/src/splent_io/splent_feature_recaptcha/commands.py +21 -0
- splent_feature_recaptcha-0.2.1/src/splent_io/splent_feature_recaptcha/config.py +21 -0
- splent_feature_recaptcha-0.2.1/src/splent_io/splent_feature_recaptcha/hooks.py +23 -0
- splent_feature_recaptcha-0.2.1/src/splent_io/splent_feature_recaptcha/routes.py +28 -0
- splent_feature_recaptcha-0.2.1/src/splent_io/splent_feature_recaptcha/services.py +70 -0
- splent_feature_recaptcha-0.2.1/src/splent_io/splent_feature_recaptcha/signals.py +24 -0
- splent_feature_recaptcha-0.2.1/src/splent_io/splent_feature_recaptcha/templates/recaptcha/admin/settings.html +53 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Compiled frontend bundles
|
|
2
|
+
recursive-include src/splent_io/splent_feature_recaptcha/assets/dist *.js *.css *.map
|
|
3
|
+
|
|
4
|
+
# Jinja templates (feature views + hook fragments)
|
|
5
|
+
recursive-include src/splent_io/splent_feature_recaptcha/templates *.html
|
|
6
|
+
|
|
7
|
+
# Alembic migration config and scripts
|
|
8
|
+
recursive-include src/splent_io/splent_feature_recaptcha/migrations *.py *.ini *.mako
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80.3.1", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "splent_feature_recaptcha"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.setuptools]
|
|
16
|
+
package-dir = { "" = "src" }
|
|
17
|
+
include-package-data = true
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["src"]
|
|
21
|
+
exclude = ["*.tests", "*.tests.*"]
|
|
22
|
+
|
|
23
|
+
[tool.splent]
|
|
24
|
+
cli_version = "1.11.0"
|
|
25
|
+
|
|
26
|
+
# ── Feature Contract (auto-generated) ────────────────────────────────────────
|
|
27
|
+
# Do not edit manually — re-run `splent feature:contract --write` to refresh.
|
|
28
|
+
# Preserved across refreshes: description, category, tags, env, and
|
|
29
|
+
# requires.features_manual (dependencies invisible to static analysis).
|
|
30
|
+
[tool.splent.contract]
|
|
31
|
+
description = "Google reCAPTCHA protection for comment and contact forms"
|
|
32
|
+
archetype = "light"
|
|
33
|
+
|
|
34
|
+
[tool.splent.contract.provides]
|
|
35
|
+
routes = ["/admin/captcha"]
|
|
36
|
+
blueprints = []
|
|
37
|
+
models = []
|
|
38
|
+
commands = ["hello"]
|
|
39
|
+
hooks = ["layout.authenticated_sidebar"]
|
|
40
|
+
services = ["CaptchaService"]
|
|
41
|
+
signals = []
|
|
42
|
+
translations = []
|
|
43
|
+
docker = []
|
|
44
|
+
|
|
45
|
+
[tool.splent.contract.requires]
|
|
46
|
+
features = ["settings"]
|
|
47
|
+
env_vars = ["RECAPTCHA_SECRET_KEY", "RECAPTCHA_SITE_KEY"]
|
|
48
|
+
signals = ["comment-submitting", "contact-submitting"]
|
|
49
|
+
|
|
50
|
+
[tool.splent.contract.extensible]
|
|
51
|
+
services = ["CaptchaService"]
|
|
52
|
+
templates = ["recaptcha/admin/settings.html"]
|
|
53
|
+
models = []
|
|
54
|
+
hooks = ["layout.authenticated_sidebar"]
|
|
55
|
+
routes = false
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/splent_feature_recaptcha.egg-info/PKG-INFO
|
|
4
|
+
src/splent_feature_recaptcha.egg-info/SOURCES.txt
|
|
5
|
+
src/splent_feature_recaptcha.egg-info/dependency_links.txt
|
|
6
|
+
src/splent_feature_recaptcha.egg-info/top_level.txt
|
|
7
|
+
src/splent_io/splent_feature_recaptcha/__init__.py
|
|
8
|
+
src/splent_io/splent_feature_recaptcha/commands.py
|
|
9
|
+
src/splent_io/splent_feature_recaptcha/config.py
|
|
10
|
+
src/splent_io/splent_feature_recaptcha/hooks.py
|
|
11
|
+
src/splent_io/splent_feature_recaptcha/routes.py
|
|
12
|
+
src/splent_io/splent_feature_recaptcha/services.py
|
|
13
|
+
src/splent_io/splent_feature_recaptcha/signals.py
|
|
14
|
+
src/splent_io/splent_feature_recaptcha/templates/recaptcha/admin/settings.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
splent_io
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from splent_framework.blueprints.base_blueprint import create_blueprint
|
|
2
|
+
from splent_framework.services.service_locator import register_service, service_proxy
|
|
3
|
+
|
|
4
|
+
from splent_io.splent_feature_recaptcha.services import RecaptchaService
|
|
5
|
+
|
|
6
|
+
recaptcha_bp = create_blueprint(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def init_feature(app):
|
|
10
|
+
# Generic "CaptchaService" name so consumers stay provider-agnostic; this is
|
|
11
|
+
# the Google reCAPTCHA implementation of the 'captcha' alternative.
|
|
12
|
+
register_service(app, "CaptchaService", RecaptchaService)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def inject_context_vars(app):
|
|
16
|
+
def captcha_widget():
|
|
17
|
+
return service_proxy("CaptchaService").widget()
|
|
18
|
+
|
|
19
|
+
def captcha_script():
|
|
20
|
+
return service_proxy("CaptchaService").script_tag()
|
|
21
|
+
|
|
22
|
+
return {"captcha_widget": captcha_widget, "captcha_script": captcha_script}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI commands contributed by splent_feature_recaptcha.
|
|
3
|
+
|
|
4
|
+
These commands are auto-discovered by the framework and exposed in the
|
|
5
|
+
SPLENT CLI under the ``feature:recaptcha`` group.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
splent feature:recaptcha hello
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command("hello")
|
|
16
|
+
def hello():
|
|
17
|
+
"""Example command — replace with your own."""
|
|
18
|
+
click.echo(" Hello from splent_feature_recaptcha!")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
cli_commands = [hello]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Google reCAPTCHA v2 configuration (anti-spam CAPTCHA).
|
|
2
|
+
|
|
3
|
+
Set RECAPTCHA_SITE_KEY / RECAPTCHA_SECRET_KEY in the product's .env (dev/prod),
|
|
4
|
+
or configure them from the admin panel. Defaults are Google's official TEST
|
|
5
|
+
keys (always pass), so development works out of the box.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
# Google's documented reCAPTCHA v2 test keys: always pass, safe for dev.
|
|
11
|
+
_TEST_SITE_KEY = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"
|
|
12
|
+
_TEST_SECRET_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def inject_config(app):
|
|
16
|
+
app.config.update(
|
|
17
|
+
{
|
|
18
|
+
"RECAPTCHA_SITE_KEY": os.getenv("RECAPTCHA_SITE_KEY", _TEST_SITE_KEY),
|
|
19
|
+
"RECAPTCHA_SECRET_KEY": os.getenv("RECAPTCHA_SECRET_KEY", _TEST_SECRET_KEY),
|
|
20
|
+
}
|
|
21
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from flask import request, url_for
|
|
2
|
+
|
|
3
|
+
from splent_framework.hooks.template_hooks import register_template_hook
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def recaptcha_admin_link():
|
|
7
|
+
"""Sidebar entry for the Captcha settings screen (the WP-plugin pattern)."""
|
|
8
|
+
active = (
|
|
9
|
+
"active"
|
|
10
|
+
if request.endpoint and request.endpoint.startswith("recaptcha.admin")
|
|
11
|
+
else ""
|
|
12
|
+
)
|
|
13
|
+
return (
|
|
14
|
+
f'<li class="sidebar-item {active}">'
|
|
15
|
+
f'<a class="sidebar-link" href="{url_for("recaptcha.admin_settings")}">'
|
|
16
|
+
'<i class="align-middle" data-feather="shield"></i> '
|
|
17
|
+
'<span class="align-middle">Captcha</span>'
|
|
18
|
+
"</a>"
|
|
19
|
+
"</li>"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
register_template_hook("layout.authenticated_sidebar", recaptcha_admin_link)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from flask import flash, redirect, render_template, request, url_for
|
|
2
|
+
from flask_login import login_required
|
|
3
|
+
|
|
4
|
+
from splent_io.splent_feature_recaptcha import recaptcha_bp
|
|
5
|
+
from splent_framework.services.service_locator import service_proxy
|
|
6
|
+
|
|
7
|
+
# Setting keys edited by the Captcha settings panel.
|
|
8
|
+
CAPTCHA_FIELDS = [
|
|
9
|
+
"recaptcha_site_key",
|
|
10
|
+
"recaptcha_secret_key",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@recaptcha_bp.route("/admin/captcha", methods=["GET", "POST"])
|
|
15
|
+
@login_required
|
|
16
|
+
def admin_settings():
|
|
17
|
+
"""Captcha settings panel for Google reCAPTCHA.
|
|
18
|
+
|
|
19
|
+
Edits the site/secret keys and persists them via SettingsService. The
|
|
20
|
+
RecaptchaService reads these keys back, so changes apply immediately.
|
|
21
|
+
"""
|
|
22
|
+
if request.method == "POST":
|
|
23
|
+
values = {field: request.form.get(field, "") for field in CAPTCHA_FIELDS}
|
|
24
|
+
service_proxy("SettingsService").set_many(values)
|
|
25
|
+
flash("Captcha settings updated.", "success")
|
|
26
|
+
return redirect(url_for("recaptcha.admin_settings"))
|
|
27
|
+
|
|
28
|
+
return render_template("recaptcha/admin/settings.html")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from flask import current_app
|
|
3
|
+
from markupsafe import Markup
|
|
4
|
+
|
|
5
|
+
from splent_framework.services.service_locator import service_proxy
|
|
6
|
+
|
|
7
|
+
VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RecaptchaService:
|
|
11
|
+
"""Google reCAPTCHA v2 — a CAPTCHA that protects forms from spam.
|
|
12
|
+
|
|
13
|
+
A utility service with no DB model. Registered under the generic
|
|
14
|
+
``CaptchaService`` name (the alternative to cloudflare). Templates use the
|
|
15
|
+
captcha_widget()/captcha_script() helpers; routes call verify() before
|
|
16
|
+
accepting a submission.
|
|
17
|
+
|
|
18
|
+
Keys are read from runtime SettingsService first (admin panel), then fall
|
|
19
|
+
back to the product config (RECAPTCHA_SITE_KEY / RECAPTCHA_SECRET_KEY).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def _setting(self, key):
|
|
23
|
+
try:
|
|
24
|
+
return service_proxy("SettingsService").get(key, None)
|
|
25
|
+
except Exception:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
def site_key(self):
|
|
29
|
+
value = self._setting("recaptcha_site_key")
|
|
30
|
+
if value:
|
|
31
|
+
return value
|
|
32
|
+
return current_app.config.get("RECAPTCHA_SITE_KEY", "")
|
|
33
|
+
|
|
34
|
+
def secret_key(self):
|
|
35
|
+
value = self._setting("recaptcha_secret_key")
|
|
36
|
+
if value:
|
|
37
|
+
return value
|
|
38
|
+
return current_app.config.get("RECAPTCHA_SECRET_KEY", "")
|
|
39
|
+
|
|
40
|
+
def enabled(self):
|
|
41
|
+
return bool(self.site_key())
|
|
42
|
+
|
|
43
|
+
def widget(self):
|
|
44
|
+
key = self.site_key()
|
|
45
|
+
if not key:
|
|
46
|
+
return Markup("")
|
|
47
|
+
return Markup(f'<div class="g-recaptcha" data-sitekey="{key}"></div>')
|
|
48
|
+
|
|
49
|
+
def script_tag(self):
|
|
50
|
+
if not self.enabled():
|
|
51
|
+
return Markup("")
|
|
52
|
+
return Markup(
|
|
53
|
+
'<script src="https://www.google.com/recaptcha/api.js"'
|
|
54
|
+
" async defer></script>"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def verify(self, token, remoteip=None):
|
|
58
|
+
secret = self.secret_key()
|
|
59
|
+
if not secret:
|
|
60
|
+
return True
|
|
61
|
+
if not token:
|
|
62
|
+
return False
|
|
63
|
+
try:
|
|
64
|
+
data = {"secret": secret, "response": token}
|
|
65
|
+
if remoteip:
|
|
66
|
+
data["remoteip"] = remoteip
|
|
67
|
+
resp = requests.post(VERIFY_URL, data=data, timeout=10)
|
|
68
|
+
return bool(resp.json().get("success"))
|
|
69
|
+
except Exception:
|
|
70
|
+
return False
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Anti-spam: validate submitted comments with Google reCAPTCHA.
|
|
2
|
+
|
|
3
|
+
Listens to the comments feature's ``comment-submitting`` signal. No hard
|
|
4
|
+
dependency on comments — if comments isn't installed the signal is never
|
|
5
|
+
defined and this handler is simply skipped.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from splent_framework.signals.signal_utils import connect_signal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@connect_signal("comment-submitting", "splent_feature_recaptcha")
|
|
12
|
+
def validate_recaptcha(sender, form=None, remoteip=None, **kwargs):
|
|
13
|
+
from splent_io.splent_feature_recaptcha.services import RecaptchaService
|
|
14
|
+
|
|
15
|
+
token = form.get("g-recaptcha-response") if form else None
|
|
16
|
+
return RecaptchaService().verify(token, remoteip)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@connect_signal("contact-submitting", "splent_feature_recaptcha")
|
|
20
|
+
def validate_recaptcha_contact(sender, form=None, remoteip=None, **kwargs):
|
|
21
|
+
from splent_io.splent_feature_recaptcha.services import RecaptchaService
|
|
22
|
+
|
|
23
|
+
token = form.get("g-recaptcha-response") if form else None
|
|
24
|
+
return RecaptchaService().verify(token, remoteip)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{% extends "base_template.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Captcha — Google reCAPTCHA{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="d-flex flex-column align-items-start gap-2 mb-4">
|
|
7
|
+
<h1 class="h3 mb-0">Captcha — Google reCAPTCHA</h1>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
11
|
+
{% for category, message in messages %}
|
|
12
|
+
<div class="alert alert-{{ 'danger' if category == 'danger' else 'success' }} py-2">{{ message }}</div>
|
|
13
|
+
{% endfor %}
|
|
14
|
+
{% endwith %}
|
|
15
|
+
|
|
16
|
+
<form method="POST">
|
|
17
|
+
<div class="card mb-4">
|
|
18
|
+
<div class="card-header">
|
|
19
|
+
<h2 class="h5 mb-0">reCAPTCHA keys</h2>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="card-body">
|
|
22
|
+
<div class="row g-3">
|
|
23
|
+
<div class="col-12">
|
|
24
|
+
<label class="form-label" for="recaptcha_site_key">Site key</label>
|
|
25
|
+
<input type="text" name="recaptcha_site_key" id="recaptcha_site_key"
|
|
26
|
+
class="form-control"
|
|
27
|
+
value="{{ setting('recaptcha_site_key', '') }}"
|
|
28
|
+
autocomplete="off">
|
|
29
|
+
<div class="form-text">The public key embedded in the widget on your forms.</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="col-12">
|
|
32
|
+
<label class="form-label" for="recaptcha_secret_key">Secret key</label>
|
|
33
|
+
<input type="password" name="recaptcha_secret_key" id="recaptcha_secret_key"
|
|
34
|
+
class="form-control"
|
|
35
|
+
value="{{ setting('recaptcha_secret_key', '') }}"
|
|
36
|
+
autocomplete="off">
|
|
37
|
+
<div class="form-text">The private key used server-side to verify submissions. Never share it.</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="alert alert-info mt-3 mb-0">
|
|
42
|
+
Leave these blank to use Google's official <strong>test keys</strong>, which always pass —
|
|
43
|
+
handy for development but they do <em>not</em> protect your forms.
|
|
44
|
+
Get real keys for your domain at
|
|
45
|
+
<a href="https://www.google.com/recaptcha/admin" target="_blank" rel="noopener">
|
|
46
|
+
https://www.google.com/recaptcha/admin</a>.
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
|
52
|
+
</form>
|
|
53
|
+
{% endblock %}
|