Feat: Update CAPTCHA service to ALTCHA self-hosted.
This commit is contained in:
@@ -42,7 +42,6 @@ class Config:
|
|||||||
SESSION_COOKIE_HTTPONLY = True
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
SESSION_COOKIE_SAMESITE = 'Strict'
|
SESSION_COOKIE_SAMESITE = 'Strict'
|
||||||
REMEMBER_COOKIE_SECURE = True
|
REMEMBER_COOKIE_SECURE = True
|
||||||
# PERMANENT_SESSION_LIFETIME = 3600
|
|
||||||
WTF_CSRF_ENABLED = True
|
WTF_CSRF_ENABLED = True
|
||||||
# WTF_CSRF_CHECK_DEFAULT = False # We'll check it manually for API routes
|
# WTF_CSRF_CHECK_DEFAULT = False # We'll check it manually for API routes
|
||||||
# WTF_CSRF_HEADERS = ['X-CSRFToken'] # Accept CSRF token from this header
|
# WTF_CSRF_HEADERS = ['X-CSRFToken'] # Accept CSRF token from this header
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Initializes the Flask application, sets the configuration based on the environme
|
|||||||
# internal
|
# internal
|
||||||
from datastores.datastore_base import DataStore_Base
|
from datastores.datastore_base import DataStore_Base
|
||||||
from forms.contact import Form_Contact
|
from forms.contact import Form_Contact
|
||||||
|
from helpers.helper_app import Helper_App
|
||||||
from models.model_view_contact import Model_View_Contact
|
from models.model_view_contact import Model_View_Contact
|
||||||
from models.model_view_home import Model_View_Home
|
from models.model_view_home import Model_View_Home
|
||||||
import lib.argument_validation as av
|
import lib.argument_validation as av
|
||||||
@@ -29,6 +30,8 @@ import json
|
|||||||
import base64
|
import base64
|
||||||
import hmac
|
import hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import datetime
|
||||||
|
from altcha import ChallengeOptions, create_challenge, verify_solution
|
||||||
|
|
||||||
routes_core = Blueprint('routes_core', __name__)
|
routes_core = Blueprint('routes_core', __name__)
|
||||||
|
|
||||||
@@ -56,60 +59,51 @@ def contact():
|
|||||||
def contact_post():
|
def contact_post():
|
||||||
try:
|
try:
|
||||||
form = Form_Contact()
|
form = Form_Contact()
|
||||||
|
Helper_App.console_log(f"Form submitted: {request.form}")
|
||||||
|
Helper_App.console_log(f"ALTCHA data in request: {request.form.get('altcha')}")
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
altcha_payload = form.altcha.data
|
|
||||||
if not altcha_payload:
|
|
||||||
flash('Please complete the ALTCHA challenge', 'danger')
|
|
||||||
return "Invalid. ALTCHA challenge failed."
|
|
||||||
# Decode and verify the ALTCHA payload
|
|
||||||
try:
|
try:
|
||||||
decoded_payload = json.loads(base64.b64decode(altcha_payload))
|
email = form.email.data
|
||||||
|
# CC = form.CC.data # not in use
|
||||||
# Verify the signature
|
contact_name = form.contact_name.data
|
||||||
if verify_altcha_signature(decoded_payload):
|
company_name = form.company_name.data
|
||||||
# Parse the verification data
|
message = form.message.data
|
||||||
verification_data = urllib.parse.parse_qs(decoded_payload['verificationData'])
|
receive_marketing = form.receive_marketing.data
|
||||||
|
receive_marketing_text = "I would like to receive marketing emails." if receive_marketing else ""
|
||||||
# Check if the verification was successful
|
# send email
|
||||||
if verification_data.get('verified', ['false'])[0] == 'true':
|
mailItem = Message("PARTS Website Contact Us Message", recipients=[current_app.config['MAIL_CONTACT_PUBLIC']])
|
||||||
# If spam filter is enabled, check the classification
|
mailItem.body = f"Dear Lord Edward Middleton-Smith,\n\n{message}\n{receive_marketing_text}\nKind regards,\n{contact_name}\n{company_name}\n{email}"
|
||||||
if 'classification' in verification_data:
|
mail.send(mailItem)
|
||||||
classification = verification_data.get('classification', [''])[0]
|
flash('Thank you for your message. We will get back to you soon!', 'success')
|
||||||
score = float(verification_data.get('score', ['0'])[0])
|
return "Submitted."
|
||||||
|
|
||||||
# If the classification is BAD and score is high, reject the submission
|
|
||||||
if classification == 'BAD' and score > 5:
|
|
||||||
flash('Your submission was flagged as potential spam', 'error')
|
|
||||||
return render_template('contact.html', form=form)
|
|
||||||
|
|
||||||
# Process the form submission
|
|
||||||
email = form.email.data
|
|
||||||
CC = form.CC.data # not in use
|
|
||||||
contact_name = form.contact_name.data
|
|
||||||
company_name = form.company_name.data
|
|
||||||
message = form.message.data
|
|
||||||
receive_marketing = form.receive_marketing.data
|
|
||||||
receive_marketing_text = "I would like to receive marketing emails." if receive_marketing else ""
|
|
||||||
# send email
|
|
||||||
mailItem = Message("PARTS Website Contact Us Message", recipients=[current_app.config['MAIL_CONTACT_PUBLIC']])
|
|
||||||
mailItem.body = f"Dear Lord Edward Middleton-Smith,\n\n{message}\n{receive_marketing_text}\nKind regards,\n{contact_name}\n{company_name}\n{email}"
|
|
||||||
mail.send(mailItem)
|
|
||||||
flash('Thank you for your message. We will get back to you soon!', 'success')
|
|
||||||
return "Submitted."
|
|
||||||
else:
|
|
||||||
flash('CAPTCHA verification failed', 'error')
|
|
||||||
else:
|
|
||||||
flash('Invalid verification signature', 'error')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(f'Error verifying CAPTCHA: {str(e)}', 'error')
|
return f"Error: {e}"
|
||||||
|
print(f"Form validation errors: {form.errors}")
|
||||||
return "Invalid. Failed to submit."
|
return "Invalid. Failed to submit."
|
||||||
# html_body = render_template('pages/core/_contact.html', model = model)
|
# html_body = render_template('pages/core/_contact.html', model = model)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify(error=str(e)), 403
|
return jsonify(error=str(e)), 403
|
||||||
|
|
||||||
|
@routes_core.route(Model_View_Contact.HASH_ALTCHA_CREATE_CHALLENGE, methods=['GET'])
|
||||||
|
def create_altcha_challenge():
|
||||||
|
options = ChallengeOptions(
|
||||||
|
expires = datetime.datetime.now() + datetime.timedelta(hours=1),
|
||||||
|
max_number = 100000, # The maximum random number
|
||||||
|
hmac_key = current_app.config["ALTCHA_SECRET_KEY"],
|
||||||
|
)
|
||||||
|
challenge = create_challenge(options)
|
||||||
|
print("Challenge created:", challenge)
|
||||||
|
# return jsonify({"challenge": challenge})
|
||||||
|
return jsonify({
|
||||||
|
"algorithm": challenge.algorithm,
|
||||||
|
"challenge": challenge.challenge,
|
||||||
|
"salt": challenge.salt,
|
||||||
|
"signature": challenge.signature,
|
||||||
|
})
|
||||||
|
|
||||||
|
"""
|
||||||
def verify_altcha_signature(payload):
|
def verify_altcha_signature(payload):
|
||||||
"""Verify the ALTCHA signature"""
|
"" "Verify the ALTCHA signature"" "
|
||||||
if 'algorithm' not in payload or 'signature' not in payload or 'verificationData' not in payload:
|
if 'algorithm' not in payload or 'signature' not in payload or 'verificationData' not in payload:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -135,4 +129,30 @@ def verify_altcha_signature(payload):
|
|||||||
).hexdigest()
|
).hexdigest()
|
||||||
|
|
||||||
# Compare the calculated signature with the provided signature
|
# Compare the calculated signature with the provided signature
|
||||||
return hmac.compare_digest(calculated_signature, signature)
|
return hmac.compare_digest(calculated_signature, signature)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def create_altcha_dummy_signature(challenge):
|
||||||
|
# Example payload to verify
|
||||||
|
payload = {
|
||||||
|
"algorithm": challenge.algorithm,
|
||||||
|
"challenge": challenge.challenge,
|
||||||
|
"number": 12345, # Example number
|
||||||
|
"salt": challenge.salt,
|
||||||
|
"signature": challenge.signature,
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
|
||||||
|
@routes_core.route(Model_View_Contact.HASH_ALTCHA_VERIFY_SOLUTION, methods=['POST'])
|
||||||
|
def verify_altcha_challenge():
|
||||||
|
payload = request.json
|
||||||
|
|
||||||
|
ok, err = verify_solution(payload, current_app.config["ALTCHA_SECRET_KEY"], check_expires=True)
|
||||||
|
if err:
|
||||||
|
return jsonify({"error": err}), 400
|
||||||
|
elif ok:
|
||||||
|
return jsonify({"verified": True})
|
||||||
|
else:
|
||||||
|
return jsonify({"verified": False}), 403
|
||||||
|
"""
|
||||||
@@ -14,65 +14,66 @@ Defines Flask-WTF form for handling user input on Contact Us page.
|
|||||||
# internal
|
# internal
|
||||||
# from business_objects.store.product_category import Filters_Product_Category # circular
|
# from business_objects.store.product_category import Filters_Product_Category # circular
|
||||||
# from models.model_view_store import Model_View_Store # circular
|
# from models.model_view_store import Model_View_Store # circular
|
||||||
|
from models.model_view_base import Model_View_Base
|
||||||
from forms.base import Form_Base
|
from forms.base import Form_Base
|
||||||
# external
|
# external
|
||||||
from flask import Flask, render_template, request, flash, redirect, url_for, current_app
|
from flask import Flask, render_template, request, flash, redirect, url_for, current_app
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, TextAreaField, SubmitField, HiddenField, BooleanField
|
from wtforms import StringField, TextAreaField, SubmitField, HiddenField, BooleanField, Field
|
||||||
from wtforms.validators import DataRequired, Email, ValidationError
|
from wtforms.validators import DataRequired, Email, ValidationError
|
||||||
|
import markupsafe
|
||||||
from flask_wtf.recaptcha import RecaptchaField
|
from flask_wtf.recaptcha import RecaptchaField
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
import requests
|
|
||||||
import json
|
import json
|
||||||
import hmac
|
from altcha import verify_solution
|
||||||
import hashlib
|
|
||||||
import base64
|
import base64
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
"""
|
class ALTCHAValidator:
|
||||||
def validate_altcha(form, field):
|
def __init__(self, message=None):
|
||||||
if not field.data:
|
self.message = message or 'ALTCHA verification failed'
|
||||||
raise ValidationError('Please complete the ALTCHA challenge')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Decode the base64-encoded payload
|
|
||||||
payload_json = base64.b64decode(field.data).decode('utf-8')
|
|
||||||
payload = json.loads(payload_json)
|
|
||||||
|
|
||||||
# Verify ALTCHA response
|
def __call__(self, form, field):
|
||||||
if not payload.get('verified', False):
|
altcha_data = field.data
|
||||||
raise ValidationError('ALTCHA verification failed')
|
|
||||||
|
|
||||||
# Verify signature
|
if not altcha_data:
|
||||||
verification_data = payload.get('verificationData', '')
|
raise ValidationError(self.message)
|
||||||
received_signature = payload.get('signature', '')
|
|
||||||
algorithm = payload.get('algorithm', 'SHA-256')
|
|
||||||
|
|
||||||
# Calculate the hash of verification data
|
try:
|
||||||
verification_hash = hashlib.sha256(verification_data.encode()).digest()
|
# The data is base64 encoded JSON
|
||||||
|
try:
|
||||||
# Calculate HMAC signature
|
# First try to decode it as JSON directly (if it's not base64 encoded)
|
||||||
hmac_key = current_app.config['ALTCHA_SECRET_KEY'].encode()
|
altcha_payload = json.loads(altcha_data)
|
||||||
calculated_signature = hmac.new(
|
except json.JSONDecodeError:
|
||||||
hmac_key,
|
# If direct JSON decoding fails, try base64 decoding first
|
||||||
verification_hash,
|
decoded_data = base64.b64decode(altcha_data).decode('utf-8')
|
||||||
getattr(hashlib, algorithm.lower().replace('-', ''))
|
altcha_payload = json.loads(decoded_data)
|
||||||
).hexdigest()
|
|
||||||
|
ok, err = verify_solution(altcha_payload, current_app.config["ALTCHA_SECRET_KEY"], check_expires=True)
|
||||||
if calculated_signature != received_signature:
|
|
||||||
raise ValidationError('Invalid ALTCHA signature')
|
|
||||||
|
|
||||||
# Optional: If using the spam filter, you could parse verification_data
|
if err or not ok:
|
||||||
# and reject submissions classified as spam
|
raise ValidationError(self.message + ': ' + (err or 'Invalid solution'))
|
||||||
# Example:
|
|
||||||
parsed_data = dict(urllib.parse.parse_qsl(verification_data))
|
except Exception as e:
|
||||||
if parsed_data.get('classification') == 'BAD':
|
raise ValidationError(f'Invalid ALTCHA data: {str(e)}')
|
||||||
raise ValidationError('This submission was classified as spam')
|
|
||||||
|
class ALTCHAField(Field):
|
||||||
except Exception as e:
|
def __init__(self, label='', validators=None, **kwargs):
|
||||||
current_app.logger.error(f"ALTCHA validation error: {str(e)}")
|
validators = validators or []
|
||||||
raise ValidationError('ALTCHA validation failed')
|
validators.append(ALTCHAValidator())
|
||||||
"""
|
|
||||||
|
super(ALTCHAField, self).__init__(label, validators, **kwargs)
|
||||||
|
|
||||||
|
def __call__(self, **kwargs):
|
||||||
|
html = f"""
|
||||||
|
<altcha-widget
|
||||||
|
challengeurl="/get-challenge"
|
||||||
|
auto="onload"
|
||||||
|
id="{self.id}"
|
||||||
|
name="{self.name}">
|
||||||
|
</altcha-widget>
|
||||||
|
"""
|
||||||
|
return markupsafe.Markup(html)
|
||||||
|
|
||||||
|
|
||||||
class Form_Contact(FlaskForm):
|
class Form_Contact(FlaskForm):
|
||||||
email = StringField('Email')
|
email = StringField('Email')
|
||||||
@@ -81,5 +82,6 @@ class Form_Contact(FlaskForm):
|
|||||||
message = TextAreaField('Message')
|
message = TextAreaField('Message')
|
||||||
receive_marketing = BooleanField('I would like to receive marketing emails.')
|
receive_marketing = BooleanField('I would like to receive marketing emails.')
|
||||||
# recaptcha = RecaptchaField()
|
# recaptcha = RecaptchaField()
|
||||||
altcha = HiddenField('ALTCHA') # , validators=[validate_altcha]
|
# altcha = HiddenField('ALTCHA') # , validators=[validate_altcha]
|
||||||
|
altcha = ALTCHAField('Verification')
|
||||||
submit = SubmitField('Send Message')
|
submit = SubmitField('Send Message')
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ class Model_View_Base(BaseModel, ABC):
|
|||||||
FLAG_USER: ClassVar[str] = User.FLAG_USER
|
FLAG_USER: ClassVar[str] = User.FLAG_USER
|
||||||
FLAG_WEBSITE: ClassVar[str] = Base.FLAG_WEBSITE
|
FLAG_WEBSITE: ClassVar[str] = Base.FLAG_WEBSITE
|
||||||
# flagIsDatePicker: ClassVar[str] = 'is-date-picker'
|
# flagIsDatePicker: ClassVar[str] = 'is-date-picker'
|
||||||
|
HASH_ALTCHA_CREATE_CHALLENGE: ClassVar[str] = '/altcha/create-challenge'
|
||||||
|
# HASH_ALTCHA_VERIFY_SOLUTION: ClassVar[str] = '/altcha/verify-solution'
|
||||||
HASH_APPLY_FILTERS_STORE_PRODUCT_PERMUTATION: ClassVar[str] = '/store/permutation_filter'
|
HASH_APPLY_FILTERS_STORE_PRODUCT_PERMUTATION: ClassVar[str] = '/store/permutation_filter'
|
||||||
HASH_CALLBACK_LOGIN: ClassVar[str] = '/callback-login'
|
HASH_CALLBACK_LOGIN: ClassVar[str] = '/callback-login'
|
||||||
HASH_PAGE_ACCESSIBILITY_REPORT: ClassVar[str] = '/accessibility-report'
|
HASH_PAGE_ACCESSIBILITY_REPORT: ClassVar[str] = '/accessibility-report'
|
||||||
|
|||||||
@@ -22,19 +22,11 @@ from pydantic import BaseModel
|
|||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
class Model_View_Contact(Model_View_Base):
|
class Model_View_Contact(Model_View_Base):
|
||||||
# Attributes
|
FLAG_ALTCHA_WIDGET: ClassVar[str] = 'altcha-widget'
|
||||||
FLAG_COMPANY_NAME: ClassVar[str] = 'company_name'
|
FLAG_COMPANY_NAME: ClassVar[str] = 'company_name'
|
||||||
FLAG_CONTACT_NAME: ClassVar[str] = 'contact_name'
|
FLAG_CONTACT_NAME: ClassVar[str] = 'contact_name'
|
||||||
FLAG_RECEIVE_MARKETING: ClassVar[str] = 'receive_marketing'
|
FLAG_RECEIVE_MARKETING: ClassVar[str] = 'receive_marketing'
|
||||||
ID_CONTACT_FORM: ClassVar[str] = 'contact-form'
|
ID_CONTACT_FORM: ClassVar[str] = 'contact-form'
|
||||||
"""
|
|
||||||
ID_EMAIL: ClassVar[str] = 'email'
|
|
||||||
ID_COMPANY_NAME: ClassVar[str] = 'company_name'
|
|
||||||
ID_CONTACT_NAME: ClassVar[str] = 'contact_name'
|
|
||||||
ID_MESSAGE: ClassVar[str] = 'msg'
|
|
||||||
ID_RECEIVE_MARKETING: ClassVar[str] = 'receive_marketing'
|
|
||||||
ID_NAME: ClassVar[str] = 'name'
|
|
||||||
"""
|
|
||||||
|
|
||||||
form_contact: Form_Contact
|
form_contact: Form_Contact
|
||||||
|
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ authlib
|
|||||||
pydantic
|
pydantic
|
||||||
# psycopg2
|
# psycopg2
|
||||||
requests
|
requests
|
||||||
cryptography
|
cryptography
|
||||||
|
altcha
|
||||||
3651
static/dist/js/main.bundle.js
vendored
3651
static/dist/js/main.bundle.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,5 +1,7 @@
|
|||||||
|
// internal
|
||||||
import BasePage from "../base.js";
|
import BasePage from "../base.js";
|
||||||
|
// vendor
|
||||||
|
import { Altcha } from "../../vendor/altcha.js";
|
||||||
|
|
||||||
export default class PageContact extends BasePage {
|
export default class PageContact extends BasePage {
|
||||||
static hash = hashPageContact;
|
static hash = hashPageContact;
|
||||||
@@ -10,11 +12,12 @@ export default class PageContact extends BasePage {
|
|||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
this.sharedInitialize();
|
this.sharedInitialize();
|
||||||
this.hookupCaptcha();
|
// this.hookupALTCHAByLocalServer();
|
||||||
this.hookupButtonSubmitFormContactUs();
|
this.hookupButtonSubmitFormContactUs();
|
||||||
}
|
}
|
||||||
|
|
||||||
hookupCaptcha() {
|
/*
|
||||||
|
hookupALTCHAByAPI() {
|
||||||
const form = document.querySelector(idContactForm);
|
const form = document.querySelector(idContactForm);
|
||||||
const altchaWidget = form.querySelector('altcha-widget');
|
const altchaWidget = form.querySelector('altcha-widget');
|
||||||
|
|
||||||
@@ -35,6 +38,22 @@ export default class PageContact extends BasePage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
hookupALTCHAByLocalServer() {
|
||||||
|
window.ALTCHA = { init: (config) => {
|
||||||
|
document.querySelectorAll(config.selector).forEach(el => {
|
||||||
|
new Altcha({
|
||||||
|
target: el,
|
||||||
|
props: {
|
||||||
|
challengeurl: config.challenge.url,
|
||||||
|
auto: 'onload'
|
||||||
|
}
|
||||||
|
}).$on('verified', (e) => {
|
||||||
|
config.challenge.onSuccess(e.detail.payload, el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
hookupButtonSubmitFormContactUs() {
|
hookupButtonSubmitFormContactUs() {
|
||||||
const button = document.querySelector('form input[type="submit"]');
|
const button = document.querySelector('form input[type="submit"]');
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
var flagTemporaryElement = "{{ model.FLAG_TEMPORARY_ELEMENT }}";
|
var flagTemporaryElement = "{{ model.FLAG_TEMPORARY_ELEMENT }}";
|
||||||
var flagUser = "{{ model.FLAG_USER }}";
|
var flagUser = "{{ model.FLAG_USER }}";
|
||||||
var flagWebsite = "{{ model.FLAG_WEBSITE }}";
|
var flagWebsite = "{{ model.FLAG_WEBSITE }}";
|
||||||
|
var hashALTCHACreateChallenge = "{{ model.HASH_ALTCHA_CREATE_CHALLENGE }}";
|
||||||
var hashApplyFiltersStoreProductPermutation = "{{ model.HASH_APPLY_FILTERS_STORE_PRODUCT_PERMUTATION }}";
|
var hashApplyFiltersStoreProductPermutation = "{{ model.HASH_APPLY_FILTERS_STORE_PRODUCT_PERMUTATION }}";
|
||||||
var hashPageAccessibilityReport = "{{ model.HASH_PAGE_ACCESSIBILITY_REPORT }}";
|
var hashPageAccessibilityReport = "{{ model.HASH_PAGE_ACCESSIBILITY_REPORT }}";
|
||||||
var hashPageAccessibilityStatement = "{{ model.HASH_PAGE_ACCESSIBILITY_STATEMENT }}";
|
var hashPageAccessibilityStatement = "{{ model.HASH_PAGE_ACCESSIBILITY_STATEMENT }}";
|
||||||
|
|||||||
@@ -5,7 +5,22 @@
|
|||||||
{#
|
{#
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@altcha/browser@latest/dist/index.js" defer></script>
|
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@altcha/browser@latest/dist/index.js" defer></script>
|
||||||
#}
|
#}
|
||||||
|
{# with CDN
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@altcha/browser@1.1.0/dist/altcha.min.js"></script>
|
||||||
|
<style>
|
||||||
|
.altcha-widget {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
#}
|
||||||
|
{# with locally stored vendor project - this is imported into contact.js
|
||||||
<script type="module" src="{{ url_for('static', filename='js/vendor/altcha.js')}}"></script>
|
<script type="module" src="{{ url_for('static', filename='js/vendor/altcha.js')}}"></script>
|
||||||
|
#}
|
||||||
|
<style>
|
||||||
|
.altcha-widget {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page_nav_links %}
|
{% block page_nav_links %}
|
||||||
@@ -35,6 +50,16 @@
|
|||||||
<h1>Contact Us</h1>
|
<h1>Contact Us</h1>
|
||||||
<p>Please fill in the form below and we'll get back to you as soon as possible.</p>
|
<p>Please fill in the form below and we'll get back to you as soon as possible.</p>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<ul class="flashes">
|
||||||
|
{% for message in messages %}
|
||||||
|
<li>{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
<form id="{{ model.ID_CONTACT_FORM }}" method="POST" action="{{ url_for('routes_core.contact') }}">
|
<form id="{{ model.ID_CONTACT_FORM }}" method="POST" action="{{ url_for('routes_core.contact') }}">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
|
|
||||||
@@ -66,10 +91,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="{{ model.FLAG_CONTAINER }} {{ model.FLAG_CAPTCHA }}">
|
<div class="{{ model.FLAG_CONTAINER }} {{ model.FLAG_CAPTCHA }}">
|
||||||
{# {{ model.form_contact.recaptcha() }} #}
|
{# {{ model.form_contact.recaptcha() }} #}
|
||||||
|
{#
|
||||||
<altcha-widget
|
<altcha-widget
|
||||||
challengeurl="https://eu.altcha.org/api/v1/challenge?apiKey={{ model.app.app_config.ALTCHA_API_KEY }}"
|
challengeurl="https://eu.altcha.org/api/v1/challenge?apiKey={{ model.app.app_config.ALTCHA_API_KEY }}"
|
||||||
spamfilter
|
spamfilter
|
||||||
></altcha-widget>
|
></altcha-widget>
|
||||||
|
#}
|
||||||
|
<div>
|
||||||
|
{{ form.altcha.label }}
|
||||||
|
{#
|
||||||
|
{{ form.altcha }}
|
||||||
|
{{ form.altcha.hidden() }}
|
||||||
|
#}
|
||||||
|
<altcha-widget
|
||||||
|
class="altcha-widget"
|
||||||
|
challengeurl="{{ url_for('routes_core.create_altcha_challenge') }}"
|
||||||
|
auto="onload"
|
||||||
|
id="{{ form.altcha.id }}"
|
||||||
|
name="{{ form.altcha.name }}"
|
||||||
|
></altcha-widget>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="{{ model.FLAG_CONTAINER_INPUT }}">
|
<div class="{{ model.FLAG_CONTAINER_INPUT }}">
|
||||||
{{ model.form_contact.submit() }}
|
{{ model.form_contact.submit() }}
|
||||||
@@ -109,7 +150,45 @@
|
|||||||
</section>
|
</section>
|
||||||
#}
|
#}
|
||||||
|
|
||||||
|
{# with CDN
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize ALTCHA widget
|
||||||
|
ALTCHA.init({
|
||||||
|
selector: '.altcha-widget',
|
||||||
|
challenge: {
|
||||||
|
url: '/get-challenge',
|
||||||
|
onSuccess: function(result, element) {
|
||||||
|
// Store the result in the hidden input field
|
||||||
|
const hiddenInput = element.parentNode.querySelector('input[type="hidden"]');
|
||||||
|
hiddenInput.value = JSON.stringify(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
#}
|
||||||
|
|
||||||
|
{# with locally stored vendor project - this is now in contact.js
|
||||||
|
<script type="module">
|
||||||
|
import { Altcha } from "{{ url_for('static', filename='js/vendor/altcha.js') }}";
|
||||||
|
window.ALTCHA = { init: (config) => {
|
||||||
|
document.querySelectorAll(config.selector).forEach(el => {
|
||||||
|
new Altcha({
|
||||||
|
target: el,
|
||||||
|
props: {
|
||||||
|
challengeurl: config.challenge.url,
|
||||||
|
auto: 'onload'
|
||||||
|
}
|
||||||
|
}).$on('verified', (e) => {
|
||||||
|
config.challenge.onSuccess(e.detail.payload, el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}};
|
||||||
|
</script>
|
||||||
|
#}
|
||||||
<script>
|
<script>
|
||||||
|
var flagALTCHAWidget = "{{ model.FLAG_ALTCHA_WIDGET }}";
|
||||||
var idContactForm = "#{{ model.ID_CONTACT_FORM }}";
|
var idContactForm = "#{{ model.ID_CONTACT_FORM }}";
|
||||||
var idEmail = "#{{ model.ID_EMAIL }}";
|
var idEmail = "#{{ model.ID_EMAIL }}";
|
||||||
var idMessage = "#{{ model.ID_MESSAGE }}";
|
var idMessage = "#{{ model.ID_MESSAGE }}";
|
||||||
|
|||||||
Reference in New Issue
Block a user