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  }