Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview/signup: set up cf turnstile

Sets up Cloudflare Turnstile for fairly non-intrusive captcha. The
client token is verified with CF when the user hits 'join now' (POST
/signup), so this should prevent bot signups.

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>

authored by

Anirudh Oppiliappan and committed by
Tangled
e4dba60f 4805af2f

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