Add hCaptcha.com support to public subscription form. (#1152)

Bots easily bypass the simple `nonce` hack. This commit adds support
for the hcaptcha.com widget.

- New `Security` tab in the admin settings UI.
- Enable/disable CAPTCHA.
- Render CAPTCHA on the public subscription form.

Closes #1116.
This commit is contained in:
Kailash Nadh 2023-01-23 21:50:10 +05:30 committed by GitHub
parent 62d3782d04
commit 8985e5c24a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 374 additions and 3 deletions

View file

@ -24,6 +24,7 @@ import (
"github.com/knadh/koanf/providers/posflag"
"github.com/knadh/listmonk/internal/bounce"
"github.com/knadh/listmonk/internal/bounce/mailbox"
"github.com/knadh/listmonk/internal/captcha"
"github.com/knadh/listmonk/internal/i18n"
"github.com/knadh/listmonk/internal/manager"
"github.com/knadh/listmonk/internal/media"
@ -69,6 +70,11 @@ type constants struct {
Exportable map[string]bool `koanf:"-"`
DomainBlocklist map[string]bool `koanf:"-"`
} `koanf:"privacy"`
Security struct {
EnableCaptcha bool `koanf:"enable_captcha"`
CaptchaKey string `koanf:"captcha_key"`
CaptchaSecret string `koanf:"captcha_secret"`
} `koanf:"security"`
AdminUsername []byte `koanf:"admin_username"`
AdminPassword []byte `koanf:"admin_password"`
@ -351,6 +357,9 @@ func initConstants() *constants {
if err := ko.Unmarshal("privacy", &c.Privacy); err != nil {
lo.Fatalf("error loading app.privacy config: %v", err)
}
if err := ko.Unmarshal("security", &c.Security); err != nil {
lo.Fatalf("error loading app.security config: %v", err)
}
if err := ko.UnmarshalWithConf("appearance", &c.Appearance, koanf.UnmarshalConf{FlatPaths: true}); err != nil {
lo.Fatalf("error loading app.appearance config: %v", err)
}
@ -735,6 +744,12 @@ func initHTTPServer(app *App) *echo.Echo {
return srv
}
func initCaptcha() *captcha.Captcha {
return captcha.New(captcha.Opt{
CaptchaSecret: ko.String("security.captcha_secret"),
})
}
func awaitReload(sigChan chan os.Signal, closerWait chan bool, closer func()) chan bool {
// The blocking signal handler that main() waits on.
out := make(chan bool)

View file

@ -17,6 +17,7 @@ import (
"github.com/knadh/koanf/providers/env"
"github.com/knadh/listmonk/internal/bounce"
"github.com/knadh/listmonk/internal/buflog"
"github.com/knadh/listmonk/internal/captcha"
"github.com/knadh/listmonk/internal/core"
"github.com/knadh/listmonk/internal/i18n"
"github.com/knadh/listmonk/internal/manager"
@ -47,6 +48,7 @@ type App struct {
i18n *i18n.I18n
bounce *bounce.Manager
paginator *paginator.Paginator
captcha *captcha.Captcha
notifTpls *notifTpls
log *log.Logger
bufLog *buflog.BufLog
@ -168,6 +170,7 @@ func main() {
messengers: make(map[string]messenger.Messenger),
log: lo,
bufLog: bufLog,
captcha: initCaptcha(),
paginator: paginator.New(paginator.Opt{
DefaultPerPage: 20,

View file

@ -80,6 +80,7 @@ type msgTpl struct {
type subFormTpl struct {
publicTpl
Lists []models.List
CaptchaKey string
}
var (
@ -418,6 +419,10 @@ func handleSubscriptionFormPage(c echo.Context) error {
out.Title = app.i18n.T("public.sub")
out.Lists = lists
if app.constants.Security.EnableCaptcha {
out.CaptchaKey = app.constants.Security.CaptchaKey
}
return c.Render(http.StatusOK, "subscription-form", out)
}
@ -433,6 +438,19 @@ func handleSubscriptionForm(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadGateway, app.i18n.T("public.invalidFeature"))
}
// Process CAPTCHA.
if app.constants.Security.EnableCaptcha {
err, ok := app.captcha.Verify(c.FormValue("h-captcha-response"))
if err != nil {
app.log.Printf("Captcha request failed: %v", err)
}
if !ok {
return c.Render(http.StatusBadRequest, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.invalidCaptcha")))
}
}
hasOptin, err := processSubForm(c)
if err != nil {
e, ok := err.(*echo.HTTPError)

View file

@ -44,6 +44,7 @@ func handleGetSettings(c echo.Context) error {
}
s.UploadS3AwsSecretAccessKey = ""
s.SendgridKey = ""
s.SecurityCaptchaSecret = ""
return c.JSON(http.StatusOK, okResp{s})
}
@ -158,6 +159,9 @@ func handleUpdateSettings(c echo.Context) error {
if set.SendgridKey == "" {
set.SendgridKey = cur.SendgridKey
}
if set.SecurityCaptchaSecret == "" {
set.SecurityCaptchaSecret = cur.SecurityCaptchaSecret
}
// Domain blocklist.
doms := make([]string, 0)

View file

@ -34,6 +34,7 @@ var migList = []migFunc{
{"v2.1.0", migrations.V2_1_0},
{"v2.2.0", migrations.V2_2_0},
{"v2.3.0", migrations.V2_3_0},
{"v2.4.0", migrations.V2_4_0},
}
// upgrade upgrades the database to the current version by running SQL migration files

View file

@ -34,6 +34,10 @@
<privacy-settings :form="form" :key="key" />
</b-tab-item><!-- privacy -->
<b-tab-item :label="$t('settings.security.name')">
<security-settings :form="form" :key="key" />
</b-tab-item><!-- security -->
<b-tab-item :label="$t('settings.media.title')">
<media-settings :form="form" :key="key" />
</b-tab-item><!-- media -->
@ -66,6 +70,7 @@ import { mapState } from 'vuex';
import GeneralSettings from './settings/general.vue';
import PerformanceSettings from './settings/performance.vue';
import PrivacySettings from './settings/privacy.vue';
import SecuritySettings from './settings/security.vue';
import MediaSettings from './settings/media.vue';
import SmtpSettings from './settings/smtp.vue';
import BounceSettings from './settings/bounces.vue';
@ -79,6 +84,7 @@ export default Vue.extend({
GeneralSettings,
PerformanceSettings,
PrivacySettings,
SecuritySettings,
MediaSettings,
SmtpSettings,
BounceSettings,
@ -136,6 +142,10 @@ export default Vue.extend({
form['bounce.sendgrid_key'] = '';
}
if (form['security.captcha_secret'] === dummyPassword) {
form['security.captcha_secret'] = '';
}
for (let i = 0; i < form.messengers.length; i += 1) {
// If it's the dummy UI password placeholder, ignore it.
if (form.messengers[i].password === dummyPassword) {
@ -203,6 +213,7 @@ export default Vue.extend({
d['upload.s3.aws_secret_access_key'] = dummyPassword;
}
d['bounce.sendgrid_key'] = dummyPassword;
d['security.captcha_secret'] = dummyPassword;
// Domain blocklist array to multi-line string.
d['privacy.domain_blocklist'] = d['privacy.domain_blocklist'].join('\n');

View file

@ -0,0 +1,42 @@
<template>
<div class="items">
<div class="columns">
<div class="column is-4">
<b-field :label="$t('settings.security.enableCaptcha')"
:message="$t('settings.security.enableCaptchaHelp')">
<b-switch v-model="data['security.enable_captcha']"
name="security.captcha" />
</b-field>
</div>
<div class="column is-8">
<b-field :label="$t('settings.security.captchaKey')" label-position="on-border"
:message="$t('settings.security.captchaKeyHelp')">
<b-input v-model="data['security.captcha_key']" name="captcha_key"
:disabled="!data['security.enable_captcha']" :maxlength="200" required />
</b-field>
<b-field :label="$t('settings.security.captchaSecret')" label-position="on-border">
<b-input v-model="data['security.captcha_secret']" name="captcha_secret" type="password"
:disabled="!data['security.enable_captcha']" :maxlength="200" required />
</b-field>
</div>
</div>
</div>
</template>
<script>
import Vue from 'vue';
export default Vue.extend({
props: {
form: {
type: Object,
},
},
data() {
return {
data: this.form,
};
},
});
</script>

View file

@ -304,6 +304,7 @@
"public.errorFetchingLists": "S'ha produït un error en obtenir les llistes. Si us plau, torna-ho a provar.",
"public.errorProcessingRequest": "S'ha produït un error en processar la sol·licitud. Si us plau, torna-ho a provar.",
"public.errorTitle": "Error",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Aquesta funció no està disponible.",
"public.invalidLink": "Enllaç no vàlid",
"public.managePrefs": "Gestiona les preferències",
@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Inclou capçaleres de cancel·lació de subscripció que permetin als clients de correu electrònic permetre als usuaris donar-se de baixa amb un sol clic.",
"settings.privacy.name": "Privadesa",
"settings.restart": "Reinicia",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Capçaleres personalitzades",
"settings.smtp.customHeadersHelp": "Matriu opcional de capçaleres de correu electrònic per incloure en tots els missatges enviats des d'aquest servidor. p. ex.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Habilitat",

View file

@ -304,6 +304,7 @@
"public.errorFetchingLists": "Chyba při načítání seznamů. Zopakujte pokus.",
"public.errorProcessingRequest": "Chyba při zpracování požadavku. Zopakujte pokus.",
"public.errorTitle": "Chyba",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Tato funkce není k dispozici.",
"public.invalidLink": "Neplatný odkaz",
"public.managePrefs": "Zpráva předvoleb",
@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Zahrnout záhlaví zrušení odběrů, která umožňují e-mailovým klientům, aby povolili uživatelům zrušit odběr jediným klepnutím.",
"settings.privacy.name": "Soukromí",
"settings.restart": "Restart",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Vlastní záhlaví",
"settings.smtp.customHeadersHelp": "Volitelné pole e-mailových záhlaví, která se mají zahrnout do všech zpráv odeslaných z tohoto serveru. Např.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Povoleno",

View file

@ -304,6 +304,7 @@
"public.errorFetchingLists": "Gwall wrth chwilio am y rhestrau. Rhowch gynnig arall arni.",
"public.errorProcessingRequest": "Gwall wrth brosesu'r cais. Rhowch gynnig arall arni.",
"public.errorTitle": "Gwall",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Nid yw'r nodwedd ar gael.",
"public.invalidLink": "Dolen annilys",
"public.managePrefs": "Rheoli dewisiadau",
@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Cynnwys penynnau dad-danysgrifio sy'n caniatáu i ddefnyddwyr dad-danysgrifio drwy glicio un botwm.",
"settings.privacy.name": "Preifatrwydd",
"settings.restart": "Ailgychwyn",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Penynnau personol",
"settings.smtp.customHeadersHelp": "Ystod eang o bennynau e-bost i'w cynnwys mewn negeseuon a anfonir gan y gweinydd hwn. ee: [{\"\"X-Custom\"\": \"\"gwerth\"\"}",
"settings.smtp.enabled": "Wedi galluogi",

View file

@ -304,6 +304,7 @@
"public.errorFetchingLists": "Fehler beim Abrufen der Listen. Bitte probiere es nochmal.",
"public.errorProcessingRequest": "Fehler bei der Anfrage. Bitte probiere es nochmal.",
"public.errorTitle": "Fehler",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Dieses Feature ist nicht verfügbar",
"public.invalidLink": "Ungültiger Link",
"public.managePrefs": "Einstellungen verwalten",
@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Inkludiere Header zum einfachen Abmelden in den E-Mails. Erlaubt es, den E-Mail Clients der Nutzer eine \",Ein Klick\"-Abmeldung anzubieten.",
"settings.privacy.name": "Privatsphäre",
"settings.restart": "Neustarten",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Benutzerdefinierte Header",
"settings.smtp.customHeadersHelp": "(Optional) Array von benutzerdefinierten E-Mail Headern, welche in die Nachricht eingefügt werden sollen. Z.B.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Aktiviert",

View file

@ -304,6 +304,7 @@
"public.errorFetchingLists": "Error fetching lists. Please retry.",
"public.errorProcessingRequest": "Error processing request. Please retry.",
"public.errorTitle": "Error",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "That feature is not available.",
"public.invalidLink": "Invalid link",
"public.managePrefs": "Manage preferences",
@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Include unsubscription headers that allow e-mail clients to allow users to unsubscribe in a single click.",
"settings.privacy.name": "Privacy",
"settings.restart": "Restart",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Custom headers",
"settings.smtp.customHeadersHelp": "Optional array of e-mail headers to include in all messages sent from this server. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Enabled",

View file

@ -305,6 +305,7 @@
"public.errorFetchingLists": "Error obteniendo listas. Por favor intente nuevamente.",
"public.errorProcessingRequest": "Error al procesar la petición. Por favor intente nuevamente.",
"public.errorTitle": "Error",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Esta función no está disponible",
"public.invalidLink": "Enlace inválido",
"public.managePrefs": "Gestionar las preferencias",
@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Incluye los encabezados de darse de baja para habilitar a los clientes de correo para permitir a los usuarios darse de baja con un solo clic.",
"settings.privacy.name": "Privacidad",
"settings.restart": "Reiniciar",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Encabezados personalizados",
"settings.smtp.customHeadersHelp": "Lista de encabezados opcionales a incluir en todos los mensajes enviados desde este servidor. Por ejemplo {{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Habilitado",

View file

@ -305,6 +305,7 @@
"public.errorFetchingLists": "Virhe noutaessa postituslistoja. Ole hyvä ja yritä uudestaan.",
"public.errorProcessingRequest": "Virhe käsitellessä pyyntöäsi. Ole hyvä ja yritä uudestaan.",
"public.errorTitle": "Tapahtui virhe",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Tämä ominaisuus ei ole saatavilla.",
"public.invalidLink": "Virheellinen linkki",
"public.managePrefs": "Manage preferences",
@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Include unsubscription headers that allow e-mail clients to allow users to unsubscribe in a single click.",
"settings.privacy.name": "Yksityisyys",
"settings.restart": "Restart",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Custom headers",
"settings.smtp.customHeadersHelp": "Optional array of e-mail headers to include in all messages sent from this server. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Enabled",

View file

@ -305,6 +305,7 @@
"public.errorFetchingLists": "Erreur lors de la récupération des listes. Veuillez réessayer.",
"public.errorProcessingRequest": "Erreur lors du traitement de la demande. Veuillez réessayer.",
"public.errorTitle": "Erreur",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Cette fonctionnalité n'est pas disponible.",
"public.invalidLink": "Lien invalide",
"public.managePrefs": "Gérer les préférences",
@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Inclure des en-têtes de désabonnement qui permettent aux utilisateurs de se désabonner en un seul clic depuis leur client de messagerie.",
"settings.privacy.name": "Vie privée",
"settings.restart": "Redémarrer",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "En-têtes personnalisées",
"settings.smtp.customHeadersHelp": "Tableau facultatif d'en-têtes à inclure dans tous les e-mails envoyés depuis ce serveur. Par exemple : [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Activé",

View file

@ -305,6 +305,7 @@
"public.errorFetchingLists": "Hiba a listák lekérésekor. Kérjük, próbálja újra.",
"public.errorProcessingRequest": "Hiba a kérelem feldolgozásakor. Kérjük, próbálja újra.",
"public.errorTitle": "Hiba",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Ez a funkció nem elérhető.",
"public.invalidLink": "A link érvénytelen",
"public.managePrefs": "Manage preferences",
@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Tartalmazzon leiratkozási fejléceket, amelyek lehetővé teszik az e-mail kliensek számára, hogy a felhasználók egyetlen kattintással leiratkozhassanak.",
"settings.privacy.name": "Magánélet",
"settings.restart": "Újraindítás",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Egyéni fejlécek",
"settings.smtp.customHeadersHelp": "Az e-mail fejlécek opcionális tömbje a szerverről küldött összes üzenetben. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Engedélyez",

View file

@ -305,6 +305,7 @@
"public.errorFetchingLists": "Errore durante il recupero delle liste. Per favore, riprova.",
"public.errorProcessingRequest": "Errore durante la gestione della richiesta. Per favore, riprova.",
"public.errorTitle": "Errore",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Questa funzione non è disponibile.",
"public.invalidLink": "Link non valido",
"public.managePrefs": "Gestiona l'impostazinoni",
@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Includere intestazioni di annullamento dell'iscrizione che consentono agli utenti di annullare l'iscrizione con un clic dal proprio client di posta elettronica.",
"settings.privacy.name": "Privacy",
"settings.restart": "Riavviare",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Intestazioni personalizzate",
"settings.smtp.customHeadersHelp": "Matrice facoltativa di intestazioni di posta elettronica da includere in tutti i messaggi inviati da questo server. Ad esempio: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Attivata",

View file

@ -305,6 +305,7 @@
"public.errorFetchingLists": "リストの取得にエラーがありました。再試行してください。",
"public.errorProcessingRequest": "リクエスト中にエラーがありました。再試行してください。",
"public.errorTitle": "エラー",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "その機能は使用できません。",
"public.invalidLink": "無効なリンク",
"public.managePrefs": "設定変更",
@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "メールクライアントがワンクリックで登録解除をできるように登録解除用のヘッダーを含める。",
"settings.privacy.name": "プライバシー",
"settings.restart": "再起動",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "カスタムヘッダー",
"settings.smtp.customHeadersHelp": "このサーバーから送信する全てのメッセージに含まれる任意のメールヘッダーの配列。 例: [{\"X-カスタム\": \"バリュー\"}, {\"X-カスタム2\": \"バリュー\"}]",
"settings.smtp.enabled": "有効",

View file

@ -304,6 +304,7 @@
"public.errorFetchingLists": "ലിസ്റ്റുകൾ വീണ്ടെടുക്കുന്നതിൽ തടസം നേരിട്ടു. വീണ്ടും ശ്രമിക്കുക.",
"public.errorProcessingRequest": "അഭ്യർത്ഥനയിന്മേൽ നടപടിയെടുക്കുന്നതിൽ തടസം നേരിട്ടു. വീണ്ടും ശ്രമിക്കുക.",
"public.errorTitle": "എറർ",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "ഈ ഫീച്ചർ ലഭ്യമല്ല",
"public.invalidLink": "കണ്ണി അസാധുവാണ്",
"public.managePrefs": "മുൻഗണനകളിൽ മാറ്റം വരുത്തുക",
@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "ഒറ്റ ക്ലിക്കിലൂടെ വരിക്കാനല്ലാതാക്കാൻ ഇ-മെയിൽ ക്ലൈന്റിൽ വരിക്കാരനല്ലാതാക്കാനുള്ള തലക്കെട്ട് കൂട്ടിച്ചേർക്കുക.",
"settings.privacy.name": "സ്വകാര്യത",
"settings.restart": "പുനരാരംഭിയ്ക്കുക",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "ഇഷ്ടാനുസൃത തലക്കെട്ടുകൾ",
"settings.smtp.customHeadersHelp": "ഈ സേർവറിൽ നിന്നും അയക്കുന്ന എല്ലാ ഈ-മെയിലിലും ഉണ്ടാകേണ്ട ഇഷ്ടാനുസൃത തലക്കെട്ടുകൾ. ഉദാഹരണം: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "പ്രവർത്തനക്ഷമമാക്കി",

View file

@ -305,6 +305,7 @@
"public.errorFetchingLists": "Fout bij ophalen lijsten. Probeer opnieuw.",
"public.errorProcessingRequest": "Fout bij behandelen verzoek. Probeer opnieuw.",
"public.errorTitle": "Fout",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Deze functie is niet beschikbaar",
"public.invalidLink": "Ongeldige link",
"public.managePrefs": "Manage preferences",
@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Voeg header toe zodat e-mailprogramma's gebruikers zich kunnen laten uitschrijven in een klik.",
"settings.privacy.name": "Privacy",
"settings.restart": "Herstarten",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Aangepaste headers",
"settings.smtp.customHeadersHelp": "Optionele lijst met e-mail headers om toe te voegen aan alle berichten van deze server. Bv.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Ingeschakeld",

View file

@ -305,6 +305,7 @@
"public.errorFetchingLists": "Błąd pobierania list. Spróbuj ponownie.",
"public.errorProcessingRequest": "Błąd przetwarzania żądania. Spróbuj ponownie.",
"public.errorTitle": "Błąd",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Ta funkcjonalność jest niedostępna.",
"public.invalidLink": "Nieprawidłowy liny.",
"public.managePrefs": "Manage preferences",
@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Dodaj nagłówki do wypisania się z subskrypcji. Niektóre programy pocztowe umożliwiają wypisanie się jednym kliknięciem.",
"settings.privacy.name": "Prywatność",
"settings.restart": "Restart",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Niestandardowe nagłówki",
"settings.smtp.customHeadersHelp": "Opcjonalna lista nagłówków do zamieszczania w wiadomościach we wszystkich wiadomościach wysłanych z tego serwera. np: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Włączone",

View file

@ -304,6 +304,7 @@
"public.errorFetchingLists": "Erro ao obter as listas. Por favor, tente novamente.",
"public.errorProcessingRequest": "Erro ao processar a solicitação. Por favor, tente novamente.",
"public.errorTitle": "Erro",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Este recurso não está disponível.",
"public.invalidLink": "Link inválido",
"public.managePrefs": "Gerenciar preferências",
@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Incluir cabeçalhos de desinscrição que permitem aos clientes de e-mail cancelem a inscrição em um único clique.",
"settings.privacy.name": "Privacidade",
"settings.restart": "Reiniciar",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Cabeçalhos personalizados",
"settings.smtp.customHeadersHelp": "Array opcional de cabeçalhos de e-mail para incluir em todas as mensagens enviadas a partir deste servidor. por exemplo: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Habilitado",

View file

@ -304,6 +304,7 @@
"public.errorFetchingLists": "Erro ao carregar listas. Por favor tente novamente.",
"public.errorProcessingRequest": "Erro ao processar pedido. Por favor tente novamente.",
"public.errorTitle": "Erro",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "That feature is not available",
"public.invalidLink": "Link inválido",
"public.managePrefs": "Gerir preferências",
@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Incluir headers de cancelamento de subscrição que permite aos clientes de email permitir ao utilizadores cancelar a subscrição num único clique.",
"settings.privacy.name": "Privacidade",
"settings.restart": "Reiniciar",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Headers customizados",
"settings.smtp.customHeadersHelp": "Array opcional de headers de email a incluir em todas as mensagens enviadas deste servidor. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Ativo",

View file

@ -305,6 +305,7 @@
"public.errorFetchingLists": "Eroare la preluarea listelor. Încearcă din nou.",
"public.errorProcessingRequest": "Eroare la procesarea cererii. Încearcă din nou.",
"public.errorTitle": "Eroare",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Această functie nu este disponibilă",
"public.invalidLink": "Link invalid",
"public.managePrefs": "Manage preferences",
@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Include anteturi de dezabonare care permit clienților de e-mail să permită utilizatorilor să se dezaboneze printr-un singur clic.",
"settings.privacy.name": "Confidențialitate",
"settings.restart": "Restart",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Anteturi personalizate",
"settings.smtp.customHeadersHelp": "Matrice opțională de antete de e-mail pentru a include în toate mesajele trimise de pe acest server. de exemplu: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Activat",

View file

@ -304,6 +304,7 @@
"public.errorFetchingLists": "Ошибка получения списков. Пожалуйста, повторите.",
"public.errorProcessingRequest": "Ошибка обработки запроса. Пожалуйста, повторите.",
"public.errorTitle": "Ошибка",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Эта функция недоступна.",
"public.invalidLink": "Неверная ссылка",
"public.managePrefs": "Параметры письма",
@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Включать заголовок отписки",
"settings.privacy.name": "Конфиденциальност",
"settings.restart": "Перезапустить",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Настраиваемые заголовки",
"settings.smtp.customHeadersHelp": "Необязательный массив заголовков e-mail, которые будут включены во все письма, отправляемые с этого сервера. Например: [{\"X-Custom\": \"значение\"}, {\"X-Custom2\": \"значение\"}]",
"settings.smtp.enabled": "Включено",

View file

@ -304,6 +304,7 @@
"public.errorFetchingLists": "Chyba pri načítání zoznamov. Zopakujte pokus.",
"public.errorProcessingRequest": "Chyba pri spracovaní požiadavky. Zopakujte pokus.",
"public.errorTitle": "Chyba",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Táto funkcia nie je k dispozícii.",
"public.invalidLink": "Neplatný odkaz",
"public.managePrefs": "Správa predvolieb",
@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Nastaví hlavičku zrušenia odberov, ktorá umožňuje e-mailovým klientom, aby povolili používateľom zrušiť odber jedným kliknutím.",
"settings.privacy.name": "Súkromie",
"settings.restart": "Restarť",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Vlastné hlavičky",
"settings.smtp.customHeadersHelp": "Voliteľné polia e-mailových hlavičiek, ktorá sa majú nastaviť do všetkých správ odoslaných z tohoto servera. Napr.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Zapnuté",

View file

@ -305,6 +305,7 @@
"public.errorFetchingLists": "Listeleri getirme hatası. Lütfen tekrarla.",
"public.errorProcessingRequest": "İstek işleme hatası. Lütfen tekrarla.",
"public.errorTitle": "Hata",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Bu özellik geçerli değil.",
"public.invalidLink": "Geçersiz link",
"public.managePrefs": "Manage preferences",
@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "E-posta istemcilerinin kullanıcıların tek bir tıklamayla abonelikten çıkmalarına olanak tanıyan abonelik iptal başlıklarını ekleyin.",
"settings.privacy.name": "Gizlilik",
"settings.restart": "Yeniden başlat",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Özel başlık bilgisi",
"settings.smtp.customHeadersHelp": "Bu sunucudan gönderilen tüm iletilere eklenecek isteğe bağlı e-posta başlıkları dizisi. Örnek: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Etkinleştirildi",

View file

@ -305,6 +305,7 @@
"public.errorFetchingLists": "Lỗi khi tìm nạp danh sách. Xin hãy thử lại.",
"public.errorProcessingRequest": "Lỗi khi xử lý yêu cầu. Xin hãy thử lại.",
"public.errorTitle": "Lỗi",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Tính năng đó không khả dụng.",
"public.invalidLink": "Link không khả dụng",
"public.managePrefs": "Manage preferences",
@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Bao gồm các tiêu đề hủy đăng ký cho phép ứng dụng e-mail cho phép người dùng hủy đăng ký chỉ bằng một cú nhấp chuột.",
"settings.privacy.name": "Sự riêng tư",
"settings.restart": "Khởi động lại",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Tiêu đề tùy chỉnh",
"settings.smtp.customHeadersHelp": "Mảng tiêu đề e-mail tùy chọn để bao gồm trong tất cả các thư được gửi từ máy chủ này. ví dụ: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Đã bật",

View file

@ -304,6 +304,7 @@
"public.errorFetchingLists": "获取列表时出错。请重试。",
"public.errorProcessingRequest": "处理请求时出错。请重试。",
"public.errorTitle": "错误",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "该功能不可用。",
"public.invalidLink": "无效的链接",
"public.managePrefs": "管理偏好设置",
@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "包括允许电子邮件客户端允许用户通过单击取消订阅的取消订阅标题",
"settings.privacy.name": "隐私",
"settings.restart": "重新开始",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "自定义标头",
"settings.smtp.customHeadersHelp": "要包含在从此服务器发送的所有消息中的可选电子邮件标头数组。例如: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "已启用",

View file

@ -305,6 +305,7 @@
"public.errorFetchingLists": "獲取列表時出錯。請重試。",
"public.errorProcessingRequest": "處理請求時出錯。請重試。",
"public.errorTitle": "錯誤",
"public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "該功能不可用。",
"public.invalidLink": "無效的鏈接",
"public.managePrefs": "Manage preferences",
@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "包括允許電子郵件客戶端允許用戶通過單擊取消訂閱的取消訂閱標題",
"settings.privacy.name": "隱私",
"settings.restart": "重新開始",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "自定義標頭",
"settings.smtp.customHeadersHelp": "要包含在從此服務器發送的所有消息中的可選電子郵件標頭數組。例如: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "已啟用",

View file

@ -0,0 +1,76 @@
package captcha
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
)
const (
rootURL = "https://hcaptcha.com/siteverify"
)
type captchaResp struct {
Success bool `json:"success"`
ErrorCodes []string `json:"error_codes"`
}
// Captcha is a simple Captcha client.
// It currently implements hcaptcha.com
type Captcha struct {
o Opt
client *http.Client
}
type Opt struct {
CaptchaSecret string `json:"captcha_secret"`
}
// New returns a new instance of the HTTP CAPTCHA client.
func New(o Opt) *Captcha {
timeout := time.Second * 5
return &Captcha{
o: o,
client: &http.Client{
Timeout: timeout,
Transport: &http.Transport{
MaxIdleConnsPerHost: 10,
MaxConnsPerHost: 100,
ResponseHeaderTimeout: timeout,
IdleConnTimeout: timeout,
},
}}
}
// Verify veries a CAPTCHA request.
func (c *Captcha) Verify(token string) (error, bool) {
resp, err := c.client.PostForm(rootURL, url.Values{
"secret": {c.o.CaptchaSecret},
"response": {token},
})
if err != nil {
return err, false
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err, false
}
var r captchaResp
if json.Unmarshal(body, &r); err != nil {
return err, true
}
if r.Success != true {
return fmt.Errorf("captcha failed: %s", strings.Join(r.ErrorCodes, ",")), false
}
return nil, true
}

View file

@ -0,0 +1,23 @@
package migrations
import (
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf"
"github.com/knadh/stuffbin"
)
// V2_4_0 performs the DB migrations.
func V2_4_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
// Insert new preference settings.
if _, err := db.Exec(`
INSERT INTO settings (key, value) VALUES
('security.enable_captcha', 'false'),
('security.captcha_key', '""'),
('security.captcha_secret', '""')
ON CONFLICT DO NOTHING;
`); err != nil {
return err
}
return nil
}

View file

@ -32,6 +32,10 @@ type Settings struct {
PrivacyExportable []string `json:"privacy.exportable"`
DomainBlocklist []string `json:"privacy.domain_blocklist"`
SecurityEnableCaptcha bool `json:"security.enable_captcha"`
SecurityCaptchaKey string `json:"security.captcha_key"`
SecurityCaptchaSecret string `json:"security.captcha_secret"`
UploadProvider string `json:"upload.provider"`
UploadFilesystemUploadPath string `json:"upload.filesystem.upload_path"`
UploadFilesystemUploadURI string `json:"upload.filesystem.upload_uri"`

View file

@ -208,6 +208,9 @@ INSERT INTO settings (key, value) VALUES
('privacy.allow_preferences', 'true'),
('privacy.exportable', '["profile", "subscriptions", "campaign_views", "link_clicks"]'),
('privacy.domain_blocklist', '[]'),
('security.enable_captcha', 'false'),
('security.captcha_key', '""'),
('security.captcha_secret', '""'),
('upload.provider', '"filesystem"'),
('upload.filesystem.upload_path', '"uploads"'),
('upload.filesystem.upload_uri', '"/uploads"'),

View file

@ -122,7 +122,7 @@ input[disabled] {
.lists {
list-style-type: none;
padding: 0;
margin-bottom: 30px;
margin: 40px 0;
}
.lists li {
margin: 0 0 5px 0;
@ -137,6 +137,9 @@ input[disabled] {
.form .nonce {
display: none;
}
.form .captcha {
margin-top: 30px;
}
.archive {
list-style-type: none;

View file

@ -15,7 +15,7 @@
<label for="name">{{ L.T "public.subName" }}</label>
<input id="name" name="name" type="text" placeholder="{{ L.T "public.subName" }}" >
</p>
<br />
<ul class="lists">
<h2>{{ L.T "globals.terms.lists" }}</h2>
{{ range $i, $l := .Data.Lists }}
@ -28,6 +28,13 @@
</li>
{{ end }}
</ul>
{{ if .Data.CaptchaKey }}
<div class="captcha">
<div class="h-captcha" data-sitekey="{{ .Data.CaptchaKey }}"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
</div>
{{ end }}
<p>
<button type="submit" class="button">{{ L.T "public.sub" }}</button>