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