+4
-2
appview/config/config.go
+4
-2
appview/config/config.go
+6
-2
appview/pages/pages.go
+6
-2
appview/pages/pages.go
···
226
return p.executePlain("user/login", w, params)
227
}
228
229
+
type SignupParams struct {
230
+
CloudflareSiteKey string
231
+
}
232
+
233
+
func (p *Pages) Signup(w io.Writer, params SignupParams) error {
234
+
return p.executePlain("user/signup", w, params)
235
}
236
237
func (p *Pages) CompleteSignup(w io.Writer) error {
+1
-1
appview/pages/templates/user/login.html
+1
-1
appview/pages/templates/user/login.html
···
36
placeholder="akshay.tngl.sh"
37
/>
38
<span class="text-sm text-gray-500 mt-1">
39
-
Use your <a href="https://atproto.com">ATProto</a>
40
handle to log in. If you're unsure, this is likely
41
your Tangled (<code>.tngl.sh</code>) or <a href="https://bsky.app">Bluesky</a> (<code>.bsky.social</code>) account.
42
</span>
···
36
placeholder="akshay.tngl.sh"
37
/>
38
<span class="text-sm text-gray-500 mt-1">
39
+
Use your <a href="https://atproto.com">AT Protocol</a>
40
handle to log in. If you're unsure, this is likely
41
your Tangled (<code>.tngl.sh</code>) or <a href="https://bsky.app">Bluesky</a> (<code>.bsky.social</code>) account.
42
</span>
+6
-1
appview/pages/templates/user/signup.html
+6
-1
appview/pages/templates/user/signup.html
···
10
<script src="/static/htmx.min.js"></script>
11
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
12
<title>sign up · tangled</title>
13
</head>
14
<body class="flex items-center justify-center min-h-screen">
15
<main class="max-w-md px-6 -mt-4">
···
39
invite code, desired username, and password in the next
40
page to complete your registration.
41
</span>
42
<button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" >
43
<span>join now</span>
44
</button>
45
</form>
46
<p class="text-sm text-gray-500">
47
-
Already have an ATProto account? <a href="/login" class="underline">Login to Tangled</a>.
48
</p>
49
50
<p id="signup-msg" class="error w-full"></p>
···
10
<script src="/static/htmx.min.js"></script>
11
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
12
<title>sign up · tangled</title>
13
+
14
+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
15
</head>
16
<body class="flex items-center justify-center min-h-screen">
17
<main class="max-w-md px-6 -mt-4">
···
41
invite code, desired username, and password in the next
42
page to complete your registration.
43
</span>
44
+
<div class="w-full mt-4">
45
+
<div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}"></div>
46
+
</div>
47
<button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" >
48
<span>join now</span>
49
</button>
50
</form>
51
<p class="text-sm text-gray-500">
52
+
Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
53
</p>
54
55
<p id="signup-msg" class="error w-full"></p>
+65
-1
appview/signup/signup.go
+65
-1
appview/signup/signup.go
···
2
3
import (
4
"bufio"
5
"fmt"
6
"log/slog"
7
"net/http"
8
"os"
9
"strings"
10
···
116
func (s *Signup) signup(w http.ResponseWriter, r *http.Request) {
117
switch r.Method {
118
case http.MethodGet:
119
-
s.pages.Signup(w)
120
case http.MethodPost:
121
if s.cf == nil {
122
http.Error(w, "signup is disabled", http.StatusFailedDependency)
123
}
124
emailId := r.FormValue("email")
125
126
noticeId := "signup-msg"
127
if !email.IsValidEmail(emailId) {
128
s.pages.Notice(w, noticeId, "Invalid email address.")
129
return
···
255
return
256
}
257
}
···
2
3
import (
4
"bufio"
5
+
"encoding/json"
6
+
"errors"
7
"fmt"
8
"log/slog"
9
"net/http"
10
+
"net/url"
11
"os"
12
"strings"
13
···
119
func (s *Signup) signup(w http.ResponseWriter, r *http.Request) {
120
switch r.Method {
121
case http.MethodGet:
122
+
s.pages.Signup(w, pages.SignupParams{
123
+
CloudflareSiteKey: s.config.Cloudflare.TurnstileSiteKey,
124
+
})
125
case http.MethodPost:
126
if s.cf == nil {
127
http.Error(w, "signup is disabled", http.StatusFailedDependency)
128
+
return
129
}
130
emailId := r.FormValue("email")
131
+
cfToken := r.FormValue("cf-turnstile-response")
132
133
noticeId := "signup-msg"
134
+
135
+
if err := s.validateCaptcha(cfToken, r); err != nil {
136
+
s.l.Warn("turnstile validation failed", "error", err)
137
+
s.pages.Notice(w, noticeId, "Captcha validation failed.")
138
+
return
139
+
}
140
+
141
if !email.IsValidEmail(emailId) {
142
s.pages.Notice(w, noticeId, "Invalid email address.")
143
return
···
269
return
270
}
271
}
272
+
273
+
type turnstileResponse struct {
274
+
Success bool `json:"success"`
275
+
ErrorCodes []string `json:"error-codes,omitempty"`
276
+
ChallengeTs string `json:"challenge_ts,omitempty"`
277
+
Hostname string `json:"hostname,omitempty"`
278
+
}
279
+
280
+
func (s *Signup) validateCaptcha(cfToken string, r *http.Request) error {
281
+
if cfToken == "" {
282
+
return errors.New("captcha token is empty")
283
+
}
284
+
285
+
if s.config.Cloudflare.TurnstileSecretKey == "" {
286
+
return errors.New("turnstile secret key not configured")
287
+
}
288
+
289
+
data := url.Values{}
290
+
data.Set("secret", s.config.Cloudflare.TurnstileSecretKey)
291
+
data.Set("response", cfToken)
292
+
293
+
// include the client IP if we have it
294
+
if remoteIP := r.Header.Get("CF-Connecting-IP"); remoteIP != "" {
295
+
data.Set("remoteip", remoteIP)
296
+
} else if remoteIP := r.Header.Get("X-Forwarded-For"); remoteIP != "" {
297
+
if ips := strings.Split(remoteIP, ","); len(ips) > 0 {
298
+
data.Set("remoteip", strings.TrimSpace(ips[0]))
299
+
}
300
+
} else {
301
+
data.Set("remoteip", r.RemoteAddr)
302
+
}
303
+
304
+
resp, err := http.PostForm("https://challenges.cloudflare.com/turnstile/v0/siteverify", data)
305
+
if err != nil {
306
+
return fmt.Errorf("failed to verify turnstile token: %w", err)
307
+
}
308
+
defer resp.Body.Close()
309
+
310
+
var turnstileResp turnstileResponse
311
+
if err := json.NewDecoder(resp.Body).Decode(&turnstileResp); err != nil {
312
+
return fmt.Errorf("failed to decode turnstile response: %w", err)
313
+
}
314
+
315
+
if !turnstileResp.Success {
316
+
s.l.Warn("turnstile validation failed", "error_codes", turnstileResp.ErrorCodes)
317
+
return errors.New("turnstile validation failed")
318
+
}
319
+
320
+
return nil
321
+
}