Skip to content

Commit 87e2d80

Browse files
committed
Implement captcha on subscribe page
1 parent 7ef4426 commit 87e2d80

File tree

7 files changed

+115
-6
lines changed

7 files changed

+115
-6
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,13 @@ If the `SMTP_HOST` is specified as an environment variable, then it won't use th
362362
# 🌐 Environment variables
363363
The values are customizable within the system and will be saved to the database.
364364
365+
**CAPTCHA**
366+
If set, the captcha will be enabled on the subscribe page.
367+
```sh
368+
export GOOGLE_RECAPTCHA_SECRET=NONE
369+
export GOOGLE_RECAPTCHA_SITE_KEY=NONE
370+
```
371+
365372
**Database**
366373
```sh
367374
export PG_HOST=localhost

nimletter.nimble

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Package
22

3-
version = "0.6.4"
3+
version = "0.6.5"
44
author = "ThomasTJdev"
55
description = "Newsletter"
66
license = "AGPL v3"

nimletter.service

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ ExecStart=/usr/bin/podman run \
4343
--label io.containers.autoupdate=registry \
4444
--log-driver journald \
4545
--log-opt tag=nimletter \
46+
--env ALLOW_GLOBAL_SUBSCRIPTION=false \
47+
--env GOOGLE_RECAPTCHA_SECRET=NONE \
48+
--env GOOGLE_RECAPTCHA_SITE_KEY=NONE \
4649
--env RUN_MODE=prod \
4750
--env PG_HOST=localhost:5432 \
4851
--env PG_USER=postgres \

src/html/optin.nimf

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@
4848
<input type="text" name="name" placeholder="Name">
4949
<input type="email" name="email" placeholder="Email" required>
5050
<input type="text" name="username_second" placeholder="Name" class="hideme">
51+
# let recaptchaSiteKey = getEnv("GOOGLE_RECAPTCHA_SITE_KEY")
52+
# if recaptchaSiteKey.len() > 0 and recaptchaSiteKey != "NONE":
53+
<div style="display: flex ; justify-content: center;">
54+
<div class="g-recaptcha" data-sitekey="${recaptchaSiteKey}" data-theme="light" style="transform:scale(0.93);-webkit-transform:scale(0.93);transform-origin:0 0;-webkit-transform-origin:0 0;"></div>
55+
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
56+
</div>
57+
# end if
5158
<div class="buttonArea">
5259
<button type="submit" class="buttonIcon">
5360
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
@@ -82,14 +89,14 @@
8289
<body class="mainbody">
8390
<main>
8491
<div class="content notLoggedIn">
85-
<h1 id="heading" class="center">Nimletter, drip it!</h1>
92+
<h1 id="heading" class="center" style="font-weight: 500;">Subscription confirmed!</h1>
8693
<div id="work subscribe">
87-
<div class="mb30" style="display: flex;">
88-
<img src="/assets/images/nimletter.png" style="width: 300px;margin: auto;">
89-
</div>
9094
<p class="center">
9195
Perfect! Glad to have you onboard! 🚀
9296
</p>
97+
<p class="center">
98+
You can <a href="#!" onclick="window.close()">close this window</a> now.
99+
</p>
93100
</div>
94101
</div>
95102
</main>

src/routes/routes_flows.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ proc(request: Request) =
206206
scheduledTime = if triggerType == "time": @"scheduledTime" else: ""
207207

208208
if name.strip() == "":
209-
resp Http400, "Subject is required"
209+
resp Http400, "Flow name is required"
210210

211211
if not mailID.isValidInt():
212212
resp Http400, "Invalid mail ID"

src/routes/routes_optin.nim

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import
2727
../email/email_optin,
2828
../utils/contacts_utils,
2929
../utils/list_utils,
30+
../utils/google_recaptcha,
3031
../utils/validate_data,
3132
../webhook/webhook_events
3233

@@ -145,6 +146,10 @@ proc(request: Request) =
145146
if not email.isValidEmail():
146147
resp Http400, nimfOptinSubscribe(true, "Invalid email", "")
147148

149+
#when not defined(dev):
150+
if not checkReCaptcha(@"g-recaptcha-response", request.ip):
151+
resp Http400, nimfOptinSubscribe(true, "Invalid captcha", "")
152+
148153

149154
var
150155
userID: string

src/utils/google_recaptcha.nim

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk
2+
3+
import
4+
std/[
5+
httpclient,
6+
json,
7+
os
8+
]
9+
10+
11+
const
12+
VerifyUrl: string = "https://www.google.com/recaptcha/api/siteverify"
13+
14+
type
15+
ReCaptcha* = object
16+
secret: string
17+
siteKey: string
18+
CaptchaVerificationError* = object of Exception
19+
20+
proc initReCaptcha*(secret, siteKey: string): ReCaptcha =
21+
result = ReCaptcha(
22+
secret: secret,
23+
siteKey: siteKey
24+
)
25+
26+
27+
proc checkVerification(mpd: MultipartData): bool =
28+
let
29+
client = newHttpClient()
30+
response = client.post(VerifyUrl, multipart=mpd)
31+
jsonContent = parseJson(response.body)
32+
success = jsonContent.getOrDefault("success")
33+
errors = jsonContent.getOrDefault("error-codes")
34+
35+
if errors != nil:
36+
for err in errors.items():
37+
case err.getStr()
38+
of "missing-input-secret":
39+
raise newException(CaptchaVerificationError, "The secret parameter is missing.")
40+
of "invalid-input-secret":
41+
raise newException(CaptchaVerificationError, "The secret parameter is invalid or malformed.")
42+
of "missing-input-response":
43+
raise newException(CaptchaVerificationError, "The response parameter is missing.")
44+
of "invalid-input-response":
45+
raise newException(CaptchaVerificationError, "The response parameter is invalid or malformed.")
46+
else: discard
47+
48+
result = if success != nil: success.getBool() else: false
49+
50+
proc verify*(rc: ReCaptcha, reCaptchaResponse, remoteIp: string): bool =
51+
let multiPart = newMultipartData({
52+
"secret": rc.secret,
53+
"response": reCaptchaResponse,
54+
"remoteip": remoteIp
55+
})
56+
result = checkVerification(multiPart)
57+
58+
proc verify*(rc: ReCaptcha, reCaptchaResponse: string): bool =
59+
let multiPart = newMultipartData({
60+
"secret": rc.secret,
61+
"response": reCaptchaResponse,
62+
})
63+
result = checkVerification(multiPart)
64+
65+
66+
proc checkReCaptcha*(antibot, userIP: string): bool =
67+
let secret = getEnv("GOOGLE_RECAPTCHA_SECRET")
68+
let siteKey = getEnv("GOOGLE_RECAPTCHA_SITE_KEY")
69+
70+
if secret == "" or siteKey == "":
71+
return true
72+
if secret == "NONE" or siteKey == "NONE":
73+
return true
74+
75+
let captcha = initReCaptcha(secret, siteKey)
76+
77+
78+
var captchaValid: bool = false
79+
try:
80+
captchaValid = captcha.verify(antibot, userIP)
81+
except:
82+
echo("checkReCaptcha(): Error checking captcha. UserIP: " & userIP & " - ErrMsg: " & getCurrentExceptionMsg())
83+
return false
84+
85+
return captchaValid
86+
87+

0 commit comments

Comments
 (0)