github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/routes/account.go (about) 1 package routes 2 3 import ( 4 "crypto/sha256" 5 "crypto/subtle" 6 "database/sql" 7 "encoding/hex" 8 "html" 9 "log" 10 "math" 11 "net/http" 12 "strconv" 13 "strings" 14 15 c "github.com/Azareal/Gosora/common" 16 p "github.com/Azareal/Gosora/common/phrases" 17 qgen "github.com/Azareal/Gosora/query_gen" 18 ) 19 20 // A blank list to fill out that parameter in Page for routes which don't use it 21 var tList []interface{} 22 23 func AccountLogin(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 24 if u.Loggedin { 25 return c.LocalError("You're already logged in.", w, r, u) 26 } 27 h.Title = p.GetTitlePhrase("login") 28 return renderTemplate("login", w, r, h, c.Page{h, tList, nil}) 29 } 30 31 // TODO: Log failed attempted logins? 32 // TODO: Lock IPS out if they have too many failed attempts? 33 // TODO: Log unusual countries in comparison to the country a user usually logs in from? Alert the user about this? 34 func AccountLoginSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 35 if u.Loggedin { 36 return c.LocalError("You're already logged in.", w, r, u) 37 } 38 39 name := c.SanitiseSingleLine(r.PostFormValue("username")) 40 uid, e, requiresExtraAuth := c.Auth.Authenticate(name, r.PostFormValue("password")) 41 if e != nil { 42 // TODO: uid is currently set to 0 as authenticate fetches the user by username and password. Get the actual uid, so we can alert the user of attempted logins? What if someone takes advantage of the response times to deduce if an account exists? 43 if !c.Config.DisableLoginLog { 44 li := &c.LoginLogItem{UID: uid, Success: false, IP: u.GetIP()} 45 if _, ie := li.Create(); ie != nil { 46 return c.InternalError(ie, w, r) 47 } 48 } 49 return c.LocalError(e.Error(), w, r, u) 50 } 51 52 // TODO: Take 2FA into account 53 if !c.Config.DisableLoginLog { 54 li := &c.LoginLogItem{UID: uid, Success: true, IP: u.GetIP()} 55 if _, e = li.Create(); e != nil { 56 return c.InternalError(e, w, r) 57 } 58 } 59 60 // TODO: Do we want to slacken this by only doing it when the IP changes? 61 if requiresExtraAuth { 62 provSession, signedSession, e := c.Auth.CreateProvisionalSession(uid) 63 if e != nil { 64 return c.InternalError(e, w, r) 65 } 66 // TODO: Use the login log ID in the provisional cookie? 67 c.Auth.SetProvisionalCookies(w, uid, provSession, signedSession) 68 http.Redirect(w, r, "/accounts/mfa_verify/", http.StatusSeeOther) 69 return nil 70 } 71 72 return loginSuccess(uid, w, r, u) 73 } 74 75 func loginSuccess(uid int, w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 76 userPtr, err := c.Users.Get(uid) 77 if err != nil { 78 return c.LocalError("Bad account", w, r, u) 79 } 80 *u = *userPtr 81 82 var session string 83 if u.Session == "" { 84 session, err = c.Auth.CreateSession(uid) 85 if err != nil { 86 return c.InternalError(err, w, r) 87 } 88 } else { 89 session = u.Session 90 } 91 92 c.Auth.SetCookies(w, uid, session) 93 if u.IsAdmin { 94 // Is this error check redundant? We already check for the error in PreRoute for the same IP 95 // TODO: Should we be logging this? 96 log.Printf("#%d has logged in with IP %s", uid, u.GetIP()) 97 } 98 http.Redirect(w, r, "/", http.StatusSeeOther) 99 return nil 100 } 101 102 func extractCookie(name string, r *http.Request) (string, error) { 103 cookie, err := r.Cookie(name) 104 if err != nil { 105 return "", err 106 } 107 return cookie.Value, nil 108 } 109 110 func mfaGetCookies(r *http.Request) (uid int, provSession, signedSession string, err error) { 111 suid, err := extractCookie("uid", r) 112 if err != nil { 113 return 0, "", "", err 114 } 115 uid, err = strconv.Atoi(suid) 116 if err != nil { 117 return 0, "", "", err 118 } 119 provSession, err = extractCookie("provSession", r) 120 if err != nil { 121 return 0, "", "", err 122 } 123 signedSession, err = extractCookie("signedSession", r) 124 return uid, provSession, signedSession, err 125 } 126 127 func mfaVerifySession(provSession, signedSession string, uid int) bool { 128 bProvSession := []byte(provSession) 129 bSignedSession := []byte(signedSession) 130 bUid := []byte(strconv.Itoa(uid)) 131 132 h := sha256.New() 133 h.Write([]byte(c.SessionSigningKeyBox.Load().(string))) 134 h.Write(bProvSession) 135 h.Write(bUid) 136 expected := hex.EncodeToString(h.Sum(nil)) 137 if subtle.ConstantTimeCompare(bSignedSession, []byte(expected)) == 1 { 138 return true 139 } 140 141 h = sha256.New() 142 h.Write([]byte(c.OldSessionSigningKeyBox.Load().(string))) 143 h.Write(bProvSession) 144 h.Write(bUid) 145 expected = hex.EncodeToString(h.Sum(nil)) 146 return subtle.ConstantTimeCompare(bSignedSession, []byte(expected)) == 1 147 } 148 149 func AccountLoginMFAVerify(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 150 if u.Loggedin { 151 return c.LocalError("You're already logged in.", w, r, u) 152 } 153 h.Title = p.GetTitlePhrase("login_mfa_verify") 154 155 uid, provSession, signedSession, err := mfaGetCookies(r) 156 if err != nil { 157 return c.LocalError("Invalid cookie", w, r, u) 158 } 159 if !mfaVerifySession(provSession, signedSession, uid) { 160 return c.LocalError("Invalid session", w, r, u) 161 } 162 163 return renderTemplate("login_mfa_verify", w, r, h, c.Page{h, tList, nil}) 164 } 165 166 func AccountLoginMFAVerifySubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 167 uid, provSession, signedSession, err := mfaGetCookies(r) 168 if err != nil { 169 return c.LocalError("Invalid cookie", w, r, u) 170 } 171 if !mfaVerifySession(provSession, signedSession, uid) { 172 return c.LocalError("Invalid session", w, r, u) 173 } 174 token := r.PostFormValue("mfa_token") 175 176 err = c.Auth.ValidateMFAToken(token, uid) 177 if err != nil { 178 return c.LocalError(err.Error(), w, r, u) 179 } 180 181 return loginSuccess(uid, w, r, u) 182 } 183 184 func AccountLogout(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 185 c.Auth.Logout(w, u.ID) 186 http.Redirect(w, r, "/", http.StatusSeeOther) 187 return nil 188 } 189 190 func AccountRegister(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 191 if u.Loggedin { 192 return c.LocalError("You're already logged in.", w, r, u) 193 } 194 h.Title = p.GetTitlePhrase("register") 195 h.AddScriptAsync("register.js") 196 197 var token string 198 if c.Config.DisableJSAntispam { 199 h := sha256.New() 200 h.Write([]byte(c.JSTokenBox.Load().(string))) 201 h.Write([]byte(u.GetIP())) 202 token = hex.EncodeToString(h.Sum(nil)) 203 } 204 205 return renderTemplate("register", w, r, h, c.RegisterPage{h, h.Settings["activation_type"] != 2, token, nil}) 206 } 207 208 func isNumeric(data string) (numeric bool) { 209 for _, ch := range data { 210 if ch < 48 || ch > 57 { 211 return false 212 } 213 } 214 return true 215 } 216 217 func AccountRegisterSubmit(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError { 218 headerLite, _ := c.SimpleUserCheck(w, r, user) 219 220 // TODO: Should we push multiple validation errors to the user instead of just one? 221 regSuccess := true 222 regErrMsg := "" 223 regErrReason := "" 224 regError := func(userMsg, reason string) { 225 regSuccess = false 226 if regErrMsg == "" { 227 regErrMsg = userMsg 228 } 229 regErrReason += reason + "|" 230 } 231 232 if r.PostFormValue("tos") != "0" { 233 regError(p.GetErrorPhrase("register_might_be_machine"), "trap-question") 234 } 235 236 { 237 h := sha256.New() 238 h.Write([]byte(c.JSTokenBox.Load().(string))) 239 h.Write([]byte(user.GetIP())) 240 if !c.Config.DisableJSAntispam { 241 if r.PostFormValue("golden-watch") != hex.EncodeToString(h.Sum(nil)) { 242 regError(p.GetErrorPhrase("register_might_be_machine"), "js-antispam") 243 } 244 } else { 245 if r.PostFormValue("areg") != hex.EncodeToString(h.Sum(nil)) { 246 regError(p.GetErrorPhrase("register_might_be_machine"), "token") 247 } 248 } 249 } 250 251 name := c.SanitiseSingleLine(r.PostFormValue("name")) 252 if name == "" { 253 regError(p.GetErrorPhrase("register_need_username"), "no-username") 254 } 255 // This is so a numeric name won't interfere with mentioning a user by ID, there might be a better way of doing this like perhaps !@ to mean IDs and @ to mean usernames in the pre-parser 256 nameBits := strings.Split(name, " ") 257 if isNumeric(nameBits[0]) { 258 regError(p.GetErrorPhrase("register_first_word_numeric"), "numeric-name") 259 } 260 if strings.Contains(name, "http://") || strings.Contains(name, "https://") || strings.Contains(name, "ftp://") || strings.Contains(name, "ssh://") { 261 regError(p.GetErrorPhrase("register_url_username"), "url-name") 262 } 263 264 // TODO: Add a dedicated function for validating emails 265 email := c.SanitiseSingleLine(r.PostFormValue("email")) 266 if headerLite.Settings["activation_type"] == 2 && email == "" { 267 regError(p.GetErrorPhrase("register_need_email"), "no-email") 268 } 269 if c.HasSuspiciousEmail(email) { 270 regError(p.GetErrorPhrase("register_suspicious_email"), "suspicious-email") 271 } 272 273 password := r.PostFormValue("password") 274 // ? Move this into Create()? What if we want to programatically set weak passwords for tests? 275 err := c.WeakPassword(password, name, email) 276 if err != nil { 277 regError(err.Error(), "weak-password") 278 } else { 279 // Do the two inputted passwords match..? 280 confirmPassword := r.PostFormValue("confirm_password") 281 if password != confirmPassword { 282 regError(p.GetErrorPhrase("register_password_mismatch"), "password-mismatch") 283 } 284 } 285 286 regLog := c.RegLogItem{Username: name, Email: email, FailureReason: regErrReason, Success: regSuccess, IP: user.GetIP()} 287 if !c.Config.DisableRegLog && regSuccess { 288 if _, e := regLog.Create(); e != nil { 289 return c.InternalError(e, w, r) 290 } 291 } 292 if !regSuccess { 293 return c.LocalError(regErrMsg, w, r, user) 294 } 295 296 var active bool 297 var group int 298 switch headerLite.Settings["activation_type"] { 299 case 1: // Activate All 300 active = true 301 group = c.Config.DefaultGroup 302 default: // Anything else. E.g. Admin Activation or Email Activation. 303 group = c.Config.ActivationGroup 304 } 305 306 pushLog := func(reason string) error { 307 if !c.Config.DisableRegLog { 308 regLog.FailureReason += reason + "|" 309 _, e := regLog.Create() 310 return e 311 } 312 return nil 313 } 314 315 canonEmail := c.CanonEmail(email) 316 uid, err := c.Users.Create(name, password, canonEmail, group, active) 317 if err != nil { 318 regLog.Success = false 319 if err == c.ErrAccountExists { 320 err = pushLog("username-exists") 321 if err != nil { 322 return c.InternalError(err, w, r) 323 } 324 return c.LocalError(p.GetErrorPhrase("register_username_unavailable"), w, r, user) 325 } else if err == c.ErrLongUsername { 326 err = pushLog("username-too-long") 327 if err != nil { 328 return c.InternalError(err, w, r) 329 } 330 return c.LocalError(p.GetErrorPhrase("register_username_too_long_prefix")+strconv.Itoa(c.Config.MaxUsernameLength), w, r, user) 331 } 332 err2 := pushLog("internal-error") 333 if err2 != nil { 334 return c.InternalError(err2, w, r) 335 } 336 return c.InternalError(err, w, r) 337 } 338 339 u, err := c.Users.Get(uid) 340 if err == sql.ErrNoRows { 341 return c.LocalError("You no longer exist.", w, r, user) 342 } else if err != nil { 343 return c.InternalError(err, w, r) 344 } 345 err = c.GroupPromotions.PromoteIfEligible(u, u.Level, u.Posts, u.CreatedAt) 346 if err != nil { 347 return c.InternalError(err, w, r) 348 } 349 u.CacheRemove() 350 351 session, err := c.Auth.CreateSession(uid) 352 if err != nil { 353 return c.InternalError(err, w, r) 354 } 355 c.Auth.SetCookies(w, uid, session) 356 357 // Check if this user actually owns this email, if email activation is on, automatically flip their account to active when the email is validated. Validation is also useful for determining whether this user should receive any alerts, etc. via email 358 if c.Site.EnableEmails && canonEmail != "" { 359 token, err := c.GenerateSafeString(80) 360 if err != nil { 361 return c.InternalError(err, w, r) 362 } 363 // TODO: Add an EmailStore and move this there 364 _, err = qgen.NewAcc().Insert("emails").Columns("email,uid,validated,token").Fields("?,?,?,?").Exec(canonEmail, uid, 0, token) 365 if err != nil { 366 return c.InternalError(err, w, r) 367 } 368 err = c.SendActivationEmail(name, canonEmail, token) 369 if err != nil { 370 return c.LocalError(p.GetErrorPhrase("register_email_fail"), w, r, user) 371 } 372 } 373 374 http.Redirect(w, r, "/", http.StatusSeeOther) 375 return nil 376 } 377 378 // TODO: Figure a way of making this into middleware? 379 func accountEditHead(titlePhrase string, w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) { 380 h.Title = p.GetTitlePhrase(titlePhrase) 381 h.Path = "/user/edit/" 382 h.AddSheet(h.Theme.Name + "/account.css") 383 h.AddScriptAsync("account.js") 384 } 385 386 func AccountEdit(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 387 accountEditHead("account", w, r, u, h) 388 switch { 389 case r.FormValue("avatar_updated") == "1": 390 h.AddNotice("account_avatar_updated") 391 case r.FormValue("name_updated") == "1": 392 h.AddNotice("account_name_updated") 393 case r.FormValue("mfa_setup_success") == "1": 394 h.AddNotice("account_mfa_setup_success") 395 } 396 397 // TODO: Find a more efficient way of doing this 398 mfaSetup := false 399 _, err := c.MFAstore.Get(u.ID) 400 if err != sql.ErrNoRows && err != nil { 401 return c.InternalError(err, w, r) 402 } else if err != sql.ErrNoRows { 403 mfaSetup = true 404 } 405 406 // Normalise the score so that the user sees their relative progress to the next level rather than showing them their total score 407 prevScore := c.GetLevelScore(u.Level) 408 score := u.Score 409 //score = 23 410 currentScore := score - prevScore 411 nextScore := c.GetLevelScore(u.Level+1) - prevScore 412 //perc := int(math.Ceil((float64(nextScore) / float64(currentScore)) * 100)) * 2 413 perc := int(math.Floor((float64(currentScore) / float64(nextScore)) * 100)) // * 2 414 415 pi := c.Account{h, "dashboard", "account_own_edit", c.AccountDashPage{h, mfaSetup, currentScore, nextScore, u.Level + 1, perc}} 416 return renderTemplate("account", w, r, h, pi) 417 } 418 419 //edit_password 420 func AccountEditPassword(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 421 accountEditHead("account_password", w, r, u, h) 422 return renderTemplate("account_own_edit_password", w, r, h, c.Page{h, tList, nil}) 423 } 424 425 // TODO: Require re-authentication if the user hasn't logged in in a while 426 func AccountEditPasswordSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 427 _, ferr := c.SimpleUserCheck(w, r, u) 428 if ferr != nil { 429 return ferr 430 } 431 432 var realPassword, salt string 433 currentPassword := r.PostFormValue("current-password") 434 newPassword := r.PostFormValue("new-password") 435 confirmPassword := r.PostFormValue("confirm-password") 436 437 // TODO: Use a reusable statement 438 err := qgen.NewAcc().Select("users").Columns("password,salt").Where("uid=?").QueryRow(u.ID).Scan(&realPassword, &salt) 439 if err == sql.ErrNoRows { 440 return c.LocalError("Your account no longer exists.", w, r, u) 441 } else if err != nil { 442 return c.InternalError(err, w, r) 443 } 444 445 err = c.CheckPassword(realPassword, currentPassword, salt) 446 if err == c.ErrMismatchedHashAndPassword { 447 return c.LocalError("That's not the correct password.", w, r, u) 448 } else if err != nil { 449 return c.InternalError(err, w, r) 450 } 451 if newPassword != confirmPassword { 452 return c.LocalError("The two passwords don't match.", w, r, u) 453 } 454 c.SetPassword(u.ID, newPassword) // TODO: Limited version of WeakPassword() 455 456 // Log the user out as a safety precaution 457 c.Auth.ForceLogout(u.ID) 458 http.Redirect(w, r, "/", http.StatusSeeOther) 459 return nil 460 } 461 462 func AccountEditAvatarSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 463 _, ferr := c.SimpleUserCheck(w, r, u) 464 if ferr != nil { 465 return ferr 466 } 467 if !u.Perms.UploadAvatars { 468 return c.NoPermissions(w, r, u) 469 } 470 471 ext, ferr := c.UploadAvatar(w, r, u, u.ID) 472 if ferr != nil { 473 return ferr 474 } 475 ferr = c.ChangeAvatar("."+ext, w, r, u) 476 if ferr != nil { 477 return ferr 478 } 479 480 // TODO: Only schedule a resize if the avatar isn't tiny 481 err := u.ScheduleAvatarResize() 482 if err != nil { 483 return c.InternalError(err, w, r) 484 } 485 http.Redirect(w, r, "/user/edit/?avatar_updated=1", http.StatusSeeOther) 486 return nil 487 } 488 489 func AccountEditRevokeAvatarSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 490 _, ferr := c.SimpleUserCheck(w, r, u) 491 if ferr != nil { 492 return ferr 493 } 494 495 ferr = c.ChangeAvatar("", w, r, u) 496 if ferr != nil { 497 return ferr 498 } 499 500 http.Redirect(w, r, "/user/edit/?avatar_updated=1", http.StatusSeeOther) 501 return nil 502 } 503 504 func AccountEditUsernameSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 505 _, ferr := c.SimpleUserCheck(w, r, u) 506 if ferr != nil { 507 return ferr 508 } 509 510 newName := c.SanitiseSingleLine(r.PostFormValue("new-name")) 511 if newName == "" { 512 return c.LocalError("You can't leave your username blank", w, r, u) 513 } 514 err := u.ChangeName(newName) 515 if err != nil { 516 return c.LocalError("Unable to change names. Does someone else already have this name?", w, r, u) 517 } 518 519 http.Redirect(w, r, "/user/edit/?name_updated=1", http.StatusSeeOther) 520 return nil 521 } 522 523 func AccountEditMFA(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 524 accountEditHead("account_mfa", w, r, u, h) 525 526 mfaItem, err := c.MFAstore.Get(u.ID) 527 if err != sql.ErrNoRows && err != nil { 528 return c.InternalError(err, w, r) 529 } else if err == sql.ErrNoRows { 530 return c.LocalError("Two-factor authentication hasn't been setup on your account", w, r, u) 531 } 532 533 pi := c.Page{h, tList, mfaItem.Scratch} 534 return renderTemplate("account_own_edit_mfa", w, r, h, pi) 535 } 536 537 // If not setup, generate a string, otherwise give an option to disable mfa given the right code 538 func AccountEditMFASetup(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 539 accountEditHead("account_mfa_setup", w, r, u, h) 540 541 // Flash an error if mfa is already setup 542 _, e := c.MFAstore.Get(u.ID) 543 if e != sql.ErrNoRows && e != nil { 544 return c.InternalError(e, w, r) 545 } else if e != sql.ErrNoRows { 546 return c.LocalError("You have already setup two-factor authentication", w, r, u) 547 } 548 549 // TODO: Entitise this? 550 code, e := c.GenerateGAuthSecret() 551 if e != nil { 552 return c.InternalError(e, w, r) 553 } 554 555 pi := c.Page{h, tList, c.FriendlyGAuthSecret(code)} 556 return renderTemplate("account_own_edit_mfa_setup", w, r, h, pi) 557 } 558 559 // Form should bounce the random mfa secret back and the otp to be verified server-side to reduce the chances of a bug arising on the JS side which makes every code mismatch 560 func AccountEditMFASetupSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 561 _, ferr := c.SimpleUserCheck(w, r, u) 562 if ferr != nil { 563 return ferr 564 } 565 566 // Flash an error if mfa is already setup 567 _, err := c.MFAstore.Get(u.ID) 568 if err != sql.ErrNoRows && err != nil { 569 return c.InternalError(err, w, r) 570 } else if err != sql.ErrNoRows { 571 return c.LocalError("You have already setup two-factor authentication", w, r, u) 572 } 573 574 code := r.PostFormValue("code") 575 otp := r.PostFormValue("otp") 576 ok, err := c.VerifyGAuthToken(code, otp) 577 if err != nil { 578 //fmt.Println("err: ", err) 579 return c.LocalError("Something weird happened", w, r, u) // TODO: Log this error? 580 } 581 // TODO: Use AJAX for this 582 if !ok { 583 return c.LocalError("The token isn't right", w, r, u) 584 } 585 586 // TODO: How should we handle races where a mfa key is already setup? Right now, it's a fairly generic error, maybe try parsing the error message? 587 err = c.MFAstore.Create(code, u.ID) 588 if err != nil { 589 return c.InternalError(err, w, r) 590 } 591 592 http.Redirect(w, r, "/user/edit/?mfa_setup_success=1", http.StatusSeeOther) 593 return nil 594 } 595 596 // TODO: Implement this 597 func AccountEditMFADisableSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 598 _, ferr := c.SimpleUserCheck(w, r, u) 599 if ferr != nil { 600 return ferr 601 } 602 603 // Flash an error if mfa is already setup 604 mfaItem, err := c.MFAstore.Get(u.ID) 605 if err != sql.ErrNoRows && err != nil { 606 return c.InternalError(err, w, r) 607 } else if err == sql.ErrNoRows { 608 return c.LocalError("You don't have two-factor enabled on your account", w, r, u) 609 } 610 err = mfaItem.Delete() 611 if err != nil { 612 return c.InternalError(err, w, r) 613 } 614 615 http.Redirect(w, r, "/user/edit/?mfa_disabled=1", http.StatusSeeOther) 616 return nil 617 } 618 619 func AccountEditPrivacy(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 620 accountEditHead("account_privacy", w, r, u, h) 621 profileComments := u.Privacy.ShowComments 622 receiveConvos := u.Privacy.AllowMessage 623 enableEmbeds := !c.DefaultParseSettings.NoEmbed 624 if u.ParseSettings != nil { 625 enableEmbeds = !u.ParseSettings.NoEmbed 626 } 627 pi := c.Account{h, "privacy", "account_own_edit_privacy", c.AccountPrivacyPage{h, profileComments, receiveConvos, enableEmbeds}} 628 return renderTemplate("account", w, r, h, pi) 629 } 630 631 func AccountEditPrivacySubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 632 //headerLite, _ := c.SimpleUserCheck(w, r, u) 633 sProfileComments := r.FormValue("profile_comments") 634 sEnableEmbeds := r.FormValue("enable_embeds") 635 oProfileComments := r.FormValue("o_profile_comments") 636 oEnableEmbeds := r.FormValue("o_enable_embeds") 637 638 if sProfileComments != oProfileComments || sEnableEmbeds != oEnableEmbeds { 639 profileComments, e := strconv.Atoi(sProfileComments) 640 enableEmbeds, e2 := strconv.Atoi(sEnableEmbeds) 641 if e != nil || e2 != nil { 642 return c.LocalError("malformed integer", w, r, u) 643 } 644 e = u.UpdatePrivacy(profileComments, enableEmbeds) 645 if e == c.ErrProfileCommentsOutOfBounds || e == c.ErrEnableEmbedsOutOfBounds { 646 return c.LocalError(e.Error(), w, r, u) 647 } else if e != nil { 648 return c.InternalError(e, w, r) 649 } 650 } 651 652 http.Redirect(w, r, "/user/edit/privacy/?updated=1", http.StatusSeeOther) 653 return nil 654 } 655 656 func AccountEditEmail(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 657 accountEditHead("account_email", w, r, u, h) 658 emails, err := c.Emails.GetEmailsByUser(u) 659 if err != nil { 660 return c.InternalError(err, w, r) 661 } 662 663 // Was this site migrated from another forum software? Most of them don't have multiple emails for a single user. 664 // This also applies when the admin switches site.EnableEmails on after having it off for a while. 665 if len(emails) == 0 && u.Email != "" { 666 emails = append(emails, c.Email{UserID: u.ID, Email: u.Email, Validated: false, Primary: true}) 667 } 668 669 if !c.Site.EnableEmails { 670 h.AddNotice("account_mail_disabled") 671 } 672 if r.FormValue("verified") == "1" { 673 h.AddNotice("account_mail_verify_success") 674 } 675 676 pi := c.Account{h, "edit_emails", "account_own_edit_email", c.EmailListPage{h, emails}} 677 return renderTemplate("account", w, r, h, pi) 678 } 679 680 func AccountEditEmailAddSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 681 email := c.SanitiseSingleLine(r.PostFormValue("email")) 682 canonEmail := c.CanonEmail(email) 683 _, err := c.Emails.Get(u, canonEmail) 684 if err == nil { 685 return c.LocalError("You have already added this email.", w, r, u) 686 } else if err != sql.ErrNoRows && err != nil { 687 return c.InternalError(err, w, r) 688 } 689 690 var token string 691 if c.Site.EnableEmails { 692 token, err = c.GenerateSafeString(80) 693 if err != nil { 694 return c.InternalError(err, w, r) 695 } 696 } 697 err = c.Emails.Add(u.ID, canonEmail, token) 698 if err != nil { 699 return c.InternalError(err, w, r) 700 } 701 if c.Site.EnableEmails { 702 err = c.SendValidationEmail(u.Name, canonEmail, token) 703 if err != nil { 704 return c.LocalError(p.GetErrorPhrase("register_email_fail"), w, r, u) 705 } 706 } 707 708 http.Redirect(w, r, "/user/edit/email/?added=1", http.StatusSeeOther) 709 return nil 710 } 711 712 func AccountEditEmailRemoveSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 713 headerLite, _ := c.SimpleUserCheck(w, r, u) 714 email := c.SanitiseSingleLine(r.PostFormValue("email")) 715 canonEmail := c.CanonEmail(email) 716 717 // Quick and dirty check 718 _, err := c.Emails.Get(u, canonEmail) 719 if err == sql.ErrNoRows { 720 return c.LocalError("This email isn't set on this user.", w, r, u) 721 } else if err != nil { 722 return c.InternalError(err, w, r) 723 } 724 if headerLite.Settings["activation_type"] == 2 && u.Email == canonEmail { 725 return c.LocalError("You can't remove your primary email when mandatory email activation is enabled.", w, r, u) 726 } 727 728 err = c.Emails.Delete(u.ID, canonEmail) 729 if err != nil { 730 return c.InternalError(err, w, r) 731 } 732 733 http.Redirect(w, r, "/user/edit/email/?removed=1", http.StatusSeeOther) 734 return nil 735 } 736 737 // TODO: Should we make this an AnonAction so someone can do this without being logged in? 738 func AccountEditEmailTokenSubmit(w http.ResponseWriter, r *http.Request, user *c.User, token string) c.RouteError { 739 header, ferr := c.UserCheck(w, r, user) 740 if ferr != nil { 741 return ferr 742 } 743 if !c.Site.EnableEmails { 744 http.Redirect(w, r, "/user/edit/email/", http.StatusSeeOther) 745 return nil 746 } 747 748 targetEmail := c.Email{UserID: user.ID} 749 emails, err := c.Emails.GetEmailsByUser(user) 750 if err == sql.ErrNoRows { 751 return c.LocalError("A verification email was never sent for you!", w, r, user) 752 } else if err != nil { 753 // TODO: Better error if we don't have an email or it's not in the emails table for some reason 754 return c.LocalError("You are not logged in", w, r, user) 755 } 756 for _, email := range emails { 757 if subtle.ConstantTimeCompare([]byte(email.Token), []byte(token)) == 1 { 758 targetEmail = email 759 } 760 } 761 762 if len(emails) == 0 { 763 return c.LocalError("A verification email was never sent for you!", w, r, user) 764 } 765 if targetEmail.Token == "" { 766 return c.LocalError("That's not a valid token!", w, r, user) 767 } 768 769 err = c.Emails.VerifyEmail(user.Email) 770 if err != nil { 771 return c.InternalError(err, w, r) 772 } 773 774 // If Email Activation is on, then activate the account while we're here 775 if header.Settings["activation_type"] == 2 { 776 if err = user.Activate(); err != nil { 777 return c.InternalError(err, w, r) 778 } 779 780 u2, err := c.Users.Get(user.ID) 781 if err == sql.ErrNoRows { 782 return c.LocalError("The user no longer exists.", w, r, user) 783 } else if err != nil { 784 return c.InternalError(err, w, r) 785 } 786 err = c.GroupPromotions.PromoteIfEligible(u2, u2.Level, u2.Posts, u2.CreatedAt) 787 if err != nil { 788 return c.InternalError(err, w, r) 789 } 790 u2.CacheRemove() 791 } 792 http.Redirect(w, r, "/user/edit/email/?verified=1", http.StatusSeeOther) 793 return nil 794 } 795 796 func AccountLogins(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 797 accountEditHead("account_logins", w, r, u, h) 798 page, _ := strconv.Atoi(r.FormValue("page")) 799 perPage := 12 800 offset, page, lastPage := c.PageOffset(c.LoginLogs.CountUser(u.ID), page, perPage) 801 802 logs, err := c.LoginLogs.GetOffset(u.ID, offset, perPage) 803 if err != nil { 804 return c.InternalError(err, w, r) 805 } 806 807 pageList := c.Paginate(page, lastPage, 5) 808 pi := c.Account{h, "logins", "account_logins", c.AccountLoginsPage{h, logs, c.Paginator{pageList, page, lastPage}}} 809 return renderTemplate("account", w, r, h, pi) 810 } 811 812 func AccountBlocked(w http.ResponseWriter, r *http.Request, user *c.User, h *c.Header) c.RouteError { 813 accountEditHead("account_blocked", w, r, user, h) 814 page, _ := strconv.Atoi(r.FormValue("page")) 815 perPage := 12 816 offset, page, lastPage := c.PageOffset(c.UserBlocks.BlockedByCount(user.ID), page, perPage) 817 818 uids, err := c.UserBlocks.BlockedByOffset(user.ID, offset, perPage) 819 if err != nil { 820 return c.InternalError(err, w, r) 821 } 822 var blocks []*c.User 823 for _, uid := range uids { 824 u, err := c.Users.Get(uid) 825 if err != nil { 826 return c.InternalError(err, w, r) 827 } 828 blocks = append(blocks, u) 829 } 830 831 pageList := c.Paginate(page, lastPage, 5) 832 pi := c.Account{h, "blocked", "account_blocked", c.AccountBlocksPage{h, blocks, c.Paginator{pageList, page, lastPage}}} 833 return renderTemplate("account", w, r, h, pi) 834 } 835 836 func LevelList(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 837 h.Title = p.GetTitlePhrase("account_level_list") 838 839 fScores := c.GetLevels(21) 840 levels := make([]c.LevelListItem, len(fScores)) 841 for i, fScore := range fScores { 842 if i == 0 { 843 continue 844 } 845 var status string 846 if u.Level > (i - 1) { 847 status = "complete" 848 } else if u.Level < (i - 1) { 849 status = "future" 850 } else { 851 status = "inprogress" 852 } 853 iScore := int(math.Ceil(fScore)) 854 //perc := int(math.Ceil((fScore/float64(u.Score))*100)) * 2 855 perc := int(math.Ceil((float64(u.Score) / fScore) * 100)) 856 levels[i] = c.LevelListItem{i - 1, iScore, status, perc} 857 } 858 859 return renderTemplate("level_list", w, r, h, c.LevelListPage{h, levels[1:]}) 860 } 861 862 func Alerts(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 863 return nil 864 } 865 866 func AccountPasswordReset(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 867 if u.Loggedin { 868 return c.LocalError("You're already logged in.", w, r, u) 869 } 870 if !c.Site.EnableEmails { 871 return c.LocalError(p.GetNoticePhrase("account_mail_disabled"), w, r, u) 872 } 873 if r.FormValue("email_sent") == "1" { 874 h.AddNotice("password_reset_email_sent") 875 } 876 h.Title = p.GetTitlePhrase("password_reset") 877 return renderTemplate("password_reset", w, r, h, c.Page{h, tList, nil}) 878 } 879 880 // TODO: Ratelimit this 881 func AccountPasswordResetSubmit(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError { 882 if user.Loggedin { 883 return c.LocalError("You're already logged in.", w, r, user) 884 } 885 if !c.Site.EnableEmails { 886 return c.LocalError(p.GetNoticePhrase("account_mail_disabled"), w, r, user) 887 } 888 889 username := r.PostFormValue("username") 890 tuser, err := c.Users.GetByName(username) 891 if err == sql.ErrNoRows { 892 // Someone trying to stir up trouble? 893 http.Redirect(w, r, "/accounts/password-reset/?email_sent=1", http.StatusSeeOther) 894 return nil 895 } else if err != nil { 896 return c.InternalError(err, w, r) 897 } 898 899 token, err := c.GenerateSafeString(80) 900 if err != nil { 901 return c.InternalError(err, w, r) 902 } 903 904 // TODO: Move these queries somewhere else 905 var disc string 906 err = qgen.NewAcc().Select("password_resets").Columns("createdAt").DateCutoff("createdAt", 1, "hour").QueryRow().Scan(&disc) 907 if err != nil && err != sql.ErrNoRows { 908 return c.InternalError(err, w, r) 909 } 910 if err == nil { 911 return c.LocalError("You can only send a password reset email for a user once an hour", w, r, user) 912 } 913 914 count, err := qgen.NewAcc().Count("password_resets").DateCutoff("createdAt", 6, "hour").Total() 915 if err != nil && err != sql.ErrNoRows { 916 return c.InternalError(err, w, r) 917 } 918 if count >= 3 { 919 return c.LocalError("You can only send a password reset email for a user three times every six hours", w, r, user) 920 } 921 922 count, err = qgen.NewAcc().Count("password_resets").DateCutoff("createdAt", 12, "hour").Total() 923 if err != nil && err != sql.ErrNoRows { 924 return c.InternalError(err, w, r) 925 } 926 if count >= 4 { 927 return c.LocalError("You can only send a password reset email for a user four times every twelve hours", w, r, user) 928 } 929 930 err = c.PasswordResetter.Create(tuser.Email, tuser.ID, token) 931 if err != nil { 932 return c.InternalError(err, w, r) 933 } 934 935 var s string 936 if c.Config.SslSchema { 937 s = "s" 938 } 939 940 err = c.SendEmail(tuser.Email, p.GetTmplPhrase("password_reset_subject"), p.GetTmplPhrasef("password_reset_body", tuser.Name, "http"+s+"://"+c.Site.URL+"/accounts/password-reset/token/?uid="+strconv.Itoa(tuser.ID)+"&token="+token)) 941 if err != nil { 942 return c.LocalError(p.GetErrorPhrase("password_reset_email_fail"), w, r, user) 943 } 944 945 http.Redirect(w, r, "/accounts/password-reset/?email_sent=1", http.StatusSeeOther) 946 return nil 947 } 948 949 func AccountPasswordResetToken(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header) c.RouteError { 950 if u.Loggedin { 951 return c.LocalError("You're already logged in.", w, r, u) 952 } 953 // TODO: Find a way to flash this notice 954 /*if r.FormValue("token_verified") == "1" { 955 h.AddNotice("password_reset_token_token_verified") 956 }*/ 957 958 uid, err := strconv.Atoi(r.FormValue("uid")) 959 if err != nil { 960 return c.LocalError("Invalid uid", w, r, u) 961 } 962 token := r.FormValue("token") 963 err = c.PasswordResetter.ValidateToken(uid, token) 964 if err == sql.ErrNoRows || err == c.ErrBadResetToken { 965 return c.LocalError("This reset token has expired.", w, r, u) 966 } else if err != nil { 967 return c.InternalError(err, w, r) 968 } 969 970 _, err = c.MFAstore.Get(uid) 971 if err != sql.ErrNoRows && err != nil { 972 return c.InternalError(err, w, r) 973 } 974 mfa := err != sql.ErrNoRows 975 976 h.Title = p.GetTitlePhrase("password_reset_token") 977 return renderTemplate("password_reset_token", w, r, h, c.ResetPage{h, uid, html.EscapeString(token), mfa}) 978 } 979 980 func AccountPasswordResetTokenSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 981 if u.Loggedin { 982 return c.LocalError("You're already logged in.", w, r, u) 983 } 984 uid, err := strconv.Atoi(r.FormValue("uid")) 985 if err != nil { 986 return c.LocalError("Invalid uid", w, r, u) 987 } 988 if !c.Users.Exists(uid) { 989 return c.LocalError("This reset token has expired.", w, r, u) 990 } 991 992 err = c.PasswordResetter.ValidateToken(uid, r.FormValue("token")) 993 if err == sql.ErrNoRows || err == c.ErrBadResetToken { 994 return c.LocalError("This reset token has expired.", w, r, u) 995 } else if err != nil { 996 return c.InternalError(err, w, r) 997 } 998 999 mfaToken := r.PostFormValue("mfa_token") 1000 err = c.Auth.ValidateMFAToken(mfaToken, uid) 1001 if err != nil && err != c.ErrNoMFAToken { 1002 return c.LocalError(err.Error(), w, r, u) 1003 } 1004 1005 newPassword := r.PostFormValue("password") 1006 confirmPassword := r.PostFormValue("confirm_password") 1007 if newPassword != confirmPassword { 1008 return c.LocalError("The two passwords don't match.", w, r, u) 1009 } 1010 c.SetPassword(uid, newPassword) // TODO: Limited version of WeakPassword() 1011 1012 err = c.PasswordResetter.FlushTokens(uid) 1013 if err != nil { 1014 return c.InternalError(err, w, r) 1015 } 1016 1017 // Log the user out as a safety precaution 1018 c.Auth.ForceLogout(uid) 1019 1020 //http.Redirect(w, r, "/accounts/password-reset/token/?token_verified=1", http.StatusSeeOther) 1021 http.Redirect(w, r, "/", http.StatusSeeOther) 1022 return nil 1023 }