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