forked from tangled.org/core
this repo has no description
1package state 2 3import ( 4 "database/sql" 5 "errors" 6 "fmt" 7 "log" 8 "net/http" 9 "net/url" 10 "strings" 11 "time" 12 13 comatproto "github.com/bluesky-social/indigo/api/atproto" 14 lexutil "github.com/bluesky-social/indigo/lex/util" 15 "github.com/gliderlabs/ssh" 16 "github.com/google/uuid" 17 "tangled.sh/tangled.sh/core/api/tangled" 18 "tangled.sh/tangled.sh/core/appview/db" 19 "tangled.sh/tangled.sh/core/appview/email" 20 "tangled.sh/tangled.sh/core/appview/pages" 21) 22 23func (s *State) Settings(w http.ResponseWriter, r *http.Request) { 24 user := s.auth.GetUser(r) 25 pubKeys, err := db.GetPublicKeys(s.db, user.Did) 26 if err != nil { 27 log.Println(err) 28 } 29 30 emails, err := db.GetAllEmails(s.db, user.Did) 31 if err != nil { 32 log.Println(err) 33 } 34 35 s.pages.Settings(w, pages.SettingsParams{ 36 LoggedInUser: user, 37 PubKeys: pubKeys, 38 Emails: emails, 39 }) 40} 41 42// buildVerificationEmail creates an email.Email struct for verification emails 43func (s *State) buildVerificationEmail(emailAddr, did, code string) email.Email { 44 verifyURL := s.verifyUrl(did, emailAddr, code) 45 46 return email.Email{ 47 APIKey: s.config.ResendApiKey, 48 From: "noreply@notifs.tangled.sh", 49 To: emailAddr, 50 Subject: "Verify your Tangled email", 51 Text: `Click the link below (or copy and paste it into your browser) to verify your email address. 52` + verifyURL, 53 Html: `<p>Click the link (or copy and paste it into your browser) to verify your email address.</p> 54<p><a href="` + verifyURL + `">` + verifyURL + `</a></p>`, 55 } 56} 57 58// sendVerificationEmail handles the common logic for sending verification emails 59func (s *State) sendVerificationEmail(w http.ResponseWriter, did, emailAddr, code string, errorContext string) error { 60 emailToSend := s.buildVerificationEmail(emailAddr, did, code) 61 62 err := email.SendEmail(emailToSend) 63 if err != nil { 64 log.Printf("sending email: %s", err) 65 s.pages.Notice(w, "settings-emails-error", fmt.Sprintf("Unable to send verification email at this moment, try again later. %s", errorContext)) 66 return err 67 } 68 69 return nil 70} 71 72func (s *State) SettingsEmails(w http.ResponseWriter, r *http.Request) { 73 switch r.Method { 74 case http.MethodGet: 75 s.pages.Notice(w, "settings-emails", "Unimplemented.") 76 log.Println("unimplemented") 77 return 78 case http.MethodPut: 79 did := s.auth.GetDid(r) 80 emAddr := r.FormValue("email") 81 emAddr = strings.TrimSpace(emAddr) 82 83 if !email.IsValidEmail(emAddr) { 84 s.pages.Notice(w, "settings-emails-error", "Invalid email address.") 85 return 86 } 87 88 // check if email already exists in database 89 existingEmail, err := db.GetEmail(s.db, did, emAddr) 90 if err != nil && !errors.Is(err, sql.ErrNoRows) { 91 log.Printf("checking for existing email: %s", err) 92 s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") 93 return 94 } 95 96 if err == nil { 97 if existingEmail.Verified { 98 s.pages.Notice(w, "settings-emails-error", "This email is already verified.") 99 return 100 } 101 102 s.pages.Notice(w, "settings-emails-error", "This email is already added but not verified. Check your inbox for the verification link.") 103 return 104 } 105 106 code := uuid.New().String() 107 108 // Begin transaction 109 tx, err := s.db.Begin() 110 if err != nil { 111 log.Printf("failed to start transaction: %s", err) 112 s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") 113 return 114 } 115 defer tx.Rollback() 116 117 if err := db.AddEmail(tx, db.Email{ 118 Did: did, 119 Address: emAddr, 120 Verified: false, 121 VerificationCode: code, 122 }); err != nil { 123 log.Printf("adding email: %s", err) 124 s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") 125 return 126 } 127 128 if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil { 129 return 130 } 131 132 // Commit transaction 133 if err := tx.Commit(); err != nil { 134 log.Printf("failed to commit transaction: %s", err) 135 s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") 136 return 137 } 138 139 s.pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.") 140 return 141 case http.MethodDelete: 142 did := s.auth.GetDid(r) 143 emailAddr := r.FormValue("email") 144 emailAddr = strings.TrimSpace(emailAddr) 145 146 // Begin transaction 147 tx, err := s.db.Begin() 148 if err != nil { 149 log.Printf("failed to start transaction: %s", err) 150 s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") 151 return 152 } 153 defer tx.Rollback() 154 155 if err := db.DeleteEmail(tx, did, emailAddr); err != nil { 156 log.Printf("deleting email: %s", err) 157 s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") 158 return 159 } 160 161 // Commit transaction 162 if err := tx.Commit(); err != nil { 163 log.Printf("failed to commit transaction: %s", err) 164 s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") 165 return 166 } 167 168 s.pages.HxLocation(w, "/settings") 169 return 170 } 171} 172 173func (s *State) verifyUrl(did string, email string, code string) string { 174 var appUrl string 175 if s.config.Dev { 176 appUrl = "http://" + s.config.ListenAddr 177 } else { 178 appUrl = "https://tangled.sh" 179 } 180 181 return fmt.Sprintf("%s/settings/emails/verify?did=%s&email=%s&code=%s", appUrl, url.QueryEscape(did), url.QueryEscape(email), url.QueryEscape(code)) 182} 183 184func (s *State) SettingsEmailsVerify(w http.ResponseWriter, r *http.Request) { 185 q := r.URL.Query() 186 187 // Get the parameters directly from the query 188 emailAddr := q.Get("email") 189 did := q.Get("did") 190 code := q.Get("code") 191 192 valid, err := db.CheckValidVerificationCode(s.db, did, emailAddr, code) 193 if err != nil { 194 log.Printf("checking email verification: %s", err) 195 s.pages.Notice(w, "settings-emails-error", "Error verifying email. Please try again later.") 196 return 197 } 198 199 if !valid { 200 s.pages.Notice(w, "settings-emails-error", "Invalid verification code. Please request a new verification email.") 201 return 202 } 203 204 // Mark email as verified in the database 205 if err := db.MarkEmailVerified(s.db, did, emailAddr); err != nil { 206 log.Printf("marking email as verified: %s", err) 207 s.pages.Notice(w, "settings-emails-error", "Error updating email verification status. Please try again later.") 208 return 209 } 210 211 http.Redirect(w, r, "/settings", http.StatusSeeOther) 212} 213 214func (s *State) SettingsEmailsVerifyResend(w http.ResponseWriter, r *http.Request) { 215 if r.Method != http.MethodPost { 216 s.pages.Notice(w, "settings-emails-error", "Invalid request method.") 217 return 218 } 219 220 did := s.auth.GetDid(r) 221 emAddr := r.FormValue("email") 222 emAddr = strings.TrimSpace(emAddr) 223 224 if !email.IsValidEmail(emAddr) { 225 s.pages.Notice(w, "settings-emails-error", "Invalid email address.") 226 return 227 } 228 229 // Check if email exists and is unverified 230 existingEmail, err := db.GetEmail(s.db, did, emAddr) 231 if err != nil { 232 if errors.Is(err, sql.ErrNoRows) { 233 s.pages.Notice(w, "settings-emails-error", "Email not found. Please add it first.") 234 } else { 235 log.Printf("checking for existing email: %s", err) 236 s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 237 } 238 return 239 } 240 241 if existingEmail.Verified { 242 s.pages.Notice(w, "settings-emails-error", "This email is already verified.") 243 return 244 } 245 246 // Check if last verification email was sent less than 10 minutes ago 247 if existingEmail.LastSent != nil { 248 timeSinceLastSent := time.Since(*existingEmail.LastSent) 249 if timeSinceLastSent < 10*time.Minute { 250 waitTime := 10*time.Minute - timeSinceLastSent 251 s.pages.Notice(w, "settings-emails-error", fmt.Sprintf("Please wait %d minutes before requesting another verification email.", int(waitTime.Minutes()+1))) 252 return 253 } 254 } 255 256 // Generate new verification code 257 code := uuid.New().String() 258 259 // Begin transaction 260 tx, err := s.db.Begin() 261 if err != nil { 262 log.Printf("failed to start transaction: %s", err) 263 s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 264 return 265 } 266 defer tx.Rollback() 267 268 // Update the verification code and last sent time 269 if err := db.UpdateVerificationCode(tx, did, emAddr, code); err != nil { 270 log.Printf("updating email verification: %s", err) 271 s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 272 return 273 } 274 275 // Send verification email 276 if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil { 277 return 278 } 279 280 // Commit transaction 281 if err := tx.Commit(); err != nil { 282 log.Printf("failed to commit transaction: %s", err) 283 s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 284 return 285 } 286 287 s.pages.Notice(w, "settings-emails-success", "Verification email resent. Click the link in the email we sent you to verify your email address.") 288} 289 290func (s *State) SettingsEmailsPrimary(w http.ResponseWriter, r *http.Request) { 291 did := s.auth.GetDid(r) 292 emailAddr := r.FormValue("email") 293 emailAddr = strings.TrimSpace(emailAddr) 294 295 if emailAddr == "" { 296 s.pages.Notice(w, "settings-emails-error", "Email address cannot be empty.") 297 return 298 } 299 300 if err := db.MakeEmailPrimary(s.db, did, emailAddr); err != nil { 301 log.Printf("setting primary email: %s", err) 302 s.pages.Notice(w, "settings-emails-error", "Error setting primary email. Please try again later.") 303 return 304 } 305 306 s.pages.HxLocation(w, "/settings") 307} 308 309func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) { 310 switch r.Method { 311 case http.MethodGet: 312 s.pages.Notice(w, "settings-keys", "Unimplemented.") 313 log.Println("unimplemented") 314 return 315 case http.MethodPut: 316 did := s.auth.GetDid(r) 317 key := r.FormValue("key") 318 key = strings.TrimSpace(key) 319 name := r.FormValue("name") 320 client, _ := s.auth.AuthorizedClient(r) 321 322 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) 323 if err != nil { 324 log.Printf("parsing public key: %s", err) 325 s.pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> key.") 326 return 327 } 328 329 rkey := s.TID() 330 331 tx, err := s.db.Begin() 332 if err != nil { 333 log.Printf("failed to start tx; adding public key: %s", err) 334 s.pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.") 335 return 336 } 337 defer tx.Rollback() 338 339 if err := db.AddPublicKey(tx, did, name, key, rkey); err != nil { 340 log.Printf("adding public key: %s", err) 341 s.pages.Notice(w, "settings-keys", "Failed to add public key.") 342 return 343 } 344 345 // store in pds too 346 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 347 Collection: tangled.PublicKeyNSID, 348 Repo: did, 349 Rkey: rkey, 350 Record: &lexutil.LexiconTypeDecoder{ 351 Val: &tangled.PublicKey{ 352 Created: time.Now().Format(time.RFC3339), 353 Key: key, 354 Name: name, 355 }}, 356 }) 357 // invalid record 358 if err != nil { 359 log.Printf("failed to create record: %s", err) 360 s.pages.Notice(w, "settings-keys", "Failed to create record.") 361 return 362 } 363 364 log.Println("created atproto record: ", resp.Uri) 365 366 err = tx.Commit() 367 if err != nil { 368 log.Printf("failed to commit tx; adding public key: %s", err) 369 s.pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.") 370 return 371 } 372 373 s.pages.HxLocation(w, "/settings") 374 return 375 376 case http.MethodDelete: 377 did := s.auth.GetDid(r) 378 q := r.URL.Query() 379 380 name := q.Get("name") 381 rkey := q.Get("rkey") 382 key := q.Get("key") 383 384 log.Println(name) 385 log.Println(rkey) 386 log.Println(key) 387 388 client, _ := s.auth.AuthorizedClient(r) 389 390 if err := db.RemovePublicKey(s.db, did, name, key); err != nil { 391 log.Printf("removing public key: %s", err) 392 s.pages.Notice(w, "settings-keys", "Failed to remove public key.") 393 return 394 } 395 396 if rkey != "" { 397 // remove from pds too 398 _, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 399 Collection: tangled.PublicKeyNSID, 400 Repo: did, 401 Rkey: rkey, 402 }) 403 404 // invalid record 405 if err != nil { 406 log.Printf("failed to delete record from PDS: %s", err) 407 s.pages.Notice(w, "settings-keys", "Failed to remove key from PDS.") 408 return 409 } 410 } 411 log.Println("deleted successfully") 412 413 s.pages.HxLocation(w, "/settings") 414 return 415 } 416}