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